diff --git a/Admin.NET-v2/AGENTS.md b/Admin.NET-v2/AGENTS.md new file mode 100644 index 0000000..6289727 --- /dev/null +++ b/Admin.NET-v2/AGENTS.md @@ -0,0 +1,144 @@ +# Repository Guidelines + +## Backend Reading Context +Before reading or changing backend code under `Admin.NET/`, read the root document `BACKEND_INFRA_CONTEXT.md` first. It summarizes the project's common backend wrappers, infrastructure entry points, SqlSugar/Furion adaptation layers, and the recommended lookup order for cache, tenant, auth, data access, scheduling, and plugin integrations. + +## Project Structure & Module Organization +`Admin.NET/` contains the backend solution `Admin.NET.sln`. Core business and infrastructure live in `Admin.NET.Core/`, example application services in `Admin.NET.Application/`, host bootstrapping in `Admin.NET.Web.Core/` and `Admin.NET.Web.Entry/`, automated tests in `Admin.NET.Test/`, and optional integrations in `Admin.NET/Plugins/`. `Web/` is the Vue 3 + Vite frontend; main code is under `Web/src/`, static files under `Web/public/`, localization under `Web/lang/`, and API generation scripts under `Web/api_build/`. Deployment and reference material live in `docker/` and `doc/`. + +## Build, Test, and Development Commands +- `dotnet restore Admin.NET/Admin.NET.sln` restores backend dependencies. +- `dotnet build Admin.NET/Admin.NET.sln -c Debug` builds all backend projects and plugins. +- `dotnet run --project Admin.NET/Admin.NET.Web.Entry` starts the backend entry host locally. +- `dotnet test Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj` runs xUnit/Furion tests. +- `pnpm install --dir Web` installs frontend dependencies. Use Node `>=18`. +- `pnpm --dir Web dev` starts the Vite dev server. +- `pnpm --dir Web build` creates the production frontend bundle. +- `pnpm --dir Web lint-fix` runs ESLint autofixes; `pnpm --dir Web format` applies Prettier. + +## Coding Style & Naming Conventions +Backend style is defined by `Admin.NET/.editorconfig`: 4-space indentation, CRLF line endings, file-scoped namespaces, braces enabled, explicit types preferred over `var`, PascalCase for types and members, and `I` prefixes for interfaces. Frontend formatting is defined in `Web/.prettierrc.cjs`: tabs with width 2, LF line endings, single quotes, semicolons, and trailing commas where valid. Keep feature folders stable and follow existing descriptive names in `Web/src/views/` and `Web/src/api-services/`. + +## Testing Guidelines +Backend tests live in `Admin.NET/Admin.NET.Test/` and currently use `Furion.Xunit`, `Microsoft.NET.Test.Sdk`, and xUnit assertions. Add tests near the relevant feature area and prefer names like `DateTimeUtilTests.cs` or `UserTest.cs`, matching the current convention. Frontend automated tests are not present in this workspace; for UI changes, run `pnpm --dir Web dev`, verify critical pages manually, and include lint/format results with the change. + +## Commit & Pull Request Guidelines +This workspace does not include `.git` metadata, so local history could not be inspected. Use concise, imperative commit messages and prefer a conventional format such as `feat(web): add tenant switcher` or `fix(core): guard null claim`. PRs should describe scope, affected modules, configuration changes, validation steps, and include screenshots for `Web/` UI updates. + +## Security & Configuration Tips +Do not hardcode connection strings, tokens, tenant rules, or approval logic. Keep environment-specific values in configuration files or deployment variables, and prefer extending dictionaries, seed data, or plugin modules over editing shared core behavior directly. + +--- + +## A. 技术栈总览 +- 运行时:后端核心项目均为 `net8.0;net10.0` 双目标(`Admin.NET.Web.Entry`、`Admin.NET.Web.Core`、`Admin.NET.Application`、`Admin.NET.Core`)。 +- 基础框架:以 Furion 为主干,使用 `AppStartup` 装配、`IDynamicApiController` 动态 API、统一返回、JWT、事件总线、任务调度、远程请求等能力。 +- 数据访问:`Startup.AddSqlSugar()` 统一接入,`SqlSugarSetup` 负责全局初始化/AOP/过滤器/种子,`SqlSugarRepository` 负责仓储访问与租户切库,`SqlSugarUnitOfWork` 适配工作单元。 +- 架构形态:分层结构 + 插件模块化。`Web.Entry` 宿主,`Web.Core` 启动装配,`Application` 应用接口,`Core` 实体与基础设施,`Plugins` 扩展模块。 +- 能力归属确认: + - 缓存:`CacheSetup` + `SysCacheService` + `SqlSugarCache` + - 鉴权:`AddJwt` + `SignatureAuthenticationHandler` + `SysAuthService`/`SysOpenAccessService` + - 事件总线:`AddEventBus` + `RetryEventHandlerExecutor` + `RedisEventSourceStorer` + `AppEventSubscriber` + - 任务调度:`AddSchedule` + `UseScheduleUI` + `DynamicJobCompiler` + `SysScheduleService` + - 多租户:`SysTenantService` + `SqlSugarRepository` + `SqlSugarSetup` 查询过滤器 +- 前端现状:README 文案仍写 `Vue3 + Element Plus + Vite5`,但 `Web/package.json` 实际依赖为 `vue ^3.5.28`、`element-plus ^2.13.2`、`vite ^7.3.1`(以代码配置为准)。 + +## B. 关键依据文件 +1. `Admin.NET/Admin.NET.sln`(解决方案与插件挂载结构) +2. `Admin.NET/Admin.NET.Web.Entry/Program.cs`(应用启动入口) +3. `Admin.NET/Admin.NET.Web.Core/Startup.cs`(核心服务注册与中间件链) +4. `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs`(配置绑定) +5. `Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj`(Furion/SqlSugar 等核心依赖) +6. `Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj`(.NET 目标框架确认) +7. `Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj` +8. `Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj`(应用层与插件引用) +9. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs`(SqlSugar 接入主入口) +10. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs`(仓储与租户切库) +11. `Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs`、`Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs`、`Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs` +12. `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs`、`Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +13. `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs`、`Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs` +14. `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +15. `Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs`、`Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs`、`Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs` +16. `Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs`、`Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs` +17. `Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs`(动态 API 与签名鉴权示例) +18. `Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs`(动态插件热加载) +19. `Admin.NET/Plugins/*/Startup.cs`(插件装配入口) +20. `README.md` + `Web/package.json`(前端栈声明与实际依赖对照) + +## C. 后端模块结构树 +- `Admin.NET.sln` + - 启动层:`Admin.NET.Web.Entry` + - 最薄宿主,`Serve.Run` + `WebComponent`(日志过滤、Kestrel 限制) + - 启动装配层:`Admin.NET.Web.Core` + - `Startup` 汇总注册中间件与基础能力,`ProjectOptions` 统一绑定配置 + - 接口/应用层:`Admin.NET.Application` + - 示例开放接口(`OpenApi/DemoOpenApi.cs`),承接业务服务暴露;默认引用 `Core` 与部分插件 + - 实体层:`Admin.NET.Core/Entity` + - 领域实体、表特性、租户/日志/系统表标记 + - 基础设施层:`Admin.NET.Core` + - `SqlSugar/`、`Cache/`、`SignatureAuth/`、`EventBus/`、`SignalR/`、`Job/`、`Service/`、`Option/`、`Utils/`、`Extension/` + - 测试层:`Admin.NET.Test` + - 插件模块:`Admin.NET/Plugins/*` + - `ApprovalFlow`、`DingTalk`、`GoView`、`K3Cloud`、`ReZero`、`WorkWeixin`,各自有 `Startup.cs` 与独立 Option/Service/Proxy/Dto(按插件而异) +- 业务模块注册/加载/扩展方式 + - 编译期:通过 `Application.csproj` 的 `ProjectReference` 显式引用插件(当前默认引用 `ApprovalFlow`、`DingTalk`、`GoView`)。 + - 启动期:Furion 扫描 `[AppStartup]`(`Application`、`Plugins`、`Web.Core`)并执行对应 `ConfigureServices/Configure`。 + - 运行期:`SysPluginService` 支持编译 C# 动态程序集,并通过 `IDynamicApiRuntimeChangeProvider` 热增删 API。 + +## D. 核心启动流程 +1. 入口阶段:`Program.cs` 调用 `Serve.Run(RunOptions.Default.AddWebComponent())` 启动应用,并设置日志过滤与 Kestrel 超时/上传限制。 +2. Startup 扫描阶段:Furion 根据 `[AppStartup]` 扫描并装配 `Application`、`Plugins`、`Web.Core` 的 `Startup`。 +3. 服务注册阶段(`Web.Core/Startup.ConfigureServices`): + - 基础配置:`AddProjectOptions` + - 基础设施:`AddCache`、`AddSqlSugar` + - 鉴权:`AddJwt(enableGlobalAuthorize: true)` + `AddSignatureAuthentication` + - 应用能力:`AddCorsAccessor`、`AddHttpRemote`、`AddTaskQueue`、`AddSchedule`、`AddEventBus` + - Web 能力:控制器与统一返回、OAuth、ElasticSearch、限流、SignalR、OSS、日志、验证码、虚拟文件系统等 +4. 中间件阶段(`Web.Core/Startup.Configure`): + - 压缩/转发/异常/静态资源 + - `UseOAuth` -> `UseUnifyResultStatusCodes` -> `UseAppLocalization` -> `UseRouting` + - `UseCorsAccessor` -> `UseAuthentication` -> `UseAuthorization` + - 限流、`UseScheduleUI`、Swagger/Scalar 注入、`MapHubs`、MVC 路由 +5. 插件扩展阶段:插件 `Startup` 按自身实现追加注册(如审批流中间件、DingTalk 声明式远程 API、GoView 返回规范化等)。 + +## E. 请求处理链路 +- 接口暴露方式 + - 主体为 `IDynamicApiController` 服务类,Furion 自动生成路由。 + - 示例:`DemoOpenApi` 通过 `[Authorize(AuthenticationSchemes = SignatureAuthenticationDefaults.AuthenticationScheme)]` 走签名鉴权。 +- 鉴权链路 + - 全局 JWT:`AddJwt(enableGlobalAuthorize: true)`。 + - 开放接口签名:`AddSignatureAuthentication` + `SignatureAuthenticationHandler` + `SysOpenAccessService.GetSignatureAuthenticationEventImpl()`。 + - 中间件顺序:`UseCorsAccessor` 之后进入 `UseAuthentication` 与 `UseAuthorization`。 +- 统一返回链路 + - `AddInjectWithUnifyResult()` 统一成功/异常响应。 +- 数据访问链路 + - API/动态控制器 -> 业务 Service -> `SqlSugarRepository` -> `SqlSugarScope` -> DB。 + - `SqlSugarRepository` 在构造时根据实体特性(系统表/日志表/租户表)+ 请求头 TenantId + 用户 Claim TenantId 决定连接。 + - `SqlSugarSetup.SetDbAop()` 统一注入软删过滤、租户过滤、数据权限过滤、审计字段与慢 SQL/错误 SQL 记录。 +- 多租户介入阶段 + - 请求进入仓储前:仓储构造函数根据 Header/Claim 切租户库。 + - 查询执行前:SqlSugar 全局过滤器按租户与数据权限过滤。 + - 租户库维护:`SysTenantService.GetTenantDbConnectionScope` + `SqlSugarSetup.InitTenantDatabase`。 +- 缓存介入阶段 + - 启动期:`AddCache` 根据配置选择 Redis,未配置则内存兜底。 + - 运行期:业务缓存统一经 `SysCacheService`,SqlSugar 二级缓存经 `SqlSugarCache` 桥接。 + - 鉴权/开放接口也依赖缓存(如黑名单、签名重放 nonce)。 +- 事件与调度介入阶段 + - 事件总线:`AddEventBus` 注册执行器、监视器;可替换 `RedisEventSourceStorer`;消费由 `EventConsumer`。 + - 调度:`AddSchedule` 注册持久化/监控,`UseScheduleUI` 提供运维看板;动态任务编译由 `DynamicJobCompiler`。 + +## F. 插件式模块机制说明 +- 插件目录与职责 + - `Admin.NET/Plugins` 下每个插件都有独立项目与 `Startup.cs`,按功能拆分集成能力(审批流、钉钉、可视化大屏、K3Cloud、ReZero、企业微信等)。 +- 插件注册与加载 + - 解决方案层面:插件项目挂在 `Admin.NET.sln`。 + - 应用层面:`Admin.NET.Application.csproj` 当前默认引用 `ApprovalFlow`、`DingTalk`、`GoView`;其它插件可按需追加引用启用。 + - 运行层面:插件 `Startup` 使用 `[AppStartup(100)]` 参与全局服务装配。 +- 插件扩展模式 + - 配置扩展:插件 `Option` + `AddConfigurableOptions()` + - 通讯扩展:声明式远程请求(如 DingTalk `AddHttpRemote().AddHttpDeclarative<...>()`) + - 返回规范扩展:插件可注册自身 `UnifyProvider`(如 GoView) + - 中间件扩展:插件可在 `Configure` 注入专属中间件(如 ApprovalFlow) +- 动态插件(运行时热扩展) + - `SysPluginService` 支持把 C# 代码编译为程序集,并通过 `IDynamicApiRuntimeChangeProvider` 进行 API 热添加/热移除。 + - 该机制使“插件化扩展”同时支持静态项目插件和动态代码插件两条路径。 diff --git a/Admin.NET-v2/Admin.NET/.dockerignore b/Admin.NET-v2/Admin.NET/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/.editorconfig b/Admin.NET-v2/Admin.NET/.editorconfig new file mode 100644 index 0000000..989c702 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/.editorconfig @@ -0,0 +1,177 @@ +[*.cs] +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# 命名样式 + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +csharp_using_directive_placement = outside_namespace:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_space_around_binary_operators = before_and_after +csharp_indent_labels = one_less_than_current + +[*.vb] +#### 命名样式 #### + +# 命名规则 + +dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion +dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface +dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始 + +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型 +dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 + +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员 +dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法 + +# 符号规范 + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.类型.required_modifiers = + +dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method +dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.非字段成员.required_modifiers = + +# 命名样式 + +dotnet_naming_style.以_i_开始.required_prefix = I +dotnet_naming_style.以_i_开始.required_suffix = +dotnet_naming_style.以_i_开始.word_separator = +dotnet_naming_style.以_i_开始.capitalization = pascal_case + +dotnet_naming_style.帕斯卡拼写法.required_prefix = +dotnet_naming_style.帕斯卡拼写法.required_suffix = +dotnet_naming_style.帕斯卡拼写法.word_separator = +dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case + +dotnet_naming_style.帕斯卡拼写法.required_prefix = +dotnet_naming_style.帕斯卡拼写法.required_suffix = +dotnet_naming_style.帕斯卡拼写法.word_separator = +dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case + +[*.{cs,vb}] +end_of_line = crlf +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +indent_size = 4 +tab_width = 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line + +# Add copyright file header +file_header_template = Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。\n\n本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。\n\n不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj new file mode 100644 index 0000000..8de4125 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj @@ -0,0 +1,35 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + + enable + True + disable + Admin.NET + Admin.NET 通用权限开发平台 + + + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/APIJSON.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/APIJSON.json new file mode 100644 index 0000000..5baf231 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/APIJSON.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "APIJSON": { + "Roles": [ + { + "RoleName": "Role1", // 权限名称 唯一 + "Select": { // 查询 + "Table": [ "*" ], // 可操作的表 + "Column": [ "*" ], // 可操作的字段 + "Filter": [] + }, + "Insert": { // 添加 + "Table": [ "table1", "table2", "table3" ], + "Column": [ "*", "*", "tb.*" ] + }, + "Update": { // 修改 + "Table": [ "table1", "table2", "table3" ], + "Column": [ "*", "tb.*", "tb.*" ] + }, + "Delete": { // 删除 + "Table": [ "table1", "table2", "table3" ] + } + }, + { + "RoleName": "Role2", + "Select": { + "Table": [ "table1" ], + "Column": [ "tb.*" ] + } + } + ] + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Alipay.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Alipay.json new file mode 100644 index 0000000..5aa74e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Alipay.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 支付宝支付配置,文档地址:https://openhome.alipay.com/develop/sandbox/app + "Alipay": { + "ServerUrl": "https://openapi-sandbox.dl.alipaydev.com/gateway.do", // 支付宝网关地址 + "WebsocketUrl": "openchannel-sandbox.dl.alipaydev.com", // websocket服务地址 + //"AuthUrl": "https://openauth.alipay.com/oauth2/publicAppAuthorize.htm", // 正式环境授权回调地址 + "AuthUrl": "https://openauth-sandbox.dl.alipaydev.com/oauth2/publicAppAuthorize.htm", // 沙箱环境授权回调地址 + "AppAuthUrl": "http://xxxxxxxxxx", // 应用授权回调地址 + "NotifyUrl": "http://xxxxxxxxx/api/sysAlipay/Notify", // 应用网关地址 + "RootCertPath": "AlipayCrt/alipayRootCert.crt", // 支付宝根证书存放路径 + "AccountList": [ + { + "Name": "sandbox 默认应用", + "AppId": "xxxxxxxxxxxxxx", + "SignType": "RSA2", + "PrivateKey": "xxxxxxxxxxxxxxxxx", + "EncryptKey": "xxxxxxxxxxxxxxxxxxxx", + "AppCertPath": "AlipayCrt/appPublicCert.crt", // 应用公钥证书存放路径 + "AlipayPublicCertPath": "AlipayCrt/alipayPublicCert.crt" // 支付宝公钥证书存放路径 + }, + { + "Name": "sandbox 默认应用2", + "AppId": "xxxxxxxxxxxxxx", + "SignType": "RSA2", + "PrivateKey": "xxxxxxxxxxxxxxxxx", + "EncryptKey": "xxxxxxxxxxxxxxxxxxxx", + "AppCertPath": "AlipayCrt/appPublicCert.crt", // 应用公钥证书存放路径 + "AlipayPublicCertPath": "AlipayCrt/alipayPublicCert.crt" // 支付宝公钥证书存放路径 + } + ] + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/App.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/App.json new file mode 100644 index 0000000..3cc1293 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/App.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Urls": "http://*:5005", // 默认端口 + "AllowedHosts": "*", // 允许所有地址 + + "AppSettings": { + "InjectSpecificationDocument": true, // 生产环境是否开启Swagger + "ExternalAssemblies": [ "plugins" ], // 插件目录 + "VirtualPath": "" // 二级虚拟目录 + }, + "DynamicApiControllerSettings": { + //"DefaultRoutePrefix": "api", // 默认路由前缀 + "CamelCaseSeparator": "", // 驼峰命名分隔符 + "SplitCamelCase": false, // 切割骆驼(驼峰)/帕斯卡命名 + "LowercaseRoute": false, // 小写路由格式 + "AsLowerCamelCase": true, // 小驼峰命名(首字母小写) + "KeepVerb": false, // 保留动作方法请求谓词 + "KeepName": false // 保持原有名称不处理 + }, + "FriendlyExceptionSettings": { + "DefaultErrorMessage": "系统异常,请联系管理员", + "ThrowBah": true, // 是否将 Oops.Oh 默认抛出为业务异常 + "LogError": false // 是否输出异常日志 + }, + // 静态资源处理方式(允许这些文件被访问),包含".*": "application/octet-stream"允许访问所有静态资源 + "StaticContentTypeMappings": { + ".dll": "application/octet-stream", + ".exe": "application/octet-stream", + ".pdb": "application/octet-stream", + ".so": "application/octet-stream" + }, + "LocalizationSettings": { + "SupportedCultures": [ "zh-CN", "en" ], // 语言列表 + "DefaultCulture": "zh-CN", // 默认语言 + "DateTimeFormatCulture": "zh-CN" // 固定时间区域为特定时区(多语言) + }, + "CorsAccessorSettings": { + //"PolicyName": "App.Cors.Policy", // 跨域策略名称 + //"WithOrigins": [ "http://localhost:5005", "https://gitee.com" ], // 允许的跨域地址 + "WithExposedHeaders": [ "Content-Disposition", "X-Pagination", "access-token", "x-access-token", "Access-Control-Expose-Headersx-access-token" ], // 如果前端不代理且是axios请求 + "SignalRSupport": true // 启用 SignalR 跨域支持 + }, + // 定时任务/作业调度 + "JobSchedule": { + "Enabled": true // 是否开启 + }, + // 雪花Id + "SnowId": { + "WorkerId": 1, // 雪花Id机器码,多服务器时全局唯一 + "WorkerIdBitLength": 6, // 机器码位长 默认值6,取值范围 [1, 19] + "SeqBitLength": 6, // 序列数位长 默认值6,取值范围 [3, 21](建议不小于4,值越大性能越高、Id位数也更长) + "WorkerPrefix": "adminnet_" // 缓存前缀 + }, + // 密码策略 + "Cryptogram": { + "StrongPassword": false, // 是否开启密码强度验证 + "PasswordStrengthValidation": "(?=^.{6,16}$)(?=.*\\d)(?=.*\\W+)(?=.*[A-Z])(?=.*[a-z])(?!.*\\n).*$", // 密码强度验证正则表达式,必须须包含大小写字母、数字和特殊字符的组合,长度在6-16之间 + "PasswordStrengthValidationMsg": "密码必须包含大小写字母、数字和特殊字符的组合,长度在6-16之间", // 密码强度验证消息提示 + "CryptoType": "SM2", // 密码加密算法:MD5、SM2、SM4 + // 新业务系统记得改密匙,通过接口(http://localhost:5005/api/sysCommon/smKeyPair)获取。记得同步修改前端公钥配置:VITE_SM_PUBLIC_KEY + "PublicKey": "0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312", // 公钥 + "PrivateKey": "8EDB615B1D48B8BE188FC0F18EC08A41DF50EA731FA28BF409E6552809E3A111" // 私钥 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CDConfig.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CDConfig.json new file mode 100644 index 0000000..ba8cd60 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CDConfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "CDConfig": { + "Enabled": true, // 启用持续部署 + "Owner": "zuohuaijun", // gitee用户名 + "Repo": "Admin.NET", // 仓库名 + "Branch": "v2", // 分支名 + "AccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxx", // gitee用户授权码 + "UpdateInterval": 0, // 最小更新间隔(分钟),0不限制 + "BackupCount": 10, // 备份文件保留数量,0不限制 + "BackendOutput": "D:\\Admin.NET", // 后端输出目录 + "Publish": { // 后端发布选项 + "Configuration": "Release", // 发布运行版本 + "TargetFramework": "net8.0", // 发布.NET版本 + "RuntimeIdentifier": "linux-x64" // 运行平台 + }, + "ExcludeFiles": [ "Configuration\\*.json" ] // 排除文件 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Cache.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Cache.json new file mode 100644 index 0000000..8601d73 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Cache.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 缓存配置 + "Cache": { + "Prefix": "adminnet_", // 全局缓存前缀 + "CacheType": "Redis", // Memory、Redis + "Redis": { + "Configuration": "server=1.13.177.47:6379;password=redis@2023;db=5;", // Redis连接字符串 + "Prefix": "adminnet_", // Redis前缀(目前没用) + "MaxMessageSize": "1048576", // 最大消息大小 默认1024 * 1024 + "AutoDetect": false // 自动检测集群节点 阿里云的Redis分布式集群使用代理模式,需要设置为false关闭自动检测;如果不用代理地址,就配置多个节点地址并打开自动检测 + } + }, + // 集群配置 + "Cluster": { + "Enabled": false, // 启用集群:前提开启Redis缓存模式 + "ServerId": "adminnet", // 服务器标识 + "ServerIp": "", // 服务器IP + "SignalR": { + "RedisConfiguration": "127.0.0.1:6379,ssl=false,password=,defaultDatabase=5", + "ChannelPrefix": "signalrPrefix_" + }, + "DataProtecteKey": "AdminNet:DataProtection-Keys", + "IsSentinel": false, // 是否哨兵模式 + "SentinelConfig": { + "DefaultDb": "4", + "EndPoints": [ // 哨兵端口 + // "10.10.0.124:26380" + ], + "MainPrefix": "adminNet:", + "Password": "123456", + "SentinelPassword": "adminNet", + "ServiceName": "adminNet", + "SignalRChannelPrefix": "signalR:" + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Captcha.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Captcha.json new file mode 100644 index 0000000..fcb1058 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Captcha.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // Lazy.Captcha.Core 组件详细文档(https://api.gitee.com/pojianbing/lazy-captcha/) + "CaptchaOptions": { + "CaptchaType": 10, // 验证码类型0、1、2、3、4、5、6、7、8、9、10、11 + "CodeLength": 1, // 验证码长度, 要放在CaptchaType设置后 当类型为算术表达式时,长度代表操作的个数, 例如2 + "ExpirySeconds": 60, // 验证码过期秒数 + "IgnoreCase": true, // 比较时是否忽略大小写 + "StoreageKeyPrefix": "", // 存储键前缀 + "ImageOption": { + "Animation": true, // 是否启用动画 + "FontSize": 36, // 字体大小 + "Width": 150, // 验证码宽度 + "Height": 50, // 验证码高度 + "BubbleMinRadius": 5, // 气泡最小半径 + "BubbleMaxRadius": 10, // 气泡最大半径 + "BubbleCount": 3, // 气泡数量 + "BubbleThickness": 1.0, // 气泡边沿厚度 + "InterferenceLineCount": 3, // 干扰线数量 + "FontFamily": "kaiti", // 包含actionj,epilog,fresnel,headache,lexo,prefix,progbot,ransom,robot,scandal,kaiti + "FrameDelay": 300, // 每帧延迟,Animation=true时有效, 默认300 + "BackgroundColor": "#ffffff", // 格式: rgb, rgba, rrggbb, or rrggbbaa format to match web syntax, 默认#fff + "ForegroundColors": "", // 颜色格式同BackgroundColor,多个颜色逗号分割,随机选取。不填,空值,则使用默认颜色集 + "Quality": 100, // 图片质量(质量越高图片越大,gif调整无效可能会更大) + "TextBold": true // 粗体 + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CodeGen.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CodeGen.json new file mode 100644 index 0000000..2792f5d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/CodeGen.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 代码生成配置项-程序集名称集合 + "CodeGen": { + "EntityAssemblyNames": [ "Admin.NET.Core", "Admin.NET.Application" ], // 实体所在程序集(自行添加新建程序集名称) + "BaseEntityNames": [ "EntityBaseId", "EntityBase", "EntityBaseDel", "EntityBaseOrg", "EntityBaseOrgDel", "EntityBaseTenant", "EntityBaseTenantDel", "EntityBaseTenantId", "EntityBaseTenantOrg", "EntityBaseTenantOrgDel" ], // 实体基类名称 + "FrontRootPath": "Web", // 前端项目根目录 + "BackendApplicationNamespaces": [ "Admin.NET.Application", "Admin.NET.Application2" ] // 后端生成到的项目(自行添加新建命名空间) + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Database.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Database.json new file mode 100644 index 0000000..f5c91c8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Database.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 详细数据库配置见SqlSugar官网(第一个为默认库),极力推荐 PostgreSQL 数据库 + // 数据库连接字符串参考地址:https://www.connectionstrings.com/ + "DbConnection": { + "EnableConsoleSql": false, // 启用控制台打印SQL + "ConnectionConfigs": [ + { + //"ConfigId": "1300000000001", // 默认库标识-禁止修改 + "DbType": "MySql", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom + "DbNickName": "系统库", + // "ConnectionString": "DataSource=./Admin.NET.db", // Sqlite + //"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL + // "ConnectionString": "Server=localhost;Database=xxx;Uid=xxx;Pwd=xxx;SslMode=None;AllowLoadLocalInfile=true;AllowUserVariables=true;", // MySql, + "ConnectionString": "Server=1.13.177.47;Database=hwsaas-cloud;Uid=root;Pwd=Haiwei123456;SslMode=None;AllowLoadLocalInfile=true;AllowUserVariables=true;", // MySql, + //"ConnectionString": "User Id=xxx; Password=xxx; Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))", // Oracle + //"ConnectionString": "Server=localhost;Database=xxx;User Id=xxx;Password=xxx;Encrypt=True;TrustServerCertificate=True;", // SqlServer + + //"SlaveConnectionConfigs": [ // 读写分离/主从 + // { + // "HitRate": 10, + // "ConnectionString": "DataSource=./Admin.NET1.db" + // }, + // { + // "HitRate": 10, + // "ConnectionString": "DataSource=./Admin.NET2.db" + // } + //], + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": true, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": false, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + } + //// 日志独立数据库配置 + //{ + // "ConfigId": "1300000000002", // 日志库标识-禁止修改 + // "DbNickName": "日志库", + // "DbType": "Sqlite", + // "ConnectionString": "DataSource=./Admin.NET.Log.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + // "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + // }, + // "SeedSettings": { + // "EnableInitSeed": false, // 启用种子初始化(若种子没有变化建议关闭) + // "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + // } + //}, + //// 其他数据库配置(可以配置多个) + //{ + // "ConfigId": "test", // 库标识 + // "DbType": "Sqlite", // 库类型 + // "ConnectionString": "DataSource=./Admin.NET.Test.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + // "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + // }, + // "SeedSettings": { + // "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + // "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + // } + //} + ] + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/DeepSeek.example b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/DeepSeek.example new file mode 100644 index 0000000..d9192df --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/DeepSeek.example @@ -0,0 +1,9 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "DeepSeekSettings": { + "SourceLang": "zh-cn", + "ApiUrl": "https://api.deepseek.com/v1/chat/completions", + "ApiKey": "你的 API KEY" + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/ElasticSearch.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/ElasticSearch.json new file mode 100644 index 0000000..78f34de --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/ElasticSearch.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ElasticSearch:Logging": { + "Enabled": true, // 启用ES日志 + "AuthType": "None", // ES认证类型,可选 Basic、ApiKey、Base64ApiKey、None + "User": "elastic", // Basic认证的用户名,使用Basic认证类型时必填 + "Password": "123456", // Basic认证的密码,使用Basic认证类型时必填 + "ApiId": "", // 使用ApiKey认证类型时必填 + "ApiKey": "", // 使用ApiKey认证类型时必填 + "Base64ApiKey": "TmtrOEszNEJuQ0NyaWlydGtROFk6SG1RZ0w3YzBTc2lCanJTYlV3aXNzZw==", // 使用Base64ApiKey认证类型时必填 + "Fingerprint": "37:08:6A:C6:06:CC:9A:43:CF:ED:25:A2:1C:A4:69:57:90:31:2C:06:CA:61:56:39:6A:9C:46:11:BD:22:51:DA", // ES使用Https时的证书指纹 + "ServerUris": [ "http://192.168.3.90:9200" ], // 地址 + "DefaultIndex": "adminnet" // 索引 + }, + "ElasticSearch:Business": { + "Enabled": false, // 启用ES日志 + "AuthType": "Basic", // ES认证类型,可选 Basic、ApiKey、Base64ApiKey、None + "User": "admin", // Basic认证的用户名,使用Basic认证类型时必填 + "Password": "123456", // Basic认证的密码,使用Basic认证类型时必填 + "ApiId": "", // 使用ApiKey认证类型时必填 + "ApiKey": "", // 使用ApiKey认证类型时必填 + "Base64ApiKey": "TmtrOEszNEJuQ0NyaWlydGtROFk6SG1RZ0w3YzBTc2lCanJTYlV3aXNzZw==", // 使用Base64ApiKey认证类型时必填 + "Fingerprint": "37:08:6A:C6:06:CC:9A:43:CF:ED:25:A2:1C:A4:69:57:90:31:2C:06:CA:61:56:39:6A:9C:46:11:BD:22:51:DA", // ES使用Https时的证书指纹 + "ServerUris": [ "http://192.168.1.100:9200" ], // 地址 + "DefaultIndex": "adminnet" // 索引 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Email.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Email.json new file mode 100644 index 0000000..93ae0a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Email.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 邮箱配置 + "Email": { + "Host": "smtp.163.com", // 主机 + "Port": 465, // 端口 465、994、25 + "EnableSsl": true, // 启用SSL + "DefaultFromEmail": "xxx@163.com", // 默认发件者邮箱 + "DefaultToEmail": "xxx@qq.com", // 默认接收人邮箱 + "UserName": "xxx@163.com", // 邮箱账号 + "Password": "", // 邮箱授权码 + "DefaultFromName": "Admin.NET 通用权限开发平台" // 默认邮件标题 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Enum.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Enum.json new file mode 100644 index 0000000..e15deae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Enum.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 枚举配置(将所有枚举转成字典) + "Enum": { + "EntityAssemblyNames": [ "Admin." ] // 枚举所在程序集(自行添加新建程序集名称) + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/EventBus.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/EventBus.json new file mode 100644 index 0000000..02b5ab2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/EventBus.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "EventBus": { + // 事件源存储器类型,默认内存存储(Redis则需要配合缓存相关配置) + "EventSourceType": "Memory", // Memory、Redis、RabbitMQ、Kafka + "RabbitMQ": { + "UserName": "adminnet", + "Password": "adminnet++123456", + "HostName": "127.0.0.1", + "Port": 5672 + }, + "Kafka": { + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/JWT.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/JWT.json new file mode 100644 index 0000000..72c7dc5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/JWT.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "JWTSettings": { + "ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true + "IssuerSigningKey": "3c1cbc3f546eda35168c3aa3cb91780fbe703f0996c6d123ea96dc85c70bbc0a", // 密钥,string 类型,必须是复杂密钥,长度大于16 + "ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true + "ValidIssuer": "Admin.NET", // 签发方,string 类型 + "ValidateAudience": true, // 是否验证签收方,bool 类型,默认true + "ValidAudience": "Admin.NET", // 签收方,string 类型 + "ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true + //"ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟,最大支持 13 年 + "ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认5秒 + "Algorithm": "HS256", // 加密算法,string 类型,默认 HS256 + "RequireExpirationTime": true // 验证过期时间,设置 false 将永不过期 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Limit.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Limit.json new file mode 100644 index 0000000..d23bbca --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Limit.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // IP限流配置 + "IpRateLimiting": { + // 例如:设置每分钟5次访问限流 + // 当False时:每个接口都加入计数,不管你访问哪个接口,只要在一分钟内累计够5次,将禁止访问。 + // 当True 时:当一分钟请求了5次GetData接口,则该接口将在时间段内禁止访问,但是还可以访问PostData()5次,总得来说是每个接口都有5次在这一分钟,互不干扰。 + "EnableEndpointRateLimiting": true, + // 如果StackBlockedRequests设置为false,拒绝的API调用不会添加到调用次数计数器上。比如:如果客户端每秒发出3个请求并且您设置了每秒一个调用的限制, + // 则每分钟或每天计数器等其他限制将仅记录第一个调用,即成功的API调用。如果您希望被拒绝的API调用计入其他时间的显示(分钟,小时等),则必须设置 + "StackBlockedRequests": false, + // 在RealIpHeader使用时,你的Kestrel服务器背后是一个反向代理,如果你的代理服务器使用不同的页眉然后提取客户端IP X-Real-IP使用此选项来设置它。 + "RealIpHeader": "X-Real-IP", + // 将ClientIdHeader被用于提取白名单的客户端ID。如果此标头中存在客户端ID并且与ClientWhitelist中指定的值匹配,则不应用速率限制。 + "ClientIdHeader": "X-ClientId", + // IP白名单:支持Ipv4和Ipv6 + "IpWhitelist": [], + // 端点白名单 + "EndpointWhitelist": [], + // 客户端白名单 + "ClientWhitelist": [], + "QuotaExceededResponse": { + "Content": "{{\"code\":429,\"type\":\"error\",\"message\":\"访问过于频繁,请稍后重试!禁止违法行为否则110 👮\",\"result\":null,\"extras\":null}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + // 返回状态码 + "HttpStatusCode": 429, + // API规则,结尾一定要带* + "GeneralRules": [ + // 1秒钟只能调用1000次 + { + "Endpoint": "*", + "Period": "1s", + "Limit": 1000 + }, + // 1分钟只能调用60000次 + { + "Endpoint": "*", + "Period": "1m", + "Limit": 60000 + } + //// 1小时只能调用3600000次 + //{ + // "Endpoint": "*", + // "Period": "1h", + // "Limit": 3600000 + //}, + //// 1天只能调用86400000次 + //{ + // "Endpoint": "*", + // "Period": "1d", + // "Limit": 86400000 + //} + ] + }, + // IP 黑名单 + "IpRateLimitPolicies": { + "IpRules": [ + { + "Ip": "0.0.0.0", // IP可用:"::1/10" + "Rules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 0 // 设置为0就是1次都不能请求,完全屏蔽 + } + ] + } + ] + }, + // 客户端限流配置 + "ClientRateLimiting": { + "EnableEndpointRateLimiting": true, + "ClientIdHeader": "X-ClientId", + "EndpointWhitelist": [], + "ClientWhitelist": [], + "QuotaExceededResponse": { + "Content": "{{\"code\":429,\"type\":\"error\",\"message\":\"访问人数过多,请稍后重试!\",\"result\":null,\"extras\":null}}", + "ContentType": "application/json", + "StatusCode": 429 + }, + "HttpStatusCode": 429, + "GeneralRules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 2000 + } + ] + }, + "ClientRateLimitPolicies": { + "ClientRules": [ + { + "ClientId": "", + "Rules": [ + { + "Endpoint": "*", + "Period": "1s", + "Limit": 2000 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Logging.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Logging.json new file mode 100644 index 0000000..4d83d40 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Logging.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information", + "AspNetCoreRateLimit": "None", + "System.Net.Http.HttpClient": "Warning" + }, + "File": { + "Enabled": true, // 启用文件日志 + "FileName": "logs/{0:yyyyMMdd}_{1}.log", // 日志文件 + "Append": true, // 追加覆盖 + "MinimumLevel": "Error", // 日志级别 + "FileSizeLimitBytes": 10485760, // 10M=10*1024*1024 + "MaxRollingFiles": 30 // 只保留30个文件 + }, + "Database": { + "Enabled": true, // 启用数据库日志 + "MinimumLevel": "Information" // 日志级别 + }, + "Monitor": { + "GlobalEnabled": true, // 启用全局拦截日志(建议生产环境关闭,否则对性能有影响) + "IncludeOfMethods": [], // 拦截特定方法,当GlobalEnabled=false有效 + "ExcludeOfMethods": [], // 排除特定方法,当GlobalEnabled=true有效 + "BahLogLevel": "Information", // Oops.Oh 和 Oops.Bah 业务日志输出级别 + "WithReturnValue": true, // 是否包含返回值,默认true + "ReturnValueThreshold": 0, // 返回值字符串阈值,默认0全量输出 + "JsonBehavior": "None", // 是否输出Json,默认None(OnlyJson、All) + "JsonIndented": false, // 是否格式化Json + "UseUtcTimestamp": false, // 时间格式UTC、LOCAL + "ConsoleLog": true // 是否显示控制台日志 + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/OAuth.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/OAuth.json new file mode 100644 index 0000000..f2f28aa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/OAuth.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "OAuth": { + "Weixin": { + "ClientId": "xxx", + "ClientSecret": "xxx" + }, + "Gitee": { + "ClientId": "xxx", + "ClientSecret": "xxx" + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/SMS.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/SMS.json new file mode 100644 index 0000000..bc5c546 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/SMS.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 短信配置 + "SMS": { + "VerifyCodeExpireSeconds": 60, // 验证码缓存过期时间(秒),默认60秒 + // 阿里云短信 + "Aliyun": { + "AccessKeyId": "", + "AccessKeySecret": "", + "Templates": [ + { + "Id": "0", + "SignName": "AdminNET 平台", + "TemplateCode": "SMS_xxx", + "Content": "您的验证码为:${code},请勿泄露于他人!" + }, + { + "Id": "1", + "SignName": "AdminNET 平台", + "TemplateCode": "SMS_xxx", + "Content": "注册成功,感谢您的注册,请妥善保管您的账户信息" + } + ] + }, + // 腾讯云短信 + "Tencentyun": { + "SdkAppId": "", + "AccessKeyId": "", + "AccessKeySecret": "", + "Templates": [ + { + "Id": "0", + "SignName": "AdminNET 平台", + "TemplateCode": "", + "Content": "" + } + ] + }, + // 自定义短信接口 + "Custom": { + "Enabled": false, // 是否启用自定义短信接口 + "Method": "GET", // 请求方法: GET 或 POST + "ApiUrl": "https://api.xxxx.com/sms?u=xxxx&key=59e03f49c3dbb5033&m={mobile}&c={content}", // API接口地址,支持占位符: {mobile}, {content}, {code} + "ContentType": "application/x-www-form-urlencoded", // POST请求的Content-Type: application/json 或 application/x-www-form-urlencoded + "PostData": "", // POST请求的数据模板,支持占位符,JSON 格式示例: {"mobile":"{mobile}","content":"{content}","apikey":"your_key"};Form 格式示例: mobile={mobile}&content={content}&apikey=your_key + "SuccessFlag": "0", // 成功响应标识,响应内容包含此字符串则认为发送成功 + "Templates": [ + { + "Id": "0", + "Content": "您的验证码为:{code},请勿泄露于他人!" + }, + { + "Id": "1", + "Content": "注册成功,感谢您的注册,请妥善保管您的账户信息" + } + ] + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Search.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Search.json new file mode 100644 index 0000000..6c0f4cb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Search.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + "HwPortalSearch": { + "Engine": "mysql_fulltext", + "EnableLegacyFallback": true, + "ConnectionString": "", + "UseMainDbConnectionWhenEmpty": true, + "BatchSize": 200, + "TakeLimit": 500, + "AutoInitSchema": true + } +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Swagger.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Swagger.json new file mode 100644 index 0000000..6d636b7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Swagger.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "SpecificationDocumentSettings": { + "DocumentTitle": "Admin.NET 通用权限开发平台", + "GroupOpenApiInfos": [ + { + "Group": "Default", + "Title": "Admin.NET 通用权限开发平台", + "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。", + "Version": "1.0.0", + "Order": 1000 + }, + { + "Group": "All Groups", + "Title": "所有接口", + "Description": "让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。", + "Version": "1.0.0", + "Order": 0 + } + ], + "DefaultGroupName": "Default", // 默认分组名 + "DocExpansionState": "List", // List、Full、None + "EnableAllGroups": true, + //"ServerDir": "xxx", // 二级虚拟目录并且开启 Servers 选择列表 + "HideServers": true, + "Servers": [ + { + "Url": "http://ip/xxx", + "Description": "二级目录应用程序名" + } + ], + "LoginInfo": { + "Enabled": true, // 是否开启Swagger登录 + "CheckUrl": "/api/swagger/checkUrl", + "SubmitUrl": "/api/swagger/submitUrl", + "EnableOnProduction": false // 是否在生产环境中自动开启 + }, + "EnumToNumber": true // 枚举类型生成值类型 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Upload.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Upload.json new file mode 100644 index 0000000..85689e8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Upload.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Upload": { + "Path": "upload/{yyyy}/{MM}/{dd}", // 文件上传目录 + "MaxSize": 51200, // 文件最大限制KB:1024*50 + "ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive" ], + "EnableMd5": false // 启用文件MDF5验证-防止重复上传 + }, + "OSSProvider": { + "Enabled": false, + "Provider": "Minio", // OSS提供者 Invalid/Minio/Aliyun/QCloud/Qiniu/HuaweiCloud + "Endpoint": "xxx.xxx.xxx.xxx:8090", // 节点/API地址(在腾讯云OSS中表示AppId) + "Region": "xxx.xxx.xxx.xxx", // 地域 + "AccessKey": "", + "SecretKey": "", + "IsEnableHttps": false, // 是否启用HTTPS + "IsEnableCache": true, // 是否启用缓存 + "Bucket": "admin.net", + "CustomHost": "" // 自定义Host:拼接外链的Host,若空则使用Endpoint拼接 + }, + "SSHProvider": { + "Enabled": false, + "Host": "127.0.0.1", + "Port": 8222, + "Username": "sshuser", + "Password": "Password.1" + }, + "MultiOSS": { + "Enabled": false, // 是否启用多OSS功能 + "Description": "启用多OSS功能后,系统将支持多个存储提供者,可以通过BucketName或ProviderId指定使用哪个存储提供者" + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Wechat.json b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Wechat.json new file mode 100644 index 0000000..25ec0d2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Configuration/Wechat.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Wechat": { + // 公众号 + "WechatAppId": "", + "WechatAppSecret": "", + "WechatToken": "", // 微信公众号服务器配置中的令牌(Token) + "WechatEncodingAESKey": "", // 微信公众号服务器配置中的消息加解密密钥(EncodingAESKey) + // 小程序 + "WxOpenAppId": "", + "WxOpenAppSecret": "", + "WxToken": "", // 小程序消息推送中的令牌(Token) + "WxEncodingAESKey": "", // 小程序消息推送中的消息加解密密钥(EncodingAESKey) + "QRImagePath": "" //小程序生成带参数二维码保存位置(绝对路径 eg: D:\\Web\\wwwroot\\upload\\QRImage),如果不配置,则默认保存在项目根目录下wwwroot\\upload\\QRImage + }, + // 微信支付 + "WechatPay": { + "AppId": "", // 微信公众平台AppId、开放平台AppId、小程序AppId、企业微信CorpId + "MerchantId": "", // 商户平台的商户号 + "MerchantV3Secret": "", // 商户平台的APIv3密钥 + "MerchantCertificateSerialNumber": "", // 商户平台的证书序列号 + "MerchantCertificatePrivateKey": "WxPayCert/apiclient_key.pem" // 商户平台的API证书私钥(apiclient_key.pem文件内容) + }, + // 支付回调 + "PayCallBack": { + "WechatPayUrl": "https://xxx/api/sysWechatPay/payCallBack", // 微信支付回调 + "WechatRefundUrl": "", // 微信退款回调 + "AlipayUrl": "", // 支付宝支付回调 + "AlipayRefundUrl": "" // 支付宝退款回调 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Const/ApplicationConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Const/ApplicationConst.cs new file mode 100644 index 0000000..70ee2b6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Const/ApplicationConst.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Application; + +/// +/// 业务应用相关常量 +/// +public class ApplicationConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "xxx业务应用"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Entity/TestViewSysUser.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Entity/TestViewSysUser.cs new file mode 100644 index 0000000..62f00e6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Entity/TestViewSysUser.cs @@ -0,0 +1,67 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using SqlSugar; + +namespace Admin.NET.Application; + +/// +/// 用户表视图(必须加IgnoreTable,防止被生成为表) +/// +[SugarTable(null, "用户表视图"), IgnoreTable] +public class TestViewSysUser : EntityBase, ISqlSugarView +{ + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号")] + public virtual string Account { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名")] + public virtual string RealName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称")] + public string? NickName { get; set; } + + /// + /// 机构名称 + /// + [SugarColumn(ColumnDescription = "机构名称")] + public string? OrgName { get; set; } + + /// + /// 职位名称 + /// + [SugarColumn(ColumnDescription = "职位名称")] + public string? PosName { get; set; } + + /// + /// 查询实例 + /// + /// + /// + public string GetQueryableSqlString(SqlSugarScopeProvider db) + { + return db.Queryable() + .LeftJoin((u, a) => u.OrgId == a.Id) + .LeftJoin((u, a, b) => u.PosId == b.Id) + .Select((u, a, b) => new TestViewSysUser + { + Id = u.Id, + Account = u.Account, + RealName = u.RealName, + NickName = u.NickName, + OrgName = a.Name, + PosName = b.Name, + }).ToMappedSqlString(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/EventBus/SysUserEventSubscriber.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/EventBus/SysUserEventSubscriber.cs new file mode 100644 index 0000000..c9a727f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/EventBus/SysUserEventSubscriber.cs @@ -0,0 +1,103 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.EventBus; + +namespace Admin.NET.Application; + +/// +/// 系统用户操作事件订阅 +/// +public class SysUserEventSubscriber : IEventSubscriber, ISingleton, IDisposable +{ + public SysUserEventSubscriber() + { + } + + /// + /// 增加系统用户 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.Add)] + public Task AddUser(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 注册用户 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.Register)] + public Task RegisterUser(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 更新系统用户 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.Update)] + public Task UpdateUser(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 删除系统用户 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.Delete)] + public Task DeleteUser(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 设置系统用户状态 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.SetStatus)] + public Task SetUserStatus(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 授权用户角色 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.UpdateRole)] + public Task UpdateUserRole(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 解除登录锁定 + /// + /// + /// + [EventSubscribe(SysUserEventTypeEnum.UnlockLogin)] + public Task UnlockUserLogin(EventHandlerExecutingContext context) + { + return Task.CompletedTask; + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/GlobalUsings.cs new file mode 100644 index 0000000..c35c4ea --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/GlobalUsings.cs @@ -0,0 +1,15 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core; +global using Furion; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.DependencyInjection; +global using System; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs new file mode 100644 index 0000000..bf3bd6d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Application; + +/// +/// 示例开放接口 +/// +[ApiDescriptionSettings("开放接口", Name = "Demo", Order = 100)] +[Authorize(AuthenticationSchemes = SignatureAuthenticationDefaults.AuthenticationScheme)] +public class DemoOpenApi : IDynamicApiController +{ + private readonly UserManager _userManager; + + public DemoOpenApi(UserManager userManager) + { + _userManager = userManager; + } + + [HttpGet("helloWord")] + public Task HelloWord() + { + return Task.FromResult($"Hello word. {_userManager.Account}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Application/Startup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Startup.cs new file mode 100644 index 0000000..29aa318 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Application/Startup.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Admin.NET.Application; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj new file mode 100644 index 0000000..ff2c6ea --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj @@ -0,0 +1,74 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + + enable + true + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CommonValidationAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CommonValidationAttribute.cs new file mode 100644 index 0000000..e58d08a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CommonValidationAttribute.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用接口参数验证特性类 +/// +[AttributeUsage(AttributeTargets.Property)] +public class CommonValidationAttribute : ValidationAttribute +{ + private readonly Dictionary _conditions; + private static readonly Dictionary CompiledConditions = new(); + + /// + /// + /// 条件对参数,长度必须为偶数
+ /// 奇数字符串参数:动态条件
+ /// 偶数字符串参数:提示消息 + /// + /// + /// + /// public class ModelInput { + /// + /// + /// public string A { get; set; } + /// + /// + /// [CommonValidation( + /// "A == 1 && B == null", "当 A == 1时,B不能为空", + /// "C == 2 && B == null", "当 C == 2时,B不能为空" + /// )] + /// public string B { get; set; } + /// } + /// + /// + public CommonValidationAttribute(params string[] conditionPairs) + { + if (conditionPairs.Length % 2 != 0) throw new ArgumentException("条件对必须以偶数个字符串的形式提供。"); + + var conditions = new Dictionary(); + for (int i = 0; i < conditionPairs.Length; i += 2) + conditions.Add(conditionPairs[i], conditionPairs[i + 1]); + + _conditions = conditions; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + foreach (var (expr, errorMessage) in _conditions) + { + var conditionKey = $"{validationContext.ObjectType.FullName}.{expr}"; + if (!CompiledConditions.TryGetValue(conditionKey, out var condition)) + { + condition = CreateCondition(validationContext.ObjectType, expr); + CompiledConditions[conditionKey] = condition; + } + + if ((bool)condition.DynamicInvoke(validationContext.ObjectInstance)!) + { + return new ValidationResult(errorMessage ?? $"[{validationContext.MemberName}]校验失败"); + } + } + + return ValidationResult.Success; + } + + private Delegate CreateCondition(Type modelType, string expression) + { + try + { + // 创建参数表达式 + var parameter = Expression.Parameter(typeof(object), "x"); + + // 构建 Lambda 表达式 + var lambda = DynamicExpressionParser.ParseLambda(new[] { Expression.Parameter(modelType, "x") }, typeof(bool), expression); + + // 创建新的 Lambda 表达式,接受 object 参数并调用编译后的表达式 + var invokeExpression = Expression.Invoke(lambda, Expression.Convert(parameter, modelType)); + var finalLambda = Expression.Lambda>(invokeExpression, parameter); + + return finalLambda.Compile(); + } + catch (Exception ex) + { + throw new ArgumentException($"无法解析表达式 '{expression}': {ex.Message}", ex); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ConstAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ConstAttribute.cs new file mode 100644 index 0000000..baf5a60 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ConstAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 常量特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class ConstAttribute : Attribute +{ + public string Name { get; set; } + + public ConstAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs new file mode 100644 index 0000000..a5895f4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomJsonPropertyAttribute.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 自定义Json转换字段名 +/// +[AttributeUsage(AttributeTargets.Property)] +public class CustomJsonPropertyAttribute : Attribute +{ + public string Name { get; } + + public CustomJsonPropertyAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs new file mode 100644 index 0000000..0543422 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/CustomUnifyResultAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 自定义规范化结果特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class CustomUnifyResultAttribute : Attribute +{ + public string Name { get; set; } + + public CustomUnifyResultAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DataMaskAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DataMaskAttribute.cs new file mode 100644 index 0000000..d7eafc2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DataMaskAttribute.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据脱敏特性(支持自定义脱敏位置和脱敏字符) +/// +[AttributeUsage(AttributeTargets.Property)] +public class DataMaskAttribute : Attribute +{ + /// + /// 脱敏起始位置(从0开始) + /// + private int StartIndex { get; } + + /// + /// 脱敏长度 + /// + private int Length { get; } + + /// + /// 脱敏字符(默认*) + /// + private char MaskChar { get; set; } = '*'; + + /// + /// 是否保留原始长度(默认true) + /// + private bool KeepLength { get; set; } = true; + + public DataMaskAttribute(int startIndex, int length) + { + if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex)); + if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length)); + + StartIndex = startIndex; + Length = length; + } + + /// + /// 执行脱敏处理 + /// + public string Mask(string input) + { + if (string.IsNullOrEmpty(input) || input.Length <= StartIndex) + return input; + + var maskedLength = Math.Min(Length, input.Length - StartIndex); + var maskStr = new string(MaskChar, KeepLength ? maskedLength : Math.Min(4, maskedLength)); + + return input.Substring(0, StartIndex) + maskStr + + (StartIndex + maskedLength < input.Length ? + input.Substring(StartIndex + maskedLength) : ""); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs new file mode 100644 index 0000000..2a1fae8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/DictAttribute.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 字典值合规性校验特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = true)] +public class DictAttribute : ValidationAttribute, ITransient +{ + /// + /// 字典编码 + /// + public string DictTypeCode { get; } + + /// + /// 是否允许空字符串 + /// + public bool AllowEmptyStrings { get; set; } = false; + + /// + /// 允许空值,有值才验证,默认 false + /// + public bool AllowNullValue { get; set; } = false; + + /// + /// 字典值合规性校验特性 + /// + /// + /// + public DictAttribute(string dictTypeCode = "", string errorMessage = "字典值不合法!") + { + DictTypeCode = dictTypeCode; + ErrorMessage = errorMessage; + } + + /// + /// 字典值合规性校验 + /// + /// + /// + /// + protected override ValidationResult IsValid(object? value, ValidationContext validationContext) + { + // 判断是否允许空值 + if (AllowNullValue && value == null) return ValidationResult.Success; + + // 获取属性的类型 + var property = validationContext.ObjectType.GetProperty(validationContext.MemberName!); + if (property == null) return new ValidationResult($"未知属性: {validationContext.MemberName}"); + + string importHeaderName = GetImporterHeaderName(property, validationContext.MemberName); + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + // 先尝试从 ValidationContext 的依赖注入容器中拿服务,拿不到或类型不匹配时,再从全局的 App 容器中获取 + if (validationContext.GetService(typeof(SysDictDataService)) is not SysDictDataService sysDictDataService) + sysDictDataService = App.GetRequiredService(); + + // 获取字典值列表 + var dictDataList = sysDictDataService.GetDataList(DictTypeCode).GetAwaiter().GetResult(); + + // 使用 HashSet 来提高查找效率 + var dictHash = new HashSet(dictDataList.Select(u => u.Value)); + + // 判断是否为集合类型 + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + // 如果是空集合并且允许空值,则直接返回成功 + if (value == null && AllowNullValue) return ValidationResult.Success; + + // 处理集合为空的情况 + var collection = value as IEnumerable; + if (collection == null) return ValidationResult.Success; + + // 获取集合的元素类型 + var elementType = propertyType.GetGenericArguments()[0]; + var underlyingElementType = Nullable.GetUnderlyingType(elementType) ?? elementType; + + // 如果元素类型是枚举,则逐个验证 + if (underlyingElementType.IsEnum) + { + foreach (var item in collection) + { + if (item == null && AllowNullValue) continue; + + if (!Enum.IsDefined(underlyingElementType, item!)) + return new ValidationResult($"提示:{ErrorMessage}|枚举值【{item}】不是有效的【{underlyingElementType.Name}】枚举类型值!", [importHeaderName]); + } + return ValidationResult.Success; + } + + foreach (var item in collection) + { + if (item == null && AllowNullValue) continue; + + var itemString = item?.ToString(); + if (!dictHash.Contains(itemString)) + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{itemString}】!", [importHeaderName]); + } + + return ValidationResult.Success; + } + + var valueAsString = value?.ToString(); + + // 是否忽略空字符串 + if (AllowEmptyStrings && string.IsNullOrEmpty(valueAsString)) return ValidationResult.Success; + + // 枚举类型验证 + if (propertyType.IsEnum) + { + if (!Enum.IsDefined(propertyType, value!)) return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!", [importHeaderName]); + return ValidationResult.Success; + } + + if (!dictHash.Contains(valueAsString)) + return new ValidationResult($"提示:{ErrorMessage}|字典【{DictTypeCode}】不包含【{valueAsString}】!", [importHeaderName]); + + return ValidationResult.Success; + } + + /// + /// 获取本字段上 [ImporterHeader(Name = "xxx")] 里的Name,如果没有则使用defaultName. + /// 用于在从excel导入数据时,能让调用者知道是哪个字段验证失败,而不是抛异常 + /// + private static string GetImporterHeaderName(PropertyInfo property, string defaultName) + { + var importerHeader = property.GetCustomAttribute(); + string importerHeaderName = importerHeader?.Name ?? defaultName; + return importerHeaderName; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/EnumAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/EnumAttribute.cs new file mode 100644 index 0000000..96e3295 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/EnumAttribute.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举值合规性校验特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Enum | AttributeTargets.Field, AllowMultiple = true)] +public class EnumAttribute : ValidationAttribute, ITransient +{ + /// + /// 枚举值合规性校验特性 + /// + /// + public EnumAttribute(string errorMessage = "枚举值不合法!") + { + ErrorMessage = errorMessage; + } + + /// + /// 枚举值合规性校验 + /// + /// + /// + /// + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + // 获取属性的类型 + var property = validationContext.ObjectType.GetProperty(validationContext.MemberName); + if (property == null) + return new ValidationResult($"未知属性: {validationContext.MemberName}"); + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + // 检查属性类型是否为枚举或可空枚举类型 + if (!propertyType.IsEnum) + return new ValidationResult($"属性类型'{validationContext.MemberName}'不是有效的枚举类型!"); + + // 检查枚举值是否有效 + if (value == null && Nullable.GetUnderlyingType(property.PropertyType) == null) + return new ValidationResult($"提示:{ErrorMessage}|枚举值不能为 null!"); + + if (value != null && !Enum.IsDefined(propertyType, value)) + return new ValidationResult($"提示:{ErrorMessage}|枚举值【{value}】不是有效的【{propertyType.Name}】枚举类型值!"); + + return ValidationResult.Success; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IdempotentAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IdempotentAttribute.cs new file mode 100644 index 0000000..ed692a4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IdempotentAttribute.cs @@ -0,0 +1,112 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; +using System.Security.Claims; + +namespace Admin.NET.Core; + +/// +/// 防止重复请求过滤器特性(此特性使用了分布式锁,需确保系统支持分布式锁) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] +public class IdempotentAttribute : Attribute, IAsyncActionFilter +{ + /// + /// 请求间隔时间/秒 + /// + public int IntervalTime { get; set; } = 5; + + /// + /// 错误提示内容 + /// + public string Message { get; set; } = "你操作频率过快,请稍后重试!"; + + /// + /// 缓存前缀: Key+请求路由+用户Id+请求参数 + /// + public string CacheKey { get; set; } = CacheConst.KeyIdempotent; + + /// + /// 是否直接抛出异常:Ture是,False返回上次请求结果 + /// + public bool ThrowBah { get; set; } + + /// + /// 锁前缀 + /// + public string LockPrefix { get; set; } = "lock_"; + + public IdempotentAttribute() + { + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var httpContext = context.HttpContext; + var path = httpContext.Request.Path.Value.ToString(); + var userId = httpContext.User?.FindFirstValue(ClaimConst.UserId); + var cacheExpireTime = TimeSpan.FromSeconds(IntervalTime); + + var parameters = JsonConvert.SerializeObject(context.ActionArguments, Formatting.None, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Include, + DefaultValueHandling = DefaultValueHandling.Include + }); + + var cacheKey = CacheKey + MD5Encryption.Encrypt($"{path}{userId}{parameters}"); + var sysCacheService = httpContext.RequestServices.GetService(); + try + { + // 分布式锁 + using var distributedLock = sysCacheService.BeginCacheLock($"{LockPrefix}{cacheKey}") ?? throw Oops.Oh(Message); + + var cacheValue = sysCacheService.Get(cacheKey); + if (cacheValue != null) + { + if (ThrowBah) throw Oops.Oh(Message); + context.Result = new ObjectResult(cacheValue.Value); + return; + } + else + { + var resultContext = await next(); + // 缓存请求结果,null值不缓存 + if (resultContext.Result is ObjectResult { Value: { } } objectResult) + { + var typeName = objectResult.Value.GetType().Name; + var responseData = new ResponseData + { + Type = typeName, + Value = objectResult.Value + }; + sysCacheService.Set(cacheKey, responseData, cacheExpireTime); + } + } + } + catch (Exception ex) + { + throw Oops.Oh($"{Message}-{ex}"); + } + } + + /// + /// 请求结果数据 + /// + private class ResponseData + { + /// + /// 结果类型 + /// + public string Type { get; set; } + + /// + /// 请求结果 + /// + public dynamic Value { get; set; } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs new file mode 100644 index 0000000..067dc0a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreEnumToDictAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略枚举类型转字典特性(标记在枚举类型) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Enum, AllowMultiple = true, Inherited = true)] +public class IgnoreEnumToDictAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs new file mode 100644 index 0000000..5fb65b9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略表结构初始化特性(标记在实体) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IgnoreTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs new file mode 100644 index 0000000..9ea18da --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略更新种子特性(标记在种子类) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IgnoreUpdateSeedAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs new file mode 100644 index 0000000..1bccc81 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 忽略更新种子列特性(标记在实体属性) +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class IgnoreUpdateSeedColumnAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ImportDictAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ImportDictAttribute.cs new file mode 100644 index 0000000..552df72 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ImportDictAttribute.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 属性字典配置 +/// +[AttributeUsage(AttributeTargets.Property)] +public class ImportDictAttribute : Attribute +{ + /// + /// 字典Code + /// + public string TypeCode { get; set; } + + /// + /// 目标对象名称 + /// + public string TargetPropName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreSeedAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreSeedAttribute.cs new file mode 100644 index 0000000..9ab1f02 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreSeedAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 增量种子特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IncreSeedAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreTableAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreTableAttribute.cs new file mode 100644 index 0000000..31f31b7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/IncreTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 增量表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class IncreTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/LogTableAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/LogTableAttribute.cs new file mode 100644 index 0000000..aa07fd4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/LogTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 日志表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class LogTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs new file mode 100644 index 0000000..a09c801 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskNewtonsoftJsonConverter.cs @@ -0,0 +1,60 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 字符串掩码 +/// +[SuppressSniffer] +public class MaskNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().Mask()); + } +} + +/// +/// 身份证掩码 +/// +[SuppressSniffer] +public class MaskIdCardNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().MaskIdCard()); + } +} + +/// +/// 邮箱掩码 +/// +[SuppressSniffer] +public class MaskEmailNewtonsoftJsonConverter : JsonConverter +{ + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.Value.ToString(); + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString().MaskEmail()); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs new file mode 100644 index 0000000..6b1a12f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaskSystemTextJsonConverter.cs @@ -0,0 +1,61 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Admin.NET.Core; + +/// +/// 字符串掩码 +/// +[SuppressSniffer] +public class MaskSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().Mask()); + } +} + +/// +/// 身份证掩码 +/// +[SuppressSniffer] +public class MaskIdCardSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().MaskIdCard()); + } +} + +/// +/// 邮箱掩码 +/// +[SuppressSniffer] +public class MaskEmailSystemTextJsonConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetString(); + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value?.ToString().MaskEmail()); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaxValueAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaxValueAttribute.cs new file mode 100644 index 0000000..8132890 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MaxValueAttribute.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 最大值校验 +/// +[SuppressSniffer] +public class MaxValueAttribute : ValidationAttribute +{ + private double MaxValue { get; } + + /// + /// 最大值 + /// + /// + public MaxValueAttribute(double value) => this.MaxValue = value; + + /// + /// 最大值校验 + /// + /// + /// + public override bool IsValid(object value) + { + return value == null || Convert.ToDouble(value) <= this.MaxValue; + } + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MinValueAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MinValueAttribute.cs new file mode 100644 index 0000000..40c7378 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/MinValueAttribute.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 最小值校验 +/// +[SuppressSniffer] +public class MinValueAttribute : ValidationAttribute +{ + private double MinValue { get; set; } + + /// + /// 最小值 + /// + /// + public MinValueAttribute(double value) => this.MinValue = value; + + /// + /// 最小值校验 + /// + /// + /// + public override bool IsValid(object value) + { + return value == null || Convert.ToDouble(value) > this.MinValue; + } + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/NotEmptyAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/NotEmptyAttribute.cs new file mode 100644 index 0000000..a021120 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/NotEmptyAttribute.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 校验集合不能为空 +/// +[SuppressSniffer] +public class NotEmptyAttribute : ValidationAttribute +{ + /// + /// 校验集合不能为空 + /// + /// + /// + public override bool IsValid(object value) => (value as IEnumerable)?.GetEnumerator().MoveNext() ?? false; + + /// + /// 错误信息 + /// + /// + /// + public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs new file mode 100644 index 0000000..468272c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 所属机构数据权限 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class OwnerOrgAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerUserAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerUserAttribute.cs new file mode 100644 index 0000000..4fc7c69 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/OwnerUserAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 所属用户数据权限 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class OwnerUserAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SeedDataAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SeedDataAttribute.cs new file mode 100644 index 0000000..1463eff --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SeedDataAttribute.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 种子数据特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class SeedDataAttribute : Attribute +{ + /// + /// 排序(越大越后执行) + /// + public int Order { get; set; } = 0; + + public SeedDataAttribute(int orderNo) + { + Order = orderNo; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SysTableAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SysTableAttribute.cs new file mode 100644 index 0000000..f526b42 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/SysTableAttribute.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统表特性 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] +public class SysTableAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ThemeAttribute.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ThemeAttribute.cs new file mode 100644 index 0000000..48be9d9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Attribute/ThemeAttribute.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举拓展主题样式 +/// +[SuppressSniffer] +[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field)] +public class ThemeAttribute : Attribute +{ + public string Theme { get; private set; } + + public ThemeAttribute(string theme) + { + this.Theme = theme; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs new file mode 100644 index 0000000..5d55ecf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.Extensions.DependencyInjection.Extensions; +using NewLife.Caching.Services; + +namespace Admin.NET.Core; + +public static class CacheSetup +{ + /// + /// 缓存注册(新生命Redis组件) + /// + /// + public static void AddCache(this IServiceCollection services) + { + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType == CacheTypeEnum.Redis.ToString()) + { + var redis = new FullRedis(new RedisOptions + { + Configuration = cacheOptions.Redis.Configuration, + Prefix = cacheOptions.Redis.Prefix + }) + { + // 自动检测集群节点 + AutoDetect = App.GetConfig("Cache:Redis:AutoDetect", true) + }; + // 最大消息大小 + if (cacheOptions.Redis.MaxMessageSize > 0) + redis.MaxMessageSize = cacheOptions.Redis.MaxMessageSize; + + // 注入 Redis 缓存提供者 + services.AddSingleton(u => new RedisCacheProvider(u) { Cache = redis }); + } + + // 内存缓存兜底。在没有配置Redis时,使用内存缓存,逻辑代码无需修改 + services.TryAddSingleton(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs new file mode 100644 index 0000000..6bbcda6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar二级缓存 +/// +public class SqlSugarCache : ICacheService +{ + /// + /// 系统缓存服务 + /// + private static readonly SysCacheService _cache = App.GetRequiredService(); + + public void Add(string key, V value) + { + _cache.Set($"{CacheConst.SqlSugar}{key}", value); + } + + public void Add(string key, V value, int cacheDurationInSeconds) + { + _cache.Set($"{CacheConst.SqlSugar}{key}", value, TimeSpan.FromSeconds(cacheDurationInSeconds)); + } + + public bool ContainsKey(string key) + { + return _cache.ExistKey($"{CacheConst.SqlSugar}{key}"); + } + + public V Get(string key) + { + return _cache.Get($"{CacheConst.SqlSugar}{key}"); + } + + public IEnumerable GetAllKey() + { + return _cache.GetKeysByPrefixKey(CacheConst.SqlSugar); + } + + public V GetOrCreate(string key, Func create, int cacheDurationInSeconds = int.MaxValue) + { + return _cache.GetOrAdd($"{CacheConst.SqlSugar}{key}", (cacheKey) => + { + return create(); + }, cacheDurationInSeconds); + } + + public void Remove(string key) + { + _cache.Remove(key); // SqlSugar调用Remove方法时,key中已包含了CacheConst.SqlSugar前缀 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/AlipayConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/AlipayConst.cs new file mode 100644 index 0000000..d6c4dda --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/AlipayConst.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝支付常量 +/// +[SuppressSniffer] +public class AlipayConst +{ + /// + /// 单笔无密转账【业务场景】固定值 + /// + public const string BizScene = "DIRECT_TRANSFER"; + + /// + /// 单笔无密转账【销售产品码】固定值 + /// + public const string ProductCode = "TRANS_ACCOUNT_NO_PWD"; + + /// + /// 交易状态参数名 + /// + public const string TradeStatus = "trade_status"; + + /// + /// 交易成功标识 + /// + public const string TradeSuccess = "TRADE_SUCCESS"; + + /// + /// 授权类型 + /// + public const string GrantType = "authorization_code"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CacheConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CacheConst.cs new file mode 100644 index 0000000..3a0a3b9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CacheConst.cs @@ -0,0 +1,128 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存相关常量 +/// +public class CacheConst +{ + /// + /// 用户权限缓存(按钮集合) + /// + public const string KeyUserButton = "sys_user_button:"; + + /// + /// 用户机构缓存 + /// + public const string KeyUserOrg = "sys_user_org:"; + + /// + /// 角色最大数据范围缓存 + /// + public const string KeyRoleMaxDataScope = "sys_role_maxDataScope:"; + + /// + /// 在线用户缓存 + /// + public const string KeyUserOnline = "sys_user_online:"; + + /// + /// 图形验证码缓存 + /// + public const string KeyVerCode = "sys_verCode:"; + + /// + /// 手机验证码缓存 + /// + public const string KeyPhoneVerCode = "sys_phoneVerCode:"; + + /// + /// 密码错误次数缓存 + /// + public const string KeyPasswordErrorTimes = "sys_password_error_times:"; + + /// + /// 租户缓存 + /// + public const string KeyTenant = "sys_tenant"; + + /// + /// 常量下拉框 + /// + public const string KeyConst = "sys_const:"; + + /// + /// 所有缓存关键字集合 + /// + public const string KeyAll = "sys_keys"; + + /// + /// SqlSugar二级缓存 + /// + public const string SqlSugar = "sys_sqlSugar:"; + + /// + /// 开放接口身份缓存 + /// + public const string KeyOpenAccess = "sys_open_access:"; + + /// + /// 开放接口身份随机数缓存 + /// + public const string KeyOpenAccessNonce = "sys_open_access_nonce:"; + + /// + /// 登录黑名单 + /// + public const string KeyBlacklist = "sys_blacklist:"; + + /// + /// 系统配置缓存 + /// + public const string KeyConfig = "sys_config:"; + + /// + /// 系统租户配置缓存 + /// + public const string KeyTenantConfig = "sys_tenant_config:"; + + /// + /// 系统用户配置缓存 + /// + public const string KeyUserConfig = "sys_user_config:"; + + /// + /// 系统字典缓存 + /// + public const string KeyDict = "sys_dict:"; + + /// + /// 系统租户字典缓存 + /// + public const string KeyTenantDict = "sys_tenant_dict:"; + + /// + /// 重复请求(幂等)字典缓存 + /// + public const string KeyIdempotent = "sys_idempotent:"; + + /// + /// Excel临时文件缓存 + /// + public const string KeyExcelTemp = "sys_excel_temp:"; + + /// + /// 系统更新命令日志缓存 + /// + public const string KeySysUpdateLog = "sys_update_log"; + + /// + /// 系统更新间隔标记缓存 + /// + public const string KeySysUpdateInterval = "sys_update_interval"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ClaimConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ClaimConst.cs new file mode 100644 index 0000000..7b3218b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ClaimConst.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Claim相关常量 +/// +public class ClaimConst +{ + /// + /// 用户Id + /// + public const string UserId = "UserId"; + + /// + /// 账号 + /// + public const string Account = "Account"; + + /// + /// 真实姓名 + /// + public const string RealName = "RealName"; + + /// + /// 昵称 + /// + public const string NickName = "NickName"; + + /// + /// 账号类型 + /// + public const string AccountType = "AccountType"; + + /// + /// 租户Id + /// + public const string TenantId = "TenantId"; + + /// + /// 组织机构Id + /// + public const string OrgId = "OrgId"; + + /// + /// 组织机构名称 + /// + public const string OrgName = "OrgName"; + + /// + /// 组织机构类型 + /// + public const string OrgType = "OrgType"; + + /// + /// 微信OpenId + /// + public const string OpenId = "OpenId"; + + /// + /// 登录模式PC、APP + /// + public const string LoginMode = "LoginMode"; + + /// + /// 语言代码 + /// + public const string LangCode = "LangCode"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CommonConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CommonConst.cs new file mode 100644 index 0000000..dec1d9f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/CommonConst.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用常量 +/// +[Const("平台配置")] +public class CommonConst +{ + /// + /// 日志分组名称 + /// + public const string SysLogCategoryName = "System.Logging.LoggingMonitor"; + + /// + /// 事件-增加异常日志 + /// + public const string AddExLog = "Add:ExLog"; + + /// + /// 事件-发送异常邮件 + /// + public const string SendErrorMail = "Send:ErrorMail"; + + /// + /// 默认基本角色名称 + /// + public const string DefaultBaseRoleName = "默认基本角色"; + + /// + /// 默认基本角色编码 + /// + public const string DefaultBaseRoleCode = "default_base_role"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ConfigConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ConfigConst.cs new file mode 100644 index 0000000..bc35f05 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/ConfigConst.cs @@ -0,0 +1,148 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 配置常量 +/// +public class ConfigConst +{ + /// + /// 演示环境 + /// + public const string SysDemoEnv = "sys_demo"; + + /// + /// 默认密码 + /// + public const string SysPassword = "sys_password"; + + /// + /// 密码最大错误次数 + /// + public const string SysPasswordMaxErrorTimes = "sys_password_max_error_times"; + + /// + /// 日志保留天数 + /// + public const string SysLogRetentionDays = "sys_log_retention_days"; + + /// + /// 记录操作日志 + /// + public const string SysOpLog = "sys_oplog"; + + /// + /// 单设备登录 + /// + public const string SysSingleLogin = "sys_single_login"; + + /// + /// 登入登出提醒 + /// + public const string SysLoginOutReminder = "sys_login_out_reminder"; + + /// + /// 登陆时隐藏租户 + /// + public const string SysHideTenantLogin = "sys_hide_tenant_login"; + + /// + /// 登录二次验证 + /// + public const string SysSecondVer = "sys_second_ver"; + + /// + /// 图形验证码 + /// + public const string SysCaptcha = "sys_captcha"; + + /// + /// Token过期时间 + /// + public const string SysTokenExpire = "sys_token_expire"; + + /// + /// RefreshToken过期时间 + /// + public const string SysRefreshTokenExpire = "sys_refresh_token_expire"; + + /// + /// 发送异常日志邮件 + /// + public const string SysErrorMail = "sys_error_mail"; + + /// + /// 域登录验证 + /// + public const string SysDomainLogin = "sys_domain_login"; + + // /// + // /// 租户域名隔离登录验证 + // /// + // public const string SysTenantHostLogin = "sys_tenant_host_login"; + + /// + /// 数据校验日志 + /// + public const string SysValidationLog = "sys_validation_log"; + + /// + /// 行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级 + /// + public const string SysRegionSyncLevel = "sys_region_sync_level"; + + /// + /// Default 分组 + /// + public const string SysDefaultGroup = "Default"; + + /// + /// 支付宝授权页面地址 + /// + public const string AlipayAuthPageUrl = "alipay_auth_page_url_"; + + // /// + // /// 系统图标 + // /// + // public const string SysWebLogo = "sys_web_logo"; + // + // /// + // /// 系统主标题 + // /// + // public const string SysWebTitle = "sys_web_title"; + // + // /// + // /// 系统副标题 + // /// + // public const string SysWebViceTitle = "sys_web_viceTitle"; + // + // /// + // /// 系统描述 + // /// + // public const string SysWebViceDesc = "sys_web_viceDesc"; + // + // /// + // /// 水印内容 + // /// + // public const string SysWebWatermark = "sys_web_watermark"; + // + // /// + // /// 版权说明 + // /// + // public const string SysWebCopyright = "sys_web_copyright"; + // + // /// + // /// ICP备案号 + // /// + // public const string SysWebIcp = "sys_web_icp"; + // + // /// + // /// ICP地址 + // /// + // public const string SysWebIcpUrl = "sys_web_icpUrl"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs new file mode 100644 index 0000000..ddbcbd4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar相关常量 +/// +public class SqlSugarConst +{ + /// + /// 默认主数据库标识(默认租户) + /// + public const string MainConfigId = "1300000000001"; + + /// + /// 默认日志数据库标识 + /// + public const string LogConfigId = "1300000000002"; + + /// + /// 默认表主键 + /// + public const string PrimaryKey = "Id"; + + /// + /// 默认租户Id + /// + public const long DefaultTenantId = 1300000000001; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs new file mode 100644 index 0000000..6c25664 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientContainer.cs @@ -0,0 +1,47 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core; + +/// +/// ES客户端容器 +/// +public class ElasticSearchClientContainer +{ + private readonly Dictionary _clients; + + /// + /// 初始化容器(通过字典注入所有客户端) + /// + public ElasticSearchClientContainer(Dictionary clients) + { + _clients = clients ?? throw new ArgumentNullException(nameof(clients)); + } + + /// + /// 日志专用客户端 + /// + public ElasticsearchClient Logging => GetClient(EsClientTypeEnum.Logging); + + /// + /// 业务数据同步客户端 + /// + public ElasticsearchClient Business => GetClient(EsClientTypeEnum.Business); + + /// + /// 根据类型获取客户端(内部校验,避免未注册的类型) + /// + private ElasticsearchClient GetClient(EsClientTypeEnum type) + { + if (_clients.TryGetValue(type, out var client)) + { + return client; + } + throw new KeyNotFoundException($"未注册的ES客户端类型:{type},请检查注册配置"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs new file mode 100644 index 0000000..95592b4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchClientFactory.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; +using Elastic.Transport; + +namespace Admin.NET.Core; + +public class ElasticSearchClientFactory +{ + /// + /// 创建 ES 客户端(通用方法) + /// + /// 配置类型(支持通用或场景专用) + /// 配置文件路径(如 "ElasticSearch:Logging") + /// ES 客户端实例(或 null if 未启用) + public static ElasticsearchClient? CreateClient(string configPath) where TOptions : ElasticSearchOptions, new() + { + // 从配置文件读取当前场景的配置 + var options = App.GetConfig(configPath); + if (options == null) + throw Oops.Oh($"未找到{configPath}配置项"); + + if (!options.Enabled) + return null; + + // 验证服务地址 + if (options.ServerUris == null || !options.ServerUris.Any()) + throw new ArgumentException($"ES 配置 {configPath} 未设置 ServerUris"); + + // 构建连接池(支持集群) + var uris = options.ServerUris.Select(uri => new Uri(uri)).ToList(); + var connectionPool = new StaticNodePool(uris); + var connectionSettings = new ElasticsearchClientSettings(connectionPool) + .DefaultIndex(options.DefaultIndex) // 设置默认索引 + .DisableDirectStreaming() // 开启请求/响应日志,方便排查问题 + .OnRequestCompleted(response => + { + if (response.HttpStatusCode == 401) + { + Console.WriteLine("ES 请求被拒绝:未提供有效认证信息"); + } + }); + + // 配置认证 + ConfigureAuthentication(connectionSettings, options); + + // 配置 HTTPS 证书指纹 + if (!string.IsNullOrEmpty(options.Fingerprint)) + connectionSettings.CertificateFingerprint(options.Fingerprint); + + return new ElasticsearchClient(connectionSettings); + } + + /// + /// 配置认证(通用逻辑) + /// + private static void ConfigureAuthentication(ElasticsearchClientSettings settings, ElasticSearchOptions options) + { + switch (options.AuthType) + { + case ElasticSearchAuthTypeEnum.Basic: + settings.Authentication(new BasicAuthentication(options.User, options.Password)); + break; + + case ElasticSearchAuthTypeEnum.ApiKey: + settings.Authentication(new ApiKey(options.ApiKey)); + break; + + case ElasticSearchAuthTypeEnum.Base64ApiKey: + settings.Authentication(new Base64ApiKey(options.Base64ApiKey)); + break; + + case ElasticSearchAuthTypeEnum.None: + // 无需认证 + break; + + default: + throw new ArgumentOutOfRangeException(nameof(options.AuthType)); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs new file mode 100644 index 0000000..8990d11 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/ElasticSearch/ElasticSearchSetup.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core.ElasticSearch; + +/// +/// ES服务注册 +/// +public static class ElasticSearchSetup +{ + /// + /// 注册所有ES客户端(日志+业务) + /// + public static void AddElasticSearchClients(this IServiceCollection services) + { + // 1. 创建客户端字典(枚举→客户端实例) + var clients = new Dictionary(); + + // 2. 注册日志客户端 + var loggingClient = ElasticSearchClientFactory.CreateClient(configPath: "ElasticSearch:Logging"); + if (loggingClient != null) + { + clients[EsClientTypeEnum.Logging] = loggingClient; + } + + // 3. 注册业务客户端 + var businessClient = ElasticSearchClientFactory.CreateClient(configPath: "ElasticSearch:Business"); + if (businessClient != null) + { + clients[EsClientTypeEnum.Business] = businessClient; + } + + // 4. 将客户端容器注册为单例(全局唯一) + services.AddSingleton(new ElasticSearchClientContainer(clients)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/EntityBase.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/EntityBase.cs new file mode 100644 index 0000000..c334a2e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/EntityBase.cs @@ -0,0 +1,203 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 框架实体基类Id +/// +public abstract class EntityBaseId +{ + /// + /// 雪花Id + /// + [SugarColumn(ColumnName = "Id", ColumnDescription = "主键Id", IsPrimaryKey = true, IsIdentity = false)] + public virtual long Id { get; set; } +} + +/// +/// 框架实体基类 +/// +[SugarIndex("index_{table}_CT", nameof(CreateTime), OrderByType.Asc)] +public abstract class EntityBase : EntityBaseId +{ + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)] + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public virtual DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + [OwnerUser] + [SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)] + public virtual long? CreateUserId { get; set; } + + ///// + ///// 创建者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateUserId))] + //public virtual SysUser CreateUser { get; set; } + + /// + /// 创建者姓名 + /// + [SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)] + public virtual string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + [SugarColumn(ColumnDescription = "修改者Id")] + public virtual long? UpdateUserId { get; set; } + + ///// + ///// 修改者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(UpdateUserId))] + //public virtual SysUser UpdateUser { get; set; } + + /// + /// 修改者姓名 + /// + [SugarColumn(ColumnDescription = "修改者姓名", Length = 64)] + public virtual string? UpdateUserName { get; set; } +} + +/// +/// 框架实体基类(删除标志) +/// +[SugarIndex("index_{table}_D", nameof(IsDelete), OrderByType.Asc)] +[SugarIndex("index_{table}_DT", nameof(DeleteTime), OrderByType.Asc)] +public abstract class EntityBaseDel : EntityBase, IDeletedFilter +{ + /// + /// 软删除 + /// + [SugarColumn(ColumnDescription = "软删除")] + public virtual bool IsDelete { get; set; } = false; + + /// + /// 软删除时间 + /// + [SugarColumn(ColumnDescription = "软删除时间")] + public virtual DateTime? DeleteTime { get; set; } +} + +/// +/// 机构实体基类(数据权限) +/// +public abstract class EntityBaseOrg : EntityBase, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } + + ///// + ///// 创建者部门Id + ///// + //[SugarColumn(ColumnDescription = "创建者部门Id", IsOnlyIgnoreUpdate = true)] + //public virtual long? CreateOrgId { get; set; } + + ///// + ///// 创建者部门 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateOrgId))] + //public virtual SysOrg CreateOrg { get; set; } + + ///// + ///// 创建者部门名称 + ///// + //[SugarColumn(ColumnDescription = "创建者部门名称", Length = 64, IsOnlyIgnoreUpdate = true)] + //public virtual string? CreateOrgName { get; set; } +} + +/// +/// 机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseOrgDel : EntityBaseDel, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } +} + +/// +/// 租户实体基类 +/// +public abstract class EntityBaseTenant : EntityBase, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类(删除标志) +/// +public abstract class EntityBaseTenantDel : EntityBaseDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类Id +/// +public abstract class EntityBaseTenantId : EntityBaseId, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限) +/// +public abstract class EntityBaseTenantOrg : EntityBaseOrg, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseTenantOrgDel : EntityBaseOrgDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/IEntityFilter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/IEntityFilter.cs new file mode 100644 index 0000000..ec4f57b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/IEntityFilter.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 假删除接口过滤器 +/// +public interface IDeletedFilter +{ + /// + /// 软删除 + /// + bool IsDelete { get; set; } +} + +/// +/// 租户Id接口过滤器 +/// +public interface ITenantIdFilter +{ + /// + /// 租户Id + /// + long? TenantId { get; set; } +} + +/// +/// 机构Id接口过滤器 +/// +public interface IOrgIdFilter +{ + /// + /// 机构Id + /// + long OrgId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs new file mode 100644 index 0000000..8842546 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayAuthInfo.cs @@ -0,0 +1,149 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝授权记录表 +/// +[SugarTable(null, "支付宝授权记录表")] +[SysTable] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(OpenId), OrderByType.Asc)] +public class SysAlipayAuthInfo : EntityBase +{ + /// + /// 商户AppId + /// + [SugarColumn(ColumnDescription = "商户AppId", Length = 64)] + public string? AppId { get; set; } + + /// + /// 开放ID + /// + [SugarColumn(ColumnDescription = "开放ID", Length = 64)] + public string? OpenId { get; set; } + + /// + /// 用户ID + /// + [SugarColumn(ColumnDescription = "用户ID", Length = 64)] + public string? UserId { get; set; } + + /// + /// 性别 + /// + [SugarColumn(ColumnDescription = "性别", Length = 8)] + public GenderEnum Gender { get; set; } + + /// + /// 年龄 + /// + [SugarColumn(ColumnDescription = "年龄", Length = 16)] + public int Age { get; set; } + + /// + /// 手机号 + /// + [SugarColumn(ColumnDescription = "手机号", Length = 32)] + public string Mobile { get; set; } + + /// + /// 显示名称 + /// + [SugarColumn(ColumnDescription = "显示名称", Length = 128)] + public string DisplayName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 64)] + public string NickName { get; set; } + + /// + /// 用户名 + /// + [SugarColumn(ColumnDescription = "用户名", Length = 64)] + public string UserName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 512)] + public string? Avatar { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "邮箱", Length = 128)] + public string? Email { get; set; } + + /// + /// 用户民族 + /// + [SugarColumn(ColumnDescription = "用户民族", Length = 32)] + public string? UserNation { get; set; } + + /// + /// 淘宝ID + /// + [SugarColumn(ColumnDescription = "淘宝ID", Length = 64)] + public string? TaobaoId { get; set; } + + /// + /// 电话 + /// + [SugarColumn(ColumnDescription = "电话", Length = 32)] + public string? Phone { get; set; } + + /// + /// 生日 + /// + [SugarColumn(ColumnDescription = "生日", Length = 32)] + public string? PersonBirthday { get; set; } + + /// + /// 职业 + /// + [SugarColumn(ColumnDescription = "职业", Length = 64)] + public string? Profession { get; set; } + + /// + /// 省份 + /// + [SugarColumn(ColumnDescription = "省份", Length = 64)] + public string? Province { get; set; } + + /// + /// 用户状态 + /// + [SugarColumn(ColumnDescription = "用户状态", Length = 32)] + public string? UserStatus { get; set; } + + /// + /// 学历 + /// + [SugarColumn(ColumnDescription = "学历", Length = 32)] + public string? Degree { get; set; } + + /// + /// 用户类型 + /// + [SugarColumn(ColumnDescription = "用户类型", Length = 32)] + public string? UserType { get; set; } + + /// + /// 邮编 + /// + [SugarColumn(ColumnDescription = "邮编", Length = 16)] + public string? Zip { get; set; } + + /// + /// 地址 + /// + [SugarColumn(ColumnDescription = "地址", Length = 256)] + public string? Address { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayTransaction.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayTransaction.cs new file mode 100644 index 0000000..5913471 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysAlipayTransaction.cs @@ -0,0 +1,108 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付宝交易记录表 +/// +[SugarTable(null, "支付宝交易记录表")] +[SysTable] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(TradeNo), OrderByType.Asc)] +[SugarIndex("index_{table}_O", nameof(OutTradeNo), OrderByType.Asc)] +public class SysAlipayTransaction : EntityBase +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id", Length = 64)] + public long UserId { get; set; } + + /// + /// 交易号 + /// + [SugarColumn(ColumnDescription = "交易号", Length = 64)] + public string? TradeNo { get; set; } + + /// + /// 商户订单号 + /// + [SugarColumn(ColumnDescription = "商户订单号", Length = 64)] + public string OutTradeNo { get; set; } + + /// + /// 交易金额 + /// + [SugarColumn(ColumnDescription = "交易金额", Length = 20)] + public decimal TotalAmount { get; set; } + + /// + /// 交易状态 + /// + [SugarColumn(ColumnDescription = "交易状态", Length = 32)] + public string TradeStatus { get; set; } + + /// + /// 交易完成时间 + /// + [SugarColumn(ColumnDescription = "交易完成时间")] + public DateTime? FinishTime { get; set; } + + /// + /// 交易标题 + /// + [SugarColumn(ColumnDescription = "交易标题", Length = 256)] + public string Subject { get; set; } + + /// + /// 交易描述 + /// + [SugarColumn(ColumnDescription = "交易描述", Length = 512)] + public string? Body { get; set; } + + /// + /// 买家支付宝账号 + /// + [SugarColumn(ColumnDescription = "买家支付宝账号", Length = 128)] + public string? BuyerLogonId { get; set; } + + /// + /// 买家支付宝用户ID + /// + [SugarColumn(ColumnDescription = "买家支付宝用户ID", Length = 32)] + public string? BuyerUserId { get; set; } + + /// + /// 卖家支付宝用户ID + /// + [SugarColumn(ColumnDescription = "卖家支付宝用户ID", Length = 32)] + public string? SellerUserId { get; set; } + + /// + /// 商户AppId + /// + [SugarColumn(ColumnDescription = "商户AppId", Length = 64)] + public string? AppId { get; set; } + + /// + /// 交易扩展信息 + /// + [SugarColumn(ColumnDescription = "交易扩展信息", Length = 1024)] + public string? ExtendInfo { get; set; } + + /// + /// 交易异常信息 + /// + [SugarColumn(ColumnDescription = "交易扩展信息", Length = 1024)] + public string? ErrorInfo { get; set; } + + /// + /// 交易备注 + /// + [SugarColumn(ColumnDescription = "交易备注", Length = 512)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGen.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGen.cs new file mode 100644 index 0000000..c6d80a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGen.cs @@ -0,0 +1,159 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成表 +/// +[SugarTable(null, "代码生成表")] +[SysTable] +[SugarIndex("index_{table}_B", nameof(BusName), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(TableName), OrderByType.Asc)] +public partial class SysCodeGen : EntityBase +{ + /// + /// 作者姓名 + /// + [SugarColumn(ColumnDescription = "作者姓名", Length = 32)] + [MaxLength(32)] + public string? AuthorName { get; set; } + + /// + /// 是否移除表前缀 + /// + [SugarColumn(ColumnDescription = "是否移除表前缀", Length = 8)] + [MaxLength(8)] + public string? TablePrefix { get; set; } + + /// + /// 生成方式 + /// + [SugarColumn(ColumnDescription = "生成方式", Length = 32)] + [MaxLength(32)] + public string? GenerateType { get; set; } + + /// + /// 库定位器名 + /// + [SugarColumn(ColumnDescription = "库定位器名", Length = 64)] + [MaxLength(64)] + public string? ConfigId { get; set; } + + /// + /// 库名 + /// + [SugarColumn(IsIgnore = true)] + public string DbNickName + { + get + { + try + { + var dbOptions = App.GetConfig("DbConnection", true); + var config = dbOptions.ConnectionConfigs.FirstOrDefault(m => m.ConfigId.ToString() == ConfigId); + return config.DbNickName; + } + catch (Exception) + { + return null; + } + } + } + + /// + /// 数据库名(保留字段) + /// + [SugarColumn(ColumnDescription = "数据库库名", Length = 64)] + [MaxLength(64)] + public string? DbName { get; set; } + + /// + /// 数据库类型 + /// + [SugarColumn(ColumnDescription = "数据库类型", Length = 64)] + [MaxLength(64)] + public string? DbType { get; set; } + + /// + /// 数据库链接 + /// + [SugarColumn(ColumnDescription = "数据库链接", Length = 256)] + [MaxLength(256)] + public string? ConnectionString { get; set; } + + /// + /// 数据库表名 + /// + [SugarColumn(ColumnDescription = "数据库表名", Length = 128)] + [MaxLength(128)] + public string? TableName { get; set; } + + /// + /// 命名空间 + /// + [SugarColumn(ColumnDescription = "命名空间", Length = 128)] + [MaxLength(128)] + public string? NameSpace { get; set; } + + /// + /// 业务名 + /// + [SugarColumn(ColumnDescription = "业务名", Length = 128)] + [MaxLength(128)] + public string? BusName { get; set; } + + /// + /// 表唯一字段配置 + /// + [SugarColumn(ColumnDescription = "表唯一字段配置", Length = 512)] + [MaxLength(128)] + public string? TableUniqueConfig { get; set; } + + /// + /// 是否生成菜单 + /// + [SugarColumn(ColumnDescription = "是否生成菜单")] + public bool GenerateMenu { get; set; } = true; + + /// + /// 菜单图标 + /// + [SugarColumn(ColumnDescription = "菜单图标", Length = 32)] + public string? MenuIcon { get; set; } = "ele-Menu"; + + /// + /// 菜单编码 + /// + [SugarColumn(ColumnDescription = "菜单编码")] + public long? MenuPid { get; set; } + + /// + /// 页面目录 + /// + [SugarColumn(ColumnDescription = "页面目录", Length = 32)] + public string? PagePath { get; set; } + + /// + /// 支持打印类型 + /// + [SugarColumn(ColumnDescription = "支持打印类型", Length = 32)] + [MaxLength(32)] + public string? PrintType { get; set; } + + /// + /// 打印模版名称 + /// + [SugarColumn(ColumnDescription = "打印模版名称", Length = 32)] + [MaxLength(32)] + public string? PrintName { get; set; } + + /// + /// 表唯一字段列表 + /// + [SugarColumn(IsIgnore = true)] + public virtual List TableUniqueList => string.IsNullOrWhiteSpace(TableUniqueConfig) ? null : JSON.Deserialize>(TableUniqueConfig); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGenConfig.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGenConfig.cs new file mode 100644 index 0000000..ff7697e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysCodeGenConfig.cs @@ -0,0 +1,207 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成字段配置表 +/// +[SugarTable(null, "代码生成字段配置表")] +[SysTable] +public partial class SysCodeGenConfig : EntityBase +{ + /// + /// 代码生成主表Id + /// + [SugarColumn(ColumnDescription = "主表Id")] + public long CodeGenId { get; set; } + + /// + /// 数据库字段名 + /// + [SugarColumn(ColumnDescription = "字段名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string ColumnName { get; set; } + + /// + /// 主键 + /// + [SugarColumn(ColumnDescription = "主键", Length = 8)] + [MaxLength(8)] + public string? ColumnKey { get; set; } + + /// + /// 实体属性名 + /// + [SugarColumn(ColumnDescription = "属性名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + [SugarColumn(ColumnDescription = "字段数据长度", DefaultValue = "0")] + public int ColumnLength { get; set; } + + /// + /// 字段描述 + /// + [SugarColumn(ColumnDescription = "字段描述", Length = 128)] + [MaxLength(128)] + public string? ColumnComment { get; set; } + + /// + /// 数据库中类型(物理类型) + /// + [SugarColumn(ColumnDescription = "数据库中类型", Length = 64)] + [MaxLength(64)] + public string? DataType { get; set; } + + /// + /// .NET数据类型 + /// + [SugarColumn(ColumnDescription = "NET数据类型", Length = 64)] + [MaxLength(64)] + public string? NetType { get; set; } + + /// + /// 字段数据默认值 + /// + [SugarColumn(ColumnDescription = "默认值")] + public string? DefaultValue { get; set; } + + /// + /// 作用类型(字典) + /// + [SugarColumn(ColumnDescription = "作用类型", Length = 64)] + [MaxLength(64)] + public string? EffectType { get; set; } + + /// + /// 外键库标识 + /// + [SugarColumn(ColumnDescription = "外键库标识", Length = 20)] + [MaxLength(20)] + public string? FkConfigId { get; set; } + + /// + /// 外键实体名称 + /// + [SugarColumn(ColumnDescription = "外键实体名称", Length = 64)] + [MaxLength(64)] + public string? FkEntityName { get; set; } + + /// + /// 外键表名称 + /// + [SugarColumn(ColumnDescription = "外键表名称", Length = 128)] + [MaxLength(128)] + public string? FkTableName { get; set; } + + /// + /// 外键显示字段 + /// + [SugarColumn(ColumnDescription = "外键显示字段", Length = 64)] + [MaxLength(64)] + public string? FkDisplayColumns { get; set; } + + /// + /// 外键链接字段 + /// + [SugarColumn(ColumnDescription = "外键链接字段", Length = 64)] + [MaxLength(64)] + public string? FkLinkColumnName { get; set; } + + /// + /// 外键显示字段.NET类型 + /// + [SugarColumn(ColumnDescription = "外键显示字段.NET类型", Length = 64)] + [MaxLength(64)] + public string? FkColumnNetType { get; set; } + + /// + /// 父级字段 + /// + [SugarColumn(ColumnDescription = "父级字段", Length = 128)] + [MaxLength(128)] + public string? PidColumn { get; set; } + + /// + /// 字典编码 + /// + [SugarColumn(ColumnDescription = "字典编码", Length = 64)] + [MaxLength(64)] + public string? DictTypeCode { get; set; } + + /// + /// 查询方式 + /// + [SugarColumn(ColumnDescription = "查询方式", Length = 16)] + [MaxLength(16)] + public string? QueryType { get; set; } + + /// + /// 是否是查询条件 + /// + [SugarColumn(ColumnDescription = "是否是查询条件", Length = 8)] + [MaxLength(8)] + public string? WhetherQuery { get; set; } + + /// + /// 列表是否缩进(字典) + /// + [SugarColumn(ColumnDescription = "列表是否缩进", Length = 8)] + [MaxLength(8)] + public string? WhetherRetract { get; set; } + + /// + /// 是否必填(字典) + /// + [SugarColumn(ColumnDescription = "是否必填", Length = 8)] + [MaxLength(8)] + public string? WhetherRequired { get; set; } + + /// + /// 是否可排序(字典) + /// + [SugarColumn(ColumnDescription = "是否可排序", Length = 8)] + [MaxLength(8)] + public string? WhetherSortable { get; set; } + + /// + /// 列表显示 + /// + [SugarColumn(ColumnDescription = "列表显示", Length = 8)] + [MaxLength(8)] + public string? WhetherTable { get; set; } + + /// + /// 增改 + /// + [SugarColumn(ColumnDescription = "增改", Length = 8)] + [MaxLength(8)] + public string? WhetherAddUpdate { get; set; } + + /// + /// 导入 + /// + [SugarColumn(ColumnDescription = "导入", Length = 8)] + [MaxLength(8)] + public string? WhetherImport { get; set; } + + /// + /// 是否通用字段 + /// + [SugarColumn(ColumnDescription = "是否通用字段", Length = 8)] + [MaxLength(8)] + public string? WhetherCommon { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysConfig.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysConfig.cs new file mode 100644 index 0000000..d5afeae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysConfig.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统配置参数表 +/// +[SugarTable(null, "系统配置参数表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +public partial class SysConfig : EntityBase +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + [IgnoreUpdateSeedColumn] + public string? Value { get; set; } + + /// + /// 是否是内置参数(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")] + public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 分组编码 + /// + [SugarColumn(ColumnDescription = "分组编码", Length = 64)] + [MaxLength(64)] + public string? GroupCode { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictData.cs new file mode 100644 index 0000000..735af86 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictData.cs @@ -0,0 +1,105 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典值表 +/// +[SugarTable(null, "系统字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc, IsUnique = true)] +public partial class SysDictData : EntityBase +{ + /// + /// 字典类型Id + /// + [SugarColumn(ColumnDescription = "字典类型Id")] + public long DictTypeId { get; set; } + + /// + /// 字典类型 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DictTypeId))] + public SysDictType DictType { get; set; } + + /// + /// 显示文本 + /// + [SugarColumn(ColumnDescription = "显示文本", Length = 256)] + [Required, MaxLength(256)] + public virtual string Label { get; set; } + + /// + /// 值 + /// + [SugarColumn(ColumnDescription = "值", Length = 256)] + [Required, MaxLength(256)] + public virtual string Value { get; set; } + + /// + /// 编码 + /// + /// + /// + [SugarColumn(ColumnDescription = "编码", Length = 256)] + public virtual string? Code { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 256)] + [MaxLength(256)] + public virtual string? Name { get; set; } + + /// + /// 显示样式-标签颜色 + /// + [SugarColumn(ColumnDescription = "显示样式-标签颜色", Length = 16)] + [MaxLength(16)] + public string? TagType { get; set; } + + /// + /// 显示样式-Style(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Style", Length = 512)] + [MaxLength(512)] + public string? StyleSetting { get; set; } + + /// + /// 显示样式-Class(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Class", Length = 512)] + [MaxLength(512)] + public string? ClassSetting { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 2048)] + [MaxLength(2048)] + public string? Remark { get; set; } + + /// + /// 拓展数据(保存业务功能的配置项) + /// + [SugarColumn(ColumnDescription = "拓展数据(保存业务功能的配置项)", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ExtData { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictDataTenant.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictDataTenant.cs new file mode 100644 index 0000000..d14eac7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictDataTenant.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户字典值表 +/// +[SugarTable(null, "系统租户字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc)] +public partial class SysDictDataTenant : SysDictData, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictType.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictType.cs new file mode 100644 index 0000000..b9d0f9f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysDictType.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典类型表 +/// +[SugarTable(null, "系统字典类型表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysDictType : EntityBase +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [Required, MaxLength(64)] + public virtual string Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 是否是内置字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置字典", DefaultValue = "1")] + public virtual YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 是否是租户字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是租户字典", DefaultValue = "2")] + public virtual YesNoEnum IsTenant { get; set; } = YesNoEnum.N; + + /// + /// 字典值集合 + /// + [Navigate(NavigateType.OneToMany, nameof(SysDictData.DictTypeId))] + public List Children { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFile.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFile.cs new file mode 100644 index 0000000..27149d5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFile.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统文件表 +/// +[SugarTable(null, "系统文件表")] +[SysTable] +[SugarIndex("index_{table}_F", nameof(FileName), OrderByType.Asc)] +public partial class SysFile : EntityBaseTenantOrg +{ + /// + /// 提供者 + /// + [SugarColumn(ColumnDescription = "提供者", Length = 128)] + [MaxLength(128)] + public string? Provider { get; set; } + + /// + /// 仓储名称 + /// + [SugarColumn(ColumnDescription = "仓储名称", Length = 128)] + [MaxLength(128)] + public string? BucketName { get; set; } + + /// + /// 文件名称(源文件名) + /// + [SugarColumn(ColumnDescription = "文件名称", Length = 128)] + [MaxLength(128)] + public string? FileName { get; set; } + + /// + /// 文件后缀 + /// + [SugarColumn(ColumnDescription = "文件后缀", Length = 16)] + [MaxLength(16)] + public string? Suffix { get; set; } + + /// + /// 存储路径 + /// + [SugarColumn(ColumnDescription = "存储路径", Length = 512)] + [MaxLength(512)] + public string? FilePath { get; set; } + + /// + /// 文件大小KB + /// + [SugarColumn(ColumnDescription = "文件大小KB")] + public long SizeKb { get; set; } + + /// + /// 文件大小信息-计算后的 + /// + [SugarColumn(ColumnDescription = "文件大小信息", Length = 64)] + [MaxLength(64)] + public string? SizeInfo { get; set; } + + /// + /// 外链地址-OSS上传后生成外链地址方便前端预览 + /// + [SugarColumn(ColumnDescription = "外链地址", Length = 512)] + [MaxLength(512)] + public string? Url { get; set; } + + /// + /// 文件MD5 + /// + [SugarColumn(ColumnDescription = "文件MD5", Length = 128)] + [MaxLength(128)] + public string? FileMd5 { get; set; } + + /// + /// 文件类别 + /// + [SugarColumn(ColumnDescription = "文件类别", Length = 128)] + [MaxLength(128)] + public virtual string? FileType { get; set; } + + /// + /// 文件别名 + /// + [SugarColumn(ColumnDescription = "文件别名", Length = 128)] + [MaxLength(128)] + public string? FileAlias { get; set; } + + /// + /// 是否公开 + /// + [SugarColumn(ColumnDescription = "是否公开")] + public virtual bool IsPublic { get; set; } = false; + + /// + /// 业务数据Id + /// + [SugarColumn(ColumnDescription = "业务数据Id")] + public long? DataId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFileProvider.cs new file mode 100644 index 0000000..6fd9b54 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysFileProvider.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 系统文件存储提供者表 +/// +[SugarTable(null, "系统文件存储提供者表")] +[SysTable] +[SugarIndex("index_{table}_BucketName", nameof(BucketName), OrderByType.Asc)] +[SugarIndex("index_{table}_IsEnable", nameof(IsEnable), OrderByType.Desc)] +[SugarIndex("index_{table}_IsDefault", nameof(IsDefault), OrderByType.Desc)] +public partial class SysFileProvider : EntityBaseTenant +{ + /// + /// 存储提供者(Minio, QCloud,Aliyun 等等) + /// + [SugarColumn(ColumnDescription = "存储提供者", Length = 16)] + [Required, MaxLength(16)] + public virtual string Provider { get; set; } + + /// + /// 存储桶名称 + /// + [SugarColumn(ColumnDescription = "存储桶名称", Length = 32)] + [Required, MaxLength(32)] + public virtual string BucketName { get; set; } + + /// + /// 访问密钥 (填入 阿里云(Aliyun)/Minio:的 AccessKey,腾讯云(QCloud): 的 SecretId) + /// + [SugarColumn(ColumnDescription = "访问密钥", Length = 128)] + [MaxLength(128)] + public virtual string? AccessKey { get; set; } + + /// + /// 密钥 + /// + [SugarColumn(ColumnDescription = "密钥", Length = 128)] + [MaxLength(128)] + public virtual string? SecretKey { get; set; } + + /// + /// 地域 + /// + [SugarColumn(ColumnDescription = "地域", Length = 64)] + [MaxLength(64)] + public virtual string? Region { get; set; } + + /// + /// 端点地址(填入 阿里云(Aliyun)/Minio:的 endpoint/Api address,腾讯云(QCloud): 的 AppId) + /// + [SugarColumn(ColumnDescription = "端点地址", Length = 256)] + [MaxLength(256)] + public virtual string? Endpoint { get; set; } + + /// + /// 是否启用HTTPS + /// + [SugarColumn(ColumnDescription = "是否启用HTTPS")] + public virtual bool? IsEnableHttps { get; set; } = true; + + /// + /// 是否启用缓存 + /// + [SugarColumn(ColumnDescription = "是否启用缓存")] + public virtual bool? IsEnableCache { get; set; } = true; + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public virtual bool? IsEnable { get; set; } = true; + + /// + /// 是否默认提供者 + /// + [SugarColumn(ColumnDescription = "是否默认提供者")] + public virtual bool? IsDefault { get; set; } = false; + + /// + /// 自定义域名 + /// + [SugarColumn(ColumnDescription = "自定义域名", Length = 256)] + [MaxLength(256)] + public virtual string? SinceDomain { get; set; } + + /// + /// 排序号 + /// + [SugarColumn(ColumnDescription = "排序号")] + public virtual int? OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 512)] + [MaxLength(512)] + public virtual string? Remark { get; set; } + + /// + /// 获取显示名称 + /// + [SugarColumn(IsIgnore = true)] + public virtual string DisplayName => $"{Provider}-{BucketName}"; + + /// + /// 获取配置键名 + /// + [SugarColumn(IsIgnore = true)] + public virtual string ConfigKey => $"{Provider}_{BucketName}_{Id}"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobCluster.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobCluster.cs new file mode 100644 index 0000000..6687cf6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobCluster.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业集群表 +/// +[SugarTable(null, "系统作业集群表")] +[SysTable] +public partial class SysJobCluster : EntityBaseId +{ + /// + /// 作业集群Id + /// + [SugarColumn(ColumnDescription = "作业集群Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string ClusterId { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public ClusterStatus Status { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobDetail.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobDetail.cs new file mode 100644 index 0000000..3c2868f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobDetail.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业信息表 +/// +[SugarTable(null, "系统作业信息表")] +[SysTable] +[SugarIndex("index_{table}_J", nameof(JobId), OrderByType.Asc)] +public partial class SysJobDetail : EntityBaseId +{ + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 组名称 + /// + [SugarColumn(ColumnDescription = "组名称", Length = 128)] + [MaxLength(128)] + public string? GroupName { get; set; } = "default"; + + /// + /// 作业类型FullName + /// + [SugarColumn(ColumnDescription = "作业类型", Length = 128)] + [MaxLength(128)] + public string? JobType { get; set; } + + /// + /// 程序集Name + /// + [SugarColumn(ColumnDescription = "程序集", Length = 128)] + [MaxLength(128)] + public string? AssemblyName { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 是否并行执行 + /// + [SugarColumn(ColumnDescription = "是否并行执行")] + public bool Concurrent { get; set; } = true; + + /// + /// 是否扫描特性触发器 + /// + [SugarColumn(ColumnDescription = "是否扫描特性触发器", ColumnName = "annotation")] + public bool IncludeAnnotation { get; set; } = false; + + /// + /// 额外数据 + /// + [SugarColumn(ColumnDescription = "额外数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Properties { get; set; } = "{}"; + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } + + /// + /// 作业创建类型 + /// + [SugarColumn(ColumnDescription = "作业创建类型")] + public JobCreateTypeEnum CreateType { get; set; } = JobCreateTypeEnum.BuiltIn; + + /// + /// 脚本代码 + /// + [SugarColumn(ColumnDescription = "脚本代码", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ScriptCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTrigger.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTrigger.cs new file mode 100644 index 0000000..dc6d5c8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTrigger.cs @@ -0,0 +1,147 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业触发器表 +/// +[SugarTable(null, "系统作业触发器表")] +[SysTable] +public partial class SysJobTrigger : EntityBaseId +{ + /// + /// 触发器Id + /// + [SugarColumn(ColumnDescription = "触发器Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string TriggerId { get; set; } + + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 触发器类型FullName + /// + [SugarColumn(ColumnDescription = "触发器类型", Length = 128)] + [MaxLength(128)] + public string? TriggerType { get; set; } + + /// + /// 程序集Name + /// + [SugarColumn(ColumnDescription = "程序集", Length = 128)] + [MaxLength(128)] + public string? AssemblyName { get; set; } = "Furion.Pure"; + + /// + /// 参数 + /// + [SugarColumn(ColumnDescription = "参数", Length = 128)] + [MaxLength(128)] + public string? Args { get; set; } + + /// + /// 描述信息 + /// + [SugarColumn(ColumnDescription = "描述信息", Length = 128)] + [MaxLength(128)] + public string? Description { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 起始时间 + /// + [SugarColumn(ColumnDescription = "起始时间")] + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间")] + public DateTime? EndTime { get; set; } + + /// + /// 最近运行时间 + /// + [SugarColumn(ColumnDescription = "最近运行时间")] + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + [SugarColumn(ColumnDescription = "下一次运行时间")] + public DateTime? NextRunTime { get; set; } + + /// + /// 触发次数 + /// + [SugarColumn(ColumnDescription = "触发次数")] + public long NumberOfRuns { get; set; } + + /// + /// 最大触发次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大触发次数")] + public long MaxNumberOfRuns { get; set; } + + /// + /// 出错次数 + /// + [SugarColumn(ColumnDescription = "出错次数")] + public long NumberOfErrors { get; set; } + + /// + /// 最大出错次数(0:不限制,n:N次) + /// + [SugarColumn(ColumnDescription = "最大出错次数")] + public long MaxNumberOfErrors { get; set; } + + /// + /// 重试次数 + /// + [SugarColumn(ColumnDescription = "重试次数")] + public int NumRetries { get; set; } + + /// + /// 重试间隔时间(ms) + /// + [SugarColumn(ColumnDescription = "重试间隔时间(ms)")] + public int RetryTimeout { get; set; } = 1000; + + /// + /// 是否立即启动 + /// + [SugarColumn(ColumnDescription = "是否立即启动")] + public bool StartNow { get; set; } = true; + + /// + /// 是否启动时执行一次 + /// + [SugarColumn(ColumnDescription = "是否启动时执行一次")] + public bool RunOnStart { get; set; } = false; + + /// + /// 是否在启动时重置最大触发次数等于一次的作业 + /// + [SugarColumn(ColumnDescription = "是否重置触发次数")] + public bool ResetOnlyOnce { get; set; } = true; + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public DateTime? UpdatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTriggerRecord.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTriggerRecord.cs new file mode 100644 index 0000000..080c5cc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysJobTriggerRecord.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统作业触发器运行记录表 +/// +[SugarTable(null, "系统作业触发器运行记录表")] +[SysTable] +public partial class SysJobTriggerRecord : EntityBaseId +{ + /// + /// 作业Id + /// + [SugarColumn(ColumnDescription = "作业Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string JobId { get; set; } + + /// + /// 触发器Id + /// + [SugarColumn(ColumnDescription = "触发器Id", Length = 64)] + [Required, MaxLength(64)] + public virtual string TriggerId { get; set; } + + /// + /// 当前运行次数 + /// + [SugarColumn(ColumnDescription = "当前运行次数")] + public long NumberOfRuns { get; set; } + + /// + /// 最近运行时间 + /// + [SugarColumn(ColumnDescription = "最近运行时间")] + public DateTime? LastRunTime { get; set; } + + /// + /// 下一次运行时间 + /// + [SugarColumn(ColumnDescription = "下一次运行时间")] + public DateTime? NextRunTime { get; set; } + + /// + /// 触发器状态 + /// + [SugarColumn(ColumnDescription = "触发器状态")] + public TriggerStatus Status { get; set; } = TriggerStatus.Ready; + + /// + /// 本次执行结果 + /// + [SugarColumn(ColumnDescription = "本次执行结果", Length = 128)] + [MaxLength(128)] + public string? Result { get; set; } + + /// + /// 本次执行耗时 + /// + [SugarColumn(ColumnDescription = "本次执行耗时")] + public long ElapsedTime { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间")] + public DateTime? CreatedTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLang.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLang.cs new file mode 100644 index 0000000..8be0706 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLang.cs @@ -0,0 +1,86 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +[SugarTable(null, "语言配置")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public class SysLang : EntityBase +{ + /// + /// 语言名称 + /// + [SugarColumn(ColumnDescription = "语言名称")] + public string Name { get; set; } + + /// + /// 语言代码(如 zh-CN) + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [SugarColumn(ColumnDescription = "ISO 语言代码")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [SugarColumn(ColumnDescription = "URL 语言代码")] + public string UrlCode { get; set; } + + /// + /// 书写方向(1=从左到右,2=从右到左) + /// + [SugarColumn(ColumnDescription = "书写方向", DefaultValue = "1")] + public DirectionEnum Direction { get; set; } = DirectionEnum.Ltr; + + /// + /// 日期格式(如 YYYY-MM-DD) + /// + [SugarColumn(ColumnDescription = "日期格式")] + public string DateFormat { get; set; } + + /// + /// 时间格式(如 HH:MM:SS) + /// + [SugarColumn(ColumnDescription = "时间格式")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日(如 0=星期日,1=星期一) + /// + [SugarColumn(ColumnDescription = "每周起始日", DefaultValue = "7")] + public WeekEnum WeekStart { get; set; } = WeekEnum.Sunday; + + /// + /// 分组符号(如 ,) + /// + [SugarColumn(ColumnDescription = "分组符号")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [SugarColumn(ColumnDescription = "小数点符号")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [SugarColumn(ColumnDescription = "千分位分隔符")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [SugarColumn(ColumnDescription = "是否启用")] + public bool Active { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLangText.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLangText.cs new file mode 100644 index 0000000..f1258cf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLangText.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +[SugarTable(null, "翻译表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(EntityName), OrderByType.Asc)] +[SugarIndex("index_{table}_F", nameof(FieldName), OrderByType.Asc)] +public class SysLangText : EntityBase +{ + /// + /// 所属实体名 + /// + [SugarColumn(ColumnDescription = "所属实体名")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [SugarColumn(ColumnDescription = "所属实体ID")] + public long EntityId { get; set; } + + /// + /// 字段名 + /// + [SugarColumn(ColumnDescription = "字段名")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string LangCode { get; set; } + + /// + /// 多语言内容 + /// + [SugarColumn(ColumnDescription = "翻译内容")] + public string Content { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLdap.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLdap.cs new file mode 100644 index 0000000..f27d1aa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLdap.cs @@ -0,0 +1,89 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统域登录信息配置表 +/// +[SugarTable(null, "系统域登录信息配置表")] +[SysTable] +public class SysLdap : EntityBaseTenantDel +{ + /// + /// 主机 + /// + [SugarColumn(ColumnDescription = "主机", Length = 128)] + [Required] + public virtual string Host { get; set; } + + /// + /// 端口 + /// + [SugarColumn(ColumnDescription = "端口")] + public virtual int Port { get; set; } + + /// + /// 用户搜索基准 + /// + [SugarColumn(ColumnDescription = "用户搜索基准", Length = 128)] + [Required] + public virtual string BaseDn { get; set; } + + /// + /// 绑定DN(有管理权限制的用户) + /// + [SugarColumn(ColumnDescription = "绑定DN", Length = 128)] + [Required] + public virtual string BindDn { get; set; } + + /// + /// 绑定密码(有管理权限制的用户密码) + /// + [SugarColumn(ColumnDescription = "绑定密码", Length = 512)] + [Required] + public virtual string BindPass { get; set; } + + /// + /// 用户过滤规则 + /// + [SugarColumn(ColumnDescription = "用户过滤规则", Length = 128)] + [Required] + public virtual string AuthFilter { get; set; } = "sAMAccountName=%s"; + + /// + /// Ldap版本 + /// + [SugarColumn(ColumnDescription = "Ldap版本")] + public int Version { get; set; } + + /// + /// 绑定域账号字段属性值 + /// + [SugarColumn(ColumnDescription = "绑定域账号字段属性值", Length = 32)] + [Required] + public virtual string BindAttrAccount { get; set; } = "sAMAccountName"; + + /// + /// 绑定用户EmployeeId属性值 + /// + [SugarColumn(ColumnDescription = "绑定用户EmployeeId属性值", Length = 32)] + [Required] + public virtual string BindAttrEmployeeId { get; set; } = "EmployeeId"; + + /// + /// 绑定Code属性值 + /// + [SugarColumn(ColumnDescription = "绑定对象Code属性值", Length = 64)] + [Required] + public virtual string BindAttrCode { get; set; } = "objectGUID"; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogDiff.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogDiff.cs new file mode 100644 index 0000000..96366b7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogDiff.cs @@ -0,0 +1,52 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统差异日志表 +/// +[SugarTable(null, "系统差异日志表")] +[SysTable] +[LogTable] +public partial class SysLogDiff : EntityBaseTenant +{ + /// + /// 差异数据 + /// + [SugarColumn(ColumnDescription = "差异数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffData { get; set; } + + /// + /// Sql + /// + [SugarColumn(ColumnDescription = "Sql", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Sql { get; set; } + + /// + /// 参数 手动传入的参数 + /// + [SugarColumn(ColumnDescription = "参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Parameters { get; set; } + + /// + /// 业务对象 + /// + [SugarColumn(ColumnDescription = "业务对象", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? BusinessData { get; set; } + + /// + /// 差异操作 + /// + [SugarColumn(ColumnDescription = "差异操作", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffType { get; set; } + + /// + /// 耗时 + /// + [SugarColumn(ColumnDescription = "耗时")] + public long? Elapsed { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogEx.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogEx.cs new file mode 100644 index 0000000..e650144 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogEx.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统异常日志表 +/// +[SugarTable(null, "系统异常日志表")] +[SysTable] +[LogTable] +public partial class SysLogEx : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogOp.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogOp.cs new file mode 100644 index 0000000..e5ba1c1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogOp.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统操作日志表 +/// +[SugarTable(null, "系统操作日志表")] +[SysTable] +[LogTable] +public partial class SysLogOp : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogVis.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogVis.cs new file mode 100644 index 0000000..6f269b5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysLogVis.cs @@ -0,0 +1,116 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统访问日志表 +/// +[SugarTable(null, "系统访问日志表")] +[SysTable] +[LogTable] +public partial class SysLogVis : EntityBaseTenant +{ + /// + /// 模块名称 + /// + [SugarColumn(ColumnDescription = "模块名称", Length = 256)] + [MaxLength(256)] + public string? ControllerName { get; set; } + + /// + /// 方法名称 + /// + [SugarColumn(ColumnDescription = "方法名称", Length = 256)] + [MaxLength(256)] + public string? ActionName { get; set; } + + /// + /// 显示名称 + /// + [SugarColumn(ColumnDescription = "显示名称", Length = 256)] + [MaxLength(256)] + public string? DisplayTitle { get; set; } + + /// + /// 执行状态 + /// + [SugarColumn(ColumnDescription = "执行状态", Length = 32)] + [MaxLength(32)] + public string? Status { get; set; } + + /// + /// IP地址 + /// + [SugarColumn(ColumnDescription = "IP地址", Length = 256)] + [MaxLength(256)] + public string? RemoteIp { get; set; } + + /// + /// 登录地点 + /// + [SugarColumn(ColumnDescription = "登录地点", Length = 128)] + [MaxLength(128)] + public string? Location { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public decimal? Longitude { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public decimal? Latitude { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 1024)] + [MaxLength(1024)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 256)] + [MaxLength(256)] + public string? Os { get; set; } + + /// + /// 操作用时 + /// + [SugarColumn(ColumnDescription = "操作用时")] + public long? Elapsed { get; set; } + + /// + /// 日志时间 + /// + [SugarColumn(ColumnDescription = "日志时间")] + public DateTime? LogDateTime { get; set; } + + /// + /// 日志级别 + /// + [SugarColumn(ColumnDescription = "日志级别")] + public LogLevel? LogLevel { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [MaxLength(32)] + public string? Account { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysMenu.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysMenu.cs new file mode 100644 index 0000000..a89a776 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysMenu.cs @@ -0,0 +1,134 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单表 +/// +[SugarTable(null, "系统菜单表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Title), OrderByType.Asc)] +[SugarIndex("index_{table}_T2", nameof(Type), OrderByType.Asc)] +public partial class SysMenu : EntityBase +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 菜单类型(1目录 2菜单 3按钮) + /// + [SugarColumn(ColumnDescription = "菜单类型")] + public MenuTypeEnum Type { get; set; } + + /// + /// 路由名称 + /// + [SugarColumn(ColumnDescription = "路由名称", Length = 64)] + [MaxLength(64)] + public string? Name { get; set; } + + /// + /// 路由地址 + /// + [SugarColumn(ColumnDescription = "路由地址", Length = 128)] + [MaxLength(128)] + public string? Path { get; set; } + + /// + /// 组件路径 + /// + [SugarColumn(ColumnDescription = "组件路径", Length = 128)] + [MaxLength(128)] + public string? Component { get; set; } + + /// + /// 重定向 + /// + [SugarColumn(ColumnDescription = "重定向", Length = 128)] + [MaxLength(128)] + public string? Redirect { get; set; } + + /// + /// 权限标识 + /// + [SugarColumn(ColumnDescription = "权限标识", Length = 128)] + [MaxLength(128)] + public string? Permission { get; set; } + + /// + /// 菜单名称 + /// + [SugarColumn(ColumnDescription = "菜单名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Title { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 128)] + [MaxLength(128)] + public string? Icon { get; set; } = "ele-Menu"; + + /// + /// 是否内嵌 + /// + [SugarColumn(ColumnDescription = "是否内嵌")] + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + [SugarColumn(ColumnDescription = "外链链接", Length = 256)] + [MaxLength(256)] + public string? OutLink { get; set; } + + /// + /// 是否隐藏 + /// + [SugarColumn(ColumnDescription = "是否隐藏")] + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + [SugarColumn(ColumnDescription = "是否缓存")] + public bool IsKeepAlive { get; set; } = true; + + /// + /// 是否固定 + /// + [SugarColumn(ColumnDescription = "是否固定")] + public bool IsAffix { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 菜单子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNotice.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNotice.cs new file mode 100644 index 0000000..7aa7d01 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNotice.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统通知公告表 +/// +[SugarTable(null, "系统通知公告表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysNotice : EntityBase +{ + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32)] + [Required, MaxLength(32)] + [SensitiveDetection('*')] + public virtual string Title { get; set; } + + /// + /// 内容 + /// + [SugarColumn(ColumnDescription = "内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + [SensitiveDetection('*')] + public virtual string Content { get; set; } + + /// + /// 类型(1通知 2公告) + /// + [SugarColumn(ColumnDescription = "类型(1通知 2公告)")] + public NoticeTypeEnum Type { get; set; } + + /// + /// 发布人Id + /// + [SugarColumn(ColumnDescription = "发布人Id")] + public long PublicUserId { get; set; } + + /// + /// 发布人姓名 + /// + [SugarColumn(ColumnDescription = "发布人姓名", Length = 32)] + [MaxLength(32)] + public string? PublicUserName { get; set; } + + /// + /// 发布机构Id + /// + [SugarColumn(ColumnDescription = "发布机构Id")] + public long PublicOrgId { get; set; } + + /// + /// 发布机构名称 + /// + [SugarColumn(ColumnDescription = "发布机构名称", Length = 64)] + [MaxLength(64)] + public string? PublicOrgName { get; set; } + + /// + /// 发布时间 + /// + [SugarColumn(ColumnDescription = "发布时间")] + public DateTime? PublicTime { get; set; } + + /// + /// 撤回时间 + /// + [SugarColumn(ColumnDescription = "撤回时间")] + public DateTime? CancelTime { get; set; } + + /// + /// 状态(0草稿 1发布 2撤回 3删除) + /// + [SugarColumn(ColumnDescription = "状态(0草稿 1发布 2撤回 3删除)")] + public NoticeStatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNoticeUser.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNoticeUser.cs new file mode 100644 index 0000000..5f71ba4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysNoticeUser.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统通知公告用户表 +/// +[SugarTable(null, "系统通知公告用户表")] +[SysTable] +public partial class SysNoticeUser : EntityBaseId +{ + /// + /// 通知公告Id + /// + [SugarColumn(ColumnDescription = "通知公告Id")] + public long NoticeId { get; set; } + + /// + /// 通知公告 + /// + [Navigate(NavigateType.OneToOne, nameof(NoticeId))] + public SysNotice SysNotice { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 阅读时间 + /// + [SugarColumn(ColumnDescription = "阅读时间")] + public DateTime? ReadTime { get; set; } + + /// + /// 状态(0未读 1已读) + /// + [SugarColumn(ColumnDescription = "状态(0未读 1已读)")] + public NoticeUserStatusEnum ReadStatus { get; set; } = NoticeUserStatusEnum.UNREAD; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOnlineUser.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOnlineUser.cs new file mode 100644 index 0000000..d4eb387 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOnlineUser.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统在线用户表 +/// +[SugarTable(null, "系统在线用户表")] +[SysTable] +public partial class SysOnlineUser : EntityBaseTenantId +{ + /// + /// 连接Id + /// + [SugarColumn(ColumnDescription = "连接Id")] + public string? ConnectionId { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string UserName { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } + + /// + /// 连接时间 + /// + [SugarColumn(ColumnDescription = "连接时间")] + public DateTime? Time { get; set; } + + /// + /// 连接IP + /// + [SugarColumn(ColumnDescription = "连接IP", Length = 256)] + [MaxLength(256)] + public string? Ip { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 128)] + [MaxLength(128)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 128)] + [MaxLength(128)] + public string? Os { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOpenAccess.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOpenAccess.cs new file mode 100644 index 0000000..8680426 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOpenAccess.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 开放接口身份表 +/// +[SugarTable(null, "开放接口身份表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(AccessKey), OrderByType.Asc)] +public partial class SysOpenAccess : EntityBase +{ + /// + /// 身份标识 + /// + [SugarColumn(ColumnDescription = "身份标识", Length = 128)] + [Required, MaxLength(128)] + public virtual string AccessKey { get; set; } + + /// + /// 密钥 + /// + [SugarColumn(ColumnDescription = "密钥", Length = 256)] + [Required, MaxLength(256)] + public virtual string AccessSecret { get; set; } + + /// + /// 绑定租户Id + /// + [SugarColumn(ColumnDescription = "绑定租户Id")] + public long BindTenantId { get; set; } + + /// + /// 绑定租户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(BindTenantId))] + public SysTenant BindTenant { get; set; } + + /// + /// 绑定用户Id + /// + [SugarColumn(ColumnDescription = "绑定用户Id")] + public virtual long BindUserId { get; set; } + + /// + /// 绑定用户 + /// + [Navigate(NavigateType.OneToOne, nameof(BindUserId))] + public SysUser BindUser { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOrg.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOrg.cs new file mode 100644 index 0000000..d64a8a3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysOrg.cs @@ -0,0 +1,96 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统机构表 +/// +[SugarTable(null, "系统机构表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysOrg : EntityBaseTenant +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 级别 + /// + [SugarColumn(ColumnDescription = "级别")] + public int? Level { get; set; } + + /// + /// 机构类型-数据字典 + /// + [SugarColumn(ColumnDescription = "机构类型", Length = 64)] + [MaxLength(64)] + public virtual string? Type { get; set; } + + /// + /// 负责人Id + /// + [SugarColumn(ColumnDescription = "负责人Id", IsNullable = true)] + public long? DirectorId { get; set; } + + /// + /// 负责人 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DirectorId))] + public SysUser Director { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } + + /// + /// 是否禁止选中 + /// + [SugarColumn(IsIgnore = true)] + public bool Disabled { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPlugin.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPlugin.cs new file mode 100644 index 0000000..86594dd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPlugin.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统动态插件表 +/// +[SugarTable(null, "系统动态插件表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +public partial class SysPlugin : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// C#代码 + /// + [SugarColumn(ColumnDescription = "C#代码", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string CsharpCode { get; set; } + + /// + /// 程序集名称 + /// + [SugarColumn(ColumnDescription = "程序集名称", Length = 512)] + [MaxLength(512)] + public string? AssemblyName { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPos.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPos.cs new file mode 100644 index 0000000..7045eb8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPos.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统职位表 +/// +[SugarTable(null, "系统职位表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysPos : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 在职人员 + /// + [SugarColumn(IsIgnore = true)] + public List UserList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPrint.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPrint.cs new file mode 100644 index 0000000..269d42a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysPrint.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统打印模板表 +/// +[SugarTable(null, "系统打印模板表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +public partial class SysPrint : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 打印模板 + /// + [SugarColumn(ColumnDescription = "打印模板", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string Template { get; set; } + + /// + /// 打印类型 + /// + [SugarColumn(ColumnDescription = "打印类型")] + [Required] + public virtual PrintTypeEnum? PrintType { get; set; } + + /// + /// 客户端服务地址 + /// + [SugarColumn(ColumnDescription = "客户端服务地址", Length = 128)] + [MaxLength(128)] + public virtual string? ClientServiceAddress { get; set; } + + /// + /// 打印参数 + /// + [SugarColumn(ColumnDescription = "打印参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? PrintParam { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 打印预览测试数据 + /// + [SugarColumn(ColumnDescription = "打印预览测试数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? PrintDataDemo { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRegion.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRegion.cs new file mode 100644 index 0000000..7ada329 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRegion.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统行政地区表 +/// +[SugarTable(null, "系统行政地区表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +public partial class SysRegion : EntityBaseId +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string Name { get; set; } + + /// + /// 简称 + /// + [SugarColumn(ColumnDescription = "简称", Length = 32)] + [MaxLength(32)] + public string? ShortName { get; set; } + + /// + /// 组合名 + /// + [SugarColumn(ColumnDescription = "组合名", Length = 64)] + [MaxLength(64)] + public string? MergerName { get; set; } + + /// + /// 行政代码 + /// + [SugarColumn(ColumnDescription = "行政代码", Length = 32)] + [MaxLength(32)] + public string? Code { get; set; } + + /// + /// 邮政编码 + /// + [SugarColumn(ColumnDescription = "邮政编码", Length = 6)] + [MaxLength(6)] + public string? ZipCode { get; set; } + + /// + /// 区号 + /// + [SugarColumn(ColumnDescription = "区号", Length = 6)] + [MaxLength(6)] + public string? CityCode { get; set; } + + /// + /// 层级 + /// + [SugarColumn(ColumnDescription = "层级")] + public int Level { get; set; } + + /// + /// 拼音 + /// + [SugarColumn(ColumnDescription = "拼音", Length = 128)] + [MaxLength(128)] + public string? PinYin { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public float Lng { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public float Lat { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRole.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRole.cs new file mode 100644 index 0000000..2f0eae4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRole.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色表 +/// +[SugarTable(null, "系统角色表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysRole : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 数据范围(1全部数据 2本部门及以下数据 3本部门数据 4仅本人数据 5自定义数据) + /// + [SugarColumn(ColumnDescription = "数据范围")] + public DataScopeEnum DataScope { get; set; } = DataScopeEnum.Self; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleMenu.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleMenu.cs new file mode 100644 index 0000000..04e3f1d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleMenu.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色菜单表 +/// +[SugarTable(null, "系统角色菜单表")] +[SysTable] +public class SysRoleMenu : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleOrg.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleOrg.cs new file mode 100644 index 0000000..c4622f8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysRoleOrg.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色机构表 +/// +[SugarTable(null, "系统角色机构表")] +[SysTable] +public class SysRoleOrg : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysSchedule.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysSchedule.cs new file mode 100644 index 0000000..4361bd3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysSchedule.cs @@ -0,0 +1,52 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统日程表 +/// +[SugarTable(null, "系统日程表")] +[SysTable] +public class SysSchedule : EntityBaseTenant +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 日程日期 + /// + [SugarColumn(ColumnDescription = "日程日期")] + public DateTime? ScheduleTime { get; set; } + + /// + /// 开始时间 + /// + [SugarColumn(ColumnDescription = "开始时间", Length = 10)] + public string? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间", Length = 10)] + public string? EndTime { get; set; } + + /// + /// 日程内容 + /// + [SugarColumn(ColumnDescription = "日程内容", Length = 256)] + [Required, MaxLength(256)] + public virtual string Content { get; set; } + + /// + /// 完成状态 + /// + [SugarColumn(ColumnDescription = "完成状态")] + public FinishStatusEnum Status { get; set; } = FinishStatusEnum.UnFinish; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTemplate.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTemplate.cs new file mode 100644 index 0000000..9364cd6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTemplate.cs @@ -0,0 +1,63 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统模板表 +/// +[SysTable] +[SugarTable(null, "系统模板表")] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +[SugarIndex("index_{table}_G", nameof(GroupName), OrderByType.Asc)] +public partial class SysTemplate : EntityBaseTenant +{ + /// + /// 名称 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "名称", Length = 128)] + public virtual string Name { get; set; } + + /// + /// 分组名称 + /// + [SugarColumn(ColumnDescription = "分组名称")] + public virtual TemplateTypeEnum Type { get; set; } + + /// + /// 编码 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "编码", Length = 128)] + public virtual string Code { get; set; } + + /// + /// 分组名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "分组名称", Length = 32)] + public virtual string GroupName { get; set; } + + /// + /// 模板内容 + /// + [SugarColumn(ColumnDescription = "模板内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string Content { get; set; } + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public virtual string? Remark { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenant.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenant.cs new file mode 100644 index 0000000..9f2090e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenant.cs @@ -0,0 +1,145 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户表 +/// +[SugarTable(null, "系统租户表")] +[SysTable] +public partial class SysTenant : EntityBase +{ + /// + /// 租管用户Id + /// + [SugarColumn(ColumnDescription = "租管用户Id")] + public virtual long UserId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public virtual long OrgId { get; set; } + + /// + /// 域名 + /// + [SugarColumn(ColumnDescription = "域名", Length = 128)] + [MaxLength(128)] + public virtual string? Host { get; set; } + + /// + /// 租户类型 + /// + [SugarColumn(ColumnDescription = "租户类型")] + public virtual TenantTypeEnum TenantType { get; set; } + + /// + /// 数据库类型 + /// + [SugarColumn(ColumnDescription = "数据库类型")] + public virtual SqlSugar.DbType DbType { get; set; } + + /// + /// 数据库连接 + /// + [SugarColumn(ColumnDescription = "数据库连接", Length = 256)] + [MaxLength(256)] + public virtual string? Connection { get; set; } + + /// + /// 数据库标识 + /// + [SugarColumn(ColumnDescription = "数据库标识", Length = 64)] + [MaxLength(64)] + public virtual string? ConfigId { get; set; } + + /// + /// 从库连接/读写分离 + /// + [SugarColumn(ColumnDescription = "从库连接/读写分离", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? SlaveConnections { get; set; } + + /// + /// 启用注册功能 + /// + [SugarColumn(ColumnDescription = "启用注册功能")] + public virtual YesNoEnum? EnableReg { get; set; } = YesNoEnum.N; + + /// + /// 默认注册方案Id + /// + [SugarColumn(ColumnDescription = "默认注册方案")] + public virtual long? RegWayId { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 256), MaxLength(256)] + public virtual string? Logo { get; set; } + + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)] + public virtual string? Title { get; set; } + + /// + /// 副标题 + /// + [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)] + public virtual string? ViceTitle { get; set; } + + /// + /// 副描述 + /// + [SugarColumn(ColumnDescription = "副描述", Length = 64), MaxLength(64)] + public virtual string? ViceDesc { get; set; } + + /// + /// 水印 + /// + [SugarColumn(ColumnDescription = "水印", Length = 32), MaxLength(32)] + public virtual string? Watermark { get; set; } + + /// + /// 版权信息 + /// + [SugarColumn(ColumnDescription = "版权信息", Length = 64), MaxLength(64)] + public virtual string? Copyright { get; set; } + + /// + /// ICP备案号 + /// + [SugarColumn(ColumnDescription = "ICP备案号", Length = 32), MaxLength(32)] + public virtual string? Icp { get; set; } + + /// + /// ICP地址 + /// + [SugarColumn(ColumnDescription = "ICP地址", Length = 32), MaxLength(32)] + public virtual string? IcpUrl { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public virtual string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public virtual StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfig.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfig.cs new file mode 100644 index 0000000..2cfd676 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfig.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数表 +/// +[SugarTable(null, "系统租户配置参数表")] +[SysTable] +public partial class SysTenantConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfigData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfigData.cs new file mode 100644 index 0000000..0741c31 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantConfigData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable(null, "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_TC", nameof(TenantId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysTenantConfigData : EntityBaseTenantId +{ + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantMenu.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantMenu.cs new file mode 100644 index 0000000..74ed1dc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysTenantMenu.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户菜单表 +/// +[SysTable] +[SugarTable(null, "系统租户菜单表")] +public class SysTenantMenu : EntityBaseId +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id")] + public long TenantId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUser.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUser.cs new file mode 100644 index 0000000..335753e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUser.cs @@ -0,0 +1,353 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户表 +/// +[SugarTable(null, "系统用户表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_P", nameof(Phone), OrderByType.Asc)] +public partial class SysUser : EntityBaseTenantOrg +{ + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string Account { get; set; } + + /// + /// 密码 + /// + [SugarColumn(ColumnDescription = "密码", Length = 512)] + [MaxLength(512)] + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string Password { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public virtual string RealName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 32)] + [MaxLength(32)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 512)] + [MaxLength(512)] + public string? Avatar { get; set; } + + /// + /// 性别-男_1、女_2 + /// + [SugarColumn(ColumnDescription = "性别")] + public GenderEnum Sex { get; set; } = GenderEnum.Male; + + /// + /// 年龄 + /// + [SugarColumn(ColumnDescription = "年龄")] + public int Age { get; set; } + + /// + /// 出生日期 + /// + [SugarColumn(ColumnDescription = "出生日期")] + public DateTime? Birthday { get; set; } + + /// + /// 民族 + /// + [SugarColumn(ColumnDescription = "民族", Length = 32)] + [MaxLength(32)] + public string? Nation { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Phone { get; set; } + + /// + /// 证件类型 + /// + [SugarColumn(ColumnDescription = "证件类型")] + public CardTypeEnum CardType { get; set; } + + /// + /// 身份证号 + /// + [SugarColumn(ColumnDescription = "身份证号", Length = 32)] + [MaxLength(32)] + public string? IdCardNum { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "邮箱", Length = 64)] + [MaxLength(64)] + public string? Email { get; set; } + + /// + /// 地址 + /// + [SugarColumn(ColumnDescription = "地址", Length = 256)] + [MaxLength(256)] + public string? Address { get; set; } + + /// + /// 文化程度 + /// + [SugarColumn(ColumnDescription = "文化程度")] + public CultureLevelEnum CultureLevel { get; set; } + + /// + /// 政治面貌 + /// + [SugarColumn(ColumnDescription = "政治面貌", Length = 16)] + [MaxLength(16)] + public string? PoliticalOutlook { get; set; } + + /// + /// 毕业院校 + /// + [SugarColumn(ColumnDescription = "毕业院校", Length = 128)] + [MaxLength(128)] + public string? College { get; set; } + + /// + /// 办公电话 + /// + [SugarColumn(ColumnDescription = "办公电话", Length = 16)] + [MaxLength(16)] + public string? OfficePhone { get; set; } + + /// + /// 紧急联系人 + /// + [SugarColumn(ColumnDescription = "紧急联系人", Length = 32)] + [MaxLength(32)] + public string? EmergencyContact { get; set; } + + /// + /// 紧急联系人电话 + /// + [SugarColumn(ColumnDescription = "紧急联系人电话", Length = 16)] + [MaxLength(16)] + public string? EmergencyPhone { get; set; } + + /// + /// 紧急联系人地址 + /// + [SugarColumn(ColumnDescription = "紧急联系人地址", Length = 256)] + [MaxLength(256)] + public string? EmergencyAddress { get; set; } + + /// + /// 个人简介 + /// + [SugarColumn(ColumnDescription = "个人简介", Length = 512)] + [MaxLength(512)] + public string? Introduction { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + ///// + ///// 直属机构Id + ///// + //[SugarColumn(ColumnDescription = "直属机构Id")] + //public long OrgId { get; set; } + + /// + /// 直属机构 + /// + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 直属主管Id + /// + [SugarColumn(ColumnDescription = "直属主管Id")] + public long? ManagerUserId { get; set; } + + /// + /// 直属主管 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(ManagerUserId))] + public SysUser ManagerUser { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 职称 + /// + [SugarColumn(ColumnDescription = "职称", Length = 32)] + [MaxLength(32)] + public string? PosTitle { get; set; } + + /// + /// 擅长领域 + /// + [SugarColumn(ColumnDescription = "擅长领域", Length = 32)] + [MaxLength(32)] + public string? Expertise { get; set; } + + /// + /// 办公区域 + /// + [SugarColumn(ColumnDescription = "办公区域", Length = 32)] + [MaxLength(32)] + public string? OfficeZone { get; set; } + + /// + /// 办公室 + /// + [SugarColumn(ColumnDescription = "办公室", Length = 32)] + [MaxLength(32)] + public string? Office { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } + + /// + /// 最新登录Ip + /// + [SugarColumn(ColumnDescription = "最新登录Ip", Length = 256)] + [MaxLength(256)] + public string? LastLoginIp { get; set; } + + /// + /// 最新登录地点 + /// + [SugarColumn(ColumnDescription = "最新登录地点", Length = 128)] + [MaxLength(128)] + public string? LastLoginAddress { get; set; } + + /// + /// 最新登录时间 + /// + [SugarColumn(ColumnDescription = "最新登录时间")] + public DateTime? LastLoginTime { get; set; } + + /// + /// 最新登录设备 + /// + [SugarColumn(ColumnDescription = "最新登录设备", Length = 128)] + [MaxLength(128)] + public string? LastLoginDevice { get; set; } + + /// + /// 电子签名 + /// + [SugarColumn(ColumnDescription = "电子签名", Length = 512)] + [MaxLength(512)] + public string? Signature { get; set; } + + /// + /// 语言代码(如 zh-CN) + /// + [SugarColumn(ColumnDescription = "语言代码")] + public string LangCode { get; set; } = App.GetOptions().DefaultCulture; + + /// + /// 个性化首页地址 + /// + [SugarColumn(ColumnDescription = "个性化首页地址", Length = 512)] + [MaxLength(512)] + public string? Homepage { get; set; } + + /// + /// 验证超级管理员类型,若账号类型为超级管理员则报错 + /// + /// 自定义错误消息 + public void ValidateIsSuperAdminAccountType(ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1014) + { + if (AccountType == AccountTypeEnum.SuperAdmin) + { + throw Oops.Oh(errorMsg); + } + } + + /// + /// 验证用户Id是否相同,若用户Id相同则报错 + /// + /// 用户Id + /// 自定义错误消息 + public void ValidateIsUserId(long userId, ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1001) + { + if (Id == userId) + { + throw Oops.Oh(errorMsg); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfig.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfig.cs new file mode 100644 index 0000000..50e5100 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfig.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户配置参数表 +/// +[SugarTable(null, "系统用户配置参数表")] +[SysTable] +public partial class SysUserConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfigData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfigData.cs new file mode 100644 index 0000000..f1bf4f9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserConfigData.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable(null, "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_UC", nameof(UserId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysUserConfigData : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserExtOrg.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserExtOrg.cs new file mode 100644 index 0000000..cc8c3cb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserExtOrg.cs @@ -0,0 +1,77 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户扩展机构表 +/// +[SugarTable(null, "系统用户扩展机构表")] +[SysTable] +public partial class SysUserExtOrg : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserLdap.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserLdap.cs new file mode 100644 index 0000000..723c6ca --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserLdap.cs @@ -0,0 +1,80 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户域配置表 +/// +[SugarTable(null, "系统用户域配置表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +public class SysUserLdap : EntityBaseTenantId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 域账号 + /// AD域对应sAMAccountName + /// Ldap对应uid + /// + [SugarColumn(ColumnDescription = "域账号", Length = 32)] + [Required] + public string Account { get; set; } + + /// + /// 域用户名 + /// + [SugarColumn(ColumnDescription = "域用户名", Length = 32)] + public string UserName { get; set; } + + /// + /// 对应EmployeeId(用于数据导入对照) + /// + [SugarColumn(ColumnDescription = "对应EmployeeId", Length = 32)] + public string? EmployeeId { get; set; } + + /// + /// 组织代码 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? DeptCode { get; set; } + + /// + /// 最后设置密码时间 + /// + [SugarColumn(ColumnDescription = "最后设置密码时间")] + public DateTime? PwdLastSetTime { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? Mail { get; set; } + + /// + /// 检查账户是否已过期 + /// + [SugarColumn(ColumnDescription = "检查账户是否已过期")] + public bool AccountExpiresFlag { get; set; } = false; + + /// + /// 密码设置是否永不过期 + /// + [SugarColumn(ColumnDescription = "密码设置是否永不过期")] + public bool DontExpiresFlag { get; set; } = false; + + /// + /// DN + /// + [SugarColumn(ColumnDescription = "DN", Length = 512)] + public string Dn { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserMenu.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserMenu.cs new file mode 100644 index 0000000..ee2a683 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserMenu.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户菜单快捷导航表 +/// +[SugarTable(null, "系统用户菜单快捷导航表")] +[SysTable] +public partial class SysUserMenu : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRegWay.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRegWay.cs new file mode 100644 index 0000000..6ae445b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRegWay.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户注册方案表 +/// +[SugarTable(null, "系统用户注册方案表")] +[SysTable] +public partial class SysUserRegWay : EntityBaseTenant +{ + /// + /// 方案名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "方案名称", Length = 32)] + public virtual string Name { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public virtual AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + /// + /// 注册用户默认角色 + /// + [SugarColumn(ColumnDescription = "角色")] + public virtual long RoleId { get; set; } + + /// + /// 注册用户默认机构 + /// + [SugarColumn(ColumnDescription = "机构")] + public virtual long OrgId { get; set; } + + /// + /// 注册用户默认职位 + /// + [SugarColumn(ColumnDescription = "职位")] + public virtual long PosId { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRole.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRole.cs new file mode 100644 index 0000000..8d5cff1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysUserRole.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户角色表 +/// +[SugarTable(null, "系统用户角色表")] +[SysTable] +public class SysUserRole : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 角色 + /// + [Navigate(NavigateType.OneToOne, nameof(RoleId))] + public SysRole SysRole { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatPay.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatPay.cs new file mode 100644 index 0000000..0676295 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatPay.cs @@ -0,0 +1,191 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信支付表 +/// +[SugarTable(null, "系统微信支付表")] +[SysTable] +[SugarIndex("index_{table}_BU", nameof(BusinessId), OrderByType.Asc)] +[SugarIndex("index_{table}_TR", nameof(TradeState), OrderByType.Asc)] +[SugarIndex("index_{table}_TA", nameof(Tags), OrderByType.Asc)] +public partial class SysWechatPay : EntityBase +{ + /// + /// 微信商户号 + /// + [SugarColumn(ColumnDescription = "微信商户号")] + [Required] + public virtual string MerchantId { get; set; } + + /// + /// 服务商AppId + /// + [SugarColumn(ColumnDescription = "服务商AppId")] + [Required] + public virtual string AppId { get; set; } + + /// + /// 商户订单号 + /// + [SugarColumn(ColumnDescription = "商户订单号")] + [Required] + public virtual string OutTradeNumber { get; set; } + + /// + /// 支付订单号 + /// + [SugarColumn(ColumnDescription = "支付订单号")] + [Required] + public virtual string TransactionId { get; set; } + + /// + /// 交易类型 + /// + [SugarColumn(ColumnDescription = "交易类型")] + public string? TradeType { get; set; } + + /// + /// 交易状态 + /// + [SugarColumn(ColumnDescription = "交易状态")] + public string? TradeState { get; set; } + + /// + /// 交易状态描述 + /// + [SugarColumn(ColumnDescription = "交易状态描述")] + public string? TradeStateDescription { get; set; } + + /// + /// 付款银行类型 + /// + [SugarColumn(ColumnDescription = "付款银行类型")] + public string? BankType { get; set; } + + /// + /// 订单总金额 + /// + [SugarColumn(ColumnDescription = "订单总金额")] + public int Total { get; set; } + + /// + /// 用户支付金额 + /// + [SugarColumn(ColumnDescription = "用户支付金额")] + public int? PayerTotal { get; set; } + + /// + /// 支付完成时间 + /// + [SugarColumn(ColumnDescription = "支付完成时间")] + public DateTime? SuccessTime { get; set; } + + /// + /// 交易结束时间 + /// + [SugarColumn(ColumnDescription = "交易结束时间")] + public DateTime? ExpireTime { get; set; } + + /// + /// 商品描述 + /// + [SugarColumn(ColumnDescription = "商品描述")] + public string? Description { get; set; } + + /// + /// 场景信息 + /// + [SugarColumn(ColumnDescription = "场景信息")] + public string? Scene { get; set; } + + /// + /// 附加数据 + /// + [SugarColumn(ColumnDescription = "附加数据")] + public string? Attachment { get; set; } + + /// + /// 优惠标记 + /// + [SugarColumn(ColumnDescription = "优惠标记")] + public string? GoodsTag { get; set; } + + /// + /// 结算信息 + /// + [SugarColumn(ColumnDescription = "结算信息")] + public string? Settlement { get; set; } + + /// + /// 回调通知地址 + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? NotifyUrl { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注")] + public string? Remark { get; set; } + + /// + /// 微信OpenId标识 + /// + [SugarColumn(ColumnDescription = "微信OpenId标识")] + public string? OpenId { get; set; } + + /// + /// 业务标签,用来区分做什么业务 + /// + /// + /// Tags标识用来区分这个支付记录对应什么业务从而确定相关联的表名, + /// 再结合BusinessId保存了对应的业务数据的ID,就可以确定这个支付 + /// 记录与哪一条业务数据相关联 + /// + [SugarColumn(ColumnDescription = "业务标签,用来区分做什么业务", Length = 64)] + public string? Tags { get; set; } + + /// + /// 对应业务的主键 + /// + [SugarColumn(ColumnDescription = "对应业务的主键")] + public long BusinessId { get; set; } + + /// + /// 付款二维码内容 + /// + [SugarColumn(ColumnDescription = "付款二维码内容")] + public string? QrcodeContent { get; set; } + + /// + /// 关联微信用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OpenId))] + public SysWechatUser SysWechatUser { get; set; } + + /// + /// 子商户号 + /// + [SugarColumn(ColumnDescription = "子商户号")] + public string? SubMerchantId { get; set; } + + /// + /// 子商户AppId + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? SubAppId { get; set; } + + /// + /// 子商户唯一标识 + /// + [SugarColumn(ColumnDescription = "子商户唯一标识")] + public string? SubOpenId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs new file mode 100644 index 0000000..cc9b938 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatRefund.cs @@ -0,0 +1,97 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信支付退款表 +/// +[SugarTable(null, "系统微信支付退款表")] +[SysTable] +[SugarIndex("index_{table}_W", nameof(WechatPayId), OrderByType.Asc)] +public partial class SysWechatRefund : EntityBase +{ + /// + /// 定单主键 + /// + [SugarColumn(ColumnDescription = "定单主键")] + public long WechatPayId { get; set; } + + /// + /// 商户退款号 + /// + [SugarColumn(ColumnDescription = "商户退款号")] + [Required] + public virtual string OutRefundNumber { get; set; } + + /// + /// 退款订单号 + /// + [SugarColumn(ColumnDescription = "退款订单号")] + [Required] + public virtual string TransactionId { get; set; } + + /// + /// 退款原因 + /// + [SugarColumn(ColumnDescription = "退款原因")] + public string? Reason { get; set; } + + /// + /// 退款渠道 + /// + [SugarColumn(ColumnDescription = "退款渠道")] + public string? Channel { get; set; } + + /// + /// 退款入账账户 + /// + /// + /// 取当前退款单的退款入账方,有以下几种情况: + /// 1)退回银行卡:{银行名称}{卡类型}{ 卡尾号} + /// 2)退回支付用户零钱: 支付用户零钱 + /// 3)退还商户: 商户基本账户商户结算银行账户 + /// 4)退回支付用户零钱通: 支付用户零钱通 + /// + [SugarColumn(ColumnDescription = "退款入账账户")] + public string? UserReceivedAccount { get; set; } + + /// + /// 退款状态 + /// + [SugarColumn(ColumnDescription = "退款状态")] + public string? TradeState { get; set; } + + /// + /// 交易状态描述 + /// + [SugarColumn(ColumnDescription = "交易状态描述")] + public string? TradeStateDescription { get; set; } + + /// + /// 订单总金额 + /// + [SugarColumn(ColumnDescription = "退款金额")] + public int Refund { get; set; } + + /// + /// 支完成时间 + /// + [SugarColumn(ColumnDescription = "完成时间")] + public DateTime? SuccessTime { get; set; } + + /// + /// 回调通知地址 + /// + [SugarColumn(ColumnDescription = "回调通知地址")] + public string? NotifyUrl { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注")] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatUser.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatUser.cs new file mode 100644 index 0000000..8aa2e58 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Entity/SysWechatUser.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统微信用户表 +/// +[SugarTable(null, "系统微信用户表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(NickName), OrderByType.Asc)] +[SugarIndex("index_{table}_M", nameof(Mobile), OrderByType.Asc)] +public partial class SysWechatUser : EntityBase +{ + /// + /// 系统用户Id + /// + [SugarColumn(ColumnDescription = "系统用户Id")] + public long UserId { get; set; } + + /// + /// 系统用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 平台类型 + /// + [SugarColumn(ColumnDescription = "平台类型")] + public PlatformTypeEnum PlatformType { get; set; } = PlatformTypeEnum.微信公众号; + + /// + /// OpenId + /// + [SugarColumn(ColumnDescription = "OpenId", Length = 64)] + [Required, MaxLength(64)] + public virtual string OpenId { get; set; } + + /// + /// 会话密钥 + /// + [SugarColumn(ColumnDescription = "会话密钥", Length = 256)] + [MaxLength(256)] + public string? SessionKey { get; set; } + + /// + /// UnionId + /// + [SugarColumn(ColumnDescription = "UnionId", Length = 64)] + [MaxLength(64)] + public string? UnionId { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 64)] + [MaxLength(64)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 256)] + [MaxLength(256)] + public string? Avatar { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Mobile { get; set; } + + /// + /// 性别 + /// + [SugarColumn(ColumnDescription = "性别")] + public int? Sex { get; set; } + + /// + /// 语言 + /// + [SugarColumn(ColumnDescription = "语言", Length = 64)] + [MaxLength(64)] + public string? Language { get; set; } + + /// + /// 城市 + /// + [SugarColumn(ColumnDescription = "城市", Length = 64)] + [MaxLength(64)] + public string? City { get; set; } + + /// + /// 省 + /// + [SugarColumn(ColumnDescription = "省", Length = 64)] + [MaxLength(64)] + public string? Province { get; set; } + + /// + /// 国家 + /// + [SugarColumn(ColumnDescription = "国家", Length = 64)] + [MaxLength(64)] + public string? Country { get; set; } + + /// + /// AccessToken + /// + [SugarColumn(ColumnDescription = "AccessToken", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? AccessToken { get; set; } + + /// + /// RefreshToken + /// + [SugarColumn(ColumnDescription = "RefreshToken", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RefreshToken { get; set; } + + /// + /// 过期时间 + /// + [SugarColumn(ColumnDescription = "ExpiresIn")] + public int? ExpiresIn { get; set; } + + /// + /// 用户授权的作用域,使用逗号分隔 + /// + [SugarColumn(ColumnDescription = "授权作用域", Length = 64)] + [MaxLength(64)] + public string? Scope { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AccountTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AccountTypeEnum.cs new file mode 100644 index 0000000..bedbec6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AccountTypeEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 账号类型枚举 +/// +[Description("账号类型枚举")] +public enum AccountTypeEnum +{ + /// + /// 超级管理员 + /// + [Description("超级管理员")] + SuperAdmin = 999, + + /// + /// 系统管理员 + /// + [Description("系统管理员")] + SysAdmin = 888, + + /// + /// 普通账号 + /// + [Description("普通账号")] + NormalUser = 777, + + /// + /// 会员 + /// + [Description("会员")] + Member = 666, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs new file mode 100644 index 0000000..8bc1c34 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayCertTypeEnum.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 参与方的证件类型枚举 +/// +[SuppressSniffer] +[Description("参与方的证件类型枚举")] +public enum AlipayCertTypeEnum +{ + [Description("身份证")] + IDENTITY_CARD, + + [Description("护照")] + PASSPORT +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs new file mode 100644 index 0000000..4b2d864 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/AlipayIdentityTypeEnum.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 参与方的标识类型枚举 +/// +[SuppressSniffer] +[Description("参与方的标识类型枚举")] +public enum AlipayIdentityTypeEnum +{ + [Description("支付宝用户UID")] + ALIPAY_USER_ID, + + [Description("支付宝登录号")] + ALIPAY_LOGON_ID +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CacheTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CacheTypeEnum.cs new file mode 100644 index 0000000..e6832ed --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CacheTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存类型枚举 +/// +[Description("缓存类型枚举")] +public enum CacheTypeEnum +{ + /// + /// 内存缓存 + /// + [Description("内存缓存")] + Memory, + + /// + /// Redis缓存 + /// + [Description("Redis缓存")] + Redis +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CardTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CardTypeEnum.cs new file mode 100644 index 0000000..774359b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CardTypeEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 证件类型枚举 +/// +[Description("证件类型枚举")] +public enum CardTypeEnum +{ + /// + /// 身份证 + /// + [Description("身份证")] + IdCard = 0, + + /// + /// 护照 + /// + [Description("护照")] + PassportCard = 1, + + /// + /// 出生证 + /// + [Description("出生证")] + BirthCard = 2, + + /// + /// 港澳台通行证 + /// + [Description("港澳台通行证")] + GatCard = 3, + + /// + /// 外国人居留证 + /// + [Description("外国人居留证")] + ForeignCard = 4, + + /// + /// 营业执照 + /// + [Description("营业执照")] + License = 5, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CryptogramEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CryptogramEnum.cs new file mode 100644 index 0000000..e259bea --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CryptogramEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 密码加密枚举 +/// +[Description("密码加密枚举")] +public enum CryptogramEnum +{ + /// + /// MD5 + /// + [Description("MD5")] + MD5 = 0, + + /// + /// SM2(国密) + /// + [Description("SM2")] + SM2 = 1, + + /// + /// SM4(国密) + /// + [Description("SM4")] + SM4 = 2 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CultureLevelEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CultureLevelEnum.cs new file mode 100644 index 0000000..ce54e33 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/CultureLevelEnum.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 文化程度枚举 +/// +[Description("文化程度枚举")] +public enum CultureLevelEnum +{ + /// + /// 其他 + /// + [Description("其他"), Theme("info")] + Level0 = 0, + + /// + /// 文盲 + /// + [Description("文盲")] + Level1 = 1, + + /// + /// 小学 + /// + [Description("小学")] + Level2 = 2, + + /// + /// 初中 + /// + [Description("初中")] + Level3 = 3, + + /// + /// 普通高中 + /// + [Description("普通高中")] + Level4 = 4, + + /// + /// 技工学校 + /// + [Description("技工学校")] + Level5 = 5, + + /// + /// 职业教育 + /// + [Description("职业教育")] + Level6 = 6, + + /// + /// 职业高中 + /// + [Description("职业高中")] + Level7 = 7, + + /// + /// 中等专科 + /// + [Description("中等专科")] + Level8 = 8, + + /// + /// 大学专科 + /// + [Description("大学专科")] + Level9 = 9, + + /// + /// 大学本科 + /// + [Description("大学本科")] + Level10 = 10, + + /// + /// 硕士研究生 + /// + [Description("硕士研究生")] + Level11 = 11, + + /// + /// 博士研究生 + /// + [Description("博士研究生")] + Level12 = 12, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataOpTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataOpTypeEnum.cs new file mode 100644 index 0000000..71ae9c0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataOpTypeEnum.cs @@ -0,0 +1,92 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据操作类型枚举 +/// +[Description("数据操作类型枚举")] +public enum DataOpTypeEnum +{ + /// + /// 其它 + /// + [Description("其它"), Theme("info")] + Other, + + /// + /// 增加 + /// + [Description("增加")] + Add, + + /// + /// 删除 + /// + [Description("删除")] + Delete, + + /// + /// 编辑 + /// + [Description("编辑")] + Edit, + + /// + /// 更新 + /// + [Description("更新")] + Update, + + /// + /// 查询 + /// + [Description("查询")] + Query, + + /// + /// 详情 + /// + [Description("详情")] + Detail, + + /// + /// 树 + /// + [Description("树")] + Tree, + + /// + /// 导入 + /// + [Description("导入")] + Import, + + /// + /// 导出 + /// + [Description("导出")] + Export, + + /// + /// 授权 + /// + [Description("授权")] + Grant, + + /// + /// 强退 + /// + [Description("强退")] + Force, + + /// + /// 清空 + /// + [Description("清空")] + Clean +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataScopeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataScopeEnum.cs new file mode 100644 index 0000000..d6fc8bd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DataScopeEnum.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 角色数据范围枚举 +/// +[Description("角色数据范围枚举")] +public enum DataScopeEnum +{ + /// + /// 全部数据 + /// + [Description("全部数据")] + All = 1, + + /// + /// 本部门及以下数据 + /// + [Description("本部门及以下数据")] + DeptChild = 2, + + /// + /// 本部门数据 + /// + [Description("本部门数据")] + Dept = 3, + + /// + /// 仅本人数据 + /// + [Description("仅本人数据")] + Self = 4, + + /// + /// 自定义数据 + /// + [Description("自定义数据")] + Define = 5 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DirectionEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DirectionEnum.cs new file mode 100644 index 0000000..caa57ad --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/DirectionEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 书写方向枚举 +/// +[Description("书写方向枚举")] +public enum DirectionEnum +{ + /// + /// 从左到右 + /// + [Description("从左到右")] + Ltr = 1, + + /// + /// 从右到左 + /// + [Description("从右到左")] + Rtl = 2 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs new file mode 100644 index 0000000..e11fb00 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ElasticSearchAuthTypeEnum.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES认证类型枚举 +/// https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/_options_on_elasticsearchclientsettings.html +/// +[Description("ES认证类型枚举")] +public enum ElasticSearchAuthTypeEnum +{ + /// + /// BasicAuthentication + /// + [Description("BasicAuthentication")] + Basic = 1, + + /// + /// ApiKey + /// + [Description("ApiKey")] + ApiKey = 2, + + /// + /// Base64ApiKey + /// + [Description("Base64ApiKey")] + Base64ApiKey = 3, + + /// + /// 不验证 + /// + [Description("None")] + None = 4 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs new file mode 100644 index 0000000..dff72be --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs @@ -0,0 +1,909 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统错误码 +/// +[ErrorCodeType] +[Description("系统错误码")] +public enum ErrorCodeEnum +{ + /// + /// 验证码错误 + /// + [ErrorCodeItemMetadata("验证码错误")] + D0008, + + /// + /// 账号不存在 + /// + [ErrorCodeItemMetadata("账号不存在")] + D0009, + + /// + /// 密匙不匹配 + /// + [ErrorCodeItemMetadata("密匙不匹配")] + D0010, + + /// + /// 账号或密码不正确 + /// + [ErrorCodeItemMetadata("账号或密码不正确")] + D1000, + + /// + /// 非法操作!禁止删除自己 + /// + [ErrorCodeItemMetadata("非法操作,禁止删除自己")] + D1001, + + /// + /// 记录不存在 + /// + [ErrorCodeItemMetadata("记录不存在")] + D1002, + + /// + /// 账号已存在 + /// + [ErrorCodeItemMetadata("账号已存在")] + D1003, + + /// + /// 旧密码不匹配 + /// + [ErrorCodeItemMetadata("旧密码输入错误")] + D1004, + + ///// + ///// 测试数据禁止更改admin密码 + ///// + //[ErrorCodeItemMetadata("测试数据禁止更改用户【admin】密码")] + //D1005, + + /// + /// 数据已存在 + /// + [ErrorCodeItemMetadata("数据已存在")] + D1006, + + /// + /// 数据不存在或含有关联引用,禁止删除 + /// + [ErrorCodeItemMetadata("数据不存在或含有关联引用,禁止删除")] + D1007, + + /// + /// 禁止为管理员分配角色 + /// + [ErrorCodeItemMetadata("禁止为管理员分配角色")] + D1008, + + /// + /// 重复数据或记录含有不存在数据 + /// + [ErrorCodeItemMetadata("重复数据或记录含有不存在数据")] + D1009, + + /// + /// 禁止为超级管理员角色分配权限 + /// + [ErrorCodeItemMetadata("禁止为超级管理员角色分配权限")] + D1010, + + /// + /// 非法操作,未登录 + /// + [ErrorCodeItemMetadata("非法操作,未登录")] + D1011, + + /// + /// Id不能为空 + /// + [ErrorCodeItemMetadata("Id不能为空")] + D1012, + + /// + /// 所属机构不在自己的数据范围内 + /// + [ErrorCodeItemMetadata("没有权限操作该数据")] + D1013, + + /// + /// 禁止删除超级管理员 + /// + [ErrorCodeItemMetadata("禁止删除超级管理员")] + D1014, + + /// + /// 禁止修改超级管理员状态 + /// + [ErrorCodeItemMetadata("禁止修改超级管理员状态")] + D1015, + + /// + /// 没有权限 + /// + [ErrorCodeItemMetadata("没有权限")] + D1016, + + /// + /// 账号已冻结 + /// + [ErrorCodeItemMetadata("账号已冻结")] + D1017, + + /// + /// 该租户下角色菜单权限集为空 + /// + [ErrorCodeItemMetadata("该租户下角色菜单权限集为空")] + D1019, + + /// + /// 禁止删除默认租户 + /// + [ErrorCodeItemMetadata("禁止删除默认租户")] + D1023, + + /// + /// 已将其他地方登录账号下线 + /// + [ErrorCodeItemMetadata("已将其他地方登录账号下线")] + D1024, + + /// + /// 此角色下面存在账号禁止删除 + /// + [ErrorCodeItemMetadata("此角色下面存在账号禁止删除")] + D1025, + + /// + /// 禁止修改本人账号状态 + /// + [ErrorCodeItemMetadata("禁止修改本人账号状态")] + D1026, + + /// + /// 密码错误次数过多,账号已锁定,请半小时后重试! + /// + [ErrorCodeItemMetadata("密码错误次数过多,账号已锁定,请半小时后重试!")] + D1027, + + /// + /// 新密码不能与旧密码相同 + /// + [ErrorCodeItemMetadata("新密码不能与旧密码相同")] + D1028, + + /// + /// 系统默认账号禁止删除 + /// + [ErrorCodeItemMetadata("系统默认账号禁止删除")] + D1029, + + /// + /// 开放接口绑定账号禁止删除 + /// + [ErrorCodeItemMetadata("开放接口绑定账号禁止删除")] + D1030, + + /// + /// 开放接口绑定租户禁止删除 + /// + [ErrorCodeItemMetadata("开放接口绑定租户禁止删除")] + D1031, + + /// + /// 手机号已存在 + /// + [ErrorCodeItemMetadata("手机号已存在")] + D1032, + + /// + /// 此角色下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此角色下存在注册方案禁止删除")] + D1033, + + /// + /// 注册功能未开启禁止注册 + /// + [ErrorCodeItemMetadata("注册功能未开启禁止注册")] + D1034, + + /// + /// 注册方案不存在 + /// + [ErrorCodeItemMetadata("注册方案不存在")] + D1035, + + /// + /// 角色不存在 + /// + [ErrorCodeItemMetadata("角色不存在")] + D1036, + + /// + /// 禁止注册超级管理员和系统管理员 + /// + [ErrorCodeItemMetadata("禁止注册超级管理员和系统管理员")] + D1037, + + /// + /// 禁止越权操作系统账户 + /// + [ErrorCodeItemMetadata("禁止越权操作系统账户")] + D1038, + + /// + /// 父机构不存在 + /// + [ErrorCodeItemMetadata("父机构不存在")] + D2000, + + /// + /// 当前机构Id不能与父机构Id相同 + /// + [ErrorCodeItemMetadata("当前机构Id不能与父机构Id相同")] + D2001, + + /// + /// 已有相同组织机构,编码或名称相同 + /// + [ErrorCodeItemMetadata("已有相同组织机构,编码或名称相同")] + D2002, + + /// + /// 没有权限操作机构 + /// + [ErrorCodeItemMetadata("没有权限操作机构")] + D2003, + + /// + /// 该机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("该机构下有用户禁止删除")] + D2004, + + /// + /// 附属机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("附属机构下有用户禁止删除")] + D2005, + + /// + /// 只能增加下级机构 + /// + [ErrorCodeItemMetadata("只能增加下级机构")] + D2006, + + /// + /// 下级机构下有用户禁止删除 + /// + [ErrorCodeItemMetadata("下级机构下有用户禁止删除")] + D2007, + + /// + /// 系统默认机构禁止删除 + /// + [ErrorCodeItemMetadata("系统默认机构禁止删除")] + D2008, + + /// + /// 禁止增加根节点机构 + /// + [ErrorCodeItemMetadata("禁止增加根节点机构")] + D2009, + + /// + /// 此机构下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此机构下存在注册方案禁止删除")] + D2010, + + /// + /// 机构不存在 + /// + [ErrorCodeItemMetadata("机构不存在")] + D2011, + + /// + /// 系统默认机构禁止修改 + /// + [ErrorCodeItemMetadata("系统默认机构禁止修改")] + D2012, + + /// + /// 字典类型不存在 + /// + [ErrorCodeItemMetadata("字典类型不存在")] + D3000, + + /// + /// 字典类型已存在 + /// + [ErrorCodeItemMetadata("字典类型已存在,名称或编码重复")] + D3001, + + /// + /// 字典类型下面有字典值禁止删除 + /// + [ErrorCodeItemMetadata("字典类型下面有字典值禁止删除")] + D3002, + + /// + /// 字典值已存在 + /// + [ErrorCodeItemMetadata("字典值已存在")] + D3003, + + /// + /// 字典值不存在 + /// + [ErrorCodeItemMetadata("字典值不存在")] + D3004, + + /// + /// 字典状态错误 + /// + [ErrorCodeItemMetadata("字典状态错误")] + D3005, + + /// + /// 字典编码不能以Enum结尾 + /// + [ErrorCodeItemMetadata("字典编码不能以Enum结尾")] + D3006, + + /// + /// 禁止修改枚举类型的字典编码 + /// + [ErrorCodeItemMetadata("禁止修改枚举类型的字典编码")] + D3007, + + /// + /// 禁止迁移枚举字典 + /// + [ErrorCodeItemMetadata("禁止迁移枚举字典")] + D3008, + + /// + /// 字典已在该租户禁止迁移 + /// + [ErrorCodeItemMetadata("字典已在该租户禁止迁移")] + D3009, + + /// + /// 非超管用户禁止操作系统字典 + /// + [ErrorCodeItemMetadata("非超管用户禁止操作系统字典")] + D3010, + + /// + /// 获取字典值集合入参有误 + /// + [ErrorCodeItemMetadata("获取字典值集合入参有误")] + D3011, + + /// + /// 禁止修改租户字典状态 + /// + [ErrorCodeItemMetadata("禁止修改租户字典状态")] + D3012, + + /// + /// 菜单已存在 + /// + [ErrorCodeItemMetadata("菜单已存在")] + D4000, + + /// + /// 路由地址为空 + /// + [ErrorCodeItemMetadata("路由地址为空")] + D4001, + + /// + /// 打开方式为空 + /// + [ErrorCodeItemMetadata("打开方式为空")] + D4002, + + /// + /// 权限标识格式为空 + /// + [ErrorCodeItemMetadata("权限标识格式为空")] + D4003, + + /// + /// 权限标识格式错误 + /// + [ErrorCodeItemMetadata("权限标识格式错误 如xxx:xxx")] + D4004, + + /// + /// 权限不存在 + /// + [ErrorCodeItemMetadata("权限不存在")] + D4005, + + /// + /// 父级菜单不能为当前节点,请重新选择父级菜单 + /// + [ErrorCodeItemMetadata("父级菜单不能为当前节点,请重新选择父级菜单")] + D4006, + + /// + /// 不能移动根节点 + /// + [ErrorCodeItemMetadata("不能移动根节点")] + D4007, + + /// + /// 禁止本节点与父节点相同 + /// + [ErrorCodeItemMetadata("禁止本节点与父节点相同")] + D4008, + + /// + /// 路由名称重复 + /// + [ErrorCodeItemMetadata("路由名称重复")] + D4009, + + /// + /// 父节点不能为按钮类型 + /// + [ErrorCodeItemMetadata("父节点不能为按钮类型")] + D4010, + + /// + /// 租户不能为空 + /// + [ErrorCodeItemMetadata("租户不能为空")] + D4011, + + /// + /// 系统菜单禁止修改 + /// + [ErrorCodeItemMetadata("系统菜单禁止修改")] + D4012, + + /// + /// 系统菜单禁止删除 + /// + [ErrorCodeItemMetadata("系统菜单禁止删除")] + D4013, + + /// + /// 已存在同名或同编码应用 + /// + [ErrorCodeItemMetadata("已存在同名或同编码应用")] + D5000, + + /// + /// 默认激活系统只能有一个 + /// + [ErrorCodeItemMetadata("默认激活系统只能有一个")] + D5001, + + /// + /// 该应用下有菜单禁止删除 + /// + [ErrorCodeItemMetadata("该应用下有菜单禁止删除")] + D5002, + + /// + /// 已存在同名或同编码应用 + /// + [ErrorCodeItemMetadata("已存在同名或同编码应用")] + D5003, + + /// + /// 已存在同名或同编码职位 + /// + [ErrorCodeItemMetadata("已存在同名或同编码职位")] + D6000, + + /// + /// 该职位下有用户禁止删除 + /// + [ErrorCodeItemMetadata("该职位下有用户禁止删除")] + D6001, + + /// + /// 无权修改本职位 + /// + [ErrorCodeItemMetadata("无权修改本职位")] + D6002, + + /// + /// 职位不存在 + /// + [ErrorCodeItemMetadata("职位不存在")] + D6003, + + /// + /// 此职位下存在注册方案禁止删除 + /// + [ErrorCodeItemMetadata("此职位下存在注册方案禁止删除")] + D6004, + + /// + /// 通知公告状态错误 + /// + [ErrorCodeItemMetadata("通知公告状态错误")] + D7000, + + /// + /// 通知公告删除失败 + /// + [ErrorCodeItemMetadata("通知公告删除失败")] + D7001, + + /// + /// 通知公告编辑失败 + /// + [ErrorCodeItemMetadata("通知公告编辑失败,类型必须为草稿")] + D7002, + + /// + /// 通知公告操作失败,非发布者不能进行操作 + /// + [ErrorCodeItemMetadata("通知公告操作失败,非发布者不能进行操作")] + D7003, + + /// + /// 文件不存在 + /// + [ErrorCodeItemMetadata("文件不存在")] + D8000, + + /// + /// 不允许的文件类型 + /// + [ErrorCodeItemMetadata("不允许的文件类型")] + D8001, + + /// + /// 文件超过允许大小 + /// + [ErrorCodeItemMetadata("文件超过允许大小")] + D8002, + + /// + /// 文件后缀错误 + /// + [ErrorCodeItemMetadata("文件后缀错误")] + D8003, + + /// + /// 文件已存在 + /// + [ErrorCodeItemMetadata("文件已存在")] + D8004, + + /// + /// 无效的文件名 + /// + [ErrorCodeItemMetadata("无效的文件名")] + D8005, + + /// + /// 已存在同名或同编码参数配置 + /// + [ErrorCodeItemMetadata("已存在同名或同编码参数配置")] + D9000, + + /// + /// 禁止删除系统参数 + /// + [ErrorCodeItemMetadata("禁止删除系统参数")] + D9001, + + /// + /// 已存在同名任务调度 + /// + [ErrorCodeItemMetadata("已存在同名任务调度")] + D1100, + + /// + /// 任务调度不存在 + /// + [ErrorCodeItemMetadata("任务调度不存在")] + D1101, + + /// + /// 演示环境禁止修改数据 + /// + [ErrorCodeItemMetadata("演示环境禁止修改数据")] + D1200, + + /// + /// 已存在同名的租户 + /// + [ErrorCodeItemMetadata("已存在同名的租户")] + D1300, + + /// + /// 已存在同名的租户管理员 + /// + [ErrorCodeItemMetadata("已存在同名的租户管理员")] + D1301, + + /// + /// 租户从库配置错误 + /// + [ErrorCodeItemMetadata("租户从库配置错误")] + D1302, + + /// + /// 已存在同名的租户域名 + /// + [ErrorCodeItemMetadata("已存在同名的租户域名")] + D1303, + + /// + /// 授权菜单存在重复项 + /// + [ErrorCodeItemMetadata("授权菜单存在重复项")] + D1304, + + /// + /// 该表代码模板已经生成过 + /// + [ErrorCodeItemMetadata("该表代码模板已经生成过")] + D1400, + + /// + /// 数据库配置不存在 + /// + [ErrorCodeItemMetadata("数据库配置不存在")] + D1401, + + /// + /// 该类型不存在 + /// + [ErrorCodeItemMetadata("该类型不存在")] + D1501, + + /// + /// 该字段不存在 + /// + [ErrorCodeItemMetadata("该字段不存在")] + D1502, + + /// + /// 该类型不是枚举类型 + /// + [ErrorCodeItemMetadata("该类型不是枚举类型")] + D1503, + + /// + /// 该实体不存在 + /// + [ErrorCodeItemMetadata("该实体不存在")] + D1504, + + /// + /// 父菜单不存在 + /// + [ErrorCodeItemMetadata("父菜单不存在")] + D1505, + + /// + /// 父资源不存在 + /// + [ErrorCodeItemMetadata("父资源不存在")] + D1600, + + /// + /// 当前资源Id不能与父资源Id相同 + /// + [ErrorCodeItemMetadata("当前资源Id不能与父资源Id相同")] + D1601, + + /// + /// 已有相同编码或名称 + /// + [ErrorCodeItemMetadata("已有相同编码或名称")] + D1602, + + /// + /// 脚本代码不能为空 + /// + [ErrorCodeItemMetadata("脚本代码不能为空")] + D1701, + + /// + /// 脚本代码中的作业类,需要定义 [JobDetail] 特性 + /// + [ErrorCodeItemMetadata("脚本代码中的作业类,需要定义 [JobDetail] 特性")] + D1702, + + /// + /// 作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致 + /// + [ErrorCodeItemMetadata("作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致")] + D1703, + + /// + /// 禁止修改作业编号 + /// + [ErrorCodeItemMetadata("禁止修改作业编号")] + D1704, + + /// + /// 执行作业失败 + /// + [ErrorCodeItemMetadata("执行作业失败")] + D1705, + + /// + /// 已存在同名打印模板 + /// + [ErrorCodeItemMetadata("已存在同名打印模板")] + D1800, + + /// + /// 已存在同名功能或同名程序及插件 + /// + [ErrorCodeItemMetadata("已存在同名功能或同名程序及插件")] + D1900, + + /// + /// 注册方案名称已存在 + /// + [ErrorCodeItemMetadata("注册方案名称已存在")] + D2101, + + /// + /// 已存在同名模板 + /// + [ErrorCodeItemMetadata("已存在同名模板")] + T1000, + + /// + /// 已存在相同编码模板 + /// + [ErrorCodeItemMetadata("已存在相同编码模板")] + T1001, + + /// + /// 禁止删除存在关联租户的应用 + /// + [ErrorCodeItemMetadata("禁止删除存在关联租户的应用")] + A1001, + + /// + /// 禁止删除存在关联菜单的应用 + /// + [ErrorCodeItemMetadata("禁止删除存在关联菜单的应用")] + A1002, + + /// + /// 找不到系统应用 + /// + [ErrorCodeItemMetadata("找不到系统应用")] + A1000, + + /// + /// 已存在同名或同编码项目 + /// + [ErrorCodeItemMetadata("已存在同名或同编码项目")] + xg1000, + + /// + /// 已存在相同证件号码人员 + /// + [ErrorCodeItemMetadata("已存在相同证件号码人员")] + xg1001, + + /// + /// 检测数据不存在 + /// + [ErrorCodeItemMetadata("检测数据不存在")] + xg1002, + + /// + /// 请添加数据列 + /// + [ErrorCodeItemMetadata("请添加数据列")] + db1000, + + /// + /// 数据表不存在 + /// + [ErrorCodeItemMetadata("数据表不存在")] + db1001, + + /// + /// 数据表不存在 + /// + [ErrorCodeItemMetadata("不允许添加相同字段名")] + db1002, + + /// + /// 实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试 + /// + [ErrorCodeItemMetadata("实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试")] + db1003, + + /// + /// 父节点不存在 + /// + [ErrorCodeItemMetadata("父节点不存在")] + R2000, + + /// + /// 当前节点Id不能与父节点Id相同 + /// + [ErrorCodeItemMetadata("当前节点Id不能与父节点Id相同")] + R2001, + + /// + /// 已有相同编码或名称 + /// + [ErrorCodeItemMetadata("已有相同编码或名称")] + R2002, + + /// + /// 行政区代码只能为6、9或12位 + /// + [ErrorCodeItemMetadata("行政区代码只能为6、9或12位")] + R2003, + + /// + /// 父节点不能为自己的子节点 + /// + [ErrorCodeItemMetadata("父节点不能为自己的子节点")] + R2004, + + /// + /// 同步国家统计局数据异常,请稍后重试 + /// + [ErrorCodeItemMetadata("同步国家统计局数据异常,请稍后重试")] + R2005, + + /// + /// 默认租户状态禁止修改 + /// + [ErrorCodeItemMetadata("默认租户状态禁止修改")] + Z1001, + + /// + /// 禁止创建此类型的数据库 + /// + [ErrorCodeItemMetadata("禁止创建此类型的数据库")] + Z1002, + + /// + /// 租户不存在或已禁用 + /// + [ErrorCodeItemMetadata("租户不存在或已禁用")] + Z1003, + + /// + /// 租户库连接不能为空 + /// + [ErrorCodeItemMetadata("租户库连接不能为空")] + Z1004, + + /// + /// 身份标识已存在 + /// + [ErrorCodeItemMetadata("身份标识已存在")] + O1000, + + /// + /// 禁止非超级管理员操作 + /// + [ErrorCodeItemMetadata("禁止非超级管理员操作")] + SA001 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/EsClientTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/EsClientTypeEnum.cs new file mode 100644 index 0000000..14d55e7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/EsClientTypeEnum.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES客户端类型(标识不同场景) +/// +public enum EsClientTypeEnum +{ + /// + /// 日志专用 + /// + Logging, + + /// + /// 业务数据 + /// + Business +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterLogicEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterLogicEnum.cs new file mode 100644 index 0000000..5e898f2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterLogicEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 过滤条件 +/// +[Description("过滤条件")] +public enum FilterLogicEnum +{ + /// + /// 并且 + /// + [Description("并且")] + And, + + /// + /// 或者 + /// + [Description("或者")] + Or, + + /// + /// 异或 + /// + [Description("异或")] + Xor +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterOperatorEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterOperatorEnum.cs new file mode 100644 index 0000000..0de28dd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FilterOperatorEnum.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 过滤逻辑运算符 +/// +[Description("过滤逻辑运算符")] +public enum FilterOperatorEnum +{ + /// + /// 等于(=) + /// + [Description("等于")] + EQ, + + /// + /// 不等于(!=) + /// + [Description("不等于")] + NEQ, + + /// + /// 小于 + /// + [Description("小于")] + LT, + + /// + /// 小于等于 + /// + [Description("小于等于")] + LTE, + + /// + /// 大于(>) + /// + [Description("大于")] + GT, + + /// + /// 大于等于(>=) + /// + [Description("大于等于")] + GTE, + + /// + /// 开始包含 + /// + [Description("开始包含")] + StartsWith, + + /// + /// 末尾包含 + /// + [Description("末尾包含")] + EndsWith, + + /// + /// 包含 + /// + [Description("包含")] + Contains +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FinishStatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FinishStatusEnum.cs new file mode 100644 index 0000000..90e188f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/FinishStatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 完成状态枚举 +/// +[Description("完成状态枚举")] +public enum FinishStatusEnum +{ + /// + /// 已完成 + /// + [Description("已完成"), Theme("success")] + Finish = 1, + + /// + /// 未完成 + /// + [Description("未完成"), Theme("danger")] + UnFinish = 0, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/GenderEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/GenderEnum.cs new file mode 100644 index 0000000..ceea591 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/GenderEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 性别枚举(GB/T 2261.1-2003) +/// +[Description("性别枚举")] +public enum GenderEnum +{ + /// + /// 未知 + /// + [Description("未知"), Theme("info")] + Unknown = 0, + + /// + /// 男性 + /// + [Description("男性"), Theme("success")] + Male = 1, + + /// + /// 女性 + /// + [Description("女性"), Theme("danger")] + Female = 2, + + ///// + ///// 未说明的性别 + ///// + //[Description("未说明的性别"), Theme("warning")] + //Unspecified = 9 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/HttpMethodEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/HttpMethodEnum.cs new file mode 100644 index 0000000..f930893 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/HttpMethodEnum.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// HTTP请求方法枚举 +/// +[Description("HTTP请求方法枚举")] +public enum HttpMethodEnum +{ + /// + /// HTTP "GET" method. + /// + [Description("HTTP \"GET\" method.")] + Get, + + /// + /// HTTP "POST" method. + /// + [Description("HTTP \"POST\" method.")] + Post, + + /// + /// HTTP "PUT" method. + /// + [Description(" HTTP \"PUT\" method.")] + Put, + + /// + /// HTTP "DELETE" method. + /// + [Description("HTTP \"DELETE\" method.")] + Delete, + + /// + /// HTTP "PATCH" method. + /// + [Description("HTTP \"PATCH\" method. ")] + Patch, + + /// + /// HTTP "HEAD" method. + /// + [Description("HTTP \"HEAD\" method.")] + Head, + + /// + /// HTTP "OPTIONS" method. + /// + [Description("HTTP \"OPTIONS\" method.")] + Options, + + /// + /// HTTP "TRACE" method. + /// + [Description(" HTTP \"TRACE\" method.")] + Trace, + + /// + /// HTTP "CONNECT" method. + /// + [Description("HTTP \"CONNECT\" method.")] + Connect +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobCreateTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobCreateTypeEnum.cs new file mode 100644 index 0000000..2d90b69 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobCreateTypeEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 作业创建类型枚举 +/// +[Description("作业创建类型枚举")] +public enum JobCreateTypeEnum +{ + /// + /// 内置 + /// + [Description("内置"), Theme("info")] + BuiltIn = 0, + + /// + /// 脚本 + /// + [Description("脚本")] + Script = 1, + + /// + /// HTTP请求 + /// + [Description("HTTP请求")] + Http = 2, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobStatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobStatusEnum.cs new file mode 100644 index 0000000..b06c65c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/JobStatusEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 岗位状态枚举 +/// +[Description("岗位状态枚举")] +public enum JobStatusEnum +{ + /// + /// 在职 + /// + [Description("在职")] + On = 1, + + /// + /// 离职 + /// + [Description("离职")] + Off = 2, + + /// + /// 请假 + /// + [Description("请假")] + Leave = 3, + + /// + /// 其他 + /// + [Description("其他")] + Other = 4, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginModeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginModeEnum.cs new file mode 100644 index 0000000..55e8ddb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginModeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 登录模式枚举 +/// +[Description("登录模式枚举")] +public enum LoginModeEnum +{ + /// + /// PC模式 + /// + [Description("PC模式")] + PC = 1, + + /// + /// APP + /// + [Description("APP")] + APP = 2 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginTypeEnum.cs new file mode 100644 index 0000000..9fa0950 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/LoginTypeEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 登录类型枚举 +/// +[Description("登录类型枚举")] +public enum LoginTypeEnum +{ + /// + /// PC登录 + /// + [Description("PC登录")] + Login = 1, + + /// + /// PC退出 + /// + [Description("PC退出")] + Logout = 2, + + /// + /// PC注册 + /// + [Description("PC注册")] + Register = 3 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MaritalStatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MaritalStatusEnum.cs new file mode 100644 index 0000000..b3787b2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MaritalStatusEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 婚姻状况枚举 +/// +[Description("婚姻状况枚举")] +public enum MaritalStatusEnum +{ + /// + /// 未婚 + /// + [Description("未婚")] + UnMarried = 1, + + /// + /// 已婚 + /// + [Description("已婚")] + Married = 2, + + /// + /// 离异 + /// + [Description("离异")] + Divorce = 3, + + /// + /// 再婚 + /// + [Description("再婚")] + Remarry = 4, + + /// + /// 丧偶 + /// + [Description("丧偶")] + Widowed = 5, + + /// + /// 未知 + /// + [Description("未知")] + None = 6, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MenuTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MenuTypeEnum.cs new file mode 100644 index 0000000..0912635 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MenuTypeEnum.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单类型枚举 +/// +[Description("系统菜单类型枚举")] +public enum MenuTypeEnum +{ + /// + /// 目录 + /// + + [Description("目录"), Theme("warning")] + Dir = 1, + + /// + /// 菜单 + /// + [Description("菜单")] + Menu = 2, + + /// + /// 按钮 + /// + [Description("按钮"), Theme("info")] + Btn = 3 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MessageTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MessageTypeEnum.cs new file mode 100644 index 0000000..1ac3bfd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/MessageTypeEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 消息类型枚举 +/// +[Description("消息类型枚举")] +public enum MessageTypeEnum +{ + /// + /// 普通信息 + /// + [Description("消息"), Theme("info")] + Info = 0, + + /// + /// 成功提示 + /// + [Description("成功"), Theme("success")] + Success = 1, + + /// + /// 警告提示 + /// + [Description("警告"), Theme("warning")] + Warning = 2, + + /// + /// 错误提示 + /// + [Description("错误"), Theme("danger")] + Error = 3 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NationEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NationEnum.cs new file mode 100644 index 0000000..25dd2ed --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NationEnum.cs @@ -0,0 +1,350 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 民族枚举 +/// +[Description("民族枚举")] +public enum NationEnum +{ + /// + /// 汉族 + /// + [Description("汉族")] + HanZu = 1, + + /// + /// 壮族 + /// + [Description("壮族")] + ZhuangZu = 2, + + /// + /// 满族 + /// + [Description("满族")] + ManZu = 3, + + /// + /// 回族 + /// + [Description("回族")] + HuiZu = 4, + + /// + /// 苗族 + /// + [Description("苗族")] + MiaoZu = 5, + + /// + /// 维吾尔族 + /// + [Description("维吾尔族")] + WeiWuErZu = 6, + + /// + /// 土家族 + /// + [Description("土家族")] + TuJiaZu = 7, + + /// + /// 彝族 + /// + [Description("彝族")] + YiZu = 8, + + /// + /// 蒙古族 + /// + [Description("蒙古族")] + MengGuZu = 9, + + /// + /// 藏族 + /// + [Description("藏族")] + ZangZu = 10, + + /// + /// 布依族 + /// + [Description("布依族")] + BuYiZu = 11, + + /// + /// 侗族 + /// + [Description("侗族")] + DongZu = 12, + + /// + /// 瑶族 + /// + [Description("瑶族")] + YaoZu = 13, + + /// + /// 朝鲜族 + /// + [Description("朝鲜族")] + ChaoXianZu = 14, + + /// + /// 白族 + /// + [Description("白族")] + BaiZu = 15, + + /// + /// 哈尼族 + /// + [Description("哈尼族")] + HaNiZu = 16, + + /// + /// 哈萨克族 + /// + [Description("哈萨克族")] + HaSaKeZu = 17, + + /// + /// 黎族 + /// + [Description("黎族")] + LiZu = 18, + + /// + /// 傣族 + /// + [Description("傣族")] + DaiZu = 19, + + /// + /// 畲族 + /// + [Description("畲族")] + SheZu = 20, + + /// + /// 傈僳族 + /// + [Description("傈僳族")] + LiSuZu = 21, + + /// + /// 仡佬族 + /// + [Description("仡佬族")] + GeLaoZu = 22, + + /// + /// 拉祜族 + /// + [Description("拉祜族")] + LaHuZu = 23, + + /// + /// 东乡族 + /// + [Description("东乡族")] + DongXiangZu = 24, + + /// + /// 纳西族 + /// + [Description("纳西族")] + NaXiZu = 25, + + /// + /// 景颇族 + /// + [Description("景颇族")] + JingPoZu = 26, + + /// + /// 柯尔克孜族 + /// + [Description("柯尔克孜族")] + KeErKeZiZu = 27, + + /// + /// 土族 + /// + [Description("土族")] + TuZu = 28, + + /// + /// 达斡尔族 + /// + [Description("达斡尔族")] + DaWoErZu = 29, + + /// + /// 仫佬族 + /// + [Description("仫佬族")] + MuLaoZu = 30, + + /// + /// 羌族 + /// + [Description("羌族")] + QiangZu = 31, + + /// + /// 布朗族 + /// + [Description("布朗族")] + BuLangZu = 32, + + /// + /// 撒拉族 + /// + [Description("撒拉族")] + SaLaZu = 33, + + /// + /// 毛南族 + /// + [Description("毛南族")] + MaoNanZu = 34, + + /// + /// 仡族 + /// + [Description("仡族")] + GeZu = 35, + + /// + /// 锡伯族 + /// + [Description("锡伯族")] + XiBoZu = 36, + + /// + /// 阿昌族 + /// + [Description("阿昌族")] + AChangZu = 37, + + /// + /// 普米族 + /// + [Description("普米族")] + PuMiZu = 38, + + /// + /// 塔吉克族 + /// + [Description("塔吉克族")] + TaJiKeZu = 39, + + /// + /// 怒族 + /// + [Description("怒族")] + NuZu = 40, + + /// + /// 乌孜别克族 + /// + [Description("乌孜别克族")] + WuZiBieKeZu = 41, + + /// + /// 俄罗斯族 + /// + [Description("俄罗斯族")] + ELuoSiZu = 42, + + /// + /// 鄂温克族 + /// + [Description("鄂温克族")] + EwenKeZu = 43, + + /// + /// 德昂族 + /// + [Description("德昂族")] + DeAngZu = 44, + + /// + /// 保安族 + /// + [Description("保安族")] + BaoAnZu = 45, + + /// + /// 裕固族 + /// + [Description("裕固族")] + YuGuZu = 46, + + /// + /// 京族 + /// + [Description("京族")] + JingZu = 47, + + /// + /// 塔塔尔族 + /// + [Description("塔塔尔族")] + TaTaErZu = 48, + + /// + /// 独龙族 + /// + [Description("独龙族")] + DuLongZu = 49, + + /// + /// 鄂伦春族 + /// + [Description("鄂伦春族")] + ELunChunZu = 50, + + /// + /// 赫哲族 + /// + [Description("赫哲族")] + HeZheZu = 51, + + /// + /// 门巴族 + /// + [Description("门巴族")] + MenBaZu = 52, + + /// + /// 珞巴族 + /// + [Description("珞巴族")] + LuoBaZu = 53, + + /// + /// 高山族 + /// + [Description("高山族")] + GaoShanZu = 54, + + /// + /// 佤族 + /// + [Description("佤族")] + WaZu = 55, + + /// + /// 基诺族 + /// + [Description("基诺族")] + JiNuoZu = 56 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeStatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeStatusEnum.cs new file mode 100644 index 0000000..0bd68e8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeStatusEnum.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告状态枚举 +/// +[Description("通知公告状态枚举")] +public enum NoticeStatusEnum +{ + /// + /// 草稿 + /// + [Description("草稿"), Theme("info")] + DRAFT = 0, + + /// + /// 发布 + /// + [Description("发布")] + PUBLIC = 1, + + /// + /// 撤回 + /// + [Description("撤回")] + CANCEL = 2, + + /// + /// 删除 + /// + [Description("删除")] + DELETED = 3 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeTypeEnum.cs new file mode 100644 index 0000000..387bf29 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告状类型枚举 +/// +[Description("通知公告状类型枚举")] +public enum NoticeTypeEnum +{ + /// + /// 通知 + /// + [Description("通知")] + NOTICE = 1, + + /// + /// 公告 + /// + [Description("公告")] + ANNOUNCEMENT = 2, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs new file mode 100644 index 0000000..a22fc26 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/NoticeUserStatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通知公告用户状态枚举 +/// +[Description("通知公告用户状态枚举")] +public enum NoticeUserStatusEnum +{ + /// + /// 未读 + /// + [Description("未读")] + UNREAD = 0, + + /// + /// 已读 + /// + [Description("已读"), Theme("info")] + READ = 1 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PlatformTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PlatformTypeEnum.cs new file mode 100644 index 0000000..38f985b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PlatformTypeEnum.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 平台类型枚举 +/// +[Description("平台类型枚举")] +public enum PlatformTypeEnum +{ + /// + /// 微信公众号 + /// + [Description("微信公众号")] + 微信公众号 = 1, + + /// + /// 微信小程序 + /// + [Description("微信小程序")] + 微信小程序 = 2, + + /// + /// QQ + /// + [Description("QQ")] + QQ = 3, + + /// + /// 支付宝 + /// + [Description("支付宝")] + Alipay = 4, + + /// + /// Gitee + /// + [Description("Gitee")] + Gitee = 5, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PrintTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PrintTypeEnum.cs new file mode 100644 index 0000000..422972f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/PrintTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 打印类型枚举 +/// +[Description("打印类型枚举")] +public enum PrintTypeEnum +{ + /// + /// 浏览器打印 + /// + [Description("浏览器打印")] + Browser = 1, + + /// + /// 浏览器打印 + /// + [Description("客户端打印")] + Client = 2, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/RequestTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/RequestTypeEnum.cs new file mode 100644 index 0000000..f1f4aee --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/RequestTypeEnum.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// HTTP请求类型 +/// +[Description("HTTP请求类型")] +public enum RequestTypeEnum +{ + /// + /// 执行内部方法 + /// + Run = 0, + + /// + /// GET + /// + Get = 1, + + /// + /// POST + /// + Post = 2, + + /// + /// PUT + /// + Put = 3, + + /// + /// DELETE + /// + Delete = 4 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/StatusEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/StatusEnum.cs new file mode 100644 index 0000000..246f6dc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/StatusEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 通用状态枚举 +/// +[Description("通用状态枚举")] +public enum StatusEnum +{ + /// + /// 启用 + /// + [Description("启用"), Theme("success")] + Enable = 1, + + /// + /// 停用 + /// + [Description("停用"), Theme("danger")] + Disable = 2, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs new file mode 100644 index 0000000..7245626 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件类型-系统用户操作枚举 +/// +[SuppressSniffer] +[Description("事件类型-系统用户操作枚举")] +public enum SysUserEventTypeEnum +{ + /// + /// 增加用户 + /// + [Description("增加用户")] + Add = 111, + + /// + /// 更新用户 + /// + [Description("更新用户")] + Update = 222, + + /// + /// 授权用户角色 + /// + [Description("授权用户角色")] + UpdateRole = 333, + + /// + /// 删除用户 + /// + [Description("删除用户")] + Delete = 444, + + /// + /// 设置用户状态 + /// + [Description("设置用户状态")] + SetStatus = 555, + + /// + /// 修改密码 + /// + [Description("修改密码")] + ChangePwd = 666, + + /// + /// 重置密码 + /// + [Description("重置密码")] + ResetPwd = 777, + + /// + /// 解除登录锁定 + /// + [Description("解除登录锁定")] + UnlockLogin = 888, + + /// + /// 注册用户 + /// + [Description("注册用户")] + Register = 999, + + /// + /// 用户登录 + /// + [Description("用户登录")] + Login = 1000, + + /// + /// 用户退出 + /// + [Description("用户退出")] + LoginOut = 1001, + + /// + /// RefreshToken + /// + [Description("刷新Token")] + RefreshToken = 1002, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TemplateTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TemplateTypeEnum.cs new file mode 100644 index 0000000..9519a7b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TemplateTypeEnum.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 消息模板类型枚举 +/// +[Description("消息模板类型枚举")] +public enum TemplateTypeEnum +{ + /// + /// 通知公告 + /// + [Description("通知")] + Notice = 1, + + /// + /// 短信 + /// + [Description("短信")] + SMS = 2, + + /// + /// 邮件 + /// + [Description("邮件")] + Email = 3, + + /// + /// 微信 + /// + [Description("微信")] + Wechat = 4, + + /// + /// 钉钉 + /// + [Description("钉钉")] + DingTalk = 5, + + /// + /// 企业微信 + /// + [Description("企业微信")] + WeChatWork = 7 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TenantTypeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TenantTypeEnum.cs new file mode 100644 index 0000000..c9eeb90 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/TenantTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 租户类型枚举 +/// +[Description("租户类型枚举")] +public enum TenantTypeEnum +{ + /// + /// ID隔离 + /// + [Description("ID隔离")] + Id = 0, + + /// + /// 库隔离 + /// + [Description("库隔离")] + Db = 1, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs new file mode 100644 index 0000000..ba30a3e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WechatReturnCodeEnum.cs @@ -0,0 +1,289 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信开发返回码 +/// +[Description("微信开发返回码")] +public enum WechatReturnCodeEnum +{ + SenparcWeixinSDK配置错误 = -99, // 0xFFFFFF9D + 系统繁忙此时请开发者稍候再试 = -1, // 0xFFFFFFFF + 请求成功 = 0, + 工商数据返回_企业已注销 = 101, // 0x00000065 + 工商数据返回_企业不存在或企业信息未更新 = 102, // 0x00000066 + 工商数据返回_企业法定代表人姓名不一致 = 103, // 0x00000067 + 工商数据返回_企业法定代表人身份证号码不一致 = 104, // 0x00000068 + 法定代表人身份证号码_工商数据未更新_请5_15个工作日之后尝试 = 105, // 0x00000069 + 工商数据返回_企业信息或法定代表人信息不一致 = 1000, // 0x000003E8 + 对方不是粉丝 = 10700, // 0x000029CC + 发送消息失败_对方关闭了接收消息 = 10703, // 0x000029CF + 发送消息失败_48小时内用户未互动 = 10706, // 0x000029D2 + POST参数非法 = 20002, // 0x00004E22 + 获取access_token时AppSecret错误或者access_token无效 = 40001, // 0x00009C41 + + /// + /// 公众号:不合法的凭证类型 + /// 小程序:暂无生成权限 + /// + 不合法的凭证类型 = 40002, // 0x00009C42 + + 不合法的OpenID = 40003, // 0x00009C43 + 不合法的媒体文件类型 = 40004, // 0x00009C44 + 不合法的文件类型 = 40005, // 0x00009C45 + 不合法的文件大小 = 40006, // 0x00009C46 + 不合法的媒体文件id = 40007, // 0x00009C47 + 不合法的消息类型_40008 = 40008, // 0x00009C48 + 不合法的图片文件大小 = 40009, // 0x00009C49 + 不合法的语音文件大小 = 40010, // 0x00009C4A + 不合法的视频文件大小 = 40011, // 0x00009C4B + 不合法的缩略图文件大小 = 40012, // 0x00009C4C + + /// + /// 微信:不合法的APPID + /// 小程序:生成权限被封禁 + /// + 不合法的APPID = 40013, // 0x00009C4D + + 不合法的access_token = 40014, // 0x00009C4E + 不合法的菜单类型 = 40015, // 0x00009C4F + 不合法的按钮个数1 = 40016, // 0x00009C50 + 不合法的按钮个数2 = 40017, // 0x00009C51 + 不合法的按钮名字长度 = 40018, // 0x00009C52 + 不合法的按钮KEY长度 = 40019, // 0x00009C53 + 不合法的按钮URL长度 = 40020, // 0x00009C54 + 不合法的菜单版本号 = 40021, // 0x00009C55 + 不合法的子菜单级数 = 40022, // 0x00009C56 + 不合法的子菜单按钮个数 = 40023, // 0x00009C57 + 不合法的子菜单按钮类型 = 40024, // 0x00009C58 + 不合法的子菜单按钮名字长度 = 40025, // 0x00009C59 + 不合法的子菜单按钮KEY长度 = 40026, // 0x00009C5A + 不合法的子菜单按钮URL长度 = 40027, // 0x00009C5B + 不合法的自定义菜单使用用户 = 40028, // 0x00009C5C + 不合法的oauth_code = 40029, // 0x00009C5D + 不合法的refresh_token = 40030, // 0x00009C5E + 不合法的openid列表 = 40031, // 0x00009C5F + 不合法的openid列表长度 = 40032, // 0x00009C60 + 不合法的请求字符不能包含uxxxx格式的字符 = 40033, // 0x00009C61 + 不合法的参数 = 40035, // 0x00009C63 + template_id不正确 = 40037, // 0x00009C65 + 不合法的请求格式 = 40038, // 0x00009C66 + 不合法的URL长度 = 40039, // 0x00009C67 + 不合法的分组id = 40050, // 0x00009C72 + 分组名字不合法 = 40051, // 0x00009C73 + + /// + /// 公众号:输入参数有误 + /// 小程序:参数expire_time填写错误 + /// + 输入参数有误 = 40097, // 0x00009CA1 + + appsecret不正确 = 40125, // 0x00009CBD + 调用接口的IP地址不在白名单中 = 40164, // 0x00009CE4 + 参数path填写错误 = 40165, // 0x00009CE5 + 小程序Appid不存在 = 40166, // 0x00009CE6 + 参数query填写错误 = 40212, // 0x00009D14 + 缺少access_token参数 = 41001, // 0x0000A029 + 缺少appid参数 = 41002, // 0x0000A02A + 缺少refresh_token参数 = 41003, // 0x0000A02B + 缺少secret参数 = 41004, // 0x0000A02C + 缺少多媒体文件数据 = 41005, // 0x0000A02D + 缺少media_id参数 = 41006, // 0x0000A02E + 缺少子菜单数据 = 41007, // 0x0000A02F + 缺少oauth_code = 41008, // 0x0000A030 + 缺少openid = 41009, // 0x0000A031 + form_id不正确_或者过期 = 41028, // 0x0000A044 + form_id已被使用 = 41029, // 0x0000A045 + page不正确 = 41030, // 0x0000A046 + access_token超时 = 42001, // 0x0000A411 + refresh_token超时 = 42002, // 0x0000A412 + oauth_code超时 = 42003, // 0x0000A413 + 需要GET请求 = 43001, // 0x0000A7F9 + 需要POST请求 = 43002, // 0x0000A7FA + 需要HTTPS请求 = 43003, // 0x0000A7FB + 需要接收者关注 = 43004, // 0x0000A7FC + 需要好友关系 = 43005, // 0x0000A7FD + + /// [小程序订阅消息]用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系 + 用户拒绝接受消息 = 43101, // 0x0000A85D + + 没有权限 = 43104, // 0x0000A860 + 多媒体文件为空 = 44001, // 0x0000ABE1 + POST的数据包为空 = 44002, // 0x0000ABE2 + 图文消息内容为空 = 44003, // 0x0000ABE3 + 文本消息内容为空 = 44004, // 0x0000ABE4 + 多媒体文件大小超过限制 = 45001, // 0x0000AFC9 + 消息内容超过限制 = 45002, // 0x0000AFCA + 标题字段超过限制 = 45003, // 0x0000AFCB + 描述字段超过限制 = 45004, // 0x0000AFCC + 链接字段超过限制 = 45005, // 0x0000AFCD + 图片链接字段超过限制 = 45006, // 0x0000AFCE + 语音播放时间超过限制 = 45007, // 0x0000AFCF + 图文消息超过限制 = 45008, // 0x0000AFD0 + 接口调用超过限制 = 45009, // 0x0000AFD1 + 创建菜单个数超过限制 = 45010, // 0x0000AFD2 + 回复时间超过限制 = 45015, // 0x0000AFD7 + 系统分组不允许修改 = 45016, // 0x0000AFD8 + 分组名字过长 = 45017, // 0x0000AFD9 + 分组数量超过上限 = 45018, // 0x0000AFDA + 超出响应数量限制 = 45047, // 0x0000AFF7 + 创建的标签数过多请注意不能超过100个 = 45056, // 0x0000B000 + 标签名非法请注意不能和其他标签重名 = 45157, // 0x0000B065 + 标签名长度超过30个字节 = 45158, // 0x0000B066 + 不存在媒体数据 = 46001, // 0x0000B3B1 + 不存在的菜单版本 = 46002, // 0x0000B3B2 + 不存在的菜单数据 = 46003, // 0x0000B3B3 + 解析JSON_XML内容错误 = 47001, // 0x0000B799 + + /// [小程序订阅消息]模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错 + 模板参数不准确 = 47003, // 0x0000B79B + + api功能未授权 = 48001, // 0x0000BB81 + 用户未授权该api = 50001, // 0x0000C351 + 名称格式不合法 = 53010, // 0x0000CF12 + 名称检测命中频率限制 = 53011, // 0x0000CF13 + 禁止使用该名称 = 53012, // 0x0000CF14 + 公众号_名称与已有公众号名称重复_小程序_该名称与已有小程序名称重复 = 53013, // 0x0000CF15 + 公众号_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_ = 53014, // 0x0000CF16 + 公众号_该名称与已有小程序名称重复_需与该小程序帐号相同主体才可申请_小程序_该名称与已有公众号名称重复_需与该公众号帐号相同主体才可申请 = 53015, // 0x0000CF17 + 公众号_该名称与已有多个小程序名称重复_暂不支持申请_小程序_该名称与已有多个公众号名称重复_暂不支持申请 = 53016, // 0x0000CF18 + 公众号_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A = 53017, // 0x0000CF19 + 名称命中微信号 = 53018, // 0x0000CF1A + 名称在保护期内 = 53019, // 0x0000CF1B + 法人姓名与微信号不一致 = 61070, // 0x0000EE8E + 系统错误system_error = 61450, // 0x0000F00A + 参数错误invalid_parameter = 61451, // 0x0000F00B + 无效客服账号invalid_kf_account = 61452, // 0x0000F00C + 客服帐号已存在kf_account_exsited = 61453, // 0x0000F00D + + /// + /// 客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)(invalid kf_acount length) + /// + 客服帐号名长度超过限制 = 61454, // 0x0000F00E + + /// + /// 客服帐号名包含非法字符(仅允许英文+数字)(illegal character in kf_account) + /// + 客服帐号名包含非法字符 = 61455, // 0x0000F00F + + /// 客服帐号个数超过限制(10个客服账号)(kf_account count exceeded) + 客服帐号个数超过限制 = 61456, // 0x0000F010 + + 无效头像文件类型invalid_file_type = 61457, // 0x0000F011 + 日期格式错误 = 61500, // 0x0000F03C + 日期范围错误 = 61501, // 0x0000F03D + 发送消息失败_该用户已被加入黑名单_无法向此发送消息 = 62751, // 0x0000F51F + 门店不存在 = 65115, // 0x0000FE5B + 该门店状态不允许更新 = 65118, // 0x0000FE5E + 标签格式错误 = 85006, // 0x00014C0E + 页面路径错误 = 85007, // 0x00014C0F + 类目填写错误 = 85008, // 0x00014C10 + 已经有正在审核的版本 = 85009, // 0x00014C11 + item_list有项目为空 = 85010, // 0x00014C12 + 标题填写错误 = 85011, // 0x00014C13 + 无效的审核id = 85012, // 0x00014C14 + 版本输入错误 = 85015, // 0x00014C17 + 没有审核版本 = 85019, // 0x00014C1B + 审核状态未满足发布 = 85020, // 0x00014C1C + 状态不可变 = 85021, // 0x00014C1D + action非法 = 85022, // 0x00014C1E + 审核列表填写的项目数不在1到5以内 = 85023, // 0x00014C1F + 需要补充相应资料_填写org_code和other_files参数 = 85024, // 0x00014C20 + 管理员手机登记数量已超过上限 = 85025, // 0x00014C21 + 该微信号已绑定5个管理员 = 85026, // 0x00014C22 + 管理员身份证已登记过5次 = 85027, // 0x00014C23 + 该主体登记数量已超过上限 = 85028, // 0x00014C24 + 商家名称已被占用 = 85029, // 0x00014C25 + 不能使用该名称 = 85031, // 0x00014C27 + 该名称在侵权投诉保护期 = 85032, // 0x00014C28 + 名称包含违规内容或微信等保留字 = 85033, // 0x00014C29 + 商家名称在改名15天保护期内 = 85034, // 0x00014C2A + 需与该帐号相同主体才可申请 = 85035, // 0x00014C2B + 介绍中含有虚假混淆内容 = 85036, // 0x00014C2C + 头像或者简介修改达到每个月上限 = 85049, // 0x00014C39 + 正在审核中_请勿重复提交 = 85050, // 0x00014C3A + 请先成功创建门店后再调用 = 85053, // 0x00014C3D + 临时mediaid无效 = 85056, // 0x00014C40 + 链接错误 = 85066, // 0x00014C4A + 测试链接不是子链接 = 85068, // 0x00014C4C + 校验文件失败 = 85069, // 0x00014C4D + 个人类型小程序无法设置二维码规则 = 85070, // 0x00014C4E + 已添加该链接_请勿重复添加 = 85071, // 0x00014C4F + 该链接已被占用 = 85072, // 0x00014C50 + 二维码规则已满 = 85073, // 0x00014C51 + 小程序未发布_小程序必须先发布代码才可以发布二维码跳转规则 = 85074, // 0x00014C52 + 个人类型小程序无法设置二维码规则1 = 85075, // 0x00014C53 + 小程序没有线上版本_不能进行灰度 = 85079, // 0x00014C57 + 小程序提交的审核未审核通过 = 85080, // 0x00014C58 + 无效的发布比例 = 85081, // 0x00014C59 + 当前的发布比例需要比之前设置的高 = 85082, // 0x00014C5A + 小程序提审数量已达本月上限 = 85085, // 0x00014C5D + 提交代码审核之前需提前上传代码 = 85086, // 0x00014C5E + 小程序已使用_api_navigateToMiniProgram_请声明跳转_appid_列表后再次提交 = 85087, // 0x00014C5F + 不是由第三方代小程序进行调用 = 86000, // 0x00014FF0 + 不存在第三方的已经提交的代码 = 86001, // 0x00014FF1 + 小程序还未设置昵称_头像_简介_请先设置完后再重新提交 = 86002, // 0x00014FF2 + 无效微信号 = 86004, // 0x00014FF4 + + /// + /// 小程序为“签名错误”。对应公众号: 87009, “errmsg” : “reply is not exists” //该回复不存在 + /// + 签名错误 = 87009, // 0x000153E1 + + 现网已经在灰度发布_不能进行版本回退 = 87011, // 0x000153E3 + 该版本不能回退_可能的原因_1_无上一个线上版用于回退_2_此版本为已回退版本_不能回退_3_此版本为回退功能上线之前的版本_不能回退 = 87012, // 0x000153E4 + 内容含有违法违规内容 = 87014, // 0x000153E6 + 没有留言权限 = 88000, // 0x000157C0 + 该图文不存在 = 88001, // 0x000157C1 + 文章存在敏感信息 = 88002, // 0x000157C2 + 精选评论数已达上限 = 88003, // 0x000157C3 + 已被用户删除_无法精选 = 88004, // 0x000157C4 + 已经回复过了 = 88005, // 0x000157C5 + 回复超过长度限制或为0 = 88007, // 0x000157C7 + 该评论不存在 = 88008, // 0x000157C8 + 获取评论数目不合法 = 88010, // 0x000157CA + 该公众号_小程序已经绑定了开放平台帐号 = 89000, // 0x00015BA8 + 业务域名无更改_无需重复设置 = 89019, // 0x00015BBB + 尚未设置小程序业务域名_请先在第三方平台中设置小程序业务域名后在调用本接口 = 89020, // 0x00015BBC + 请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名 = 89021, // 0x00015BBD + 业务域名数量超过限制_最多可以添加100个业务域名 = 89029, // 0x00015BC5 + 个人小程序不支持调用_setwebviewdomain_接口 = 89231, // 0x00015C8F + 内部错误 = 89247, // 0x00015C9F + 企业代码类型无效_请选择正确类型填写 = 89248, // 0x00015CA0 + 该主体已有任务执行中_距上次任务24h后再试 = 89249, // 0x00015CA1 + 未找到该任务 = 89250, // 0x00015CA2 + 待法人人脸核身校验 = 89251, // 0x00015CA3 + 法人_企业信息一致性校验中 = 89252, // 0x00015CA4 + 缺少参数 = 89253, // 0x00015CA5 + 第三方权限集不全_补全权限集全网发布后生效 = 89254, // 0x00015CA6 + 系统不稳定_请稍后再试_如多次失败请通过社区反馈 = 89401, // 0x00015D39 + 该审核单不在待审核队列_请检查是否已提交审核或已审完 = 89402, // 0x00015D3A + 本单属于平台不支持加急种类_请等待正常审核流程 = 89403, // 0x00015D3B + 本单已加速成功_请勿重复提交 = 89404, // 0x00015D3C + 本月加急额度不足_请提升提审质量以获取更多额度 = 89405, // 0x00015D3D + 该经营资质已添加_请勿重复添加 = 92000, // 0x00016760 + 附近地点添加数量达到上线_无法继续添加 = 92002, // 0x00016762 + 地点已被其它小程序占用 = 92003, // 0x00016763 + 附近功能被封禁 = 92004, // 0x00016764 + 地点正在审核中 = 92005, // 0x00016765 + 地点正在展示小程序 = 92006, // 0x00016766 + 地点审核失败 = 92007, // 0x00016767 + 程序未展示在该地点 = 92008, // 0x00016768 + 小程序未上架或不可见 = 92009, // 0x00016769 + 地点不存在 = 93010, // 0x00016B52 + 个人类型小程序不可用 = 93011, // 0x00016B53 + 已下发的模板消息法人并未确认且已超时_24h_未进行身份证校验 = 100001, // 0x000186A1 + 已下发的模板消息法人并未确认且已超时_24h_未进行人脸识别校验 = 100002, // 0x000186A2 + 已下发的模板消息法人并未确认且已超时_24h = 100003, // 0x000186A3 + 此账号已被封禁_无法操作 = 200011, // 0x00030D4B + 私有模板数已达上限_上限_50_个 = 200012, // 0x00030D4C + 此模版已被封禁_无法选用 = 200013, // 0x00030D4D + 模版tid参数错误 = 200014, // 0x00030D4E + 关键词列表kidList参数错误 = 200020, // 0x00030D54 + 场景描述sceneDesc参数错误 = 200021, // 0x00030D55 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WeekEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WeekEnum.cs new file mode 100644 index 0000000..279c1c6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/WeekEnum.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 周枚举 +/// +[Description("周枚举")] +public enum WeekEnum +{ + /// + /// 周一 + /// + [Description("周一")] + Monday = 1, + + /// + /// 周二 + /// + [Description("周二")] + Tuesday = 2, + + /// + /// 周三 + /// + [Description("周三")] + Wednesday = 3, + + /// + /// 周四 + /// + [Description("周四")] + Thursday = 4, + + /// + /// 周五 + /// + [Description("周五")] + Friday = 5, + + /// + /// 周六 + /// + [Description("周六")] + Saturday = 6, + + /// + /// 周日 + /// + [Description("周日")] + Sunday = 7, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/YesNoEnum.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/YesNoEnum.cs new file mode 100644 index 0000000..d26d141 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Enum/YesNoEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 是否枚举 +/// +[Description("是否枚举")] +public enum YesNoEnum +{ + /// + /// 是 + /// + [Description("是"), Theme("success")] + Y = 1, + + /// + /// 否 + /// + [Description("否"), Theme("danger")] + N = 2 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs new file mode 100644 index 0000000..e44ffd5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件订阅 +/// +public class AppEventSubscriber : IEventSubscriber, ISingleton, IDisposable +{ + private static readonly ISugarQueryable SysTenantQueryable = App.GetService().Queryable(); + private readonly IServiceScope _serviceScope; + + public AppEventSubscriber(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + } + + /// + /// 增加异常日志 + /// + /// + /// + [EventSubscribe(CommonConst.AddExLog)] + public async Task CreateExLog(EventHandlerExecutingContext context) + { + // 切换日志独立数据库 + var db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) + ? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) + : SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + await db.CopyNew().Insertable((SysLogEx)context.Source.Payload).ExecuteCommandAsync(); + } + + /// + /// 发送异常邮件 + /// + /// + /// + [EventSubscribe(CommonConst.SendErrorMail)] + public async Task SendOrderErrorMail(EventHandlerExecutingContext context) + { + long.TryParse(App.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value, out var tenantId); + var tenant = await SysTenantQueryable.FirstAsync(t => t.Id == tenantId); + var title = $"{tenant?.Title} 系统异常"; + await _serviceScope.ServiceProvider.GetRequiredService().SendEmail(JSON.Serialize(context.Source.Payload), title); + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventConsumer.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventConsumer.cs new file mode 100644 index 0000000..971d2e7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventConsumer.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Redis 消息扩展 +/// +/// +public class EventConsumer : IDisposable +{ + /// + /// + /// + private Task _consumerTask; + + /// + /// + /// + private CancellationTokenSource _consumerCts; + + /// + /// 消费者 + /// + public IProducerConsumer Consumer { get; } + + /// + /// 消息回调 + /// + public event EventHandler Received; + + /// + /// 构造函数 + /// + /// + public EventConsumer(IProducerConsumer consumer) => Consumer = consumer; + + /// + /// 启动 + /// + /// + public void Start() + { + if (Consumer is null) + { + throw new InvalidOperationException("Subscribe first using the Consumer.Subscribe() function"); + } + if (_consumerTask != null) + { + return; + } + _consumerCts = new CancellationTokenSource(); + var ct = _consumerCts.Token; + _consumerTask = Task.Factory.StartNew(async () => + { + while (!ct.IsCancellationRequested) + { + try + { + var cr = Consumer.TakeOne(10); + if (cr == null) continue; + Received?.Invoke(this, cr); + } + catch (Exception ex) + { + Console.WriteLine($"消息消费异常: {ex.Message}"); + await Task.Delay(1000); // 短暂等待后继续尝试 + } + } + }, ct, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + /// + /// 停止 + /// + /// + public async Task Stop() + { + if (_consumerCts == null || _consumerTask == null) return; + _consumerCts.Cancel(); + try + { + await _consumerTask; + } + finally + { + _consumerTask = null; + _consumerCts = null; + } + } + + /// + /// 释放 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 释放 + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_consumerTask != null) + { + Stop().Wait(); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventHandlerMonitor.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventHandlerMonitor.cs new file mode 100644 index 0000000..7f229ed --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/EventHandlerMonitor.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class EventHandlerMonitor : IEventHandlerMonitor +{ + public Task OnExecutingAsync(EventHandlerExecutingContext context) + { + //_logger.LogInformation("执行之前:{EventId}", context.Source.EventId); + return Task.CompletedTask; + } + + public Task OnExecutedAsync(EventHandlerExecutedContext context) + { + //_logger.LogInformation("执行之后:{EventId}", context.Source.EventId); + + if (context.Exception != null) + { + Log.Error($"EventHandlerMonitor.OnExecutedAsync 执行出错啦:{context.Source.EventId}", context.Exception); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs new file mode 100644 index 0000000..5c28f6c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs @@ -0,0 +1,146 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Threading.Channels; + +namespace Admin.NET.Core; + +/// +/// RabbitMQ自定义事件源存储器 +/// +public class RabbitMQEventSourceStore : IEventSourceStorer, IDisposable +{ + /// + /// 内存通道事件源存储器 + /// + private Channel _channelEventSource; + + /// + /// 路由键 + /// + private string _routeKey; + + /// + /// 连接对象 + /// + private IConnection _connection; + + /// + /// 通道对象 + /// + private IChannel _channel; + + /// + /// 构造函数 + /// + /// 连接工厂 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RabbitMQEventSourceStore(ConnectionFactory factory, string routeKey, int capacity) + { + InitEventSourceStore(factory, routeKey, capacity).GetAwaiter().GetResult(); + } + + /// + /// 初始化事件源存储器 + /// + /// 连接工厂 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + private async Task InitEventSourceStore(ConnectionFactory factory, string routeKey, int capacity) + { + // 配置通道(超出默认容量后进入等待) + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + // 创建有限容量通道 + _channelEventSource = Channel.CreateBounded(boundedChannelOptions); + + // 创建连接 + _connection = await factory.CreateConnectionAsync(); + // 路由键名 + _routeKey = routeKey; + + // 创建通道 + _channel = await _connection.CreateChannelAsync(); + + // 声明路由队列 + await _channel.QueueDeclareAsync(routeKey, false, false, false, null); + + // 创建消息订阅者 + var consumer = new AsyncEventingBasicConsumer(_channel); + + // 订阅消息并写入内存 Channel + consumer.ReceivedAsync += async (ch, ea) => + { + // 读取原始消息 + var stringEventSource = Encoding.UTF8.GetString(ea.Body.ToArray()); + + // 转换为 IEventSource,如果自定义了 EventSource,注意属性是可读可写 + var eventSource = JSON.Deserialize(stringEventSource); + + // 写入内存管道存储器 + await _channelEventSource.Writer.WriteAsync(eventSource); + + // 确认该消息已被消费 + await _channel.BasicAckAsync(ea.DeliveryTag, false); + }; + + // 启动消费者且设置为手动应答消息 + await _channel.BasicConsumeAsync(routeKey, false, consumer); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + if (eventSource == default) + throw new ArgumentNullException(nameof(eventSource)); + + // 判断是否是 ChannelEventSource 或自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 序列化及发布 + var data = Encoding.UTF8.GetBytes(JSON.Serialize(source)); + var props = new BasicProperties(); + props.ContentType = "text/plain"; + props.DeliveryMode = DeliveryModes.Persistent; + await _channel.BasicPublishAsync("", _routeKey, false, props, data); + } + else + { + // 处理动态订阅 + await _channelEventSource.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + var eventSource = await _channelEventSource.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs new file mode 100644 index 0000000..f507e7b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs @@ -0,0 +1,165 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Caching.Queues; +using Newtonsoft.Json; +using System.Threading.Channels; + +namespace Admin.NET.Core; + +/// +/// Redis自定义事件源存储器 +/// +/// +/// 在集群部署时,一般每一个消息只由一个服务节点消费一次。 +/// 有些特殊情情要通知到服务器群中的每一个节点(比如需要强制加载某些配置、重点服务等), +/// 在这种情况下就要以“broadcast:”开头来定义EventId, +/// 本系统会把“broadcast:”开头的事件视为“广播消息”保证集群中的每一个服务节点都能消费得到这个消息 +/// +public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable +{ + /// + /// 消费者 + /// + private readonly EventConsumer _eventConsumer; + + /// + /// 内存通道事件源存储器 + /// + private readonly Channel _channel; + + private IProducerConsumer _queueSingle; + + private RedisStream _queueBroadcast; + + private ILogger _logger; + + /// + /// 构造函数 + /// + /// Redis 连接对象 + /// 路由键 + /// 存储器最多能够处理多少消息,超过该容量进入等待写入 + public RedisEventSourceStorer(ICacheProvider cacheProvider, string routeKey, int capacity) + { + _logger = App.GetRequiredService>(); + + // 配置通道,设置超出默认容量后进入等待 + var boundedChannelOptions = new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }; + + // 创建有限容量通道 + _channel = Channel.CreateBounded(boundedChannelOptions); + + //_redis = redis as FullRedis; + + // 创建广播消息订阅者,即所有服务器节点都能收到消息(用来发布重启、Reload配置等消息) + FullRedis redis = (FullRedis)cacheProvider.Cache; + var clusterOpt = App.GetConfig("Cluster", true); + _queueBroadcast = redis.GetStream(routeKey + ":broadcast"); + _queueBroadcast.Group = clusterOpt.ServerId;//根据服务器标识分配到不同的分组里 + _queueBroadcast.Expire = TimeSpan.FromSeconds(10);//消息10秒过期() + _queueBroadcast.ConsumeAsync(OnConsumeBroadcast); + + // 创建队列消息订阅者,只要有一个服务节点消费了消息即可 + _queueSingle = redis.GetQueue(routeKey + ":single"); + _eventConsumer = new EventConsumer(_queueSingle); + + // 订阅消息写入 Channel + _eventConsumer.Received += async (send, cr) => + { + // var oriColor = Console.ForegroundColor; + try + { + ChannelEventSource ces = (ChannelEventSource)cr; + await ConsumeChannelEventSourceAsync(ces, ces.CancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "处理Received中的消息产生错误!"); + } + }; + _eventConsumer.Start(); + } + + private async Task OnConsumeBroadcast(string source, Message message, CancellationToken token) + { + ChannelEventSource ces = JsonConvert.DeserializeObject(source); + await ConsumeChannelEventSourceAsync(ces, token); + } + + private async Task ConsumeChannelEventSourceAsync(ChannelEventSource ces, CancellationToken cancel = default) + { + // 打印测试事件 + if (ces.EventId != null && ces.EventId.IndexOf(":Test") > 0) + { + var oriColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"有消息要处理{ces.EventId},{ces.Payload}"); + Console.ForegroundColor = oriColor; + } + await _channel.Writer.WriteAsync(ces, cancel); + } + + /// + /// 将事件源写入存储器 + /// + /// 事件源对象 + /// 取消任务 Token + /// + public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken) + { + // 空检查 + if (eventSource == default) + throw new ArgumentNullException(nameof(eventSource)); + + // 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource + if (eventSource is ChannelEventSource source) + { + // 异步发布 + await Task.Factory.StartNew(() => + { + if (source.EventId != null && source.EventId.StartsWith("broadcast:")) + { + string str = JsonConvert.SerializeObject(source); + _queueBroadcast.Add(str); + } + else + { + _queueSingle.Add(source); + } + }, cancellationToken, TaskCreationOptions.LongRunning, System.Threading.Tasks.TaskScheduler.Default); + } + else + { + // 处理动态订阅问题 + await _channel.Writer.WriteAsync(eventSource, cancellationToken); + } + } + + /// + /// 从存储器中读取一条事件源 + /// + /// 取消任务 Token + /// 事件源对象 + public async ValueTask ReadAsync(CancellationToken cancellationToken) + { + // 读取一条事件源 + var eventSource = await _channel.Reader.ReadAsync(cancellationToken); + return eventSource; + } + + /// + /// 释放非托管资源 + /// + public async void Dispose() + { + await _eventConsumer.Stop(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisQueue.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisQueue.cs new file mode 100644 index 0000000..3478e25 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RedisQueue.cs @@ -0,0 +1,202 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Caching.Queues; + +namespace Admin.NET.Core; + +/// +/// Redis 消息队列 +/// +public static class RedisQueue +{ + private static ICacheProvider _cacheProvider = App.GetRequiredService(); + + /// 创建Redis消息队列。默认消费一次,指定消费者group时使用STREAM结构,支持多消费组共享消息 + /// + /// 使用队列时,可根据是否设置消费组来决定使用简单队列还是完整队列。 简单队列(如RedisQueue)可用作命令队列,Topic很多,但几乎没有消息。 完整队列(如RedisStream)可用作消息队列,Topic很少,但消息很多,并且支持多消费组。 + /// + /// + /// 主题 + /// 消费组。未指定消费组时使用简单队列(如RedisQueue),指定消费组时使用完整队列(如RedisStream) + /// + public static IProducerConsumer GetQueue(String topic, String group = null) + { + // 队列需要单列 + var key = $"myStream:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = _cacheProvider.GetQueue(topic, group); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 获取可信队列,需要确认 + /// + /// + /// + /// + public static RedisReliableQueue GetRedisReliableQueue(string topic) + { + // 队列需要单列 + var key = $"myQueue:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = (_cacheProvider.Cache as FullRedis).GetReliableQueue(topic); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 可信队列回滚 + /// + /// + /// + /// + public static int RollbackAllAck(string topic, int retryInterval = 60) + { + var queue = GetRedisReliableQueue(topic); + queue.RetryInterval = retryInterval; + return queue.RollbackAllAck(); + } + + /// + /// 发送一个数据列表到可信队列 + /// + /// + /// + /// + /// + public static int AddReliableQueueList(string topic, List value) + { + var queue = GetRedisReliableQueue(topic); + var count = queue.Count; + var result = queue.Add(value.ToArray()); + return result - count; + } + + /// + /// 发送一条数据到可信队列 + /// + /// + /// + /// + /// + public static int AddReliableQueue(string topic, T value) + { + var queue = GetRedisReliableQueue(topic); + var count = queue.Count; + var result = queue.Add(value); + return result - count; + } + + /// + /// 获取延迟队列 + /// + /// + /// + /// + public static RedisDelayQueue GetDelayQueue(string topic) + { + // 队列需要单列 + var key = $"myDelay:{topic}"; + if (_cacheProvider.InnerCache.TryGetValue>(key, out var queue)) return queue; + + queue = (_cacheProvider.Cache as FullRedis).GetDelayQueue(topic); + _cacheProvider.Cache.Set(key, queue); + + return queue; + } + + /// + /// 发送一条数据到延迟队列 + /// + /// + /// + /// 延迟时间。单位秒 + /// + /// + public static int AddDelayQueue(string topic, T value, int delay) + { + var queue = GetDelayQueue(topic); + return queue.Add(value, delay); + } + + /// + /// 发送数据列表到延迟队列 + /// + /// + /// + /// + /// 延迟时间。单位秒 + /// + public static int AddDelayQueue(string topic, List value, int delay) + { + var queue = GetDelayQueue(topic); + queue.Delay = delay; + return queue.Add(value.ToArray()); + } + + /// + /// 在可信队列获取一条数据 + /// + /// + /// + /// + public static T ReliableTakeOne(string topic) + { + var queue = GetRedisReliableQueue(topic); + return queue.TakeOne(1); + } + + /// + /// 异步在可信队列获取一条数据 + /// + /// + /// + /// + public static async Task ReliableTakeOneAsync(string topic) + { + var queue = GetRedisReliableQueue(topic); + return await queue.TakeOneAsync(1); + } + + /// + /// 在可信队列获取多条数据 + /// + /// + /// + /// + /// + public static List ReliableTake(string topic, int count) + { + var queue = GetRedisReliableQueue(topic); + return queue.Take(count).ToList(); + } + + /// + /// 申请分布式锁 + /// + /// 要锁定的key + /// 申请锁等待的时间,单位毫秒 + /// 锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒 + /// 失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败 + /// + public static IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true) + { + try + { + return _cacheProvider.Cache.AcquireLock(key, msTimeout, msExpire, throwOnFailure); + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs new file mode 100644 index 0000000..59f9cec --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件执行器-超时控制、失败重试熔断等等 +/// +public class RetryEventHandlerExecutor : IEventHandlerExecutor +{ + public async Task ExecuteAsync(EventHandlerExecutingContext context, Func handler) + { + var eventSubscribeAttribute = context.Attribute; + // 判断是否自定义了重试失败回调服务 + var fallbackPolicyService = eventSubscribeAttribute?.FallbackPolicy == null + ? null + : App.GetRequiredService(eventSubscribeAttribute.FallbackPolicy) as IEventFallbackPolicy; + + await Retry.InvokeAsync(async () => + { + try + { + await handler(context); + } + catch (Exception ex) + { + Log.Error($"Invoke EventHandler {context.Source.EventId} Error", ex); + throw; + } + } + , eventSubscribeAttribute?.NumRetries ?? 0 + , eventSubscribeAttribute?.RetryTimeout ?? 1000 + , exceptionTypes: eventSubscribeAttribute?.ExceptionTypes + , fallbackPolicy: fallbackPolicyService == null ? null : async (Exception ex) => { await fallbackPolicyService.CallbackAsync(context, ex); } + , retryAction: (total, times) => + { + // 输出重试日志 + Log.Warning($"Retrying {times}/{total} times for EventHandler {context.Source.EventId}"); + }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ConsoleLogoSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ConsoleLogoSetup.cs new file mode 100644 index 0000000..be20e97 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ConsoleLogoSetup.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 控制台logo +/// +public static class ConsoleLogoSetup +{ + public static void AddConsoleLogo(this IServiceCollection services) + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(@" + _ _ _ _ ______ _______ + /\ | | (_) | \ | | ____|__ __| + / \ __| |_ __ ___ _ _ __ | \| | |__ | | + / /\ \ / _` | '_ ` _ \| | '_ \ | . ` | __| | | + / ____ \ (_| | | | | | | | | | |_| |\ | |____ | | + /_/ \_\__,_|_| |_| |_|_|_| |_(_)_| \_|______| |_| "); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(@"让.NET更简单、更通用、更流行!"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/DataTypeExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/DataTypeExtension.cs new file mode 100644 index 0000000..0f9d4ab --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/DataTypeExtension.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 基本数据类型扩展(作为NewLife.Core的Utility的补充) +/// +public static class DataTypeExtension +{ + /// 转为SByte整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static sbyte ToSByte(this object value, sbyte defaultValue = default) + { + if (value is sbyte num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (sbyte.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为Byte整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static byte ToByte(this object value, byte defaultValue = default) + { + if (value is byte num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (byte.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为Int16整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static short ToInt16(this object value, short defaultValue = default) + { + if (value is short num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (short.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt16整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static ushort ToUInt16(this object value, ushort defaultValue = default) + { + if (value is ushort num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (ushort.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt32整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static uint ToUInt32(this object value, uint defaultValue = default) + { + if (value is uint num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (uint.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } + + /// 转为UInt64整数,转换失败时返回默认值。 + /// + /// 待转换对象 + /// 默认值。待转换对象无效时使用 + /// + public static ulong ToUInt64(this object value, ulong defaultValue = default) + { + if (value is ulong num) return num; + if (value == null || value == DBNull.Value) return defaultValue; + + if (ulong.TryParse(value.ToString(), out var result)) + return result; + else + return defaultValue; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumExtension.cs new file mode 100644 index 0000000..6900d76 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumExtension.cs @@ -0,0 +1,239 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举拓展 +/// +public static class EnumExtension +{ + // 枚举显示字典缓存 + private static readonly ConcurrentDictionary> EnumDisplayValueDict = new(); + + // 枚举值字典缓存 + private static readonly ConcurrentDictionary> EnumNameValueDict = new(); + + // 枚举类型缓存 + private static ConcurrentDictionary _enumTypeDict; + + /// + /// 获取枚举对象Key与名称的字典(缓存) + /// + /// + /// + public static Dictionary GetEnumDictionary(this Type enumType) + { + if (!enumType.IsEnum) + throw new ArgumentException("Type '" + enumType.Name + "' is not an enum."); + + // 查询缓存 + var enumDic = EnumNameValueDict.TryGetValue(enumType, out var value) ? value : new Dictionary(); + if (enumDic.Count != 0) + return enumDic; + // 取枚举类型的Key/Value字典集合 + enumDic = GetEnumDictionaryItems(enumType); + + // 缓存 + EnumNameValueDict[enumType] = enumDic; + + return enumDic; + } + + /// + /// 获取枚举对象Key与名称的字典 + /// + /// + /// + private static Dictionary GetEnumDictionaryItems(this Type enumType) + { + // 获取类型的字段,初始化一个有限长度的字典 + var enumFields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static); + Dictionary enumDic = new(enumFields.Length); + + // 遍历字段数组获取key和name + foreach (var enumField in enumFields) + { + var intValue = (int)enumField.GetValue(enumType)!; + enumDic[intValue] = enumField.Name; + } + + return enumDic; + } + + /// + /// 获取枚举类型key与描述的字典(缓存) + /// + /// + /// + /// + public static Dictionary GetEnumDescDictionary(this Type enumType) + { + if (!enumType.IsEnum) + throw new ArgumentException("Type '" + enumType.Name + "' is not an enum."); + + // 查询缓存 + var enumDic = EnumDisplayValueDict.TryGetValue(enumType, out var value) + ? value + : new Dictionary(); + if (enumDic.Count != 0) + return enumDic; + // 取枚举类型的Key/Value字典集合 + enumDic = GetEnumDescDictionaryItems(enumType); + + // 缓存 + EnumDisplayValueDict[enumType] = enumDic; + + return enumDic; + } + + /// + /// 获取枚举类型key与描述的字典(没有描述则获取name) + /// + /// + /// + /// + private static Dictionary GetEnumDescDictionaryItems(this Type enumType) + { + // 获取类型的字段,初始化一个有限长度的字典 + var enumFields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static); + Dictionary enumDic = new(enumFields.Length); + + // 遍历字段数组获取key和name + foreach (var enumField in enumFields) + { + var intValue = (int)enumField.GetValue(enumType)!; + var desc = enumField.GetDescriptionValue(); + enumDic[intValue] = desc != null && !string.IsNullOrEmpty(desc.Description) ? desc.Description : enumField.Name; + } + + return enumDic; + } + + /// + /// 从程序集中查找指定枚举类型 + /// + /// + /// + /// + public static Type TryToGetEnumType(Assembly assembly, string typeName) + { + // 枚举缓存为空则重新加载枚举类型字典 + _enumTypeDict ??= LoadEnumTypeDict(assembly); + + // 按名称查找 + return _enumTypeDict.TryGetValue(typeName, out var value) ? value : null; + } + + /// + /// 从程序集中加载所有枚举类型 + /// + /// + /// + private static ConcurrentDictionary LoadEnumTypeDict(Assembly assembly) + { + // 取程序集中所有类型 + var typeArray = assembly.GetTypes(); + + // 过滤非枚举类型,转成字典格式并返回 + var dict = typeArray.Where(o => o.IsEnum).ToDictionary(o => o.Name, o => o); + ConcurrentDictionary enumTypeDict = new(dict); + return enumTypeDict; + } + + /// + /// 获取枚举的Description + /// + /// + /// + public static string GetEnumDescription(this Enum value) + { + return value.GetType().GetField(value.ToString())?.GetCustomAttribute()?.Description; + } + + /// + /// 获取枚举的Description + /// + /// + /// + public static string GetEnumDescription(this object value) + { + return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute()?.Description; + } + + /// + /// 获取枚举的Theme + /// + /// + /// + public static string GetTheme(this object value) + { + return value.GetType().GetField(value.ToString()!)?.GetCustomAttribute()?.Theme; + } + + /// + /// 将枚举转成枚举信息集合 + /// + /// + /// + public static List EnumToList(this Type type) + { + if (!type.IsEnum) + throw new ArgumentException("Type '" + type.Name + "' is not an enum."); + var arr = Enum.GetNames(type); + return arr.Select(sl => + { + var item = Enum.Parse(type, sl); + return new EnumEntity + { + Name = item.ToString(), + Describe = item.GetEnumDescription() ?? item.ToString(), + Theme = item.GetTheme() ?? string.Empty, + Value = item.GetHashCode() + }; + }).ToList(); + } + + /// + /// 枚举ToList + /// + /// + /// + /// + public static List EnumToList(this Type type) + { + if (!type.IsEnum) + throw new ArgumentException("Type '" + type.Name + "' is not an enum."); + var arr = Enum.GetNames(type); + return arr.Select(name => (T)Enum.Parse(type, name)).ToList(); + } +} + +/// +/// 枚举实体 +/// +public class EnumEntity +{ + /// + /// 枚举的描述 + /// + public string Describe { get; set; } + + /// + /// 枚举的样式 + /// + public string Theme { get; set; } + + /// + /// 枚举名称 + /// + public string Name { get; set; } + + /// + /// 枚举对象的值 + /// + public int Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumerableExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumerableExtension.cs new file mode 100644 index 0000000..de6c71e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/EnumerableExtension.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据集合拓展类 +/// +public static class EnumerableExtension +{ + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点父级字段值 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + object topParentIdValue, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + var propId = GetPropertyInfo(idExpression); + var propParentId = GetPropertyInfo(parentIdExpression); + + // 查找所有顶级节点 + var topNodes = list.Where(item => Equals(propId.GetValue(item), topParentIdValue)).ToList(); + + return TraverseHierarchy(list, propId, propParentId, topNodes, isContainOneself); + } + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点的选择条件 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + Expression> topLevelPredicate, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + // 获取顶级节点 + var topNodes = list.Where(topLevelPredicate.Compile()).ToList(); + + if (!topNodes.Any()) return Enumerable.Empty(); + + var idPropertyInfo = GetPropertyInfo(idExpression); + var parentPropertyInfo = GetPropertyInfo(parentIdExpression); + + return TraverseHierarchy(list, idPropertyInfo, parentPropertyInfo, topNodes, isContainOneself); + } + + /// + /// 辅助方法,从表达式中提取属性信息并使用临时缓存 + /// + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + // 使用 ConcurrentDictionary 确保线程安全 + return PropertyCache.GetOrAdd(typeof(T).FullName + "." + ((MemberExpression)expression.Body).Member.Name, k => + { + if (expression.Body is UnaryExpression { Operand: MemberExpression member }) return (PropertyInfo)member.Member; + if (expression.Body is MemberExpression memberExpression) return (PropertyInfo)memberExpression.Member; + throw Oops.Oh("表达式必须是一个属性访问: " + expression); + }); + } + + /// + /// 使用队列遍历层级结构 + /// + private static IEnumerable TraverseHierarchy(IEnumerable list, + PropertyInfo idPropertyInfo, + PropertyInfo parentPropertyInfo, + List topNodes, + bool isContainOneself) + { + var queue = new Queue(topNodes); + var result = new HashSet(topNodes); + + while (queue.Count > 0) + { + var currentNode = queue.Dequeue(); + var children = list.Where(item => Equals(parentPropertyInfo.GetValue(item), idPropertyInfo.GetValue(currentNode))).ToList(); + children.Where(child => result.Add(child)).ForEach(child => queue.Enqueue(child)); + } + if (isContainOneself) return result; + + // 如果不需要包含顶级节点本身,则移除它们 + topNodes.ForEach(e => result.Remove(e)); + + return result; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/HttpContextExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/HttpContextExtension.cs new file mode 100644 index 0000000..88074a7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/HttpContextExtension.cs @@ -0,0 +1,94 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +public static class HttpContextExtension +{ + public static async Task GetExternalProvidersAsync(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var schemes = context.RequestServices.GetRequiredService(); + + return (from scheme in await schemes.GetAllSchemesAsync() + where !string.IsNullOrEmpty(scheme.DisplayName) + select scheme).ToArray(); + } + + public static async Task IsProviderSupportedAsync(this HttpContext context, string provider) + { + ArgumentNullException.ThrowIfNull(context); + + return (from scheme in await context.GetExternalProvidersAsync() + where string.Equals(scheme.Name, provider, StringComparison.OrdinalIgnoreCase) + select scheme).Any(); + } + + /// + /// 获取设备信息 + /// + /// + /// + public static string GetClientDeviceInfo(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return CommonUtil.GetClientDeviceInfo(context.Request.Headers.UserAgent); + } + + /// + /// 获取浏览器信息 + /// + /// + /// + public static string GetClientBrowser(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + string userAgent = context.Request.Headers.UserAgent; + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + } + } + catch + { } + return "未知"; + } + + /// + /// 获取操作系统信息 + /// + /// + /// + public static string GetClientOs(this HttpContext context) + { + ArgumentNullException.ThrowIfNull(context); + + string userAgent = context.Request.Headers.UserAgent; + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}"; + } + } + catch + { } + return "未知"; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ListExtensions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ListExtensions.cs new file mode 100644 index 0000000..dcf9e9c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ListExtensions.cs @@ -0,0 +1,48 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class ListExtensions +{ + public static async Task ForEachAsync(this List list, Func func) + { + foreach (var value in list) + { + await func(value); + } + } + + public static async Task ForEachAsync(this IEnumerable source, Func action) + { + foreach (var value in source) + { + await action(value); + } + } + + public static void ForEach(this IEnumerable enumerable, Action consumer) + { + foreach (T item in enumerable) + { + consumer(item); + } + } + + public static void AddRange(this IList list, IEnumerable items) + { + if (list is List list2) + { + list2.AddRange(items); + return; + } + + foreach (T item in items) + { + list.Add(item); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs new file mode 100644 index 0000000..21fd46b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs @@ -0,0 +1,521 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 对象拓展 +/// +[SuppressSniffer] +public static partial class ObjectExtension +{ + /// + /// 类型属性列表映射表 + /// + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 脱敏特性缓存映射表 + /// + private static readonly ConcurrentDictionary AttributeCache = new(); + + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + } + + /// + /// 将字典转化为QueryString格式 + /// + /// + /// + /// + public static string ToQueryString(this Dictionary dict, bool urlEncode = true) + { + return string.Join("&", dict.Select(p => $"{(urlEncode ? p.Key?.UrlEncode() : "")}={(urlEncode ? p.Value?.UrlEncode() : "")}")); + } + + /// + /// 将字符串URL编码 + /// + /// + /// + public static string UrlEncode(this string str) + { + return string.IsNullOrEmpty(str) ? "" : System.Uri.EscapeDataString(str); + } + + /// + /// 对象序列化成Json字符串 + /// + /// + /// + public static string ToJson(this object obj) + { + var jsonSettings = SetNewtonsoftJsonSetting(); + return JSON.GetJsonSerializer().Serialize(obj, jsonSettings); + } + + private static JsonSerializerSettings SetNewtonsoftJsonSetting() + { + JsonSerializerSettings setting = new JsonSerializerSettings(); + setting.DateFormatHandling = DateFormatHandling.IsoDateFormat; + setting.DateTimeZoneHandling = DateTimeZoneHandling.Local; + setting.DateFormatString = "yyyy-MM-dd HH:mm:ss"; // 时间格式化 + setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 忽略循环引用 + //setting.ContractResolver = new HelErpContractResolver("StartTime", customName); + return setting; + } + + /// + /// Json字符串反序列化成对象 + /// + /// + /// + /// + public static T ToObject(this string json) + { + return JSON.GetJsonSerializer().Deserialize(json); + } + + /// + /// 将object转换为long,若失败则返回0 + /// + /// + /// + public static long ParseToLong(this object obj) + { + try + { + return long.Parse(obj.ToString()); + } + catch + { + return 0L; + } + } + + /// + /// 将object转换为long,若失败则返回指定值 + /// + /// + /// + /// + public static long ParseToLong(this string str, long defaultValue) + { + try + { + return long.Parse(str); + } + catch + { + return defaultValue; + } + } + + /// + /// 将object转换为double,若失败则返回0 + /// + /// + /// + public static double ParseToDouble(this object obj) + { + try + { + return double.Parse(obj.ToString()); + } + catch + { + return 0; + } + } + + /// + /// 将object转换为double,若失败则返回指定值 + /// + /// + /// + /// + public static double ParseToDouble(this object str, double defaultValue) + { + try + { + return double.Parse(str.ToString()); + } + catch + { + return defaultValue; + } + } + + /// + /// 将string转换为DateTime,若失败则返回日期最小值 + /// + /// + /// + public static DateTime ParseToDateTime(this string str) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return DateTime.MinValue; + } + if (str.Contains('-') || str.Contains('/')) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return DateTime.MinValue; + } + } + + /// + /// 将string转换为DateTime,若失败则返回默认值 + /// + /// + /// + /// + public static DateTime ParseToDateTime(this string str, DateTime? defaultValue) + { + try + { + if (string.IsNullOrWhiteSpace(str)) + { + return defaultValue.GetValueOrDefault(); + } + if (str.Contains('-') || str.Contains('/')) + { + return DateTime.Parse(str); + } + else + { + int length = str.Length; + switch (length) + { + case 4: + return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture); + + case 6: + return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture); + + case 8: + return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture); + + case 10: + return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture); + + case 12: + return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture); + + case 14: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + + default: + return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture); + } + } + } + catch + { + return defaultValue.GetValueOrDefault(); + } + } + + /// + /// 将 string 时间日期格式转换成字符串 如 {yyyy} => 2024 + /// + /// + /// + public static string ParseToDateTimeForRep(this string str) + { + if (string.IsNullOrWhiteSpace(str)) + str = $"{DateTime.Now.Year}/{DateTime.Now.Month}/{DateTime.Now.Day}"; + + var date = DateTime.Now; + var reg = new Regex(@"(\{.+?})"); + var match = reg.Matches(str); + match.ToList().ForEach(u => + { + var temp = date.ToString(u.ToString().Substring(1, u.Length - 2)); + str = str.Replace(u.ToString(), temp); + }); + return str; + } + + /// + /// 是否有值 + /// + /// + /// + public static bool IsNullOrEmpty(this object obj) + { + return obj == null || string.IsNullOrEmpty(obj.ToString()); + } + + /// + /// 字符串掩码 + /// + /// 字符串 + /// 掩码符 + /// + public static string Mask(this string str, char mask = '*') + { + if (string.IsNullOrWhiteSpace(str?.Trim())) + return str; + + str = str.Trim(); + var masks = mask.ToString().PadLeft(4, mask); + return str.Length switch + { + >= 11 => Regex.Replace(str, "(.{3}).*(.{4})", $"$1{masks}$2"), + 10 => Regex.Replace(str, "(.{3}).*(.{3})", $"$1{masks}$2"), + 9 => Regex.Replace(str, "(.{2}).*(.{3})", $"$1{masks}$2"), + 8 => Regex.Replace(str, "(.{2}).*(.{2})", $"$1{masks}$2"), + 7 => Regex.Replace(str, "(.{1}).*(.{2})", $"$1{masks}$2"), + 6 => Regex.Replace(str, "(.{1}).*(.{1})", $"$1{masks}$2"), + _ => Regex.Replace(str, "(.{1}).*", $"$1{masks}") + }; + } + + /// + /// 身份证号掩码 + /// + /// 身份证号 + /// 掩码符 + /// + public static string MaskIdCard(this string idCard, char mask = '*') + { + if (!idCard.TryValidate(ValidationTypes.IDCard).IsValid) return idCard; + + var masks = mask.ToString().PadLeft(8, mask); + return Regex.Replace(idCard, @"^(.{6})(.*)(.{4})$", $"$1{masks}$3"); + } + + /// + /// 邮箱掩码 + /// + /// 邮箱 + /// 掩码符 + /// + public static string MaskEmail(this string email, char mask = '*') + { + if (!email.TryValidate(ValidationTypes.EmailAddress).IsValid) return email; + + var pos = email.IndexOf("@"); + return Mask(email[..pos], mask) + email[pos..]; + } + + /// + /// 将字符串转为值类型,若没有得到或者错误返回为空 + /// + /// 指定值类型 + /// 传入字符串 + /// 可空值 + public static T? ParseTo(this string str) where T : struct + { + try + { + if (!string.IsNullOrWhiteSpace(str)) + { + MethodInfo method = typeof(T).GetMethod("Parse", new Type[] { typeof(string) }); + if (method != null) + { + T result = (T)method.Invoke(null, new string[] { str }); + return result; + } + } + } + catch + { + } + return null; + } + + /// + /// 将字符串转为值类型,若没有得到或者错误返回为空 + /// + /// 传入字符串 + /// 目标类型 + /// 可空值 + public static object ParseTo(this string str, Type type) + { + try + { + if (type.Name == "String") + return str; + + if (!string.IsNullOrWhiteSpace(str)) + { + var _type = type; + if (type.Name.StartsWith("Nullable")) + _type = type.GetGenericArguments()[0]; + + MethodInfo method = _type.GetMethod("Parse", new Type[] { typeof(string) }); + if (method != null) + return method.Invoke(null, new string[] { str }); + } + } + catch + { + } + return null; + } + + /// + /// 将一个对象属性值赋给另一个指定对象属性, 只复制相同属性的 + /// + /// 原数据对象 + /// 目标数据对象 + /// 属性集,键为原属性,值为目标属性 + /// 属性集,目标不修改的属性 + public static void CopyTo(object src, object target, Dictionary changeProperties = null, string[] unChangeProperties = null) + { + if (src == null || target == null) + throw new ArgumentException("src == null || target == null "); + + var SourceType = src.GetType(); + var TargetType = target.GetType(); + + if (changeProperties == null || changeProperties.Count == 0) + { + var fields = TargetType.GetProperties(); + changeProperties = fields.Select(m => m.Name).ToDictionary(m => m); + } + + if (unChangeProperties == null || unChangeProperties.Length == 0) + { + foreach (var item in changeProperties) + { + var srcProperty = SourceType.GetProperty(item.Key); + if (srcProperty != null) + { + var sourceVal = srcProperty.GetValue(src, null); + + var tarProperty = TargetType.GetProperty(item.Value); + tarProperty?.SetValue(target, sourceVal, null); + } + } + } + else + { + foreach (var item in changeProperties) + { + if (!unChangeProperties.Any(m => m == item.Value)) + { + var srcProperty = SourceType.GetProperty(item.Key); + if (srcProperty != null) + { + var sourceVal = srcProperty.GetValue(src, null); + + var tarProperty = TargetType.GetProperty(item.Value); + tarProperty?.SetValue(target, sourceVal, null); + } + } + } + } + } + + /// + /// 深复制 + /// + /// 深复制源对象 + /// 对象 + /// + public static T DeepCopy(this T obj) + { + var jsonSettings = SetNewtonsoftJsonSetting(); + var json = JSON.Serialize(obj, jsonSettings); + return JSON.Deserialize(json); + } + + /// + /// 对带有特性字段进行脱敏处理 + /// + public static T MaskSensitiveData(this T obj) where T : class + { + if (obj == null) return null; + + var type = typeof(T); + + // 获取或缓存属性集合 + var properties = PropertyCache.GetOrAdd(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType == typeof(string) && p.GetCustomAttribute() != null) + .ToArray()); + + // 并行处理可写属性 + Parallel.ForEach(properties, prop => + { + if (!prop.CanWrite) return; + + // 获取或缓存特性 + var maskAttr = AttributeCache.GetOrAdd(prop, p => p.GetCustomAttribute()); + + if (maskAttr == null) return; + + // 处理非空字符串 + if (prop.GetValue(obj) is string { Length: > 0 } value) + { + prop.SetValue(obj, maskAttr.Mask(value)); + } + }); + + return obj; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs new file mode 100644 index 0000000..e52918e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs @@ -0,0 +1,454 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MapsterMapper; + +namespace Admin.NET.Core; + +public static class RepositoryExtension +{ + /// + /// 实体假删除 _rep.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISugarRepository repository, T entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDelete(entity); + } + + /// + /// 实体假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISqlSugarClient db, T entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommand(); + } + + /// + /// 实体集合批量假删除 _rep.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISugarRepository repository, List entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDelete(entity); + } + + /// + /// 实体集合批量假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static int FakeDelete(this ISqlSugarClient db, List entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommand(); + } + + /// + /// 实体假删除异步 _rep.FakeDeleteAsync(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISugarRepository repository, T entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDeleteAsync(entity); + } + + /// + /// 实体假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISqlSugarClient db, T entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommandAsync(); + } + + /// + /// 实体集合批量假删除异步 _rep.FakeDeleteAsync(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISugarRepository repository, List entity) where T : EntityBaseDel, new() + { + return repository.Context.FakeDeleteAsync(entity); + } + + /// + /// 实体集合批量假删除 db.FakeDelete(entity) + /// + /// + /// + /// + /// + public static Task FakeDeleteAsync(this ISqlSugarClient db, List entity) where T : EntityBaseDel, new() + { + return db.Updateable(entity).AS().ReSetValue(x => { x.IsDelete = true; }) + .IgnoreColumns(ignoreAllNullColumns: true) + .EnableDiffLogEvent() // 记录差异日志 + .UpdateColumns(x => new { x.IsDelete, x.DeleteTime, x.UpdateTime, x.UpdateUserId }) // 允许更新的字段-AOP拦截自动设置UpdateTime、UpdateUserId + .ExecuteCommandAsync(); + } + + /// + /// 排序方式(默认降序) + /// + /// + /// + /// + /// 默认排序字段 + /// 是否降序 + /// + public static ISugarQueryable OrderBuilder(this ISugarQueryable queryable, BasePageInput pageInput, string prefix = "", string defaultSortField = "Id", bool descSort = true) + { + var iSqlBuilder = InstanceFactory.GetSqlBuilderWithContext(queryable.Context); + + // 约定默认每张表都有Id排序 + var orderStr = string.IsNullOrWhiteSpace(defaultSortField) ? "" : $"{prefix}{iSqlBuilder.GetTranslationColumnName(defaultSortField)}" + (descSort ? " Desc" : " Asc"); + + TypeAdapterConfig typeAdapterConfig = new(); + typeAdapterConfig.ForType().IgnoreNullValues(true); + Mapper mapper = new(typeAdapterConfig); // 务必将mapper设为单实例 + var nowPagerInput = mapper.Map(pageInput); + // 排序是否可用-排序字段为非空才启用排序,排序顺序默认为倒序 + if (!string.IsNullOrEmpty(nowPagerInput.Field)) + { + nowPagerInput.Field = Regex.Replace(nowPagerInput.Field, @"[\s;()\-'@=/%]", ""); //过滤掉一些关键字符防止构造特殊SQL语句注入 + var orderByDbName = queryable.Context.EntityMaintenance.GetDbColumnName(nowPagerInput.Field);//防止注入,类中只要不存在属性名就会报错 + orderStr = $"{prefix}{iSqlBuilder.GetTranslationColumnName(orderByDbName)} {(string.IsNullOrEmpty(nowPagerInput.Order) || nowPagerInput.Order.Equals(nowPagerInput.DescStr, StringComparison.OrdinalIgnoreCase) ? "Desc" : "Asc")}"; + } + return queryable.OrderByIF(!string.IsNullOrWhiteSpace(orderStr), orderStr); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLog(entity) + /// + /// + /// + /// + /// + /// + public static int UpdateWithDiffLog(this ISugarRepository repository, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return repository.Context.UpdateWithDiffLog(entity, ignoreAllNullColumns); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLog(entity) + /// + /// + /// + /// + /// + /// + public static int UpdateWithDiffLog(this ISqlSugarClient db, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return db.Updateable(entity).AS() + .IgnoreColumns(ignoreAllNullColumns: ignoreAllNullColumns) + .EnableDiffLogEvent() + .ExecuteCommand(); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + /// + public static Task UpdateWithDiffLogAsync(this ISugarRepository repository, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return repository.Context.UpdateWithDiffLogAsync(entity, ignoreAllNullColumns); + } + + /// + /// 更新实体并记录差异日志 _rep.UpdateWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + /// + public static Task UpdateWithDiffLogAsync(this ISqlSugarClient db, T entity, bool ignoreAllNullColumns = true) where T : EntityBase, new() + { + return db.Updateable(entity) + .IgnoreColumns(ignoreAllNullColumns: ignoreAllNullColumns) + .EnableDiffLogEvent() + .ExecuteCommandAsync(); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static int InsertWithDiffLog(this ISugarRepository repository, T entity) where T : EntityBase, new() + { + return repository.Context.InsertWithDiffLog(entity); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static int InsertWithDiffLog(this ISqlSugarClient db, T entity) where T : EntityBase, new() + { + return db.Insertable(entity).AS().EnableDiffLogEvent().ExecuteCommand(); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLogAsync(entity) + /// + /// + /// + /// + /// + public static Task InsertWithDiffLogAsync(this ISugarRepository repository, T entity) where T : EntityBase, new() + { + return repository.Context.InsertWithDiffLogAsync(entity); + } + + /// + /// 新增实体并记录差异日志 _rep.InsertWithDiffLog(entity) + /// + /// + /// + /// + /// + public static Task InsertWithDiffLogAsync(this ISqlSugarClient db, T entity) where T : EntityBase, new() + { + return db.Insertable(entity).AS().EnableDiffLogEvent().ExecuteCommandAsync(); + } + + /// + /// 多库查询 + /// + /// + /// + public static ISugarQueryable AS(this ISugarQueryable queryable) + { + var info = GetTableInfo(); + return queryable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库查询 + /// + /// + /// + /// + /// + public static ISugarQueryable AS(this ISugarQueryable queryable) + { + var info = GetTableInfo(); + return queryable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库更新 + /// + /// + /// + public static IUpdateable AS(this IUpdateable updateable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return updateable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库新增 + /// + /// + /// + public static IInsertable AS(this IInsertable insertable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return insertable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 多库删除 + /// + /// + /// + public static IDeleteable AS(this IDeleteable deleteable) where T : EntityBase, new() + { + var info = GetTableInfo(); + return deleteable.AS($"{info.Item1}.{info.Item2}"); + } + + /// + /// 根据实体类型获取表信息 + /// + /// + /// + private static Tuple GetTableInfo() + { + var entityType = typeof(T); + var attr = entityType.GetCustomAttribute(); + var configId = attr == null ? SqlSugarConst.MainConfigId : attr.configId.ToString(); + var tableName = entityType.GetCustomAttribute().TableName; + return new Tuple(configId, tableName); + } + + /// + /// 禁用过滤器-适用于更新和删除操作(只对当前请求有效,禁止使用异步) + /// + /// + /// 禁止异步 + /// + public static void RunWithoutFilter(this ISugarRepository repository, Action action) + { + repository.Context.QueryFilter.ClearAndBackup(); // 清空并备份过滤器 + action.Invoke(); + repository.Context.QueryFilter.Restore(); // 还原过滤器 + + // 用例 + //_rep.RunWithoutFilter(() => + //{ + // 执行更新或者删除 + // 禁止使用异步函数 + //}); + } + + /// + /// 忽略租户 + /// + /// + /// 是否忽略 默认true + /// + public static ISugarQueryable IgnoreTenant(this ISugarQueryable queryable, bool ignore = true) + { + return ignore ? queryable.ClearFilter() : queryable; + } + + /// + /// 只更新某些列 + /// + /// + /// + /// + /// + public static IUpdateable OnlyUpdateColumn(this IUpdateable updateable) where T : EntityBase, new() where R : class, new() + { + if (updateable.UpdateBuilder.UpdateColumns == null) + updateable.UpdateBuilder.UpdateColumns = new List(); + + foreach (PropertyInfo info in typeof(R).GetProperties()) + { + // 判断是否是相同属性 + if (typeof(T).GetProperty(info.Name) != null) + updateable.UpdateBuilder.UpdateColumns.Add(info.Name); + } + return updateable; + } + + /// + /// 导航只更新(主表)某些列 + /// + /// + /// + /// + /// + /// + public static UpdateNavRootOptions OnlyNavUpdateColumn(this T t, R r) + { + UpdateNavRootOptions uNOption = new UpdateNavRootOptions(); + var updateColumns = new List(); + + foreach (PropertyInfo info in r.GetType().GetProperties()) + { + //判断是否是相同属性 + PropertyInfo pro = t.GetType().GetProperty(info.Name); + var attr = pro.GetCustomAttribute(); + if (pro != null && attr != null && !attr.IsPrimaryKey) + updateColumns.Add(info.Name); + } + uNOption.UpdateColumns = updateColumns.ToArray(); + return uNOption; + } + + /// + /// 批量列表in查询 + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> BulkListQuery(this ISugarQueryable queryable, + Expression, bool>> exp, + IEnumerable queryList, + CancellationToken stoppingToken) where T1 : class, new() + { + // 创建临时表 (用真表兼容性好,表名随机) + var tableName = "Temp" + SnowFlakeSingle.Instance.NextId(); + try + { + var type = queryable.Context.DynamicBuilder().CreateClass(tableName, new SugarTable()) + .CreateProperty("ColumnName", typeof(string), new SugarColumn() { IsPrimaryKey = true }) // 主键不要自增 + .BuilderType(); + // 创建表 + queryable.Context.CodeFirst.InitTables(type); + var insertData = queryList.Select(it => new SingleColumnEntity() { ColumnName = it }).ToList(); + // 插入临时表 + queryable.Context.Fastest>() + .AS(tableName) + .BulkCopy(insertData); + var queryTemp = queryable.Context.Queryable>() + .AS(tableName); + + var systemData = await queryable + .InnerJoin(queryTemp, exp) + .ToListAsync(stoppingToken); + + queryable.Context.DbMaintenance.DropTable(tableName); + return systemData; + } + catch (Exception error) + { + queryable.Context.DbMaintenance.DropTable(tableName); + throw Oops.Oh(error); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RequestExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RequestExtension.cs new file mode 100644 index 0000000..b7e8068 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/RequestExtension.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class RequestExtension +{ + /// + /// 获取请求地址源 + /// + /// + /// + public static string GetOrigin(this HttpRequest request) + { + string scheme = request.Scheme; + string host = request.Host.Host; + int port = request.Host.Port ?? (-1); + + string url = $"{scheme}://{host}"; + if (port != 80 && port != 443 && port != -1) url += $":{port}"; + + return url; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs new file mode 100644 index 0000000..fd16ee1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs @@ -0,0 +1,367 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; + +namespace Admin.NET.Core; + +/// +/// Sqlsugar 动态查询扩展方法 +/// +public static class SqlSugarExtension +{ + public static ISugarQueryable SearchBy(this ISugarQueryable queryable, BaseFilter filter) + { + return queryable.SearchByKeyword(filter.Keyword) + .AdvancedSearch(filter.Search) + .AdvancedFilter(filter.Filter); + } + + public static ISugarQueryable SearchByKeyword(this ISugarQueryable queryable, string keyword) + { + return queryable.AdvancedSearch(new Search { Keyword = keyword }); + } + + public static ISugarQueryable AdvancedSearch(this ISugarQueryable queryable, Search search) + { + if (!string.IsNullOrWhiteSpace(search?.Keyword)) + { + var paramExpr = Expression.Parameter(typeof(T)); + + Expression right = Expression.Constant(false); + + if (search.Fields?.Any() is true) + { + foreach (string field in search.Fields) + { + MemberExpression propertyExpr = GetPropertyExpression(field, paramExpr); + + var left = AddSearchPropertyByKeyword(propertyExpr, search.Keyword); + + right = Expression.Or(left, right); + } + } + else + { + var properties = typeof(T).GetProperties() + .Where(prop => Nullable.GetUnderlyingType(prop.PropertyType) == null + && !prop.PropertyType.IsEnum + && Type.GetTypeCode(prop.PropertyType) != TypeCode.Object); + + foreach (var property in properties) + { + var propertyExpr = Expression.Property(paramExpr, property); + + var left = AddSearchPropertyByKeyword(propertyExpr, search.Keyword); + + right = Expression.Or(left, right); + } + } + + var lambda = Expression.Lambda>(right, paramExpr); + + return queryable.Where(lambda); + } + + return queryable; + } + + public static ISugarQueryable AdvancedFilter(this ISugarQueryable queryable, Filter filter) + { + if (filter is not null) + { + var parameter = Expression.Parameter(typeof(T)); + + Expression binaryExpresioFilter; + + if (filter.Logic.HasValue) + { + if (filter.Filters is null) throw new ArgumentException("The Filters attribute is required when declaring a logic"); + binaryExpresioFilter = CreateFilterExpression(filter.Logic.Value, filter.Filters, parameter); + } + else + { + var filterValid = GetValidFilter(filter); + binaryExpresioFilter = CreateFilterExpression(filterValid.Field!, filterValid.Operator.Value, filterValid.Value, parameter); + } + + var lambda = Expression.Lambda>(binaryExpresioFilter, parameter); + + return queryable.Where(lambda); + } + return queryable; + } + + private static Expression CombineFilter( + FilterLogicEnum filterLogic, + Expression bExpresionBase, + Expression bExpresion) + { + return filterLogic switch + { + FilterLogicEnum.And => Expression.AndAlso(bExpresionBase, bExpresion), + FilterLogicEnum.Or => Expression.OrElse(bExpresionBase, bExpresion), + FilterLogicEnum.Xor => Expression.ExclusiveOr(bExpresionBase, bExpresion), + _ => throw new ArgumentException("FilterLogic is not valid.", nameof(filterLogic)), + }; + } + + private static Filter GetValidFilter(Filter filter) + { + if (string.IsNullOrEmpty(filter.Field)) throw new ArgumentException("The field attribute is required when declaring a filter"); + if (filter.Operator.IsNullOrEmpty()) throw new ArgumentException("The Operator attribute is required when declaring a filter"); + return filter; + } + + private static Expression CreateFilterExpression( + FilterLogicEnum filterLogic, + IEnumerable filters, + ParameterExpression parameter) + { + Expression filterExpression = default!; + + foreach (var filter in filters) + { + Expression bExpresionFilter; + + if (filter.Logic.HasValue) + { + if (filter.Filters is null) throw new ArgumentException("The Filters attribute is required when declaring a logic"); + bExpresionFilter = CreateFilterExpression(filter.Logic.Value, filter.Filters, parameter); + } + else + { + var filterValid = GetValidFilter(filter); + bExpresionFilter = CreateFilterExpression(filterValid.Field!, filterValid.Operator.Value, filterValid.Value, parameter); + } + + filterExpression = filterExpression is null ? bExpresionFilter : CombineFilter(filterLogic, filterExpression, bExpresionFilter); + } + + return filterExpression; + } + + private static Expression CreateFilterExpression( + string field, + FilterOperatorEnum filterOperator, + object? value, + ParameterExpression parameter) + { + var propertyExpresion = GetPropertyExpression(field, parameter); + var valueExpresion = GeValuetExpression(field, value, propertyExpresion.Type); + return CreateFilterExpression(propertyExpresion, valueExpresion, filterOperator); + } + + private static Expression CreateFilterExpression( + MemberExpression memberExpression, + ConstantExpression constantExpression, + FilterOperatorEnum filterOperator) + { + return filterOperator switch + { + FilterOperatorEnum.EQ => Expression.Equal(memberExpression, constantExpression), + FilterOperatorEnum.NEQ => Expression.NotEqual(memberExpression, constantExpression), + FilterOperatorEnum.LT => Expression.LessThan(memberExpression, constantExpression), + FilterOperatorEnum.LTE => Expression.LessThanOrEqual(memberExpression, constantExpression), + FilterOperatorEnum.GT => Expression.GreaterThan(memberExpression, constantExpression), + FilterOperatorEnum.GTE => Expression.GreaterThanOrEqual(memberExpression, constantExpression), + FilterOperatorEnum.Contains => Expression.Call(memberExpression, nameof(FilterOperatorEnum.Contains), null, constantExpression), + FilterOperatorEnum.StartsWith => Expression.Call(memberExpression, nameof(FilterOperatorEnum.StartsWith), null, constantExpression), + FilterOperatorEnum.EndsWith => Expression.Call(memberExpression, nameof(FilterOperatorEnum.EndsWith), null, constantExpression), + _ => throw new ArgumentException("Filter Operator is not valid."), + }; + } + + private static string GetStringFromJsonElement(object value) + { + if (value is JsonElement) return ((JsonElement)value).GetString()!; + if (value is string) return (string)value; + return value?.ToString(); + } + + private static ConstantExpression GeValuetExpression( + string field, + object? value, + Type propertyType) + { + if (value == null) return Expression.Constant(null, propertyType); + + if (propertyType.IsEnum) + { + string? stringEnum = GetStringFromJsonElement(value); + + if (!Enum.TryParse(propertyType, stringEnum, true, out object? valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + if (propertyType == typeof(long) || propertyType == typeof(long?)) + { + string? stringLong = GetStringFromJsonElement(value); + + if (!long.TryParse(stringLong, out long valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + + if (propertyType == typeof(Guid)) + { + string? stringGuid = GetStringFromJsonElement(value); + + if (!Guid.TryParse(stringGuid, out Guid valueparsed)) throw new ArgumentException(string.Format("Value {0} is not valid for {1}", value, field)); + + return Expression.Constant(valueparsed, propertyType); + } + + if (propertyType == typeof(string)) + { + string? text = GetStringFromJsonElement(value); + + return Expression.Constant(text, propertyType); + } + + if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) + { + string? text = GetStringFromJsonElement(value); + return Expression.Constant(ChangeType(text, propertyType), propertyType); + } + + return Expression.Constant(ChangeType(((JsonElement)value).GetRawText(), propertyType), propertyType); + } + + private static dynamic? ChangeType(object value, Type conversion) + { + var t = conversion; + + if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if (value == null) + { + return null; + } + + t = Nullable.GetUnderlyingType(t); + } + + return Convert.ChangeType(value, t!); + } + + private static MemberExpression GetPropertyExpression( + string propertyName, + ParameterExpression parameter) + { + Expression propertyExpression = parameter; + foreach (string member in propertyName.Split('.')) + { + propertyExpression = Expression.PropertyOrField(propertyExpression, member); + } + + return (MemberExpression)propertyExpression; + } + + private static Expression AddSearchPropertyByKeyword( + Expression propertyExpr, + string keyword, + FilterOperatorEnum operatorSearch = FilterOperatorEnum.Contains) + { + if (propertyExpr is not MemberExpression memberExpr || memberExpr.Member is not PropertyInfo property) + { + throw new ArgumentException("propertyExpr must be a property expression.", nameof(propertyExpr)); + } + + ConstantExpression constant = Expression.Constant(keyword); + + MethodInfo method = operatorSearch switch + { + FilterOperatorEnum.Contains => typeof(string).GetMethod(nameof(FilterOperatorEnum.Contains), new Type[] { typeof(string) }), + FilterOperatorEnum.StartsWith => typeof(string).GetMethod(nameof(FilterOperatorEnum.StartsWith), new Type[] { typeof(string) }), + FilterOperatorEnum.EndsWith => typeof(string).GetMethod(nameof(FilterOperatorEnum.EndsWith), new Type[] { typeof(string) }), + _ => throw new ArgumentException("Filter Operator is not valid."), + }; + + Expression selectorExpr = + property.PropertyType == typeof(string) + ? propertyExpr + : Expression.Condition( + Expression.Equal(Expression.Convert(propertyExpr, typeof(object)), Expression.Constant(null, typeof(object))), + Expression.Constant(null, typeof(string)), + Expression.Call(propertyExpr, "ToString", null, null)); + + return Expression.Call(selectorExpr, method, constant); + } + + #region 视图操作 + + /// + /// 获取映射SQL语句, 用于创建视图 + /// + /// + /// + /// + public static string ToMappedSqlString(this ISugarQueryable queryable) where T : class + { + ArgumentNullException.ThrowIfNull(queryable); + + // 获取实体映射信息 + var entityInfo = queryable.Context.EntityMaintenance.GetEntityInfo(typeof(T)); + if (entityInfo?.Columns == null || entityInfo.Columns.Count == 0) return queryable.ToSqlString(); + + // 构建需要替换的字段名映射(只处理实际有差异的字段) + var nameMap = entityInfo.Columns + .Where(c => !string.Equals(c.PropertyName, c.DbColumnName, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(k => k.PropertyName.ToLower(), v => v.DbColumnName, StringComparer.OrdinalIgnoreCase); + if (nameMap.Count == 0) return queryable.ToSqlString(); + + // 预编译正则表达式提升性能 + var sql = queryable.ToSqlString(); + foreach (var kv in nameMap) + { + sql = Regex.Replace(sql, $@"\b{kv.Key}\b", kv.Value ?? kv.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled); // 单词边界匹配 + } + return sql; + } + + #endregion 视图操作 + + /// + /// 列表转换为树形结构 + /// + /// + /// 列表数据 + /// 设置子节点列表。例如:item => item.Children + /// 设置元素的父级 Id。例如:item => item.ParentId + /// 根节点的父级 Id,默认为 0 + /// + /// + public static IEnumerable ToTree( + this IEnumerable source, + Func> childrenSelector, + Func parentIdSelector, + int rootParentId) + where T : class + { + var lookup = source.ToLookup(parentIdSelector); + List BuildTree(long parentId) + { + return lookup[parentId].Select(item => + { + var children = BuildTree(GetId(item)); + childrenSelector(item).Clear(); + childrenSelector(item).AddRange(children); + return item; + }).ToList(); + } + + // 需要提供获取Id的方法,可以用反射或者自己传参数 + long GetId(T item) + { + var prop = typeof(T).GetProperty("Id"); + if (prop == null) throw new Exception("没有找到Id属性"); + return (long)prop.GetValue(item); + } + + return BuildTree(rootParentId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs new file mode 100644 index 0000000..aba6fb5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class SqlSugarFilterExtension +{ + /// + /// 根据指定Attribute获取属性 + /// + /// + /// + /// + private static List GetPropertyNames(this Type type) where T : Attribute + { + return type.GetProperties() + .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(T))) + .Select(x => x.Name).ToList(); + } + + /// + /// 获取过滤表达式 + /// + /// + /// + /// + /// + public static LambdaExpression GetConditionExpression(this Type type, List owners) where T : Attribute + { + var fieldNames = type.GetPropertyNames(); + ParameterExpression parameter = Expression.Parameter(type, "c"); + + Expression right = Expression.Constant(false); + ConstantExpression ownersCollection = Expression.Constant(owners); + foreach (var fieldName in fieldNames) + { + var property = type.GetProperty(fieldName); + Expression memberExp = Expression.Property(parameter, property!); + + // 如果属性是可为空的类型,则转换为其基础类型 + var baseType = Nullable.GetUnderlyingType(property.PropertyType); + if (baseType != null) memberExp = Expression.Convert(memberExp, baseType); + + // 调用ownersCollection.Contains方法,检查是否包含属性值 + right = Expression.OrElse(Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Contains), + new[] { memberExp.Type }, + ownersCollection, + memberExp + ), right); + } + return Expression.Lambda(right, parameter); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/StringExtension.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/StringExtension.cs new file mode 100644 index 0000000..f0d1e39 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/StringExtension.cs @@ -0,0 +1,172 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 字符串扩展方法 +/// +public static class StringExtension +{ + /// + /// 字符串截断 + /// + public static string Truncate(this string str, int maxLength, string ellipsis = "...") + { + if (string.IsNullOrWhiteSpace(str)) return str; + if (maxLength <= 0) return string.Empty; + if (str.Length <= maxLength) return str; + + // 确保省略号不会导致字符串超出最大长度 + int ellipsisLength = ellipsis?.Length ?? 0; + int truncateLength = Math.Min(maxLength, str.Length - ellipsisLength); + return str[..truncateLength] + ellipsis; + } + + /// + /// 单词首字母全部大写 + /// + public static string ToTitleCase(this string str) + { + return string.IsNullOrWhiteSpace(str) ? str : System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower()); + } + + /// + /// 检查是否包含子串,忽略大小写 + /// + public static bool ContainsIgnoreCase(this string str, string substring) + { + if (string.IsNullOrWhiteSpace(str) || string.IsNullOrWhiteSpace(substring)) return false; + return str.Contains(substring, StringComparison.OrdinalIgnoreCase); + } + + /// + /// 判断是否是 JSON 数据 + /// + public static bool IsJson(this string str) + { + if (string.IsNullOrWhiteSpace(str)) return false; + str = str.Trim(); + return (str.StartsWith("{") && str.EndsWith("}")) || (str.StartsWith("[") && str.EndsWith("]")); + } + + /// + /// 判断是否是 HTML 数据 + /// + public static bool IsHtml(this string str) + { + if (string.IsNullOrWhiteSpace(str)) return false; + str = str.Trim(); + + // 检查是否以 或 开头 + if (str.StartsWith("", StringComparison.OrdinalIgnoreCase) || str.StartsWith("", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // 检查是否包含 HTML 标签 + return Regex.IsMatch(str, @"<\s*[^>]+>.*<\s*/\s*[^>]+>|<\s*[^>]+\s*/>", RegexOptions.Singleline | RegexOptions.IgnoreCase); + } + + /// + /// 字符串反转 + /// + public static string Reverse(this string str) + { + if (string.IsNullOrEmpty(str)) return str; + + // 使用 Span 提高性能 + Span charSpan = stackalloc char[str.Length]; + for (int i = 0; i < str.Length; i++) + { + charSpan[str.Length - 1 - i] = str[i]; + } + return new string(charSpan); + } + + /// + /// 转首字母小写 + /// + public static string ToFirstLetterLowerCase(this string input) + { + if (string.IsNullOrWhiteSpace(input)) return input; + if (input.Length == 1) return input.ToLower(); // 处理单字符字符串 + + return char.ToLower(input[0]) + input[1..]; + } + + /// + /// 渲染字符串,替换占位符 + /// + /// 模板内容 + /// 参数对象 + /// + public static string Render(this string template, object parameters) + { + if (string.IsNullOrWhiteSpace(template)) return template; + + // 将参数转换为字典(忽略大小写) + var paramDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (parameters != null) + { + foreach (var prop in parameters.GetType().GetProperties()) + { + paramDict[prop.Name] = prop.GetValue(parameters)?.ToString() ?? string.Empty; + } + } + + // 使用正则表达式替换占位符 + return Regex.Replace(template, @"\{(\w+)\}", match => + { + string key = match.Groups[1].Value; // 获取占位符中的 key + return paramDict.TryGetValue(key, out string value) ? value : string.Empty; + }); + } + + /// + /// 驼峰转下划线 + /// + /// + /// + /// + public static string ToUnderLine(this string str, bool isToUpper = false) + { + if (string.IsNullOrEmpty(str) || str.Contains("_")) + { + return str; + } + + int length = str.Length; + var result = new System.Text.StringBuilder(length + (length / 3)); + + result.Append(char.ToLowerInvariant(str[0])); + + int lastIndex = length - 1; + + for (int i = 1; i < length; i++) + { + char current = str[i]; + if (!char.IsUpper(current)) + { + result.Append(current); + continue; + } + + bool prevIsLower = char.IsLower(str[i - 1]); + bool nextIsLower = (i < lastIndex) && char.IsLower(str[i + 1]); + + if (prevIsLower || nextIsLower) + { + result.Append('_'); + } + + result.Append((char)(current | 0x20)); + } + + string converted = result.ToString(); + return isToUpper ? converted.ToUpperInvariant() : converted; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/UseApplicationBuilder.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/UseApplicationBuilder.cs new file mode 100644 index 0000000..f10ca84 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Extension/UseApplicationBuilder.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AspNetCoreRateLimit; +using Microsoft.AspNetCore.Builder; + +namespace Admin.NET.Core; + +/// +/// 配置中间件扩展 +/// +public static class UseApplicationBuilder +{ + // 配置限流中间件策略 + public static void UsePolicyRateLimit(this IApplicationBuilder app) + { + var ipPolicyStore = app.ApplicationServices.GetRequiredService(); + ipPolicyStore.SeedAsync().GetAwaiter().GetResult(); + + var clientPolicyStore = app.ApplicationServices.GetRequiredService(); + clientPolicyStore.SeedAsync().GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/GlobalUsings.cs new file mode 100644 index 0000000..a1a6b8b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/GlobalUsings.cs @@ -0,0 +1,61 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core.Service; +global using Furion; +global using Furion.ConfigurableOptions; +global using Furion.DatabaseAccessor; +global using Furion.DataEncryption; +global using Furion.DataValidation; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.EventBus; +global using Furion.FriendlyException; +global using Furion.HttpRemote; +global using Furion.JsonSerialization; +global using Furion.Logging; +global using Furion.Schedule; +global using Furion.UnifyResult; +global using Furion.ViewEngine; +global using Magicodes.ExporterAndImporter.Core; +global using Magicodes.ExporterAndImporter.Core.Extension; +global using Magicodes.ExporterAndImporter.Excel; +global using Mapster; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using NewLife; +global using NewLife.Caching; +global using Newtonsoft.Json.Linq; +global using SKIT.FlurlHttpClient; +global using SKIT.FlurlHttpClient.Wechat.Api; +global using SKIT.FlurlHttpClient.Wechat.Api.Models; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Events; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models; +global using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings; +global using SqlSugar; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Data; +global using System.Diagnostics; +global using System.Linq.Dynamic.Core; +global using System.Linq.Expressions; +global using System.Reflection; +global using System.Runtime.InteropServices; +global using System.Text; +global using System.Text.RegularExpressions; +global using System.Web; +global using UAParser; +global using Yitter.IdGenerator; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs new file mode 100644 index 0000000..386ad74 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubInput.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class OnlineUserHubInput +{ + public string ConnectionId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs new file mode 100644 index 0000000..90c4425 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserHubOutput.cs @@ -0,0 +1,16 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class OnlineUserList +{ + public string RealName { get; set; } + + public bool Online { get; set; } + + public List UserList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/IOnlineUserHub.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/IOnlineUserHub.cs new file mode 100644 index 0000000..64fa9a2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/IOnlineUserHub.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public interface IOnlineUserHub +{ + /// + /// 在线用户列表 + /// + /// + /// + Task OnlineUserList(OnlineUserList context); + + /// + /// 强制下线 + /// + /// + /// + Task ForceOffline(object context); + + /// + /// 发布站内消息 + /// + /// + /// + Task PublicNotice(SysNotice context); + + /// + /// 接收消息 + /// + /// + /// + Task ReceiveMessage(object context); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs new file mode 100644 index 0000000..3df9cc6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs @@ -0,0 +1,182 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.InstantMessaging; +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core; + +/// +/// 在线用户集线器 +/// +[MapHub("/hubs/onlineUser")] +public class OnlineUserHub : Hub +{ + private const string GROUP_ONLINE = "GROUP_ONLINE_"; // 租户分组前缀 + + private readonly SqlSugarRepository _sysOnlineUerRep; + private readonly SysMessageService _sysMessageService; + private readonly IHubContext _onlineUserHubContext; + private readonly SysCacheService _sysCacheService; + private readonly SysConfigService _sysConfigService; + + public OnlineUserHub(SqlSugarRepository sysOnlineUerRep, + SysMessageService sysMessageService, + IHubContext onlineUserHubContext, + SysCacheService sysCacheService, + SysConfigService sysConfigService) + { + _sysOnlineUerRep = sysOnlineUerRep; + _sysMessageService = sysMessageService; + _onlineUserHubContext = onlineUserHubContext; + _sysCacheService = sysCacheService; + _sysConfigService = sysConfigService; + } + + /// + /// 连接 + /// + /// + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + var userId = (httpContext.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); + var account = httpContext.User.FindFirst(ClaimConst.Account)?.Value; + var realName = httpContext.User.FindFirst(ClaimConst.RealName)?.Value; + var tenantId = (httpContext.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong(); + + if (userId < 0 || string.IsNullOrWhiteSpace(account)) return; + var user = new SysOnlineUser + { + ConnectionId = Context.ConnectionId, + UserId = userId, + UserName = account, + RealName = realName, + Time = DateTime.Now, + Ip = httpContext.GetRemoteIpAddressToIPv4(true), + Browser = httpContext.GetClientBrowser(), + Os = httpContext.GetClientOs(), + TenantId = tenantId, + }; + await _sysOnlineUerRep.InsertAsync(user); + + // 是否开启单用户登录 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + _sysCacheService.HashAddOrUpdate(CacheConst.KeyUserOnline, "" + user.UserId, user); + } + else // 非单用户登录则绑定用户连接Id + { + _sysCacheService.HashAdd(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId, user); + } + + // 以租户Id进行分组 + var groupName = $"{GROUP_ONLINE}{user.TenantId}"; + await _onlineUserHubContext.Groups.AddToGroupAsync(Context.ConnectionId, groupName); + + var userList = await _sysOnlineUerRep.AsQueryable().Filter("", true) + .Where(u => u.TenantId == user.TenantId).Take(10).ToListAsync(); + + if (await _sysConfigService.GetConfigValue(ConfigConst.SysLoginOutReminder)) + await _onlineUserHubContext.Clients.Groups(groupName).OnlineUserList(new OnlineUserList + { + RealName = user.RealName, + Online = true, + UserList = userList + }); + } + + /// + /// 断开 + /// + /// + /// + public override async Task OnDisconnectedAsync(Exception exception) + { + if (string.IsNullOrEmpty(Context.ConnectionId)) return; + + var httpContext = Context.GetHttpContext(); + + var user = await _sysOnlineUerRep.AsQueryable().Filter("", true).FirstAsync(u => u.ConnectionId == Context.ConnectionId); + if (user == null) return; + + await _sysOnlineUerRep.DeleteByIdAsync(user.Id); + + // 是否开启单用户登录 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + _sysCacheService.HashDel(CacheConst.KeyUserOnline, "" + user.UserId); + // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId); + } + else + { + _sysCacheService.HashDel(CacheConst.KeyUserOnline, user.UserId + Context.ConnectionId); + // _sysCacheService.Remove(CacheConst.KeyUserOnline + user.UserId + Context.ConnectionId); + } + + // 通知当前组用户变动 + var userList = await _sysOnlineUerRep.AsQueryable().Filter("", true) + .Where(u => u.TenantId == user.TenantId).Take(10).ToListAsync(); + + if (await _sysConfigService.GetConfigValue(ConfigConst.SysLoginOutReminder)) + await _onlineUserHubContext.Clients.Groups($"{GROUP_ONLINE}{user.TenantId}").OnlineUserList(new OnlineUserList + { + RealName = user.RealName, + Online = false, + UserList = userList + }); + } + + /// + /// 强制下线 + /// + /// + /// + public async Task ForceOffline(OnlineUserHubInput input) + { + await _onlineUserHubContext.Clients.Client(input.ConnectionId).ForceOffline("强制下线"); + } + + /// + /// 发送信息给某个人 + /// + /// + /// + public async Task ClientsSendMessage(MessageInput message) + { + await _sysMessageService.SendUser(message); + } + + /// + /// 发送信息给所有人 + /// + /// + /// + public async Task ClientsSendMessageToAll(MessageInput message) + { + await _sysMessageService.SendAllUser(message); + } + + /// + /// 发送消息给某些人(除了本人) + /// + /// + /// + public async Task ClientsSendMessageToOther(MessageInput message) + { + await _sysMessageService.SendOtherUser(message); + } + + /// + /// 发送消息给某些人 + /// + /// + /// + public async Task ClientsSendMessageToUsers(MessageInput message) + { + await _sysMessageService.SendUsers(message); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs new file mode 100644 index 0000000..67eb8fa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 动态作业编译 +/// +public class DynamicJobCompiler : ISingleton +{ + /// + /// 编译代码并返回其中实现 IJob 的类型 + /// + /// 动态编译的作业代码 + /// + public Type BuildJob(string script) + { + var jobAssembly = Schedular.CompileCSharpClassCode(script); + var jobType = jobAssembly.GetTypes().FirstOrDefault(u => typeof(IJob).IsAssignableFrom(u)); + return jobType; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs new file mode 100644 index 0000000..4b84903 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs @@ -0,0 +1,146 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举转字典 +/// +[JobDetail("job_EnumToDictJob", Description = "枚举转字典", GroupName = "default", Concurrent = false)] +[PeriodSeconds(1, TriggerId = "trigger_EnumToDictJob", Description = "枚举转字典", MaxNumberOfRuns = 1, RunOnStart = true)] +public class EnumToDictJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private const string DefaultTagType = null; + private const int OrderOffset = 10; + + public EnumToDictJob(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典"); + + using var serviceScope = _scopeFactory.CreateScope(); + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + + var sysEnumService = serviceScope.ServiceProvider.GetRequiredService(); + var sysDictTypeList = GetDictByEnumType(sysEnumService.GetEnumTypeList()); + + // 校验枚举类命名规范,字典相关功能中需要通过后缀判断是否为枚举类型 + Console.ForegroundColor = ConsoleColor.Red; + foreach (var dictType in sysDictTypeList.Where(x => !x.Code.EndsWith("Enum"))) + Console.WriteLine($"【{DateTime.Now}】系统枚举转换字典的枚举类名称必须以Enum结尾: {dictType.Code} ({dictType.Name})"); + sysDictTypeList = sysDictTypeList.Where(x => x.Code.EndsWith("Enum")).ToList(); + + await SyncEnumToDictInfoAsync(db, sysDictTypeList); + + Console.ForegroundColor = ConsoleColor.Yellow; + try + { + await db.BeginTranAsync(); + var storageable1 = await db.Storageable(sysDictTypeList) + .SplitUpdate(it => it.Any()) + .SplitInsert(_ => true) + .ToStorageAsync(); + await storageable1.AsInsertable.ExecuteCommandAsync(stoppingToken); + await storageable1.AsUpdateable.ExecuteCommandAsync(stoppingToken); + + Console.WriteLine($"【{DateTime.Now}】系统枚举类转字典类型数据: 插入{storageable1.InsertList.Count}条, 更新{storageable1.UpdateList.Count}条, 共{storageable1.TotalList.Count}条。"); + + var storageable2 = await db.Storageable(sysDictTypeList.SelectMany(x => x.Children).ToList()) + .WhereColumns(it => new { it.DictTypeId, it.Value }) + .SplitUpdate(it => it.Any()) + .SplitInsert(_ => true) + .ToStorageAsync(); + await storageable2.AsInsertable.ExecuteCommandAsync(stoppingToken); + await storageable2.AsUpdateable.UpdateColumns(u => new + { + u.Label, + u.Code, + u.Value + }).ExecuteCommandAsync(stoppingToken); + + Console.WriteLine($"【{DateTime.Now}】系统枚举项转字典值数据: 插入{storageable2.InsertList.Count}条, 更新{storageable2.UpdateList.Count}条, 共{storageable2.TotalList.Count}条。"); + + await db.CommitTranAsync(); + } + catch (Exception error) + { + await db.RollbackTranAsync(); + Log.Error($"系统枚举转换字典操作错误:{error.Message}\n堆栈跟踪:{error.StackTrace}", error); + throw; + } + finally + { + Console.ForegroundColor = originColor; + } + } + + /// + /// 用于同步枚举转字典数据 + /// + /// + /// + private async Task SyncEnumToDictInfoAsync(SqlSugarClient db, List list) + { + var codeList = list.Select(x => x.Code).ToList(); + foreach (var dbDictType in await db.Queryable().ClearFilter().Where(x => codeList.Contains(x.Code)).ToListAsync() ?? new()) + { + var enumDictType = list.First(x => x.Code == dbDictType.Code); + if (enumDictType.Id == dbDictType.Id) + { + // 字典值表字段改变后每条字典值记录会多出一条,此处用于删除多余的字典值数据 + var dataValueList = enumDictType.Children.Select(e => e.Value).ToList(); + await db.Deleteable().Where(x => x.DictTypeId == dbDictType.Id && !dataValueList.Contains(x.Value)).ExecuteCommandAsync(); + continue; + } + + // 数据不一致则删除 + await db.Deleteable().Where(x => x.DictTypeId == dbDictType.Id).ExecuteCommandAsync(); + await db.Deleteable().Where(x => x.Id == dbDictType.Id).ExecuteCommandAsync(); + Console.WriteLine($"【{DateTime.Now}】删除字典数据: {dbDictType.Name}-{dbDictType.Code}"); + } + } + + /// + /// 枚举信息转字典 + /// + /// + /// + private List GetDictByEnumType(List enumTypeList) + { + var orderNo = 1; + var list = new List(); + foreach (var type in enumTypeList) + { + var dictType = new SysDictType + { + Id = 900000000000 + CommonUtil.GetFixedHashCode(type.TypeFullName), + SysFlag = YesNoEnum.Y, + Code = type.TypeName, + Name = type.TypeDescribe, + Remark = type.TypeFullName + }; + dictType.Children = type.EnumEntities.Select(x => new SysDictData + { + Id = dictType.Id + orderNo++, + DictTypeId = dictType.Id, + Code = x.Name, + Label = x.Describe, + Value = x.Value.ToString(), + OrderNo = x.Value + OrderOffset, + TagType = x.Theme != "" ? x.Theme : DefaultTagType + }).ToList(); + list.Add(dictType); + } + return list; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/LogJob.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/LogJob.cs new file mode 100644 index 0000000..eba51dc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/LogJob.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 清理日志作业任务 +/// +[JobDetail("job_log", Description = "清理操作日志", GroupName = "default", Concurrent = false)] +[Daily(TriggerId = "trigger_log", Description = "清理操作日志")] +public class LogJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public LogJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + var sysConfigService = serviceScope.ServiceProvider.GetRequiredService(); + + var daysAgo = await sysConfigService.GetConfigValue(ConfigConst.SysLogRetentionDays); // 日志保留天数 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除访问日志 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除操作日志 + await db.Deleteable().Where(u => u.CreateTime < DateTime.Now.AddDays(-daysAgo)).ExecuteCommandAsync(stoppingToken); // 删除差异日志 + + string msg = $"【{DateTime.Now}】清理系统日志成功,删除 {daysAgo} 天前的日志数据!"; + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg); + Console.ForegroundColor = originColor; + + // 自定义日志 + _logger.LogInformation(msg); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs new file mode 100644 index 0000000..7e49e69 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Job/OnlineUserJob.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; + +namespace Admin.NET.Core; + +/// +/// 清理在线用户作业任务 +/// +[JobDetail("job_onlineUser", Description = "清理在线用户", GroupName = "default", Concurrent = false)] +[PeriodSeconds(1, TriggerId = "trigger_onlineUser", Description = "清理在线用户", MaxNumberOfRuns = 1, RunOnStart = true)] +public class OnlineUserJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public OnlineUserJob(IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + + var db = serviceScope.ServiceProvider.GetRequiredService().CopyNew(); + await db.Deleteable().ExecuteCommandAsync(stoppingToken); + + string msg = $"【{DateTime.Now}】清理在线用户成功!服务已重启..."; + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(msg); + Console.ForegroundColor = originColor; + + // 自定义日志 + _logger.LogInformation(msg); + + // 缓存租户列表 + await serviceScope.ServiceProvider.GetRequiredService().CacheTenant(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs new file mode 100644 index 0000000..d8db2c8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs @@ -0,0 +1,206 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据库日志写入器 +/// +public class DatabaseLoggingWriter : IDatabaseLoggingWriter, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly ISqlSugarClient _db; + private readonly SysConfigService _sysConfigService; // 参数配置服务 + private readonly ILogger _logger; // 日志组件 + + public DatabaseLoggingWriter(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + //_db = _serviceScope.ServiceProvider.GetRequiredService(); + _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService(); + _logger = _serviceScope.ServiceProvider.GetRequiredService>(); + + // 切换日志独立数据库 + _db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) + ? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) + : SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + } + + public async Task WriteAsync(LogMessage logMsg, bool flush) + { + var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString(); + if (string.IsNullOrWhiteSpace(jsonStr)) + { + await _db.Insertable(new SysLogOp + { + DisplayTitle = "自定义操作日志", + LogDateTime = logMsg.LogDateTime, + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = logMsg.Exception == null ? null : JSON.Serialize(logMsg.Exception), + Message = logMsg.Message, + LogLevel = logMsg.LogLevel, + Status = "200", + }).ExecuteCommandAsync(); + return; + } + + var loggingMonitor = JSON.Deserialize(jsonStr); + // 记录数据校验日志 + if (loggingMonitor.validation != null && !await _sysConfigService.GetConfigValue(ConfigConst.SysValidationLog)) return; + + // 获取当前操作者 + string account = "", realName = "", userId = "", tenantId = ""; + if (loggingMonitor.authorizationClaims != null) + { + var map = (loggingMonitor.authorizationClaims as IEnumerable) + !.ToDictionary(u => u.type.ToString(), u => u.value.ToString()); + account = map.GetValueOrDefault(ClaimConst.Account); + realName = map.GetValueOrDefault(ClaimConst.RealName); + tenantId = map.GetValueOrDefault(ClaimConst.TenantId); + userId = map.GetValueOrDefault(ClaimConst.UserId); + } + + // 优先获取 X-Forwarded-For 头部信息携带的IP地址(如nginx代理配置转发) + var remoteIPv4 = ((JArray)loggingMonitor.requestHeaders).OfType() + .FirstOrDefault(header => (string)header["key"] == "X-Forwarded-For")?["value"]?.ToString(); + + if (string.IsNullOrEmpty(remoteIPv4)) + remoteIPv4 = loggingMonitor.remoteIPv4; + + remoteIPv4 = remoteIPv4?.Split(',')?.FirstOrDefault()?.Trim(); + + (string ipLocation, double? longitude, double? latitude) = CommonUtil.GetIpAddress(remoteIPv4); + + var browser = ""; + var os = ""; + if (loggingMonitor.userAgent != null) + { + var client = Parser.GetDefault().Parse(loggingMonitor.userAgent.ToString()); + browser = $"{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + os = $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}"; + } + + // 捕捉异常,否则会由于 unhandled exception 导致程序崩溃 + try + { + // 记录异常日志-发送邮件 + if (logMsg.Exception != null || loggingMonitor.exception != null) + { + await _db.Insertable(new SysLogEx + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + + // 将异常日志发送到邮件 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysErrorMail)) + { + await App.GetRequiredService().PublishAsync(CommonConst.SendErrorMail, logMsg.Exception ?? loggingMonitor.exception); + } + + return; + } + + // 记录访问日志-登录退出 + if (loggingMonitor.actionName == "userInfo" || loggingMonitor.actionName == "logout") + { + await _db.Insertable(new SysLogVis + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + return; + } + + // 记录操作日志 + if (!await _sysConfigService.GetConfigValue(ConfigConst.SysOpLog)) return; + await _db.Insertable(new SysLogOp + { + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation?.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = browser, // loggingMonitor.userAgent, + Os = os, // loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = loggingMonitor.returnInformation == null ? null : JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = loggingMonitor.exception == null ? null : JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId), + LogLevel = logMsg.LogLevel + }).ExecuteCommandAsync(); + + await Task.Delay(50); // 延迟 0.05 秒写入数据库,有效减少高频写入数据库导致死锁问题 + } + catch (Exception ex) + { + _logger.LogError(ex, "操作日志入库"); + } + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs new file mode 100644 index 0000000..64bd80a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Elastic.Clients.Elasticsearch; + +namespace Admin.NET.Core; + +/// +/// ES日志写入器 +/// +public class ElasticSearchLoggingWriter : IDatabaseLoggingWriter, IDisposable +{ + private readonly IServiceScope _serviceScope; + private readonly ElasticsearchClient _esClient; + private readonly SysConfigService _sysConfigService; + + public ElasticSearchLoggingWriter(IServiceScopeFactory scopeFactory) + { + _serviceScope = scopeFactory.CreateScope(); + _esClient = _serviceScope.ServiceProvider.GetRequiredService()?.Logging; + _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService(); + } + + public async Task WriteAsync(LogMessage logMsg, bool flush) + { + // 是否启用操作日志 + var sysOpLogEnabled = await _sysConfigService.GetConfigValue(ConfigConst.SysOpLog); + if (!sysOpLogEnabled) return; + + var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString(); + if (string.IsNullOrWhiteSpace(jsonStr)) return; + + var loggingMonitor = JSON.Deserialize(jsonStr); + + // 不记录登录退出日志 + if (loggingMonitor.actionName == "userInfo" || loggingMonitor.actionName == "logout") + return; + + // 获取当前操作者 + string account = "", realName = "", userId = "", tenantId = ""; + if (loggingMonitor.authorizationClaims != null) + { + foreach (var item in loggingMonitor.authorizationClaims) + { + if (item.type == ClaimConst.Account) + account = item.value; + if (item.type == ClaimConst.RealName) + realName = item.value; + if (item.type == ClaimConst.TenantId) + tenantId = item.value; + if (item.type == ClaimConst.UserId) + userId = item.value; + } + } + + string remoteIPv4 = loggingMonitor.remoteIPv4; + (string ipLocation, double? longitude, double? latitude) = CommonUtil.GetIpAddress(remoteIPv4); + + var sysLogOp = new SysLogOp + { + Id = DateTime.Now.Ticks, + ControllerName = loggingMonitor.controllerName, + ActionName = loggingMonitor.actionTypeName, + DisplayTitle = loggingMonitor.displayTitle, + Status = loggingMonitor.returnInformation.httpStatusCode, + RemoteIp = remoteIPv4, + Location = ipLocation, + Longitude = (decimal?)longitude, + Latitude = (decimal?)latitude, + Browser = loggingMonitor.userAgent, + Os = loggingMonitor.osDescription + " " + loggingMonitor.osArchitecture, + Elapsed = loggingMonitor.timeOperationElapsedMilliseconds, + LogDateTime = logMsg.LogDateTime, + Account = account, + RealName = realName, + HttpMethod = loggingMonitor.httpMethod, + RequestUrl = loggingMonitor.requestUrl, + RequestParam = (loggingMonitor.parameters == null || loggingMonitor.parameters.Count == 0) ? null : JSON.Serialize(loggingMonitor.parameters[0].value), + ReturnResult = JSON.Serialize(loggingMonitor.returnInformation), + EventId = logMsg.EventId.Id, + ThreadId = logMsg.ThreadId, + TraceId = logMsg.TraceId, + Exception = (loggingMonitor.exception == null) ? null : JSON.Serialize(loggingMonitor.exception), + Message = logMsg.Message, + CreateUserId = string.IsNullOrWhiteSpace(userId) ? 0 : long.Parse(userId), + TenantId = string.IsNullOrWhiteSpace(tenantId) ? 0 : long.Parse(tenantId) + }; + await _esClient.IndexAsync(sysLogOp); + } + + /// + /// 释放服务作用域 + /// + public void Dispose() + { + _serviceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LogExceptionHandler.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LogExceptionHandler.cs new file mode 100644 index 0000000..7895563 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LogExceptionHandler.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +//using Microsoft.AspNetCore.Mvc.Controllers; +//using System.Security.Claims; + +//namespace Admin.NET.Core.Logging; + +///// +///// 全局异常处理 +///// +//public class LogExceptionHandler : IGlobalExceptionHandler, ISingleton +//{ +// private readonly IEventPublisher _eventPublisher; + +// public LogExceptionHandler(IEventPublisher eventPublisher) +// { +// _eventPublisher = eventPublisher; +// } + +// public async Task OnExceptionAsync(ExceptionContext context) +// { +// var actionMethod = (context.ActionDescriptor as ControllerActionDescriptor)?.MethodInfo; +// var displayNameAttribute = actionMethod.IsDefined(typeof(DisplayNameAttribute), true) ? actionMethod.GetCustomAttribute(true) : default; + +// var sysLogEx = new SysLogEx +// { +// Account = App.User?.FindFirstValue(ClaimConst.Account), +// RealName = App.User?.FindFirstValue(ClaimConst.RealName), +// ControllerName = actionMethod.DeclaringType.FullName, +// ActionName = actionMethod.Name, +// DisplayTitle = displayNameAttribute?.DisplayName, +// Exception = $"异常信息:{context.Exception.Message} 异常来源:{context.Exception.Source} 堆栈信息:{context.Exception.StackTrace}", +// Message = "全局异常", +// RequestParam = context.Exception.TargetSite.GetParameters().ToString(), +// HttpMethod = context.HttpContext.Request.Method, +// RequestUrl = context.HttpContext.Request.GetRequestUrlAddress(), +// RemoteIp = context.HttpContext.GetRemoteIpAddressToIPv4(), +// Browser = context.HttpContext.Request.Headers["User-Agent"], +// TraceId = App.GetTraceId(), +// ThreadId = App.GetThreadId(), +// LogDateTime = DateTime.Now, +// LogLevel = LogLevel.Error +// }; + +// await _eventPublisher.PublishAsync(new ChannelEventSource("Add:ExLog", sysLogEx)); + +// await _eventPublisher.PublishAsync("Send:ErrorMail", sysLogEx); +// } +//} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LoggingSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LoggingSetup.cs new file mode 100644 index 0000000..17cc785 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Logging/LoggingSetup.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class LoggingSetup +{ + /// + /// 日志注册 + /// + /// + public static void AddLoggingSetup(this IServiceCollection services) + { + // 日志监听 + services.AddMonitorLogging(options => + { + options.IgnorePropertyNames = new[] { "Byte" }; + options.IgnorePropertyTypes = new[] { typeof(byte[]) }; + }); + + // 控制台日志 + var consoleLog = App.GetConfig("Logging:Monitor:ConsoleLog", true); + services.AddConsoleFormatter(options => + { + options.DateFormat = "yyyy-MM-dd HH:mm:ss(zzz) dddd"; + //options.WithTraceId = true; // 显示线程Id + //options.WithStackFrame = true; // 显示程序集 + options.WriteFilter = (logMsg) => + { + return consoleLog; + }; + }); + + // 日志写入文件 + if (App.GetConfig("Logging:File:Enabled", true)) + { + var loggingMonitorSettings = App.GetConfig("Logging:Monitor", true); + Array.ForEach(new[] { LogLevel.Information, LogLevel.Warning, LogLevel.Error }, logLevel => + { + services.AddFileLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.FileNameRule = fileName => string.Format(fileName, DateTime.Now, logLevel.ToString()); // 每天创建一个文件 + options.WriteFilter = logMsg => logMsg.LogLevel == logLevel; // 日志级别 + options.HandleWriteError = (writeError) => // 写入失败时启用备用文件 + { + writeError.UseRollbackFileName(Path.GetFileNameWithoutExtension(writeError.CurrentFileName) + "-oops" + Path.GetExtension(writeError.CurrentFileName)); + }; + if (loggingMonitorSettings.JsonBehavior == JsonBehavior.OnlyJson) + { + options.MessageFormat = LoggerFormatter.Json; + // options.MessageFormat = LoggerFormatter.JsonIndented; + options.MessageFormat = (logMsg) => + { + var jsonString = logMsg.Context.Get("loggingMonitor"); + return jsonString?.ToString(); + }; + } + }); + }); + } + + // 日志写入ElasticSearch + if (App.GetConfig("ElasticSearch:Logging:Enabled", true)) + { + services.AddDatabaseLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.IgnoreReferenceLoop = false; // 忽略循环检测 + options.MessageFormat = LoggerFormatter.Json; + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == CommonConst.SysLogCategoryName; // 只写LoggingMonitor日志 + }; + }); + } + + // 日志写入数据库 + if (App.GetConfig("Logging:Database:Enabled", true)) + { + services.AddDatabaseLogging(options => + { + options.WithTraceId = true; // 显示线程Id + options.WithStackFrame = true; // 显示程序集 + options.IgnoreReferenceLoop = false; // 忽略循环检测 + options.MessageFormat = (logMsg) => + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(logMsg.Message); + return stringBuilder.ToString(); + }; + options.WriteFilter = (logMsg) => + { + return logMsg.LogName == CommonConst.SysLogCategoryName; // 只写LoggingMonitor日志 + }; + }); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/APIJSONOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/APIJSONOptions.cs new file mode 100644 index 0000000..c12625a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/APIJSONOptions.cs @@ -0,0 +1,70 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// APIJSON配置选项 +/// +public sealed class APIJSONOptions : IConfigurableOptions +{ + /// + /// 角色集合 + /// + public List Roles { get; set; } +} + +/// +/// APIJSON角色权限 +/// +public class APIJSON_Role +{ + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 查询 + /// + public APIJSON_RoleItem Select { get; set; } + + /// + /// 增加 + /// + public APIJSON_RoleItem Insert { get; set; } + + /// + /// 更新 + /// + public APIJSON_RoleItem Update { get; set; } + + /// + /// 删除 + /// + public APIJSON_RoleItem Delete { get; set; } +} + +/// +/// APIJSON角色权限内容 +/// +public class APIJSON_RoleItem +{ + /// + /// 表集合 + /// + public string[] Table { get; set; } + + /// + /// 列集合 + /// + public string[] Column { get; set; } + + /// + /// 过滤器 + /// + public string[] Filter { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/AlipayOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/AlipayOptions.cs new file mode 100644 index 0000000..ddd1bb0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/AlipayOptions.cs @@ -0,0 +1,119 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api; + +namespace Admin.NET.Core; + +/// +/// 支付宝支付配置选项 +/// +public sealed class AlipayOptions : IConfigurableOptions +{ + /// + /// 支付宝网关地址 + /// + public string ServerUrl { get; init; } + + /// + /// 支付宝授权回调地址 + /// + public string AuthUrl { get; init; } + + /// + /// 应用授权回调地址 + /// + public string AppAuthUrl { get; init; } + + /// + /// 支付宝 websocket 服务地址 + /// + public string WebsocketUrl { get; init; } + + /// + /// 应用回调地址 + /// + public string NotifyUrl { get; init; } + + /// + /// 支付宝根证书存放路径 + /// + public string RootCertPath { get; init; } + + /// + /// 支付宝商户账号列表 + /// + public List AccountList { get; init; } + + /// + /// 获取支付宝客户端 + /// + /// + public DefaultAopClient GetClient(AlipayMerchantAccount account) + { + account = account ?? throw new Exception("未找到支付宝商户账号"); + string path = App.WebHostEnvironment.ContentRootPath; + return new DefaultAopClient(new AlipayConfig + { + Format = "json", + Charset = "UTF-8", + ServerUrl = ServerUrl, + AppId = account.AppId, + SignType = account.SignType, + PrivateKey = account.PrivateKey, + EncryptKey = account.EncryptKey, + RootCertPath = Path.Combine(path, RootCertPath), + AppCertPath = Path.Combine(path, account.AppCertPath), + AlipayPublicCertPath = Path.Combine(path, account.AlipayPublicCertPath) + }); + } +} + +/// +/// 支付宝商户账号信息 +/// +public class AlipayMerchantAccount +{ + /// + /// 配置Id + /// + public long Id { get; init; } + + /// + /// 商户名称 + /// + public string Name { get; init; } + + /// + /// 商户AppId + /// + public string AppId { get; init; } + + /// + /// 应用私钥 + /// + public string PrivateKey { get; init; } + + /// + /// 从支付宝获取敏感信息时的加密密钥(可选) + /// + public string EncryptKey { get; init; } + + /// + /// 加密算法 + /// + public string SignType { get; init; } + + /// + /// 应用公钥证书路径 + /// + public string AppCertPath { get; init; } + + /// + /// 支付宝公钥证书路径 + /// + public string AlipayPublicCertPath { get; init; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CDConfigOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CDConfigOptions.cs new file mode 100644 index 0000000..5ed01e9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CDConfigOptions.cs @@ -0,0 +1,84 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// CI/CD 配置选项 +/// +public class CDConfigOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } + + /// + /// 用户名 + /// + public string Owner { get; set; } + + /// + /// 仓库名 + /// + public string Repo { get; set; } + + /// + /// 分支名 + /// + public string Branch { get; set; } + + /// + /// 用户授权码 + /// + public string AccessToken { get; set; } + + /// + /// 更新间隔限制(分钟)0 不限制 + /// + public int UpdateInterval { get; set; } + + /// + /// 保留备份文件的数量, 0 不限制 + /// + public int BackupCount { get; set; } + + /// + /// 输出目录配置 + /// + public string BackendOutput { get; set; } + + /// + /// 发布配置选项 + /// + public PublishOptions Publish { get; set; } + + /// + /// 排除文件列表 + /// + public List ExcludeFiles { get; set; } +} + +/// +/// 编译发布配置选项 +/// +public class PublishOptions +{ + /// + /// 发布环境配置 + /// + public string Configuration { get; set; } + + /// + /// 目标框架 + /// + public string TargetFramework { get; set; } + + /// + /// 运行环境 + /// + public string RuntimeIdentifier { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CacheOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CacheOptions.cs new file mode 100644 index 0000000..b9ee81b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CacheOptions.cs @@ -0,0 +1,142 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 缓存配置选项 +/// +public sealed class CacheOptions : IConfigurableOptions +{ + /// + /// 缓存前缀 + /// + public string Prefix { get; set; } + + /// + /// 缓存类型 + /// + public string CacheType { get; set; } + + /// + /// Redis缓存 + /// + public RedisOption Redis { get; set; } + + public void PostConfigure(CacheOptions options, IConfiguration configuration) + { + options.Prefix = string.IsNullOrWhiteSpace(options.Prefix) ? "" : options.Prefix.Trim(); + } +} + +/// +/// Redis缓存 +/// +public sealed class RedisOption : RedisOptions +{ + /// + /// 最大消息大小 + /// + public int MaxMessageSize { get; set; } +} + +/// +/// 集群配置选项 +/// +public sealed class ClusterOptions : IConfigurableOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } + + /// + /// 服务器标识 + /// + public string ServerId { get; set; } + + /// + /// 服务器IP + /// + public string ServerIp { get; set; } + + /// + /// SignalR配置 + /// + public ClusterSignalR SignalR { get; set; } + + /// + /// 数据保护key + /// + public string DataProtecteKey { get; set; } + + /// + /// 是否哨兵模式 + /// + public bool IsSentinel { get; set; } + + /// + /// 哨兵配置 + /// + public StackExchangeSentinelConfig SentinelConfig { get; set; } +} + +/// +/// 集群SignalR配置 +/// +public sealed class ClusterSignalR +{ + /// + /// Redis连接字符串 + /// + public string RedisConfiguration { get; set; } + + /// + /// 缓存前缀 + /// + public string ChannelPrefix { get; set; } +} + +/// +/// 哨兵配置 +/// +public sealed class StackExchangeSentinelConfig +{ + /// + /// master名称 + /// + public string ServiceName { get; set; } + + /// + /// master访问密码 + /// + public string Password { get; set; } + + /// + /// 哨兵访问密码 + /// + public string SentinelPassword { get; set; } + + /// + /// 哨兵端口 + /// + public List EndPoints { get; set; } + + /// + /// 默认库 + /// + public int DefaultDb { get; set; } + + /// + /// 主前缀 + /// + public string MainPrefix { get; set; } + + /// + /// SignalR前缀 + /// + public string SignalRChannelPrefix { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CodeGenOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CodeGenOptions.cs new file mode 100644 index 0000000..66735f7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CodeGenOptions.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 代码生成配置选项 +/// +public sealed class CodeGenOptions : IConfigurableOptions +{ + /// + /// 数据库实体程序集名称集合 + /// + public List EntityAssemblyNames { get; set; } + + /// + /// 数据库实体基类名称集合 + /// + public List BaseEntityNames { get; set; } + + /// + /// 前端文件根目录 + /// + public string FrontRootPath { get; set; } + + /// + /// 后端生成到的项目 + /// + public List BackendApplicationNamespaces { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CryptogramOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CryptogramOptions.cs new file mode 100644 index 0000000..44a43ec --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/CryptogramOptions.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 密码配置选项 +/// +public sealed class CryptogramOptions : IConfigurableOptions +{ + /// + /// 是否开启密码强度验证 + /// + public bool StrongPassword { get; set; } + + /// + /// 密码强度验证正则表达式 + /// + public string PasswordStrengthValidation { get; set; } + + /// + /// 密码强度验证提示 + /// + public string PasswordStrengthValidationMsg { get; set; } + + /// + /// 密码类型 + /// + public string CryptoType { get; set; } + + /// + /// 公钥 + /// + public string PublicKey { get; set; } + + /// + /// 私钥 + /// + public string PrivateKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs new file mode 100644 index 0000000..f1c046f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs @@ -0,0 +1,136 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据库配置选项 +/// +public sealed class DbConnectionOptions : IConfigurableOptions +{ + /// + /// 启用控制台打印SQL + /// + public bool EnableConsoleSql { get; set; } + + /// + /// 超级管理员是否忽略逻辑删除过滤器 + /// + public bool SuperAdminIgnoreIDeletedFilter { get; set; } + + /// + /// 数据库集合 + /// + public List ConnectionConfigs { get; set; } + + public void PostConfigure(DbConnectionOptions options, IConfiguration configuration) + { + foreach (var dbConfig in options.ConnectionConfigs) + { + if (dbConfig.ConfigId == null || string.IsNullOrWhiteSpace(dbConfig.ConfigId.ToString())) + dbConfig.ConfigId = SqlSugarConst.MainConfigId; + } + } +} + +/// +/// 数据库连接配置 +/// +public sealed class DbConnectionConfig : ConnectionConfig +{ + /// + /// 数据库名称 + /// + public string DbNickName { get; set; } + + /// + /// 数据库配置 + /// + public DbSettings DbSettings { get; set; } + + /// + /// 表配置 + /// + public TableSettings TableSettings { get; set; } + + /// + /// 种子配置 + /// + public SeedSettings SeedSettings { get; set; } + + /// + /// 隔离方式 + /// + public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Id; + + /// + /// 数据库存储目录(仅SqlServer支持指定目录创建) + /// + public string DatabaseDirectory { get; set; } +} + +/// +/// 数据库配置 +/// +public sealed class DbSettings +{ + /// + /// 启用库表初始化 + /// + public bool EnableInitDb { get; set; } + + /// + /// 启用视图初始化 + /// + public bool EnableInitView { get; set; } + + /// + /// 启用库表差异日志 + /// + public bool EnableDiffLog { get; set; } + + /// + /// 启用驼峰转下划线 + /// + public bool EnableUnderLine { get; set; } + + /// + /// 启用数据库连接串加密策略 + /// + public bool EnableConnStringEncrypt { get; set; } +} + +/// +/// 表配置 +/// +public sealed class TableSettings +{ + /// + /// 启用表初始化 + /// + public bool EnableInitTable { get; set; } + + /// + /// 启用表增量更新 + /// + public bool EnableIncreTable { get; set; } +} + +/// +/// 种子配置 +/// +public sealed class SeedSettings +{ + /// + /// 启用种子初始化 + /// + public bool EnableInitSeed { get; set; } + + /// + /// 启用种子增量更新 + /// + public bool EnableIncreSeed { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DeepSeekOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DeepSeekOptions.cs new file mode 100644 index 0000000..5f9b0b3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/DeepSeekOptions.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public sealed class DeepSeekOptions : IConfigurableOptions +{ + /// + /// 源语言 + /// + public string SourceLang { get; set; } + + /// + /// Api地址 + /// + public string ApiUrl { get; set; } + + /// + /// API KEY + /// + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/ElasticSearchOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/ElasticSearchOptions.cs new file mode 100644 index 0000000..1e6f73a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/ElasticSearchOptions.cs @@ -0,0 +1,64 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// ES配置选项 +/// +public class ElasticSearchOptions +{ + /// + /// 是否启用 + /// + public bool Enabled { get; set; } = false; + + /// + /// ES认证类型,可选 Basic、ApiKey、Base64ApiKey + /// + public ElasticSearchAuthTypeEnum AuthType { get; set; } + + /// + /// Basic认证的用户名 + /// + public string User { get; set; } + + /// + /// Basic认证的密码 + /// + public string Password { get; set; } + + /// + /// ApiKey认证的ApiId + /// + public string ApiId { get; set; } + + /// + /// ApiKey认证的ApiKey + /// + public string ApiKey { get; set; } + + /// + /// Base64ApiKey认证时加密的加密字符串 + /// + public string Base64ApiKey { get; set; } + + /// + /// ES使用Https时的证书指纹,使用证书请自行实现 + /// https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/connecting.html + /// + public string Fingerprint { get; set; } + + /// + /// 地址 + /// + public List ServerUris { get; set; } = new List(); + + /// + /// 索引 + /// + public string DefaultIndex { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EmailOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EmailOptions.cs new file mode 100644 index 0000000..10933b4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EmailOptions.cs @@ -0,0 +1,58 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 邮件配置选项 +/// +public sealed class EmailOptions : IConfigurableOptions +{ + /// + /// 主机 + /// + public string Host { get; set; } + + /// + /// 端口 + /// + public int Port { get; set; } + + /// + /// 默认发件者邮箱 + /// + public string DefaultFromEmail { get; set; } + + /// + /// 默认接收人邮箱 + /// + public string DefaultToEmail { get; set; } + + /// + /// 启用SSL + /// + public bool EnableSsl { get; set; } + + ///// + ///// 是否使用默认凭据 + ///// + //public bool UseDefaultCredentials { get; set; } + + /// + /// 邮箱账号 + /// + public string UserName { get; set; } + + /// + /// 邮箱密码 + /// + public string Password { get; set; } + + /// + /// 默认邮件标题 + /// + public string DefaultFromName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EnumOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EnumOptions.cs new file mode 100644 index 0000000..2da8992 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EnumOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 枚举配置选项 +/// +public sealed class EnumOptions : IConfigurableOptions +{ + /// + /// 枚举实体程序集名称集合 + /// + public List EntityAssemblyNames { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EventBusOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EventBusOptions.cs new file mode 100644 index 0000000..45e7261 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/EventBusOptions.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 事件总线配置选项 +/// +public sealed class EventBusOptions : IConfigurableOptions +{ + /// + /// RabbitMQ + /// + public RabbitMQSettings RabbitMQ { get; set; } +} + +/// +/// RabbitMQ +/// +public sealed class RabbitMQSettings +{ + /// + /// 账号 + /// + public string UserName { get; set; } + + /// + /// 密码 + /// + public string Password { get; set; } + + /// + /// 主机 + /// + public string HostName { get; set; } + + /// + /// 端口 + /// + public int Port { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/LocalizationSettingsOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/LocalizationSettingsOptions.cs new file mode 100644 index 0000000..c4da9ef --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/LocalizationSettingsOptions.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public sealed class LocalizationSettingsOptions : IConfigurableOptions +{ + /// + /// 语言列表 + /// + public List SupportedCultures { get; set; } + + /// + /// 默认语言 + /// + public string DefaultCulture { get; set; } + + /// + /// 固定时间区域为特定时区(多语言) + /// + public string DateTimeFormatCulture { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/OAuthOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/OAuthOptions.cs new file mode 100644 index 0000000..4ea822b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/OAuthOptions.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 第三方登录授权配置选项 +/// +public sealed class OAuthOptions : IConfigurableOptions +{ + /// + /// Weixin配置 + /// + public Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions Weixin { get; set; } + + /// + /// Gitee配置 + /// + public Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions Gitee { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/PayCallBackOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/PayCallBackOptions.cs new file mode 100644 index 0000000..b110fd0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/PayCallBackOptions.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 支付回调配置选项 +/// +public sealed class PayCallBackOptions : IConfigurableOptions +{ + /// + /// 微信支付回调 + /// + public string WechatPayUrl { get; set; } + + /// + /// 微信退款回调 + /// + public string WechatRefundUrl { get; set; } + + /// + /// 支付宝支付回调 + /// + public string AlipayUrl { get; set; } + + /// + /// 支付宝退款回调 + /// + public string AlipayRefundUrl { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/RateLimitOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/RateLimitOptions.cs new file mode 100644 index 0000000..057166a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/RateLimitOptions.cs @@ -0,0 +1,37 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AspNetCoreRateLimit; + +namespace Admin.NET.Core; + +/// +/// IP限流配置选项 +/// +public sealed class IpRateLimitingOptions : IpRateLimitOptions +{ +} + +/// +/// IP限流策略配置选项 +/// +public sealed class IpRateLimitPoliciesOptions : IpRateLimitPolicies, IConfigurableOptions +{ +} + +/// +/// 客户端限流配置选项 +/// +public sealed class ClientRateLimitingOptions : ClientRateLimitOptions, IConfigurableOptions +{ +} + +/// +/// 客户端限流策略配置选项 +/// +public sealed class ClientRateLimitPoliciesOptions : ClientRateLimitPolicies, IConfigurableOptions +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SMSOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SMSOptions.cs new file mode 100644 index 0000000..524e08b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SMSOptions.cs @@ -0,0 +1,138 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 短信配置选项 +/// +public sealed class SMSOptions : IConfigurableOptions +{ + /// + /// 验证码缓存过期时间(秒) + /// 默认: 60秒 + /// + public int VerifyCodeExpireSeconds { get; set; } = 60; + + /// + /// Aliyun + /// + public SMSSettings Aliyun { get; set; } + + /// + /// Tencentyun + /// + public SMSSettings Tencentyun { get; set; } + + /// + /// Custom 自定义短信接口 + /// + public CustomSMSSettings Custom { get; set; } +} + +public sealed class SMSSettings +{ + /// + /// SdkAppId + /// + public string SdkAppId { get; set; } + + /// + /// AccessKey ID + /// + public string AccessKeyId { get; set; } + + /// + /// AccessKey Secret + /// + public string AccessKeySecret { get; set; } + + /// + /// Templates + /// + public List Templates { get; set; } + + /// + /// GetTemplate + /// + public SmsTemplate GetTemplate(string id = "0") + { + foreach (var template in Templates) + { + if (template.Id == id) { return template; } + } + return null; + } +} + +public class SmsTemplate +{ + public string Id { get; set; } = string.Empty; + public string SignName { get; set; } + public string TemplateCode { get; set; } + public string Content { get; set; } +} + +/// +/// 自定义短信配置 +/// +public sealed class CustomSMSSettings +{ + /// + /// 是否启用自定义短信接口 + /// + public bool Enabled { get; set; } + + /// + /// API 接口地址模板 + /// 支持占位符: {mobile} - 手机号, {content} - 短信内容, {code} - 验证码 + /// + /// 示例: https://api.xxxx.com/sms?u=xxxx&key=59e03f49c3dbb5033&m={mobile}&c={content} + public string ApiUrl { get; set; } + + /// + /// 请求方法 (GET/POST) + /// + public string Method { get; set; } = "GET"; + + /// + /// POST 请求的 Content-Type (application/json 或 application/x-www-form-urlencoded) + /// 默认: application/x-www-form-urlencoded + /// + public string ContentType { get; set; } = "application/x-www-form-urlencoded"; + + /// + /// POST 请求的数据模板(支持占位符) + /// + /// + /// JSON 格式示例: {"mobile":"{mobile}","content":"{content}","apikey":"your_key"}
+ /// Form 格式示例: mobile={mobile}&content={content}&apikey=your_key + ///
+ public string PostData { get; set; } + + /// + /// 成功响应标识(用于判断发送是否成功) + /// 如果响应内容包含此字符串,则认为发送成功 + /// + public string SuccessFlag { get; set; } = "0"; + + /// + /// 短信模板列表 + /// + public List Templates { get; set; } + + /// + /// 获取模板 + /// + public SmsTemplate GetTemplate(string id = "0") + { + foreach (var template in Templates) + { + if (template.Id == id) { return template; } + } + return null; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SnowIdOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SnowIdOptions.cs new file mode 100644 index 0000000..07b2b53 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/SnowIdOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 雪花Id配置选项 +/// +public sealed class SnowIdOptions : IdGeneratorOptions, IConfigurableOptions +{ + /// + /// 缓存前缀 + /// + public string WorkerPrefix { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs new file mode 100644 index 0000000..8eb7393 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/UploadOptions.cs @@ -0,0 +1,60 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core; + +/// +/// 文件上传配置选项 +/// +public sealed class UploadOptions : IConfigurableOptions +{ + /// + /// 路径 + /// + public string Path { get; set; } + + /// + /// 大小 + /// + public long MaxSize { get; set; } + + /// + /// 上传格式 + /// + public List ContentType { get; set; } + + /// + /// 启用文件MD5验证 + /// + /// 防止重复上传 + public bool EnableMd5 { get; set; } +} + +/// +/// 对象存储配置选项 +/// +public sealed class OSSProviderOptions : OSSOptions, IConfigurableOptions +{ + /// + /// 是否启用OSS存储 + /// + public bool Enabled { get; set; } + + /// + /// 自定义桶名称 不能直接使用Provider来替代桶名称 + /// 例:阿里云 1.只能包括小写字母,数字,短横线(-)2.必须以小写字母或者数字开头 3.长度必须在3-63字节之间 + /// + public string Bucket { get; set; } + + /// + /// 自定义Host + /// 拼接外链的Host,若空则使用Endpoint拼接 + /// + /// + public string CustomHost { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatOptions.cs new file mode 100644 index 0000000..1c47b6d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatOptions.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信相关配置选项 +/// +public sealed class WechatOptions : IConfigurableOptions +{ + // 公众号 + public string WechatAppId { get; set; } + + public string WechatAppSecret { get; set; } + + /// + /// 微信公众号服务器配置中的令牌(Token) + /// + public string WechatToken { get; set; } + + /// + /// 微信公众号服务器配置中的消息加解密密钥(EncodingAESKey) + /// + public string WechatEncodingAESKey { get; set; } + + // 小程序 + public string WxOpenAppId { get; set; } + + public string WxOpenAppSecret { get; set; } + + /// + /// 小程序消息推送中的令牌(Token) + /// + public string WxToken { get; set; } + + /// + /// 小程序消息推送中的消息加解密密钥(EncodingAESKey) + /// + public string WxEncodingAESKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatPayOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatPayOptions.cs new file mode 100644 index 0000000..cca5bb6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Option/WechatPayOptions.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 微信支付配置选项 +/// +public sealed class WechatPayOptions : WechatTenpayClientOptions, IConfigurableOptions +{ + /// + /// 微信公众平台AppId、开放平台AppId、小程序AppId、企业微信CorpId + /// + public string AppId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs new file mode 100644 index 0000000..142b18c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysConfigSeedData.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统配置表种子数据 +/// +public class SysConfigSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysConfig{ Id=1300000000101, Name="演示环境", Code=ConfigConst.SysDemoEnv, Value="False", SysFlag=YesNoEnum.Y, Remark="演示环境", OrderNo=10, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000111, Name="默认密码", Code=ConfigConst.SysPassword, Value="Admin.NET++010101", SysFlag=YesNoEnum.Y, Remark="默认密码", OrderNo=20, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000121, Name="密码最大错误次数", Code=ConfigConst.SysPasswordMaxErrorTimes, Value="5", SysFlag=YesNoEnum.Y, Remark="允许密码最大输入错误次数", OrderNo=30, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000131, Name="日志保留天数", Code=ConfigConst.SysLogRetentionDays, Value="180", SysFlag=YesNoEnum.Y, Remark="日志保留天数(天)", OrderNo=40, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000141, Name="记录操作日志", Code=ConfigConst.SysOpLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否记录操作日志", OrderNo=50, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000151, Name="单设备登录", Code=ConfigConst.SysSingleLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启单设备登录", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000152, Name="登入登出提醒", Code=ConfigConst.SysLoginOutReminder, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登入登出提醒", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000161, Name="登录二次验证", Code=ConfigConst.SysSecondVer, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登录二次验证", OrderNo=70, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000171, Name="图形验证码", Code=ConfigConst.SysCaptcha, Value="True", SysFlag=YesNoEnum.Y, Remark="是否开启图形验证码", OrderNo=80, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000172, Name="登录时隐藏租户", Code=ConfigConst.SysHideTenantLogin, Value="True", SysFlag=YesNoEnum.Y, Remark="登录时隐藏租户", OrderNo=90, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000181, Name="Token过期时间", Code=ConfigConst.SysTokenExpire, Value="10080", SysFlag=YesNoEnum.Y, Remark="Token过期时间(分钟)", OrderNo=100, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000191, Name="RefreshToken过期时间", Code=ConfigConst.SysRefreshTokenExpire, Value="20160", SysFlag=YesNoEnum.Y, Remark="刷新Token过期时间(分钟)(一般 refresh_token 的有效时间 > 2 * access_token 的有效时间)", OrderNo=110, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000201, Name="发送异常日志邮件", Code=ConfigConst.SysErrorMail, Value="False", SysFlag=YesNoEnum.Y, Remark="是否发送异常日志邮件", OrderNo=120, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000211, Name="域登录验证", Code=ConfigConst.SysDomainLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启域登录验证", OrderNo=130, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000221, Name="数据校验日志", Code=ConfigConst.SysValidationLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否数据校验日志", OrderNo=140, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000231, Name="行政区域同步层级", Code=ConfigConst.SysRegionSyncLevel, Value="3", SysFlag=YesNoEnum.Y, Remark="行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级", OrderNo=150, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictDataSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictDataSeedData.cs new file mode 100644 index 0000000..9483362 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictDataSeedData.cs @@ -0,0 +1,86 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典值表种子数据 +/// +public class SysDictDataSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var typeList = new SysDictTypeSeedData().HasData().ToList(); + return new[] + { + new SysDictData{ Id=1300000000101, DictTypeId=typeList[0].Id, Label="输入框", Value="Input", OrderNo=100, Remark="输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000102, DictTypeId=typeList[0].Id, Label="字典选择器", Value="DictSelector", OrderNo=100, Remark="字典选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000103, DictTypeId=typeList[0].Id, Label="常量选择器", Value="ConstSelector", OrderNo=100, Remark="常量选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000104, DictTypeId=typeList[0].Id, Label="枚举选择器", Value="EnumSelector", OrderNo=100, Remark="枚举选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000105, DictTypeId=typeList[0].Id, Label="树选择器", Value="ApiTreeSelector", OrderNo=100, Remark="树选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000106, DictTypeId=typeList[0].Id, Label="外键", Value="ForeignKey", OrderNo=100, Remark="外键", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000107, DictTypeId=typeList[0].Id, Label="数字输入框", Value="InputNumber", OrderNo=100, Remark="数字输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000108, DictTypeId=typeList[0].Id, Label="时间选择", Value="DatePicker", OrderNo=100, Remark="时间选择", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000109, DictTypeId=typeList[0].Id, Label="文本域", Value="InputTextArea", OrderNo=100, Remark="文本域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000110, DictTypeId=typeList[0].Id, Label="上传", Value="Upload", OrderNo=100, Remark="上传", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000111, DictTypeId=typeList[0].Id, Label="开关", Value="Switch", OrderNo=100, Remark="开关", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000112, DictTypeId=typeList[0].Id, Label="上传单文件", Value="Upload_SingleFile", OrderNo=120, Remark="上传单文件", Status=StatusEnum.Enable, CreateTime= DateTime.Now }, + new SysDictData{ Id=1300000000113, DictTypeId=typeList[0].Id, Label="富文本编辑器", Value="Editor", OrderNo=130, Remark="富文本编辑器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2025-12-25 00:00:00") }, + + new SysDictData{ Id=1300000000201, DictTypeId=typeList[1].Id, Label="等于", Value="==", OrderNo=1, Remark="等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000202, DictTypeId=typeList[1].Id, Label="模糊", Value="like", OrderNo=1, Remark="模糊", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000203, DictTypeId=typeList[1].Id, Label="大于", Value=">", OrderNo=1, Remark="大于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000204, DictTypeId=typeList[1].Id, Label="小于", Value="<", OrderNo=1, Remark="小于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000205, DictTypeId=typeList[1].Id, Label="不等于", Value="!=", OrderNo=1, Remark="不等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000206, DictTypeId=typeList[1].Id, Label="大于等于", Value=">=", OrderNo=1, Remark="大于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000207, DictTypeId=typeList[1].Id, Label="小于等于", Value="<=", OrderNo=1, Remark="小于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000208, DictTypeId=typeList[1].Id, Label="不为空", Value="isNotNull", OrderNo=1, Remark="不为空", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000209, DictTypeId=typeList[1].Id, Label="时间范围", Value="~", OrderNo=1, Remark="时间范围", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000301, DictTypeId=typeList[2].Id, Label="long", Value="long", OrderNo=1, Remark="long", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000302, DictTypeId=typeList[2].Id, Label="string", Value="string", OrderNo=1, Remark="string", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000303, DictTypeId=typeList[2].Id, Label="DateTime", Value="DateTime", OrderNo=1, Remark="DateTime", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000304, DictTypeId=typeList[2].Id, Label="bool", Value="bool", OrderNo=1, Remark="bool", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000305, DictTypeId=typeList[2].Id, Label="int", Value="int", OrderNo=1, Remark="int", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000306, DictTypeId=typeList[2].Id, Label="double", Value="double", OrderNo=1, Remark="double", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000307, DictTypeId=typeList[2].Id, Label="float", Value="float", OrderNo=1, Remark="float", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000308, DictTypeId=typeList[2].Id, Label="decimal", Value="decimal", OrderNo=1, Remark="decimal", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000309, DictTypeId=typeList[2].Id, Label="Guid", Value="Guid", OrderNo=1, Remark="Guid", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000310, DictTypeId=typeList[2].Id, Label="DateTimeOffset", Value="DateTimeOffset", OrderNo=1, Remark="DateTimeOffset", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000401, DictTypeId=typeList[3].Id, Label="下载压缩包", Value="100", OrderNo=1, Remark="下载压缩包", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000402, DictTypeId=typeList[3].Id, Label="下载压缩包(前端)", Value="111", OrderNo=2, Remark="下载压缩包(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000403, DictTypeId=typeList[3].Id, Label="下载压缩包(后端)", Value="121", OrderNo=3, Remark="下载压缩包(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000404, DictTypeId=typeList[3].Id, Label="生成到本项目", Value="200", OrderNo=4, Remark="生成到本项目", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000405, DictTypeId=typeList[3].Id, Label="生成到本项目(前端)", Value="211", OrderNo=5, Remark="生成到本项目(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000406, DictTypeId=typeList[3].Id, Label="生成到本项目(后端)", Value="221", OrderNo=6, Remark="生成到本项目(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000501, DictTypeId=typeList[4].Id, Label="EntityBaseId【基础实体Id】", Value="EntityBaseId", OrderNo=1, Remark="【基础实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000502, DictTypeId=typeList[4].Id, Label="EntityBase【基础实体】", Value="EntityBase", OrderNo=1, Remark="【基础实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000503, DictTypeId=typeList[4].Id, Label="EntityBaseDel【基础软删除实体】", Value="EntityBaseDel", OrderNo=1, Remark="【基础软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000504, DictTypeId=typeList[4].Id, Label="EntityBaseOrg【机构实体】", Value="EntityBaseOrg", OrderNo=1, Remark="【机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000505, DictTypeId=typeList[4].Id, Label="EntityBaseOrgDel【机构软删除实体】", Value="EntityBaseOrgDel", OrderNo=1, Remark="【机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000506, DictTypeId=typeList[4].Id, Label="EntityBaseTenantId【租户实体Id】", Value="EntityBaseTenantId", OrderNo=1, Remark="【租户实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000507, DictTypeId=typeList[4].Id, Label="EntityBaseTenant【租户实体】", Value="EntityBaseTenant", OrderNo=1, Remark="【租户实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000508, DictTypeId=typeList[4].Id, Label="EntityBaseTenantDel【租户软删除实体】", Value="EntityBaseTenantDel", OrderNo=1, Remark="【租户软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000509, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrg【租户机构实体】", Value="EntityBaseTenantOrg", OrderNo=1, Remark="【租户机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000510, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrgDel【租户机构软删除实体】", Value="EntityBaseTenantOrgDel", OrderNo=1, Remark="【租户机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000601, DictTypeId=typeList[5].Id, Label="不需要", Value="off", OrderNo=100, Remark="不需要打印支持", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictData{ Id=1300000000602, DictTypeId=typeList[5].Id, Label="绑定打印模版", Value="custom", OrderNo=101, Remark="绑定打印模版", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + + new SysDictData{ Id=1300000000701, DictTypeId=typeList[6].Id, Label="集团", Value="101", OrderNo=100, Remark="集团", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000702, DictTypeId=typeList[6].Id, Label="公司", Value="201", OrderNo=101, Remark="公司", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000703, DictTypeId=typeList[6].Id, Label="部门", Value="301", OrderNo=102, Remark="部门", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000704, DictTypeId=typeList[6].Id, Label="区域", Value="401", OrderNo=103, Remark="区域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000705, DictTypeId=typeList[6].Id, Label="组", Value="501", OrderNo=104, Remark="组", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs new file mode 100644 index 0000000..47b6def --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysDictTypeSeedData.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统字典类型表种子数据 +/// +public class SysDictTypeSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysDictType{ Id=1300000000111, Name="代码生成控件类型", Code="code_gen_effect_type", SysFlag=YesNoEnum.Y, OrderNo=100, Remark="代码生成控件类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000121, Name="代码生成查询类型", Code="code_gen_query_type", SysFlag=YesNoEnum.Y, OrderNo=101, Remark="代码生成查询类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000131, Name="代码生成.NET类型", Code="code_gen_net_type", SysFlag=YesNoEnum.Y, OrderNo=102, Remark="代码生成.NET类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000141, Name="代码生成方式", Code="code_gen_create_type", SysFlag=YesNoEnum.Y, OrderNo=103, Remark="代码生成方式", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000151, Name="代码生成基类", Code="code_gen_base_class", SysFlag=YesNoEnum.Y, OrderNo=104, Remark="代码生成基类", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000161, Name="代码生成打印类型", Code="code_gen_print_type", SysFlag=YesNoEnum.Y, OrderNo=105, Remark="代码生成打印类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictType{ Id=1300000000171, Name="机构类型", Code="org_type", SysFlag=YesNoEnum.Y, OrderNo=201, Remark="机构类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysLangSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysLangSeedData.cs new file mode 100644 index 0000000..b527bd3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysLangSeedData.cs @@ -0,0 +1,112 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言表种子数据 +/// +public class SysLangSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysLang{ Id=1300000000001,Name="Chinese (Simplified) / 简体中文", Code="zh-CN", IsoCode="zh_CN", UrlCode="zh-cn", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H时%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000002,Name="Chinese (HK) / 繁體中文", Code="zh-HK", IsoCode="zh_HK", UrlCode="zh-hk", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日 %A", TimeFormat="%I時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000003,Name="Chinese (Traditional) / 繁體中文", Code="zh-TW", IsoCode="zh_TW", UrlCode="zh-tw", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000004,Name="Italian / Italiano", Code="it-IT", IsoCode="it", UrlCode="it", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=true}, + new SysLang{ Id=1300000000005,Name="English (US)", Code="en-US", IsoCode="en", UrlCode="en", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true}, + new SysLang{ Id=1300000000006,Name="Amharic / አምሃርኛ", Code="am-ET", IsoCode="am_ET", UrlCode="am-et", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000007,Name="Arabic / الْعَرَبيّة", Code="ar-001", IsoCode="ar", UrlCode="ar", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000008,Name="Arabic (Syria) / الْعَرَبيّة", Code="ar-SY", IsoCode="ar_SY", UrlCode="ar-sy", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000009,Name="Azerbaijani / Azərbaycanca", Code="az-AZ", IsoCode="az", UrlCode="az", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000010,Name="Basque / Euskara", Code="eu-ES", IsoCode="eu_ES", UrlCode="eu-es", Direction=DirectionEnum.Ltr, DateFormat="%a, %Y.eko %bren %da", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000011,Name="Bengali / বাংলা", Code="bn-IN", IsoCode="bn_IN", UrlCode="bn-in", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000012,Name="Bosnian / bosanski jezik", Code="bs-BA", IsoCode="bs", UrlCode="bs", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000013,Name="Bulgarian / български език", Code="bg-BG", IsoCode="bg", UrlCode="bg", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H,%M,%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000014,Name="Catalan / Català", Code="ca-ES", IsoCode="ca_ES", UrlCode="ca-es", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000015,Name="Croatian / hrvatski jezik", Code="hr-HR", IsoCode="hr", UrlCode="hr", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000016,Name="Czech / Čeština", Code="cs-CZ", IsoCode="cs_CZ", UrlCode="cs-cz", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000017,Name="Danish / Dansk", Code="da-DK", IsoCode="da_DK", UrlCode="da-dk", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000018,Name="Dutch (BE) / Nederlands (BE)", Code="nl-BE", IsoCode="nl_BE", UrlCode="nl-be", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000019,Name="Dutch / Nederlands", Code="nl-NL", IsoCode="nl", UrlCode="nl", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000020,Name="English (AU)", Code="en-AU", IsoCode="en_AU", UrlCode="en-au", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000021,Name="English (CA)", Code="en-CA", IsoCode="en_CA", UrlCode="en-ca", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000022,Name="English (UK)", Code="en-GB", IsoCode="en_GB", UrlCode="en-gb", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000023,Name="English (IN)", Code="en-IN", IsoCode="en_IN", UrlCode="en-in", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,2,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000024,Name="Estonian / Eesti keel", Code="et-EE", IsoCode="et", UrlCode="et", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000025,Name="Finnish / Suomi", Code="fi-FI", IsoCode="fi", UrlCode="fi", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H.%M.%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000026,Name="French (BE) / Français (BE)", Code="fr-BE", IsoCode="fr_BE", UrlCode="fr-be", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000027,Name="French (CA) / Français (CA)", Code="fr-CA", IsoCode="fr_CA", UrlCode="fr-ca", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000028,Name="French (CH) / Français (CH)", Code="fr-CH", IsoCode="fr_CH", UrlCode="fr-ch", Direction=DirectionEnum.Ltr, DateFormat="%d. %m. %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000029,Name="French / Français", Code="fr-FR", IsoCode="fr", UrlCode="fr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000030,Name="Galician / Galego", Code="gl-ES", IsoCode="gl", UrlCode="gl", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000031,Name="Georgian / ქართული ენა", Code="ka-GE", IsoCode="ka", UrlCode="ka", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000032,Name="German / Deutsch", Code="de-DE", IsoCode="de", UrlCode="de", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000033,Name="German (CH) / Deutsch (CH)", Code="de-CH", IsoCode="de_CH", UrlCode="de-ch", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,3]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000034,Name="Greek / Ελληνικά", Code="el-GR", IsoCode="el_GR", UrlCode="el-gr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000035,Name="Gujarati / ગુજરાતી", Code="gu-IN", IsoCode="gu", UrlCode="gu", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000036,Name="Hebrew / עִבְרִי", Code="he-IL", IsoCode="he", UrlCode="he", Direction=DirectionEnum.Rtl, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000037,Name="Hindi / हिंदी", Code="hi-IN", IsoCode="hi", UrlCode="hi", Direction=DirectionEnum.Ltr, DateFormat="%A %d %b %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000038,Name="Hungarian / Magyar", Code="hu-HU", IsoCode="hu", UrlCode="hu", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000039,Name="Indonesian / Bahasa Indonesia", Code="id-ID", IsoCode="id", UrlCode="id", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000040,Name="Japanese / 日本語", Code="ja-JP", IsoCode="ja", UrlCode="ja", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000041,Name="Kabyle / Taqbaylit", Code="kab-DZ", IsoCode="kab", UrlCode="kab", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Saturday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000042,Name="Khmer / ភាសាខ្មែរ", Code="km-KH", IsoCode="km", UrlCode="km", Direction=DirectionEnum.Ltr, DateFormat="%d %B %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000043,Name="Korean (KP) / 한국어 (KP)", Code="ko-KP", IsoCode="ko_KP", UrlCode="ko-kp", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000044,Name="Korean (KR) / 한국어 (KR)", Code="ko-KR", IsoCode="ko_KR", UrlCode="ko-kr", Direction=DirectionEnum.Ltr, DateFormat="%Y년 %m월 %d일", TimeFormat="%H시 %M분 %S초",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000045,Name="Lao / ພາສາລາວ", Code="lo-LA", IsoCode="lo", UrlCode="lo", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000046,Name="Latvian / latviešu valoda", Code="lv-LV", IsoCode="lv", UrlCode="lv", Direction=DirectionEnum.Ltr, DateFormat="%Y.%m.%d.", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000047,Name="Lithuanian / Lietuvių kalba", Code="lt-LT", IsoCode="lt", UrlCode="lt", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000048,Name="Luxembourgish", Code="lb-LU", IsoCode="lb", UrlCode="lb", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000049,Name="Macedonian / македонски јазик", Code="mk-MK", IsoCode="mk", UrlCode="mk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000050,Name="Malayalam / മലയാളം", Code="ml-IN", IsoCode="ml", UrlCode="ml", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000051,Name="Mongolian / монгол", Code="mn-MN", IsoCode="mn", UrlCode="mn", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep="'",Active=false}, + new SysLang{ Id=1300000000052,Name="Malay / Bahasa Melayu", Code="ms-MY", IsoCode="ms", UrlCode="ms", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000053,Name="Norwegian Bokmål / Norsk bokmål", Code="nb-NO", IsoCode="nb_NO", UrlCode="nb-no", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000054,Name="Persian / فارسی", Code="fa-IR", IsoCode="fa", UrlCode="fa", Direction=DirectionEnum.Rtl, DateFormat="%Y/%m/%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000055,Name="Polish / Język polski", Code="pl-PL", IsoCode="pl", UrlCode="pl", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000056,Name="Portuguese (AO) / Português (AO)", Code="pt-AO", IsoCode="pt_AO", UrlCode="pt-ao", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000057,Name="Portuguese (BR) / Português (BR)", Code="pt-BR", IsoCode="pt_BR", UrlCode="pt-br", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000058,Name="Portuguese / Português", Code="pt-PT", IsoCode="pt", UrlCode="pt", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000059,Name="Romanian / română", Code="ro-RO", IsoCode="ro", UrlCode="ro", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000060,Name="Russian / русский язык", Code="ru-RU", IsoCode="ru", UrlCode="ru", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000061,Name="Serbian (Cyrillic) / српски", Code="sr-Cyrl-RS", IsoCode="sr_RS", UrlCode="sr-rs", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y.", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=",",ThousandsSep="null",Active=false}, + new SysLang{ Id=1300000000062,Name="Serbian (Latin) / srpski", Code="sr-Latn-RS", IsoCode="sr@latin", UrlCode="sr@latin", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000063,Name="Slovak / Slovenský jazyk", Code="sk-SK", IsoCode="sk", UrlCode="sk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000064,Name="Slovenian / slovenščina", Code="sl-SI", IsoCode="sl", UrlCode="sl", Direction=DirectionEnum.Ltr, DateFormat="%d. %m. %Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000065,Name="Spanish (AR) / Español (AR)", Code="es-AR", IsoCode="es_AR", UrlCode="es-ar", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000066,Name="Spanish (BO) / Español (BO)", Code="es-BO", IsoCode="es_BO", UrlCode="es-bo", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000067,Name="Spanish (CL) / Español (CL)", Code="es-CL", IsoCode="es_CL", UrlCode="es-cl", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000068,Name="Spanish (CO) / Español (CO)", Code="es-CO", IsoCode="es_CO", UrlCode="es-co", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000069,Name="Spanish (CR) / Español (CR)", Code="es-CR", IsoCode="es_CR", UrlCode="es-cr", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000070,Name="Spanish (DO) / Español (DO)", Code="es-DO", IsoCode="es_DO", UrlCode="es-do", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000071,Name="Spanish (EC) / Español (EC)", Code="es-EC", IsoCode="es_EC", UrlCode="es-ec", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000072,Name="Spanish (GT) / Español (GT)", Code="es-GT", IsoCode="es_GT", UrlCode="es-gt", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000073,Name="Spanish (MX) / Español (MX)", Code="es-MX", IsoCode="es_MX", UrlCode="es-mx", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000074,Name="Spanish (PA) / Español (PA)", Code="es-PA", IsoCode="es_PA", UrlCode="es-pa", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000075,Name="Spanish (PE) / Español (PE)", Code="es-PE", IsoCode="es_PE", UrlCode="es-pe", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000076,Name="Spanish (PY) / Español (PY)", Code="es-PY", IsoCode="es_PY", UrlCode="es-py", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000077,Name="Spanish (UY) / Español (UY)", Code="es-UY", IsoCode="es_UY", UrlCode="es-uy", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000078,Name="Spanish (VE) / Español (VE)", Code="es-VE", IsoCode="es_VE", UrlCode="es-ve", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000079,Name="Spanish / Español", Code="es-ES", IsoCode="es", UrlCode="es", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000080,Name="Swedish / Svenska", Code="sv-SE", IsoCode="sv", UrlCode="sv", Direction=DirectionEnum.Ltr, DateFormat="%Y-%m-%d", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000081,Name="Thai / ภาษาไทย", Code="th-TH", IsoCode="th", UrlCode="th", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000082,Name="Tagalog / Filipino", Code="tl-PH", IsoCode="tl", UrlCode="tl", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000083,Name="Turkish / Türkçe", Code="tr-TR", IsoCode="tr", UrlCode="tr", Direction=DirectionEnum.Ltr, DateFormat="%d-%m-%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000084,Name="Ukrainian / українська", Code="uk-UA", IsoCode="uk", UrlCode="uk", Direction=DirectionEnum.Ltr, DateFormat="%d.%m.%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=" ",Active=false}, + new SysLang{ Id=1300000000085,Name="Vietnamese / Tiếng Việt", Code="vi-VN", IsoCode="vi", UrlCode="vi", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000086,Name="Albanian / Shqip", Code="sq-AL", IsoCode="sq", UrlCode="sq", Direction=DirectionEnum.Ltr, DateFormat="%Y-%b-%d", TimeFormat="%I.%M.%S.",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=false}, + new SysLang{ Id=1300000000087,Name="Telugu / తెలుగు", Code="te-IN", IsoCode="te", UrlCode="te", Direction=DirectionEnum.Ltr, DateFormat="%B %d %A %Y", TimeFormat="%p%I.%M.%S",WeekStart=WeekEnum.Sunday,Grouping="[]",DecimalPoint=".",ThousandsSep=",",Active=false}, + new SysLang{ Id=1300000000088,Name="Burmese / ဗမာစာ", Code="my-MM", IsoCode="my", UrlCode="mya", Direction=DirectionEnum.Ltr, DateFormat="%Y %b %d %A", TimeFormat="%I:%M:%S %p",WeekStart=WeekEnum.Sunday,Grouping="[3,3]",DecimalPoint=".",ThousandsSep=",",Active=false}, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs new file mode 100644 index 0000000..bc5aa88 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs @@ -0,0 +1,313 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统菜单表种子数据 +/// +public class SysMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysMenu{ Id=1300100000101, Pid=0, Title="工作台", Path="/dashboard", Name="dashboard", Component="Layout", Icon="ele-HomeFilled", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=0 }, + new SysMenu{ Id=1300100010101, Pid=1300100000101, Title="工作台", Path="/dashboard/home", Name="home", Component="/home/index", IsAffix=true, Icon="ele-HomeFilled", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300100010201, Pid=1300100000101, Title="站内信", Path="/dashboard/notice", Name="notice", Component="/home/notice/index", Icon="ele-Bell", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=101 }, + + // 建议此处Id范围之间放置具体业务应用菜单 + + #region 系统管理 + + new SysMenu{ Id=1300200000101, Pid=0, Title="系统管理", Path="/system", Name="system", Component="Layout", Icon="ele-Setting", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=10000 }, + + // 账号管理 + new SysMenu{ Id=1300200010101, Pid=1300200000101, Title="账号管理", Path="/system/user", Name="sysUser", Component="/system/user/index", Icon="ele-User", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010201, Pid=1300200010101, Title="查询", Permission="sysUser:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010301, Pid=1300200010101, Title="编辑", Permission="sysUser:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010401, Pid=1300200010101, Title="增加", Permission="sysUser:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010501, Pid=1300200010101, Title="删除", Permission="sysUser:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010601, Pid=1300200010101, Title="详情", Permission="sysUser:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010701, Pid=1300200010101, Title="授权角色", Permission="sysUser:grantRole", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010801, Pid=1300200010101, Title="重置密码", Permission="sysUser:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010901, Pid=1300200010101, Title="设置状态", Permission="sysUser:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011001, Pid=1300200010101, Title="强制下线", Permission="sysOnlineUser:forceOffline", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011101, Pid=1300200010101, Title="解除锁定", Permission="sysUser:unlockLogin", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 角色管理 + new SysMenu{ Id=1300200020101, Pid=1300200000101, Title="角色管理", Path="/system/role", Name="sysRole", Component="/system/role/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200020201, Pid=1300200020101, Title="查询", Permission="sysRole:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020301, Pid=1300200020101, Title="编辑", Permission="sysRole:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020401, Pid=1300200020101, Title="增加", Permission="sysRole:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020501, Pid=1300200020101, Title="删除", Permission="sysRole:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020601, Pid=1300200020101, Title="授权菜单", Permission="sysRole:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020701, Pid=1300200020101, Title="授权数据", Permission="sysRole:grantDataScope", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020801, Pid=1300200020101, Title="设置状态", Permission="sysRole:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 机构管理 + new SysMenu{ Id=1300200030101, Pid=1300200000101, Title="机构管理", Path="/system/org", Name="sysOrg", Component="/system/org/index", Icon="ele-OfficeBuilding", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200030201, Pid=1300200030101, Title="查询", Permission="sysOrg:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030301, Pid=1300200030101, Title="编辑", Permission="sysOrg:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030401, Pid=1300200030101, Title="增加", Permission="sysOrg:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030501, Pid=1300200030101, Title="删除", Permission="sysOrg:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 职位管理 + new SysMenu{ Id=1300200040101, Pid=1300200000101, Title="职位管理", Path="/system/pos", Name="sysPos", Component="/system/pos/index",Icon="ele-Mug", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200040201, Pid=1300200040101, Title="查询", Permission="sysPos:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040301, Pid=1300200040101, Title="编辑", Permission="sysPos:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040401, Pid=1300200040101, Title="增加", Permission="sysPos:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040501, Pid=1300200040101, Title="删除", Permission="sysPos:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 个人中心 + new SysMenu{ Id=1300200050101, Pid=1300200000101, Title="个人中心", Path="/system/userCenter", Name="sysUserCenter", Component="/system/user/component/userCenter",Icon="ele-Medal", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200050201, Pid=1300200050101, Title="修改密码", Permission="sysUser:changePwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050301, Pid=1300200050101, Title="基本信息", Permission="sysUser:baseInfo", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050401, Pid=1300200050101, Title="电子签名", Permission="sysFile:uploadSignature", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050501, Pid=1300200050101, Title="上传头像", Permission="sysFile:uploadAvatar", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 通知公告 + new SysMenu{ Id=1300200060101, Pid=1300200000101, Title="通知公告", Path="/system/notice", Name="sysNotice", Component="/system/notice/index",Icon="ele-Bell", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200060201, Pid=1300200060101, Title="查询", Permission="sysNotice:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060301, Pid=1300200060101, Title="编辑", Permission="sysNotice:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060401, Pid=1300200060101, Title="增加", Permission="sysNotice:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060501, Pid=1300200060101, Title="删除", Permission="sysNotice:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060601, Pid=1300200060101, Title="发布", Permission="sysNotice:public", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060701, Pid=1300200060101, Title="撤回", Permission="sysNotice:cancel", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 三方账号 + new SysMenu{ Id=1300200070101, Pid=1300200000101, Title="三方账号", Path="/system/weChatUser", Name="sysWechatUser", Component="/system/weChatUser/index",Icon="ele-ChatDotRound", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + new SysMenu{ Id=1300200070201, Pid=1300200070101, Title="查询", Permission="sysWechatUser:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070301, Pid=1300200070101, Title="编辑", Permission="sysWechatUser:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070401, Pid=1300200070101, Title="增加", Permission="sysWechatUser:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200070501, Pid=1300200070101, Title="删除", Permission="sysWechatUser:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // AD域配置 + new SysMenu{ Id=1300200080101, Pid=1300200000101, Title="AD域配置", Path="/system/ldap", Name="sysLdap", Component="/system/ldap/index",Icon="ele-Place", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300200080201, Pid=1300200080101, Title="查询", Permission="sysLdap:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200080301, Pid=1300200080101, Title="详情", Permission="sysLdap:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200080401, Pid=1300200080101, Title="编辑", Permission="sysLdap:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200080501, Pid=1300200080101, Title="增加", Permission="sysLdap:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200080601, Pid=1300200080101, Title="删除", Permission="sysLdap:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200080701, Pid=1300200080101, Title="同步域账户", Permission="sysLdap:syncUser", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200080801, Pid=1300200080101, Title="同步域组织", Permission="sysLdap:syncOrg", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + + #endregion 系统管理 + + #region 平台管理 + + new SysMenu{ Id=1300300000101, Pid=0, Title="平台管理", Path="/platform", Name="platform", Component="Layout", Icon="ele-Operation", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=11000 }, + + // 租户管理 + new SysMenu{ Id=1300300010101, Pid=1300300000101, Title="租户管理", Path="/platform/tenant", Name="sysTenant", Component="/system/tenant/index", Icon="ele-School", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010201, Pid=1300300010101, Title="查询", Permission="sysTenant:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010301, Pid=1300300010101, Title="编辑", Permission="sysTenant:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010401, Pid=1300300010101, Title="增加", Permission="sysTenant:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010501, Pid=1300300010101, Title="删除", Permission="sysTenant:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010601, Pid=1300300010101, Title="授权菜单", Permission="sysTenant:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010701, Pid=1300300010101, Title="重置密码", Permission="sysTenant:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010801, Pid=1300300010101, Title="生成库", Permission="sysTenant:createDb", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010901, Pid=1300300010101, Title="设置状态", Permission="sysTenant:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011001, Pid=1300300010101, Title="同步授权", Permission="sysTenant:syncGrantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011101, Pid=1300300010101, Title="切换租户", Permission="sysTenant:changeTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011201, Pid=1300300010101, Title="进入租管端", Permission="sysTenant:goTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 注册方案 + new SysMenu{ Id=1300300020101, Pid=1300300000101, Title="注册方案", Path="/platform/regWay", Name="sysUserRegWay", Component="/system/userRegWay/index", Icon="ele-Pointer", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=105 }, + new SysMenu{ Id=1300300020201, Pid=1300300020101, Title="查询", Permission="sysUserRegWay:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020301, Pid=1300300020101, Title="编辑", Permission="sysUserRegWay:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020401, Pid=1300300020101, Title="增加", Permission="sysUserRegWay:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300020501, Pid=1300300020101, Title="删除", Permission="sysUserRegWay:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 菜单管理 + new SysMenu{ Id=1300300030101, Pid=1300300000101, Title="菜单管理", Path="/platform/menu", Name="sysMenu", Component="/system/menu/index", Icon="ele-Menu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300300030201, Pid=1300300030101, Title="查询", Permission="sysMenu:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030301, Pid=1300300030101, Title="编辑", Permission="sysMenu:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030401, Pid=1300300030101, Title="增加", Permission="sysMenu:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030501, Pid=1300300030101, Title="删除", Permission="sysMenu:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 平台参数 + new SysMenu{ Id=1300300040101, Pid=1300300000101, Title="平台参数", Path="/platform/config", Name="sysConfig", Component="/system/config/index", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300300040201, Pid=1300300040101, Title="查询", Permission="sysConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040301, Pid=1300300040101, Title="编辑", Permission="sysConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040401, Pid=1300300040101, Title="增加", Permission="sysConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040501, Pid=1300300040101, Title="删除", Permission="sysConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 字典管理 + new SysMenu{ Id=1300300050101, Pid=1300300000101, Title="字典管理", Path="/platform/dict", Name="sysDict", Component="/system/dict/index", Icon="ele-Collection", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300300050201, Pid=1300300050101, Title="查询", Permission="sysDictType:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050301, Pid=1300300050101, Title="编辑", Permission="sysDictType:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050401, Pid=1300300050101, Title="增加", Permission="sysDictType:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050501, Pid=1300300050101, Title="删除", Permission="sysDictType:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050601, Pid=1300300050101, Title="增加字典值", Permission="sysDictData:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050701, Pid=1300300050101, Title="删除字典值", Permission="sysDictData:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050801, Pid=1300300050101, Title="编辑字典值", Permission="sysDictData:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050901, Pid=1300300050101, Title="字典迁移", Permission="sysDictType:move", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 模板管理 + new SysMenu{ Id=1300300051101, Pid=1300300000101, Title="模板管理", Path="/platform/template", Name="sysTemplate", Component="/system/template/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=135 }, + new SysMenu{ Id=1300300051201, Pid=1300300051101, Title="查询", Permission="sysTemplate:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051301, Pid=1300300051101, Title="编辑", Permission="sysTemplate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051401, Pid=1300300051101, Title="增加", Permission="sysTemplate:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051501, Pid=1300300051101, Title="删除", Permission="sysTemplate:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051601, Pid=1300300051101, Title="预览", Permission="sysTemplate:preview", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 任务调度 + new SysMenu{ Id=1300300060101, Pid=1300300000101, Title="任务调度", Path="/platform/job", Name="sysJob", Component="/system/job/index", Icon="ele-AlarmClock", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300300060201, Pid=1300300060101, Title="查询", Permission="sysJob:pageJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060301, Pid=1300300060101, Title="编辑", Permission="sysJob:updateJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060401, Pid=1300300060101, Title="增加", Permission="sysJob:addJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060501, Pid=1300300060101, Title="删除", Permission="sysJob:deleteJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统参数 + new SysMenu{ Id=1300200090101, Pid=1300200000101, Title="系统参数", Path="/system/tenantConfig", Name="sysTenantConfig", Component="/system/tenantConfig/index", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300200090201, Pid=1300200090101, Title="查询", Permission="sysTenantConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090301, Pid=1300200090101, Title="编辑", Permission="sysTenantConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090401, Pid=1300200090101, Title="增加", Permission="sysTenantConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090501, Pid=1300200090101, Title="删除", Permission="sysTenantConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 语言管理 + new SysMenu{ Id=1300200100101, Pid=1300200000101, Title="语言管理", Path="/system/lang", Name="sysLang", Component="/system/lang/index", Icon="iconfont icon-diqiu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=190 }, + new SysMenu{ Id=1300200100201, Pid=1300200100101, Title="查询", Permission="sysLang:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100301, Pid=1300200100101, Title="编辑", Permission="sysLang:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100401, Pid=1300200100101, Title="增加", Permission="sysLang:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200100501, Pid=1300200100101, Title="删除", Permission="sysLang:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 翻译管理 + new SysMenu{ Id=1300200200101, Pid=1300200000101, Title="翻译管理", Path="/system/langText", Name="sysLangText", Component="/system/langText/index", Icon="iconfont icon-zhongyingwen", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=200 }, + new SysMenu{ Id=1300200200201, Pid=1300200200101, Title="查询", Permission="sysLangText:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200301, Pid=1300200200101, Title="编辑", Permission="sysLangText:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200401, Pid=1300200200101, Title="增加", Permission="sysLangText:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200200501, Pid=1300200200101, Title="删除", Permission="sysLangText:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统监控 + new SysMenu{ Id=1300300070101, Pid=1300300000101, Title="系统监控", Path="/platform/server", Name="sysServer", Component="/system/server/index", Icon="ele-Monitor", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + + // 缓存管理 + new SysMenu{ Id=1300300080101, Pid=1300300000101, Title="缓存管理", Path="/platform/cache", Name="sysCache", Component="/system/cache/index", Icon="ele-Loading", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + new SysMenu{ Id=1300300080201, Pid=1300300080101, Title="查询", Permission="sysCache:keyList", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300080301, Pid=1300300080101, Title="删除", Permission="sysCache:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300080401, Pid=1300300080101, Title="清空", Permission="sysCache:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 行政区域 + new SysMenu{ Id=1300300090101, Pid=1300300000101, Title="行政区域", Path="/platform/region", Name="sysRegion", Component="/system/region/index", Icon="ele-LocationInformation", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300300090201, Pid=1300300090101, Title="查询", Permission="sysRegion:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090301, Pid=1300300090101, Title="编辑", Permission="sysRegion:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090401, Pid=1300300090101, Title="增加", Permission="sysRegion:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090501, Pid=1300300090101, Title="删除", Permission="sysRegion:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090601, Pid=1300300090101, Title="同步", Permission="sysRegion:sync", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 文件管理 + new SysMenu{ Id=1300300100101, Pid=1300300000101, Title="文件管理", Path="/platform/file", Name="sysFile", Component="/system/file/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300300100201, Pid=1300300100101, Title="查询", Permission="sysFile:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100301, Pid=1300300100101, Title="上传", Permission="sysFile:uploadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100401, Pid=1300300100101, Title="下载", Permission="sysFile:downloadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100501, Pid=1300300100101, Title="删除", Permission="sysFile:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100601, Pid=1300300100101, Title="编辑", Permission="sysFile:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2023-10-27 00:00:00"), OrderNo=100 }, + + // 打印模板 + new SysMenu{ Id=1300300110101, Pid=1300300000101, Title="打印模板", Path="/platform/print", Name="sysPrint", Component="/system/print/index", Icon="ele-Printer", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=190 }, + new SysMenu{ Id=1300300110201, Pid=1300300110101, Title="查询", Permission="sysPrint:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110301, Pid=1300300110101, Title="编辑", Permission="sysPrint:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110401, Pid=1300300110101, Title="增加", Permission="sysPrint:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110501, Pid=1300300110101, Title="删除", Permission="sysPrint:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 动态插件 + new SysMenu{ Id=1300300120101, Pid=1300300000101, Title="动态插件", Path="/platform/plugin", Name="sysPlugin", Component="/system/plugin/index", Icon="ele-Connection", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=200 }, + new SysMenu{ Id=1300300120201, Pid=1300300120101, Title="查询", Permission="sysPlugin:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120301, Pid=1300300120101, Title="编辑", Permission="sysPlugin:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120401, Pid=1300300120101, Title="增加", Permission="sysPlugin:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300120501, Pid=1300300120101, Title="删除", Permission="sysPlugin:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 开放接口 + new SysMenu{ Id=1300300130101, Pid=1300300000101, Title="开放接口", Path="/platform/openAccess", Name="sysOpenAccess", Component="/system/openAccess/index", Icon="ele-Link", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 }, + new SysMenu{ Id=1300300130201, Pid=1300300130101, Title="查询", Permission="sysOpenAccess:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130301, Pid=1300300130101, Title="编辑", Permission="sysOpenAccess:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130401, Pid=1300300130101, Title="增加", Permission="sysOpenAccess:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300130501, Pid=1300300130101, Title="删除", Permission="sysOpenAccess:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统配置 + new SysMenu{ Id=1300300140101, Pid=1300300000101, Title="系统配置", Path="/platform/infoSetting", Name="sysInfoSetting", Component="/system/infoSetting/index", Icon="ele-Setting", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=220 }, + + // 微信支付 + new SysMenu{ Id=1300300150101, Pid=1300300000101, Title="微信支付", Path="/platform/wechatpay", Name="sysWechatPay", Component="/system/weChatPay/index", Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=230 }, + new SysMenu{ Id=1300300150201, Pid=1300300150101, Title="微信支付下单Native", Permission="sysWechatPay:payTransactionNative", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150301, Pid=1300300150101, Title="查询退款信息", Permission="sysWechatPay:listRefund", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150401, Pid=1300300150101, Title="获取支付订单详情(本地库)", Permission="sysWechatPay:payInfo", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150501, Pid=1300300150101, Title="获取支付订单详情(微信接口)", Permission="sysWechatPay:payInfoFromWechat", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300150601, Pid=1300300150101, Title="退款申请", Permission="sysWechatPay:refundDomestic", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统更新 + new SysMenu{ Id=1300300160101, Pid=1300300000101, Title="系统更新", Path="/platform/update", Name="sysUpdate", Component="/system/update/index", Icon="ele-Upload", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=240 }, + new SysMenu{ Id=1300300160201, Pid=1300300160101, Title="更新", Permission="sysUpdate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160301, Pid=1300300160101, Title="还原", Permission="sysUpdate:restore", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160401, Pid=1300300160101, Title="备份列表", Permission="sysUpdate:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160501, Pid=1300300160101, Title="日志列表", Permission="sysUpdate:logs", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160601, Pid=1300300160101, Title="清除日志", Permission="sysUpdate:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300160701, Pid=1300300160101, Title="获取密钥", Permission="sysUpdate:webHookKey", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + #endregion 平台管理 + + #region 日志管理 + + new SysMenu{ Id=1300400000101, Pid=0, Title="日志管理", Path="/log", Name="log", Component="Layout", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=12000 }, + + // 访问日志 + new SysMenu{ Id=1300400010101, Pid=1300400000101, Title="访问日志", Path="/log/vislog", Name="sysVisLog", Component="/system/log/vislog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010201, Pid=1300400010101, Title="查询", Permission="sysVislog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010301, Pid=1300400010101, Title="清空", Permission="sysVislog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 操作日志 + new SysMenu{ Id=1300400020101, Pid=1300400000101, Title="操作日志", Path="/log/oplog", Name="sysOpLog", Component="/system/log/oplog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300400020201, Pid=1300400020101, Title="查询", Permission="sysOplog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020301, Pid=1300400020101, Title="清空", Permission="sysOplog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020401, Pid=1300400020101, Title="导出", Permission="sysOplog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 异常日志 + new SysMenu{ Id=1300400030101, Pid=1300400000101, Title="异常日志", Path="/log/exlog", Name="sysExLog", Component="/system/log/exlog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300400030201, Pid=1300400030101, Title="查询", Permission="sysExlog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030301, Pid=1300400030101, Title="清空", Permission="sysExlog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030401, Pid=1300400030101, Title="导出", Permission="sysExlog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 差异日志 + new SysMenu{ Id=1300400040101, Pid=1300400000101, Title="差异日志", Path="/log/difflog", Name="sysDiffLog", Component="/system/log/difflog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300400040201, Pid=1300400040101, Title="查询", Permission="sysDifflog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400040301, Pid=1300400040101, Title="清空", Permission="sysDifflog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + #endregion 日志管理 + + #region 开发工具 + + // 开发工具 + new SysMenu{ Id=1300500000101, Pid=0, Title="开发工具", Path="/develop", Name="develop", Component="Layout", Icon="ele-Cpu", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=13000 }, + new SysMenu{ Id=1300500010101, Pid=1300500000101, Title="库表管理", Path="/develop/database", Name="sysDatabase", Component="/system/database/index",Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300500020101, Pid=1300500000101, Title="代码生成", Path="/develop/codeGen", Name="sysCodeGen", Component="/system/codeGen/index", Icon="ele-Crop", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300500030101, Pid=1300500000101, Title="表单设计", Path="/develop/formDes", Name="sysFormDes", Component="/system/formDes/index", Icon="ele-Edit", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300500040101, Pid=1300500000101, Title="接口压测", Path="/develop/stressTest", Name="SysStressTest", Component="/system/stressTest/index", Icon="ele-DataLine", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300500050101, Pid=1300500000101, Title="系统接口", Path="/develop/api", Name="sysApi", Component="layout/routerView/iframe", IsIframe=true, OutLink="http://localhost:5005", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + + #endregion 开发工具 + + #region 帮助文档 + + // 帮助文档 + new SysMenu{ Id=1300600000101, Pid=0, Title="帮助文档", Path="/doc", Name="doc", Component="Layout", Icon="ele-Notebook", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=14000 }, + new SysMenu{ Id=1300600010101, Pid=1300600000101, Title="框架教程", Path="/doc/admin", Name="sysAdmin", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="http://101.43.53.74:5050/", Icon="ele-Sunny", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300600020101, Pid=1300600000101, Title="后台教程", Path="/doc/furion", Name="sysFurion", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://furion.baiqian.ltd/", Icon="ele-Promotion", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300600030101, Pid=1300600000101, Title="前端教程", Path="/doc/element", Name="sysElement", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://element-plus.gitee.io/zh-CN/", Icon="ele-Position", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300600040101, Pid=1300600000101, Title="SqlSugar", Path="/doc/SqlSugar", Name="sysSqlSugar", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="https://www.donet5.com/Home/Doc", Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + + #endregion 帮助文档 + + // 关于项目 + new SysMenu{ Id=1300700000101, Pid=0, Title="关于项目", Path="/about", Name="about", Component="/about/index", Icon="ele-InfoFilled", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2023-03-12 00:00:00"), OrderNo=15000 }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysOrgSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysOrgSeedData.cs new file mode 100644 index 0000000..35ba85d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysOrgSeedData.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统机构表种子数据 +/// +[IgnoreUpdateSeed] +public class SysOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var admin = new SysUserSeedData().HasData().First(u => u.Account == "Admin.NET"); + return new[] + { + new SysOrg{ Id=SqlSugarConst.DefaultTenantId, Pid=0, Name="系统默认", Code="1001", Type="101", Level=1, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统默认", TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 1, Pid=SqlSugarConst.DefaultTenantId, Name="市场部", Code="100101", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="市场部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 2, Pid=SqlSugarConst.DefaultTenantId, Name="开发部", Code="100102", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="开发部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 3, Pid=SqlSugarConst.DefaultTenantId, Name="售后部", Code="100103", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="售后部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 4, Pid=SqlSugarConst.DefaultTenantId, Name="其他", Code="10010301", Level=3, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysPosSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysPosSeedData.cs new file mode 100644 index 0000000..64b62e6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysPosSeedData.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统职位表种子数据 +/// +public class SysPosSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysPos{ Id=1300000000101, Name="党委书记", Code="dwsj", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="党委书记", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000102, Name="董事长", Code="dsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000103, Name="副董事长", Code="fdsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000104, Name="总经理", Code="zjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000105, Name="副总经理", Code="fzjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000106, Name="部门经理", Code="bmjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000107, Name="部门副经理", Code="bmfjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门副经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000108, Name="主任", Code="zr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000109, Name="副主任", Code="fzr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000110, Name="局长", Code="jz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000111, Name="副局长", Code="fjz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000112, Name="科长", Code="kz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000113, Name="副科长", Code="fkz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000114, Name="财务", Code="cw", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="财务", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000115, Name="职员", Code="zy", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="职员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000116, Name="其他", Code="qt", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs new file mode 100644 index 0000000..2c4bdb1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色菜单表种子数据 +/// +public class SysRoleMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var roleMenuList = new List(); + + var roleList = new SysRoleSeedData().HasData().ToList(); + var menuList = new SysMenuSeedData().HasData().ToList(); + var defaultMenuList = new SysTenantMenuSeedData().HasData().ToList(); + + // 第一个角色拥有全部默认租户菜单 + roleMenuList.AddRange(defaultMenuList.Select(u => new SysRoleMenu { Id = u.MenuId + (roleList[0].Id % 1300000000000), RoleId = roleList[0].Id, MenuId = u.MenuId })); + + // 其他角色权限:工作台、系统管理、个人中心、帮助文档、关于项目 + var otherRoleMenuList = menuList.ToChildList(u => u.Id, u => u.Pid, u => new[] { "工作台", "帮助文档", "关于项目", "个人中心" }.Contains(u.Title)).ToList(); + otherRoleMenuList.Add(menuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "系统管理")); + foreach (var role in roleList.Skip(1)) roleMenuList.AddRange(otherRoleMenuList.Select(u => new SysRoleMenu { Id = u.Id + (role.Id % 1300000000000), RoleId = role.Id, MenuId = u.Id })); + + return roleMenuList; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleSeedData.cs new file mode 100644 index 0000000..5dc8c8f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysRoleSeedData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统角色表种子数据 +/// +public class SysRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysRole{ Id=1300000000101, Name="系统管理员", DataScope=DataScopeEnum.All, Code="sys_admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统管理员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000102, Name="本部门及以下数据", DataScope=DataScopeEnum.DeptChild, Code="sys_deptChild", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门及以下数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000103, Name="本部门数据", DataScope=DataScopeEnum.Dept, Code="sys_dept", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000104, Name="仅本人数据", DataScope=DataScopeEnum.Self, Code="sys_self", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="仅本人数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000105, Name="自定义数据", DataScope=DataScopeEnum.Define, Code="sys_define", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="自定义数据", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs new file mode 100644 index 0000000..50fad12 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantMenuSeedData.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户菜单表种子数据 +/// +[IgnoreUpdateSeed] +public class SysTenantMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return App.GetService().GetTenantDefaultMenuList() + .Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.MenuId, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.MenuId + }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantSeedData.cs new file mode 100644 index 0000000..ccef80d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysTenantSeedData.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统租户表种子数据 +/// +public class SysTenantSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var defaultDbConfig = App.GetOptions().ConnectionConfigs[0]; + var userList = new SysUserSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "Admin.NET"); + return new[] + { + new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=admin.Id, Host="gitee.com", TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Logo="/upload/logo.png", Title="Admin.NET", ViceTitle="Admin.NET", ViceDesc="站在巨人肩膀上的 .NET 通用权限开发框架", Watermark="Admin.NET", Copyright="Copyright \u00a9 2021-present Admin.NET All rights reserved.", Icp="省ICP备12345678号", IcpUrl="https://beian.miit.gov.cn", Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs new file mode 100644 index 0000000..a54ee7d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserExtOrgSeedData.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户扩展机构表种子数据 +/// +public class SysUserExtOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var orgList = new SysOrgSeedData().HasData().ToList(); + var posList = new SysPosSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "Admin.NET"); + var user3 = userList.First(u => u.Account == "TestUser3"); + var org1 = orgList.First(u => u.Name == "系统默认"); + var org2 = orgList.First(u => u.Name == "开发部"); + var pos1 = posList.First(u => u.Name == "部门经理"); + var pos2 = posList.First(u => u.Name == "主任"); + return new[] + { + new SysUserExtOrg{ Id=1300000000101, UserId=admin.Id, OrgId=org1.Id, PosId=pos1.Id }, + new SysUserExtOrg{ Id=1300000000102, UserId=user3.Id, OrgId=org2.Id, PosId=pos2.Id } + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs new file mode 100644 index 0000000..29f89ec --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserRoleSeedData.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户角色表种子数据 +/// +public class SysUserRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var roleList = new SysRoleSeedData().HasData().ToList(); + return new[] + { + new SysUserRole{ Id=1300000000101, UserId=userList.First(u => u.Account == "TestUser1").Id, RoleId=roleList.First(u => u.Code == "sys_deptChild").Id }, + new SysUserRole{ Id=1300000000102, UserId=userList.First(u => u.Account == "TestUser2").Id, RoleId=roleList.First(u => u.Code == "sys_dept").Id }, + new SysUserRole{ Id=1300000000103, UserId=userList.First(u => u.Account == "TestUser3").Id, RoleId=roleList.First(u => u.Code == "sys_self").Id }, + new SysUserRole{ Id=1300000000104, UserId=userList.First(u => u.Account == "TestUser4").Id, RoleId=roleList.First(u => u.Code == "sys_define").Id }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserSeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserSeedData.cs new file mode 100644 index 0000000..7c1f8a7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SeedData/SysUserSeedData.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 系统用户表种子数据 +/// +[IgnoreUpdateSeed] +public class SysUserSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var encryptPassword = CryptogramUtil.Encrypt(new SysConfigSeedData().HasData().First(u => u.Code == ConfigConst.SysPassword).Value); + var posList = new SysPosSeedData().HasData().ToList(); + return new[] + { + new SysUser{ Id=1300000000101, Account="superAdmin.NET", Password=encryptPassword, NickName="超级管理员", RealName="超级管理员", Phone="18012345678", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SuperAdmin, Remark="超级管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000111, Account="Admin.NET", Password=encryptPassword, NickName="系统管理员", RealName="系统管理员", Phone="18012345677", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SysAdmin, Remark="系统管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId, PosId=posList[0].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000112, Account="TestUser1", Password=encryptPassword, NickName="部门主管", RealName="部门主管", Phone="18012345676", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门主管", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 1, PosId=posList[1].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000113, Account="TestUser2", Password=encryptPassword, NickName="部门职员", RealName="部门职员", Phone="18012345675", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门职员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 2, PosId=posList[2].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000114, Account="TestUser3", Password=encryptPassword, NickName="普通用户", RealName="普通用户", Phone="18012345674", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="普通用户", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 3, PosId=posList[3].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000115, Account="TestUser4", Password=encryptPassword, NickName="其他", RealName="其他", Phone="18012345673", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.Member, Remark="会员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 4, PosId=posList[4].Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/APIJSONService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/APIJSONService.cs new file mode 100644 index 0000000..17bd21d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/APIJSONService.cs @@ -0,0 +1,214 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// APIJSON服务 🧩 +/// +[ApiDescriptionSettings(Order = 100)] +public class APIJSONService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + private readonly IdentityService _identityService; + private readonly TableMapper _tableMapper; + private readonly SelectTable _selectTable; + + public APIJSONService(ISqlSugarClient db, + IdentityService identityService, + TableMapper tableMapper) + { + _db = db; + _tableMapper = tableMapper; + _identityService = identityService; + _selectTable = new SelectTable(_identityService, _tableMapper, _db); + } + + /// + /// 统一查询入口 🔖 + /// + /// + /// 参数:{"[]":{"SYSLOGOP":{}}} + /// + [HttpPost("get")] + [DisplayName("APIJSON统一查询")] + public JObject Query([FromBody] JObject jobject) + { + var database = jobject["@database"]?.ToString(); + if (!string.IsNullOrEmpty(database)) + { + // 设置数据库 + var provider = _db.AsTenant().GetConnectionScope(database); + jobject.Remove("@database"); + return new SelectTable(_identityService, _tableMapper, provider).Query(jobject); + } + return _selectTable.Query(jobject); + } + + /// + /// 查询 🔖 + /// + /// + /// + /// + [HttpPost("get/{table}")] + [DisplayName("APIJSON查询")] + public JObject QueryByTable([FromRoute] string table, [FromBody] JObject jobject) + { + var ht = new JObject + { + { table + "[]", jobject } + }; + + // 自动添加总计数量 + if (jobject["query"] != null && jobject["query"].ToString() != "0" && jobject["total@"] == null) + ht.Add("total@", ""); + + // 每页最大1000条数据 + if (jobject["count"] != null && int.Parse(jobject["count"].ToString()) > 1000) + throw Oops.Bah("count分页数量最大不能超过1000"); + + jobject.Remove("@debug"); + + var hasTableKey = false; + var ignoreConditions = new List { "page", "count", "query" }; + var tableConditions = new JObject(); // 表的其它查询条件,比如过滤、字段等 + foreach (var item in jobject) + { + if (item.Key.Equals(table, StringComparison.CurrentCultureIgnoreCase)) + { + hasTableKey = true; + break; + } + if (!ignoreConditions.Contains(item.Key.ToLower())) + tableConditions.Add(item.Key, item.Value); + } + + foreach (var removeKey in tableConditions) + { + jobject.Remove(removeKey.Key); + } + + if (!hasTableKey) + jobject.Add(table, tableConditions); + + return Query(ht); + } + + /// + /// 新增 🔖 + /// + /// 表对象或数组,若没有传Id则后端生成Id + /// + [HttpPost("add")] + [DisplayName("APIJSON新增")] + [UnitOfWork] + public JObject Add([FromBody] JObject tables) + { + var ht = new JObject(); + foreach (var table in tables) + { + var talbeName = table.Key.Trim(); + var role = _identityService.GetRole(); + if (!role.Insert.Table.Contains(talbeName, StringComparer.CurrentCultureIgnoreCase)) + throw Oops.Bah($"没权限添加{talbeName}"); + + JToken result; + // 批量插入 + if (table.Value is JArray) + { + var ids = new List(); + foreach (var record in table.Value) + { + var cols = record.ToObject(); + var id = _selectTable.InsertSingle(talbeName, cols, role); + ids.Add(id); + } + result = JToken.FromObject(new { id = ids, count = ids.Count }); + } + // 单条插入 + else + { + var cols = table.Value.ToObject(); + var id = _selectTable.InsertSingle(talbeName, cols, role); + result = JToken.FromObject(new { id }); + } + ht.Add(talbeName, result); + } + return ht; + } + + /// + /// 更新(只支持Id作为条件) 🔖 + /// + /// 支持多表、多Id批量更新 + /// + [HttpPost("update")] + [DisplayName("APIJSON更新")] + [UnitOfWork] + public JObject Edit([FromBody] JObject tables) + { + var ht = new JObject(); + foreach (var table in tables) + { + var tableName = table.Key.Trim(); + var role = _identityService.GetRole(); + var count = _selectTable.UpdateSingleTable(tableName, table.Value, role); + ht.Add(tableName, JToken.FromObject(new { count })); + } + return ht; + } + + /// + /// 删除(支持非Id条件、支持批量) 🔖 + /// + /// + /// + [HttpPost("delete")] + [DisplayName("APIJSON删除")] + [UnitOfWork] + public JObject Delete([FromBody] JObject tables) + { + var ht = new JObject(); + var role = _identityService.GetRole(); + foreach (var table in tables) + { + var talbeName = table.Key.Trim(); + if (role.Delete == null || role.Delete.Table == null) + throw Oops.Bah("delete权限未配置"); + if (!role.Delete.Table.Contains(talbeName, StringComparer.CurrentCultureIgnoreCase)) + throw Oops.Bah($"没权限删除{talbeName}"); + //if (!value.ContainsKey("id")) + // throw Oops.Bah("未传主键id"); + + var value = JObject.Parse(table.Value.ToString()); + var sb = new StringBuilder(100); + var parameters = new List(); + foreach (var f in value) + { + if (f.Value is JArray) + { + sb.Append($"{f.Key} in (@{f.Key}) and "); + var paraArray = FuncList.TransJArrayToSugarPara(f.Value); + parameters.Add(new SugarParameter($"@{f.Key}", paraArray)); + } + else + { + sb.Append($"{f.Key}=@{f.Key} and "); + parameters.Add(new SugarParameter($"@{f.Key}", FuncList.TransJObjectToSugarPara(f.Value))); + } + } + if (!parameters.Any()) + throw Oops.Bah("请输入删除条件"); + + var whereSql = sb.ToString().TrimEnd(" and "); + var count = _db.Deleteable().AS(talbeName).Where(whereSql, parameters).ExecuteCommand(); // 无实体删除 + value.Add("count", count); // 命中数量 + ht.Add(talbeName, value); + } + return ht; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/FuncList.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/FuncList.cs new file mode 100644 index 0000000..139e02f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/FuncList.cs @@ -0,0 +1,116 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义方法 +/// +public class FuncList +{ + /// + /// 字符串相加 + /// + /// + /// + /// + public string Merge(object a, object b) + { + return a.ToString() + b.ToString(); + } + + /// + /// 对象合并 + /// + /// + /// + /// + public object MergeObj(object a, object b) + { + return new { a, b }; + } + + /// + /// 是否包含 + /// + /// + /// + /// + public bool IsContain(object a, object b) + { + return a.ToString().Split(',').Contains(b); + } + + /// + /// 根据jtoken的实际类型来转换SugarParameter,避免全转成字符串 + /// + /// + /// + public static dynamic TransJObjectToSugarPara(JToken jToken) + { + JTokenType jTokenType = jToken.Type; + return jTokenType switch + { + JTokenType.Integer => jToken.ToObject(typeof(long)), + JTokenType.Float => jToken.ToObject(typeof(decimal)), + JTokenType.Boolean => jToken.ToObject(typeof(bool)), + JTokenType.Date => jToken.ToObject(typeof(DateTime)), + JTokenType.Bytes => jToken.ToObject(typeof(byte)), + JTokenType.Guid => jToken.ToObject(typeof(Guid)), + JTokenType.TimeSpan => jToken.ToObject(typeof(TimeSpan)), + JTokenType.Array => TransJArrayToSugarPara(jToken), + _ => jToken + }; + } + + /// + /// 根据jArray的实际类型来转换SugarParameter,避免全转成字符串 + /// + /// + /// + public static dynamic TransJArrayToSugarPara(JToken jToken) + { + if (jToken is not JArray) return jToken; + if (jToken.Any()) + { + JTokenType jTokenType = jToken.First().Type; + return jTokenType switch + { + JTokenType.Integer => jToken.ToObject(), + JTokenType.Float => jToken.ToObject(), + JTokenType.Boolean => jToken.ToObject(), + JTokenType.Date => jToken.ToObject(), + JTokenType.Bytes => jToken.ToObject(), + JTokenType.Guid => jToken.ToObject(), + JTokenType.TimeSpan => jToken.ToObject(), + _ => jToken.ToArray() + }; + } + + return (JArray)jToken; + } + + /// + /// 获取字符串里的值的真正类型 + /// + /// + /// + public static string GetValueCSharpType(string input) + { + if (DateTime.TryParse(input, out _)) + return "DateTime"; + else if (int.TryParse(input, out _)) + return "int"; + else if (long.TryParse(input, out _)) + return "long"; + else if (decimal.TryParse(input, out _)) + return "decimal"; + else if (bool.TryParse(input, out _)) + return "bool"; + else + return "string"; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/IdentityService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/IdentityService.cs new file mode 100644 index 0000000..f62bde5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/IdentityService.cs @@ -0,0 +1,96 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Claims; + +namespace Admin.NET.Core.Service; + +/// +/// 权限验证 +/// +public class IdentityService : ITransient +{ + private readonly IHttpContextAccessor _context; + private readonly List _roles; + + public IdentityService(IHttpContextAccessor context, IOptions roles) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _roles = roles.Value.Roles; + } + + /// + /// 获取当前用户Id + /// + /// + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + } + + /// + /// 获取当前用户权限名称 + /// + /// + public string GetUserRoleName() + { + return _context.HttpContext.User.FindFirstValue(ClaimTypes.Role); + } + + /// + /// 获取当前用户权限 + /// + /// + public APIJSON_Role GetRole() + { + var role = string.IsNullOrEmpty(GetUserRoleName()) + ? _roles.FirstOrDefault() + : _roles.FirstOrDefault(it => it.RoleName.Equals(GetUserRoleName(), StringComparison.CurrentCultureIgnoreCase)); + return role; + } + + /// + /// 获取当前表的可查询字段 + /// + /// + /// + public (bool, string) GetSelectRole(string table) + { + var role = GetRole(); + if (role == null || role.Select == null || role.Select.Table == null) + return (false, $"appsettings.json权限配置不正确!"); + + var tablerole = role.Select.Table.FirstOrDefault(it => it == "*" || it.Equals(table, StringComparison.CurrentCultureIgnoreCase)); + if (string.IsNullOrEmpty(tablerole)) + return (false, $"表名{table}没权限查询!"); + + var index = Array.IndexOf(role.Select.Table, tablerole); + var selectrole = role.Select.Column[index]; + return (true, selectrole); + } + + /// + /// 当前列是否在角色里面 + /// + /// + /// + /// + public bool ColIsRole(string col, string[] selectrole) + { + if (selectrole.Contains("*")) return true; + + if (col.Contains('(') && col.Contains(')')) + { + var reg = new Regex(@"\(([^)]*)\)"); + var match = reg.Match(col); + return selectrole.Contains(match.Result("$1"), StringComparer.CurrentCultureIgnoreCase); + } + else + { + return selectrole.Contains(col, StringComparer.CurrentCultureIgnoreCase); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/SelectTable.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/SelectTable.cs new file mode 100644 index 0000000..8b83cb9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/SelectTable.cs @@ -0,0 +1,979 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AspectCore.Extensions.Reflection; +using System.Dynamic; + +namespace Admin.NET.Core.Service; + +/// +/// +/// +public class SelectTable : ISingleton +{ + private readonly IdentityService _identitySvc; + private readonly TableMapper _tableMapper; + private readonly ISqlSugarClient _db; + + public SelectTable(IdentityService identityService, TableMapper tableMapper, ISqlSugarClient dbClient) + { + _identitySvc = identityService; + _tableMapper = tableMapper; + _db = dbClient; + } + + /// + /// 判断表名是否正确,若不正确则抛异常 + /// + /// + /// + public virtual bool IsTable(string table) + { + return _db.DbMaintenance.GetTableInfoList().Any(it => it.Name.Equals(table, StringComparison.CurrentCultureIgnoreCase)) + ? true + : throw new Exception($"表名【{table}】不正确!"); + } + + /// + /// 判断表的列名是否正确,如果不正确则抛异常,更早地暴露给调用方 + /// + /// + /// + /// + public virtual bool IsCol(string table, string col) + { + return _db.DbMaintenance.GetColumnInfosByTableName(table).Any(it => it.DbColumnName.Equals(col, StringComparison.CurrentCultureIgnoreCase)) + ? true + : throw new Exception($"表【{table}】不存在列【{col}】!请检查输入参数"); + } + + /// + /// 查询列表数据 + /// + /// + /// + /// + /// + /// + /// + /// + public virtual Tuple GetTableData(string subtable, int page, int count, int query, string json, JObject dd) + { + var role = _identitySvc.GetSelectRole(subtable); + if (!role.Item1) + throw new Exception(role.Item2); + + var selectrole = role.Item2; + subtable = _tableMapper.GetTableName(subtable); + + var values = JObject.Parse(json); + page = values["page"] == null ? page : int.Parse(values["page"].ToString()); + count = values["count"] == null ? count : int.Parse(values["count"].ToString()); + query = values["query"] == null ? query : int.Parse(values["query"].ToString()); + values.Remove("page"); + values.Remove("count"); + // 构造查询过程 + var tb = SugarQueryable(subtable, selectrole, values, dd); + + // 实际会在这里执行 + if (query == 1) // 1-总数 + { + return new Tuple(null, tb.MergeTable().Count()); + } + else + { + if (page > 0) // 分页 + { + int total = 0; + if (query == 0) + return new Tuple(tb.ToPageList(page, count), total); // 0-对象 + else + return new Tuple(tb.ToPageList(page, count, ref total), total); // 2-以上全部 + } + else // 列表 + { + IList l = tb.ToList(); + return query == 0 ? new Tuple(l, 0) : new Tuple(l, l.Count); + } + } + } + + /// + /// 解析并查询 + /// + /// + /// + public virtual JObject Query(string queryJson) + { + var queryJobj = JObject.Parse(queryJson); + return Query(queryJobj); + } + + /// + /// 单表查询 + /// + /// + /// 返回数据的节点名称 默认为 infos + /// + public virtual JObject QuerySingle(JObject queryObj, string nodeName = "infos") + { + var resultObj = new JObject(); + + var total = 0; + foreach (var item in queryObj) + { + var key = item.Key.Trim(); + if (key.EndsWith("[]")) + { + total = QuerySingleList(resultObj, item, nodeName); + } + else if (key.Equals("func")) + { + ExecFunc(resultObj, item); + } + else if (key.Equals("total@") || key.Equals("total")) + { + resultObj.Add("total", total); + } + } + return resultObj; + } + + /// + /// 获取查询语句 + /// + /// + /// + public virtual string ToSql(JObject queryObj) + { + foreach (var item in queryObj) + { + if (item.Key.Trim().EndsWith("[]")) + return ToSql(item); + } + return string.Empty; + } + + /// + /// 解析并查询 + /// + /// + /// + public virtual JObject Query(JObject queryObj) + { + var resultObj = new JObject(); + + int total; + foreach (var item in queryObj) + { + var key = item.Key.Trim(); + if (key.Equals("[]")) // 列表 + { + total = QueryMoreList(resultObj, item); + resultObj.Add("total", total); // 只要是列表查询都自动返回总数 + } + else if (key.EndsWith("[]")) + { + total = QuerySingleList(resultObj, item); + } + else if (key.Equals("func")) + { + ExecFunc(resultObj, item); + } + else if (key.Equals("total@") || key.Equals("total")) + { + // resultObj.Add("total", total); + continue; + } + else // 单条 + { + var template = GetFirstData(key, item.Value.ToString(), resultObj); + if (template != null) + resultObj.Add(key, JToken.FromObject(template)); + } + } + return resultObj; + } + + // 动态调用方法 + private static object ExecFunc(string funcname, object[] param, Type[] types) + { + var method = typeof(FuncList).GetMethod(funcname); + var reflector = method.GetReflector(); + var result = reflector.Invoke(new FuncList(), param); + return result; + } + + // 生成sql + private string ToSql(string subtable, int page, int count, int query, string json) + { + var values = JObject.Parse(json); + page = values["page"] == null ? page : int.Parse(values["page"].ToString()); + count = values["count"] == null ? count : int.Parse(values["count"].ToString()); + _ = values["query"] == null ? query : int.Parse(values["query"].ToString()); + values.Remove("page"); + values.Remove("count"); + subtable = _tableMapper.GetTableName(subtable); + var tb = SugarQueryable(subtable, "*", values, null); + var sqlObj = tb.Skip((page - 1) * count).Take(10).ToSql(); + return sqlObj.Key; + } + + /// + /// 查询第一条数据 + /// + /// + /// + /// + /// + /// + private dynamic GetFirstData(string subtable, string json, JObject job) + { + var role = _identitySvc.GetSelectRole(subtable); + if (!role.Item1) + throw new Exception(role.Item2); + + var selectrole = role.Item2; + subtable = _tableMapper.GetTableName(subtable); + + var values = JObject.Parse(json); + values.Remove("page"); + values.Remove("count"); + var tb = SugarQueryable(subtable, selectrole, values, job).First(); + var dic = (IDictionary)tb; + foreach (var item in values.Properties().Where(it => it.Name.EndsWith("()"))) + { + if (item.Value.IsNullOrEmpty()) + { + var func = item.Value.ToString().Substring(0, item.Value.ToString().IndexOf("(")); + var param = item.Value.ToString().Substring(item.Value.ToString().IndexOf("(") + 1).TrimEnd(')'); + var types = new List(); + var paramss = new List(); + foreach (var va in param.Split(',')) + { + types.Add(typeof(object)); + paramss.Add(tb.Where(it => it.Key.Equals(va)).Select(i => i.Value)); + } + dic[item.Name] = ExecFunc(func, paramss.ToArray(), types.ToArray()); + } + } + return tb; + } + + // 单表查询,返回的数据在指定的NodeName节点 + private int QuerySingleList(JObject resultObj, KeyValuePair item, string nodeName) + { + var key = item.Key.Trim(); + var jb = JObject.Parse(item.Value.ToString()); + int page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString()); + int count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString()); + int query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量 + int total = 0; + + jb.Remove("page"); jb.Remove("count"); jb.Remove("query"); + + var htt = new JArray(); + foreach (var t in jb) + { + var datas = GetTableData(t.Key, page, count, query, t.Value.ToString(), null); + if (query > 0) + total = datas.Item2; + + foreach (var data in datas.Item1) + { + htt.Add(JToken.FromObject(data)); + } + } + + if (!string.IsNullOrEmpty(nodeName)) + resultObj.Add(nodeName, htt); + else + resultObj.Add(key, htt); + + return total; + } + + // 生成sql + private string ToSql(KeyValuePair item) + { + var jb = JObject.Parse(item.Value.ToString()); + int page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString()); + int count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString()); + int query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量 + + jb.Remove("page"); jb.Remove("count"); jb.Remove("query"); + foreach (var t in jb) + { + return ToSql(t.Key, page, count, query, t.Value.ToString()); + } + return string.Empty; + } + + // 单表查询 + private int QuerySingleList(JObject resultObj, KeyValuePair item) + { + var key = item.Key.TrimEnd("[]"); + return QuerySingleList(resultObj, item, key); + } + + /// + /// 多列表查询 + /// + /// + /// + /// + private int QueryMoreList(JObject resultObj, KeyValuePair item) + { + int total = 0; + + var jb = JObject.Parse(item.Value.ToString()); + var page = jb["page"] == null ? 0 : int.Parse(jb["page"].ToString()); + var count = jb["count"] == null ? 10 : int.Parse(jb["count"].ToString()); + var query = jb["query"] == null ? 2 : int.Parse(jb["query"].ToString()); // 默认输出数据和数量 + jb.Remove("page"); jb.Remove("count"); jb.Remove("query"); + var htt = new JArray(); + List tables = new List(), where = new List(); + foreach (var t in jb) + { + tables.Add(t.Key); where.Add(t.Value.ToString()); + } + if (tables.Count > 0) + { + string table = tables[0].TrimEnd("[]"); + var temp = GetTableData(table, page, count, query, where[0], null); + if (query > 0) + total = temp.Item2; + + // 关联查询,先查子表数据,再根据外键循环查询主表 + foreach (var dd in temp.Item1) + { + var zht = new JObject + { + { table, JToken.FromObject(dd) } + }; + for (int i = 1; i < tables.Count; i++) // 从第二个表开始循环 + { + string subtable = tables[i]; + // 有bug,暂不支持[]分支 + //if (subtable.EndsWith("[]")) + //{ + // string tableName = subtable.TrimEnd("[]".ToCharArray()); + // var jbb = JObject.Parse(where[i]); + // page = jbb["page"] == null ? 0 : int.Parse(jbb["page"].ToString()); + // count = jbb["count"] == null ? 0 : int.Parse(jbb["count"].ToString()); + + // var lt = new JArray(); + // foreach (var d in GetTableData(tableName, page, count, query, item.Value[subtable].ToString(), zht).Item1) + // { + // lt.Add(JToken.FromObject(d)); + // } + // zht.Add(tables[i], lt); + //} + //else + //{ + var ddf = GetFirstData(subtable, where[i].ToString(), zht); + if (ddf != null) + zht.Add(subtable, JToken.FromObject(ddf)); + } + htt.Add(zht); + } + } + if (query != 1) + resultObj.Add("[]", htt); + + // 分页自动添加当前页数和数量 + if (page > 0 && count > 0) + { + resultObj.Add("page", page); + resultObj.Add("count", count); + resultObj.Add("max", (int)Math.Ceiling((decimal)total / count)); + } + + return total; + } + + // 执行方法 + private void ExecFunc(JObject resultObj, KeyValuePair item) + { + var jb = JObject.Parse(item.Value.ToString()); + + var dataJObj = new JObject(); + foreach (var f in jb) + { + var types = new List(); + var param = new List(); + foreach (var va in JArray.Parse(f.Value.ToString())) + { + types.Add(typeof(object)); + param.Add(va); + } + dataJObj.Add(f.Key, JToken.FromObject(ExecFunc(f.Key, param.ToArray(), types.ToArray()))); + } + resultObj.Add("func", dataJObj); + } + + /// + /// 构造查询过程 + /// + /// + /// + /// + /// + /// + private ISugarQueryable SugarQueryable(string subtable, string selectrole, JObject values, JObject dd) + { + IsTable(subtable); + + var tb = _db.Queryable(subtable, "tb"); + + // select + if (!values["@column"].IsNullOrEmpty()) + { + ProcessColumn(subtable, selectrole, values, tb); + } + else + { + tb.Select(selectrole); + } + + // 前几行 + ProcessLimit(values, tb); + + // where + ProcessWhere(subtable, values, tb, dd); + + // 排序 + ProcessOrder(subtable, values, tb); + + // 分组 + PrccessGroup(subtable, values, tb); + + // Having + ProcessHaving(values, tb); + + return tb; + } + + // 处理字段重命名 "@column":"toId:parentId",对应SQL是toId AS parentId,将查询的字段toId变为parentId返回 + private void ProcessColumn(string subtable, string selectrole, JObject values, ISugarQueryable tb) + { + var str = new System.Text.StringBuilder(100); + foreach (var item in values["@column"].ToString().Split(',')) + { + var ziduan = item.Split(':'); + var colName = ziduan[0]; + var ma = new Regex(@"\((\w+)\)").Match(colName); + // 处理max、min这样的函数 + if (ma.Success && ma.Groups.Count > 1) + colName = ma.Groups[1].Value; + + // 判断列表是否有权限 sum(1)、sum(*)、Count(1)这样的值直接有效 + if (colName == "*" || int.TryParse(colName, out int colNumber) || (IsCol(subtable, colName) && _identitySvc.ColIsRole(colName, selectrole.Split(',')))) + { + // 字段名加引号,防止和SQL关键字冲突(mysql为反引号) + string qm = "\""; + if (tb.Context.CurrentConnectionConfig.DbType is SqlSugar.DbType.MySql) + qm = "`"; + + if (ziduan.Length > 1) + { + if (ziduan[1].Length > 20) + throw new Exception("别名不能超过20个字符"); + + str.Append(ziduan[0] + " as " + qm + ReplaceSQLChar(ziduan[1]) + qm + ","); + } + // 不对函数加``,解决sum(*)、Count(1)等不能使用的问题 + else if (ziduan[0].Contains('(')) + { + str.Append(ziduan[0] + ","); + } + else + str.Append(qm + ziduan[0] + qm + ","); + } + } + if (string.IsNullOrEmpty(str.ToString())) + throw new Exception($"表名{subtable}没有可查询的字段!"); + + tb.Select(str.ToString().TrimEnd(',')); + } + + /// + /// 构造查询条件 where + /// + /// + /// + /// + /// + private void ProcessWhere(string subtable, JObject values, ISugarQueryable tb, JObject dd) + { + var conModels = new List(); + if (!values["identity"].IsNullOrEmpty()) + conModels.Add(new ConditionalModel() { FieldName = values["identity"].ToString(), ConditionalType = ConditionalType.Equal, FieldValue = _identitySvc.GetUserIdentity() }); + + foreach (var va in values) + { + string key = va.Key.Trim(); + string fieldValue = va.Value.ToString(); + if (key.StartsWith("@")) + { + continue; + } + if (key.EndsWith("$")) // 模糊查询 + { + FuzzyQuery(subtable, conModels, va); + } + else if (key.EndsWith("{}")) // 逻辑运算 + { + ConditionQuery(subtable, conModels, va); + } + else if (key.EndsWith("%")) // bwtween查询 + { + ConditionBetween(subtable, conModels, va, tb); + } + else if (key.EndsWith("@")) // 关联上一个table + { + if (dd == null) + continue; + + var str = fieldValue.Split('/'); + var lastTableRecord = ((JObject)dd[str[^2]]); + if (!lastTableRecord.ContainsKey(str[^1])) + throw new Exception($"找不到关联列:{str},请在{str[^2]}@column中设置"); + + var value = lastTableRecord[str[^1]].ToString(); + conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('@'), ConditionalType = ConditionalType.Equal, FieldValue = value }); + } + else if (key.EndsWith("~")) // 不等于(应该是正则匹配) + { + //conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('~'), ConditionalType = ConditionalType.NoEqual, FieldValue = fieldValue }); + } + else if (IsCol(subtable, key.TrimEnd('!'))) // 其他where条件 + { + ConditionEqual(subtable, conModels, va); + } + } + if (conModels.Any()) + tb.Where(conModels); + } + + // "@having":"function0(...)?value0;function1(...)?value1;function2(...)?value2...", + // SQL函数条件,一般和 @group一起用,函数一般在 @column里声明 + private static void ProcessHaving(JObject values, ISugarQueryable tb) + { + if (!values["@having"].IsNullOrEmpty()) + { + var hw = new List(); + var havingItems = new List(); + if (values["@having"].HasValues) + { + havingItems = values["@having"].Select(p => p.ToString()).ToList(); + } + else + { + havingItems.Add(values["@having"].ToString()); + } + foreach (var item in havingItems) + { + var and = item.ToString(); + var model = new ConditionalModel(); + if (and.Contains(">=")) + { + model.FieldName = and.Split(new string[] { ">=" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.GreaterThanOrEqual; + model.FieldValue = and.Split(new string[] { ">=" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + else if (and.Contains("<=")) + { + model.FieldName = and.Split(new string[] { "<=" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.LessThanOrEqual; + model.FieldValue = and.Split(new string[] { "<=" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + else if (and.Contains('>')) + { + model.FieldName = and.Split(new string[] { ">" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.GreaterThan; + model.FieldValue = and.Split(new string[] { ">" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + else if (and.Contains('<')) + { + model.FieldName = and.Split(new string[] { "<" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.LessThan; + model.FieldValue = and.Split(new string[] { "<" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + else if (and.Contains("!=")) + { + model.FieldName = and.Split(new string[] { "!=" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.NoEqual; + model.FieldValue = and.Split(new string[] { "!=" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + else if (and.Contains('=')) + { + model.FieldName = and.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries)[0]; + model.ConditionalType = ConditionalType.Equal; + model.FieldValue = and.Split(new string[] { "=" }, StringSplitOptions.RemoveEmptyEntries)[1]; + } + hw.Add(model); + } + //var d = db.Context.Utilities.ConditionalModelToSql(hw); + //tb.Having(d.Key, d.Value); + tb.Having(string.Join(",", havingItems)); + } + } + + // "@group":"column0,column1...",分组方式。如果 @column里声明了Table的id,则id也必须在 @group中声明;其它情况下必须满足至少一个条件: + // 1.分组的key在 @column里声明 + // 2.Table主键在 @group中声明 + private void PrccessGroup(string subtable, JObject values, ISugarQueryable tb) + { + if (!values["@group"].IsNullOrEmpty()) + { + var groupList = new List(); // 多库兼容写法 + foreach (var col in values["@group"].ToString().Split(',')) + { + if (IsCol(subtable, col)) + { + // str.Append(and + ","); + groupList.Add(new GroupByModel() { FieldName = col }); + } + } + if (groupList.Any()) + tb.GroupBy(groupList); + } + } + + // 处理排序 "@order":"name-,id"查询按 name降序、id默认顺序 排序的User数组 + private void ProcessOrder(string subtable, JObject values, ISugarQueryable tb) + { + if (!values["@order"].IsNullOrEmpty()) + { + var orderList = new List(); // 多库兼容写法 + foreach (var item in values["@order"].ToString().Split(',')) + { + string col = item.Replace("-", "").Replace("+", "").Replace(" desc", "").Replace(" asc", ""); // 增加对原生排序的支持 + if (IsCol(subtable, col)) + { + orderList.Add(new OrderByModel() + { + FieldName = col, + OrderByType = item.EndsWith("-") || item.EndsWith(" desc") ? OrderByType.Desc : OrderByType.Asc + }); + } + } + + if (orderList.Any()) + tb.OrderBy(orderList); + } + } + + /// + /// 表内参数"@count"(int):查询前几行,不能同时使用count和@count函数 + /// + /// + /// + private static void ProcessLimit(JObject values, ISugarQueryable tb) + { + if (!values["@count"].IsNullOrEmpty()) + { + int c = values["@count"].ToObject(); + tb.Take(c); + } + } + + // 条件查询 "key{}":"条件0,条件1...",条件为任意SQL比较表达式字符串,非Number类型必须用''包含条件的值,如'a' + // &, |, ! 逻辑运算符,对应数据库 SQL 中的 AND, OR, NOT。 + // 横或纵与:同一字段的值内条件默认 | 或连接,不同字段的条件默认 & 与连接。 + // ① & 可用于"key&{}":"条件"等 + // ② | 可用于"key|{}":"条件", "key|{}":[] 等,一般可省略 + // ③ ! 可单独使用,如"key!":Object,也可像&,|一样配合其他功能符使用 + private void ConditionQuery(string subtable, List conModels, KeyValuePair va) + { + var vakey = va.Key.Trim(); + var field = vakey.TrimEnd("{}".ToCharArray()); + var columnName = field.TrimEnd(new char[] { '&', '|' }); + IsCol(subtable, columnName); + var ddt = new List>(); + foreach (var and in va.Value.ToString().Split(',')) + { + var model = new ConditionalModel + { + FieldName = columnName + }; + + if (and.StartsWith(">=")) + { + model.ConditionalType = ConditionalType.GreaterThanOrEqual; + model.FieldValue = and.TrimStart(">=".ToCharArray()); + } + else if (and.StartsWith("<=")) + { + model.ConditionalType = ConditionalType.LessThanOrEqual; + model.FieldValue = and.TrimStart("<=".ToCharArray()); + } + else if (and.StartsWith(">")) + { + model.ConditionalType = ConditionalType.GreaterThan; + model.FieldValue = and.TrimStart('>'); + } + else if (and.StartsWith("<")) + { + model.ConditionalType = ConditionalType.LessThan; + model.FieldValue = and.TrimStart('<'); + } + model.CSharpTypeName = FuncList.GetValueCSharpType(model.FieldValue); + ddt.Add(new KeyValuePair(field.EndsWith("!") ? WhereType.Or : WhereType.And, model)); + } + conModels.Add(new ConditionalCollections() { ConditionalList = ddt }); + } + + /// + /// "key%":"start,end" => "key%":["start,end"],其中 start 和 end 都只能为 Boolean, Number, String 中的一种,如 "2017-01-01,2019-01-01" ,["1,90000", "82001,100000"] ,可用于连续范围内的筛选 + /// 目前不支持数组形式 + /// + /// + /// + /// + /// + private static void ConditionBetween(string subtable, List conModels, KeyValuePair va, ISugarQueryable tb) + { + var vakey = va.Key.Trim(); + var field = vakey.TrimEnd("%".ToCharArray()); + var inValues = new List(); + if (va.Value.HasValues) + { + foreach (var cm in va.Value) + { + inValues.Add(cm.ToString()); + } + } + else + { + inValues.Add(va.Value.ToString()); + } + + for (var i = 0; i < inValues.Count; i++) + { + var fileds = inValues[i].Split(','); + if (fileds.Length == 2) + { + var type = FuncList.GetValueCSharpType(fileds[0]); + ObjectFuncModel f = ObjectFuncModel.Create("between", field, $"{{{type}}}:{fileds[0]}", $"{{{type}}}:{fileds[1]}"); + tb.Where(f); + } + } + } + + /// + /// 等于、不等于、in 、not in + /// + /// + /// + /// + private void ConditionEqual(string subtable, List conModels, KeyValuePair va) + { + var key = va.Key; + var fieldValue = va.Value.ToString(); + // in / not in + if (va.Value is JArray) + { + conModels.Add(new ConditionalModel() + { + FieldName = key.TrimEnd('!'), + ConditionalType = key.EndsWith("!") ? ConditionalType.NotIn : ConditionalType.In, + FieldValue = va.Value.ToObject().Aggregate((a, b) => a + "," + b) + }); + } + else + { + if (string.IsNullOrEmpty(fieldValue)) + { + // is not null or '' + if (key.EndsWith("!")) + { + conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), ConditionalType = ConditionalType.IsNot, FieldValue = null }); + conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), ConditionalType = ConditionalType.IsNot, FieldValue = "" }); + } + //is null or '' + else + { + conModels.Add(new ConditionalModel() { FieldName = key.TrimEnd('!'), FieldValue = null }); + } + } + // = / != + else + { + conModels.Add(new ConditionalModel() + { + FieldName = key.TrimEnd('!'), + ConditionalType = key.EndsWith("!") ? ConditionalType.NoEqual : ConditionalType.Equal, + FieldValue = fieldValue + }); + } + } + } + + // 模糊搜索 "key$":"SQL搜索表达式" => "key$":["SQL搜索表达式"],任意SQL搜索表达式字符串,如 %key%(包含key), key%(以key开始), %k%e%y%(包含字母k,e,y) 等,%表示任意字符 + private void FuzzyQuery(string subtable, List conModels, KeyValuePair va) + { + var vakey = va.Key.Trim(); + var fieldValue = va.Value.ToString(); + var conditionalType = ConditionalType.Like; + if (IsCol(subtable, vakey.TrimEnd('$'))) + { + // 支持三种like查询 + if (fieldValue.StartsWith("%") && fieldValue.EndsWith("%")) + { + conditionalType = ConditionalType.Like; + } + else if (fieldValue.StartsWith("%")) + { + conditionalType = ConditionalType.LikeRight; + } + else if (fieldValue.EndsWith("%")) + { + conditionalType = ConditionalType.LikeLeft; + } + conModels.Add(new ConditionalModel() { FieldName = vakey.TrimEnd('$'), ConditionalType = conditionalType, FieldValue = fieldValue.TrimEnd("%".ToArray()).TrimStart("%".ToArray()) }); + } + } + + // 处理sql注入 + private string ReplaceSQLChar(string str) + { + if (string.IsNullOrWhiteSpace(str)) + return string.Empty; + + str = str.Replace("'", ""); + str = str.Replace(";", ""); + str = str.Replace(",", ""); + str = str.Replace("?", ""); + str = str.Replace("<", ""); + str = str.Replace(">", ""); + str = str.Replace("(", ""); + str = str.Replace(")", ""); + str = str.Replace("@", ""); + str = str.Replace("=", ""); + str = str.Replace("+", ""); + str = str.Replace("*", ""); + str = str.Replace("&", ""); + str = str.Replace("#", ""); + str = str.Replace("%", ""); + str = str.Replace("$", ""); + str = str.Replace("\"", ""); + + // 删除与数据库相关的词 + str = Regex.Replace(str, "delete from", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "drop table", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "truncate", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "xp_cmdshell", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "exec master", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "net localgroup administrators", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "net user", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "-", "", RegexOptions.IgnoreCase); + str = Regex.Replace(str, "truncate", "", RegexOptions.IgnoreCase); + return str; + } + + /// + /// 单条插入 + /// + /// + /// + /// + /// (各种类型的)id + public object InsertSingle(string tableName, JObject cols, APIJSON_Role role = null) + { + role ??= _identitySvc.GetRole(); + var dt = new Dictionary(); + + foreach (var f in cols) // 遍历字段 + { + if (//f.Key.ToLower() != "id" && //是否一定要传id + IsCol(tableName, f.Key) && + (role.Insert.Column.Contains("*") || role.Insert.Column.Contains(f.Key, StringComparer.CurrentCultureIgnoreCase))) + dt.Add(f.Key, FuncList.TransJObjectToSugarPara(f.Value)); + } + // 如果外部没传Id,就后端生成或使用数据库默认值,如果都没有会出错 + object id; + if (!dt.ContainsKey("id")) + { + id = YitIdHelper.NextId();//自己生成id的方法,可以由外部传入 + dt.Add("id", id); + } + else + { + id = dt["id"]; + } + _db.Insertable(dt).AS(tableName).ExecuteCommand();//根据主键类型设置返回雪花或自增,目前返回条数 + + return id; + } + + /// + /// 为每天记录创建udpate sql + /// + /// + /// + /// + /// + public int UpdateSingleRecord(string tableName, JObject record, APIJSON_Role role = null) + { + role ??= _identitySvc.GetRole(); + if (!record.ContainsKey("id")) + throw Oops.Bah("未传主键id"); + + var dt = new Dictionary(); + var sb = new StringBuilder(100); + object id = null; + foreach (var f in record)//遍历每个字段 + { + if (f.Key.Equals("id", StringComparison.OrdinalIgnoreCase)) + { + if (f.Value is JArray) + { + sb.Append($"{f.Key} in (@{f.Key})"); + id = FuncList.TransJArrayToSugarPara(f.Value); + } + else + { + sb.Append($"{f.Key}=@{f.Key}"); + id = FuncList.TransJObjectToSugarPara(f.Value); + } + } + else if (IsCol(tableName, f.Key) && (role.Update.Column.Contains("*") || role.Update.Column.Contains(f.Key, StringComparer.CurrentCultureIgnoreCase))) + { + dt.Add(f.Key, FuncList.TransJObjectToSugarPara(f.Value)); + } + } + string whereSql = sb.ToString(); + int count = _db.Updateable(dt).AS(tableName).Where(whereSql, new { id }).ExecuteCommand(); + return count; + } + + /// + /// 更新单表,支持同表多条记录 + /// + /// + /// + /// + /// + public int UpdateSingleTable(string tableName, JToken records, APIJSON_Role role = null) + { + role ??= _identitySvc.GetRole(); + int count = 0; + if (records is JArray) + { + foreach (var record in records.ToObject()) + { + count += UpdateSingleRecord(tableName, record, role); + } + } + else + { + count = UpdateSingleRecord(tableName, records.ToObject(), role); + } + return count; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/TableMapper.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/TableMapper.cs new file mode 100644 index 0000000..1cbb5ff --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/TableMapper.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 表名映射 +/// +public class TableMapper : ITransient +{ + private readonly Dictionary _options = new(StringComparer.OrdinalIgnoreCase); + + public TableMapper(IOptions> options) + { + foreach (var item in options.Value) + { + _options.Add(item.Key, item.Value); + } + } + + /// + /// 获取表别名 + /// + /// + /// + public string GetTableName(string oldname) + { + return _options.ContainsKey(oldname) ? _options[oldname] : oldname; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/用例APIFOX.json b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/用例APIFOX.json new file mode 100644 index 0000000..5b48a4e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/APIJSON/用例APIFOX.json @@ -0,0 +1,23120 @@ +{ + "apifoxProject": "1.0.0", + "$schema": { + "app": "apifox", + "type": "project", + "version": "1.2.0" + }, + "info": { + "name": "Admin.Net", + "description": "", + "mockRule": { + "rules": [ + ], + "enableSystemRule": true + } + }, + "apiCollection": [ + { + "name": "根目录", + "id": 29711526, + "auth": { + }, + "parentId": 0, + "serverId": "", + "description": "", + "identityPattern": { + "httpApi": { + "type": "methodAndPath", + "bodyType": "" + } + }, + "preProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "postProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + }, + "items": [ + { + "name": "aPIJSON", + "id": 29711668, + "auth": { + }, + "parentId": 0, + "serverId": "", + "description": "说明文档:https://github.com/Tencent/APIJSON/blob/master/Document.md#3.1", + "identityPattern": { + "httpApi": { + "type": "inherit", + "bodyType": "" + } + }, + "preProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "postProcessors": [ + { + "id": "inheritProcessors", + "type": "inheritProcessors", + "data": { + } + } + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + }, + "items": [ + { + "name": "统一查询入口", + "api": { + "id": "151219333", + "method": "post", + "path": "/api/aPIJSON/get", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "405647728", + "name": "成功", + "code": 200, + "contentType": "json", + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + }, + "result": { + "type": "object", + "properties": { + "[]": { + "type": "array", + "items": { + "type": "object", + "properties": { + }, + "x-apifox-orders": [ + ] + } + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "count": { + "type": "integer", + "description": "每页条数" + }, + "max": { + "type": "integer", + "description": "最大页数" + }, + "total": { + "type": "integer", + "description": "总条数" + } + }, + "x-apifox-orders": [ + "[]", + "page", + "count", + "max", + "total" + ] + }, + "extras": { + "type": "null" + }, + "time": { + "type": "string" + } + }, + "required": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ], + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ], + "properties": { + } + }, + "example": "{\r\n \"table1\": {\r\n \"@column\": \"id\",\r\n \"httpmethod\": \"Get\",\r\n }\r\n}" + }, + "description": "参数:{\"[]\":{\"SYS_LOG_OP\":{}}}", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-Post", + "sourceUrl": "", + "ordering": 0, + "cases": [ + { + "id": 143284761, + "type": "http", + "path": null, + "name": "单条查询", + "responseId": 405647728, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"@column\": \"id,createtime,Actionname,loglevel,httpmethod,RequestParam\",//显示列\r\n //\"Actionname\":\"SwaggerCheckUrl\",\r\n //\"loglevel\":2,\r\n //\"httpmethod!\":[\"POST\",\"GET\"],//! not in\r\n \"createtime{}\": \">=2024-3-1\", //逻辑运算\r\n //\"isdelete\":0, //bool 支持 1、0、true、false \r\n \"RequestParam!\": null //not null\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.decba6c4-b3e2-40af-a30e-8c8d09865bf1\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"decba6c4-b3e2-40af-a30e-8c8d09865bf1\",\"requestIndex\":0,\"httpRequestId\":\"7e107ae6-74d4-44c8-8582-e9bfefb5e611\"},\"type\":\"http\",\"response\":{\"id\":\"7b5fc148-4f73-4e56-b054-26bebe9db57a\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 13 Mar 2024 08:19:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86375\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-14T07:18:51.6721222Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,57,54,51,53,53,48,48,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,51,58,50,51,46,51,56,55,53,57,48,51,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,76,111,103,76,101,118,101,108,34,58,50,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,51,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,51,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,51,32,49,54,58,49,57,58,48,54,34,125]},\"cookie\":[],\"responseTime\":99,\"responseSize\":328,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.698800027370453,\"wait\":0.3564000129699707,\"dns\":0,\"tcp\":0,\"firstByte\":96.46359997987747,\"download\":1.736199975013733,\"process\":0.03470003604888916,\"total\":101.28970003128052}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":157,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,createtime,Actionname,loglevel,httpmethod,RequestParam\\\",\\n \\\"createtime{}\\\": \\\">=2024-3-1\\\",\\n \\\"RequestParam!\\\": null\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"157\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 13 Mar 2024 08:19:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86375\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-14T07:18:51.6721222Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710317946491,\"requestStart\":1710317946493,\"offset\":{\"request\":2.698800027370453,\"socket\":3.0552000403404236,\"response\":99.5188000202179,\"end\":101.25499999523163,\"lookup\":3.0552000403404236,\"connect\":3.0552000403404236,\"done\":101.28970003128052}}}]}},\"responseValidation\":{\"schema\":{\"valid\":true,\"message\":\"\",\"errors\":null},\"responseCode\":{\"valid\":true}},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143284761,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"单条查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143272865, + "type": "http", + "path": null, + "name": "列表查询", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {\r\n \"table1\": {\r\n \"@column\": \"id,createtime,httpmethod,RequestUrl,actionname,RequestParam,Elapsed\",//需要显示的列名\r\n //\"httpmethod\": \"POST\",//条件查询\r\n //\"RequestUrl$\": \"%swagger%\", //$模糊查询\r\n //\"@order\": \"RequestParam desc,createtime,actionname desc\", //按最新时间排序:-/desc 均为倒序 \r\n //\"@count\": \"10\", //前n条 很少用到 \r\n //\"RequestParam\":null,//匹配null or '',\r\n \"createtime%\":\"2024-3-5,2024-3-14\",// between 日期过滤\r\n \"Elapsed%\":\"1,20\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.1c48ba22-4372-4ab1-a874-a79449144e5d\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"1c48ba22-4372-4ab1-a874-a79449144e5d\",\"requestIndex\":0,\"httpRequestId\":\"70f19533-b333-460b-beeb-2e5ecbc5298c\"},\"type\":\"http\",\"response\":{\"id\":\"0f169b35-6729-4512-956d-2081d9fdb91d\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 02:00:47 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T02:00:05.6689995Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,51,48,49,56,48,54,55,57,50,51,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,53,32,49,54,58,50,48,58,52,53,46,50,57,56,49,57,49,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,50,54,50,54,48,51,48,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,52,55,58,52,53,46,51,49,51,48,53,50,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,50,57,56,51,50,55,55,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,50,58,48,55,46,57,50,57,50,54,52,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,54,51,52,53,55,56,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,52,58,49,57,46,50,55,55,52,54,53,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,51,56,54,55,57,55,56,57,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,48,57,58,53,53,58,53,48,46,52,52,57,55,52,57,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,54,57,50,57,50,49,52,50,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,49,58,48,57,46,50,54,51,55,53,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,48,50,52,48,55,57,57,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,51,55,58,49,57,46,56,51,50,49,53,50,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,51,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,49,56,56,53,57,57,49,48,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,48,58,52,56,58,48,50,46,52,56,53,55,57,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,83,89,83,76,79,71,79,80,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,66,121,84,97,98,108,101,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,92,34,83,89,83,76,79,71,79,80,92,34,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,52,57,57,55,57,52,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,49,58,51,52,46,49,50,55,53,49,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,49,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,53,52,49,49,53,57,48,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,49,58,53,48,46,50,56,53,52,56,54,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,44,92,34,113,117,101,114,121,92,34,58,48,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,51,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,53,55,51,56,52,48,48,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,50,58,48,51,46,48,53,49,50,55,54,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,55,53,54,54,57,51,57,57,55,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,49,50,58,52,48,46,51,55,57,52,48,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,50,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,52,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,52,56,49,56,50,52,56,53,51,49,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,49,58,53,50,58,52,52,46,56,53,51,53,57,52,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,50,57,49,51,50,52,55,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,48,58,48,50,46,53,48,55,49,56,48,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,50,50,50,57,55,49,53,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,56,58,51,52,46,49,49,56,52,57,56,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,51,48,57,53,54,56,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,49,57,58,48,55,46,57,52,53,57,49,56,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,48,52,52,56,49,56,55,55,49,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,50,48,58,49,53,46,50,53,51,55,52,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,113,117,101,114,121,92,34,58,92,34,50,92,34,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,47,91,93,47,116,111,116,97,108,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,48,52,49,52,53,53,52,50,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,52,58,53,56,58,53,50,46,55,48,53,55,50,56,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,52,54,54,50,53,57,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,48,54,46,52,56,51,53,49,52,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,45,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,50,48,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,54,51,53,51,50,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,49,51,46,48,56,54,57,57,50,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,43,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,52,55,55,57,53,50,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,49,56,46,55,50,48,51,52,54,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,105,100,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,53,50,55,51,52,50,55,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,51,56,46,48,49,51,48,56,48,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,50,53,53,49,56,53,50,50,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,49,50,58,52,55,46,53,56,55,53,49,52,53,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,51,57,57,55,52,57,55,48,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,50,58,49,50,46,50,57,50,57,51,57,49,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,52,55,48,54,51,51,48,50,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,54,58,52,57,46,49,56,48,51,57,54,54,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,53,48,44,92,34,113,117,101,114,121,92,34,58,50,125,44,92,34,116,111,116,97,108,64,92,34,58,92,34,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,53,49,52,50,48,56,48,54,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,50,57,58,51,57,46,51,57,53,52,54,52,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,53,49,57,52,57,53,50,51,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,48,58,48,48,46,48,52,56,49,51,52,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,91,93,92,34,58,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,64,111,114,100,101,114,92,34,58,92,34,99,114,101,97,116,101,116,105,109,101,45,92,34,125,44,92,34,112,97,103,101,92,34,58,49,44,92,34,99,111,117,110,116,92,34,58,53,48,44,92,34,113,117,101,114,121,92,34,58,50,125,44,92,34,116,111,116,97,108,92,34,58,92,34,92,34,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,54,48,57,52,49,56,48,53,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,53,58,53,49,46,51,48,57,56,56,51,55,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,50,48,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,49,54,52,48,51,49,54,52,56,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,53,58,51,55,58,53,50,46,48,48,54,56,55,49,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,105,100,92,34,58,92,34,51,50,54,53,49,54,48,57,52,49,56,48,53,51,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,48,49,50,52,57,54,48,53,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,52,58,50,51,46,49,53,49,54,53,53,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,50,52,48,55,48,57,56,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,53,58,53,50,46,50,57,55,55,48,50,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,105,100,41,58,115,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,50,54,54,55,48,54,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,48,50,46,52,53,50,52,56,51,56,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,115,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,48,53,51,57,51,51,51,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,49,55,46,53,54,52,54,54,55,51,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,99,111,117,110,116,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,53,48,48,48,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,51,52,46,57,57,48,54,50,51,50,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,230,149,176,233,135,143,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,50,51,57,48,54,52,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,54,58,49,54,58,53,48,46,56,54,53,56,49,49,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,97,112,105,47,97,80,73,74,83,79,78,47,103,101,116,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,99,111,117,110,116,40,49,41,58,230,157,161,230,149,176,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,56,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,50,57,57,51,49,57,51,55,57,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,48,53,58,53,54,46,54,56,51,50,51,48,57,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,110,117,108,108,44,34,69,108,97,112,115,101,100,34,58,49,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,54,54,50,50,48,51,53,55,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,50,51,58,52,51,46,49,57,51,54,55,53,52,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,85,84,34,44,34,82,101,113,117,101,115,116,85,114,108,34,58,34,104,116,116,112,58,47,47,108,111,99,97,108,104,111,115,116,58,53,48,48,53,47,115,119,97,103,103,101,114,47,99,104,101,99,107,85,114,108,34,44,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,82,101,113,117,101,115,116,80,97,114,97,109,34,58,34,123,92,34,83,89,83,76,79,71,79,80,92,34,58,123,92,34,64,99,111,108,117,109,110,92,34,58,92,34,105,100,44,99,114,101,97,116,101,116,105,109,101,92,34,44,92,34,105,100,92,34,58,92,34,51,50,54,53,49,54,48,57,52,49,56,48,53,51,92,34,125,125,34,44,34,69,108,97,112,115,101,100,34,58,49,54,125,125,93,44,34,116,111,116,97,108,34,58,51,55,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,52,32,49,48,58,48,48,58,52,55,34,125]},\"cookie\":[],\"responseTime\":137,\"responseSize\":9324,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.5074999928474426,\"wait\":0.32520002126693726,\"dns\":0,\"tcp\":0,\"firstByte\":135.28540003299713,\"download\":1.2343999743461609,\"process\":0.05519998073577881,\"total\":138.40770000219345}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":195,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,createtime,httpmethod,RequestUrl,actionname,RequestParam,Elapsed\\\",\\n \\\"createtime%\\\": \\\"2024-3-5,2024-3-14\\\",\\n \\\"Elapsed%\\\": \\\"1,20\\\"\\n }\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"195\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 02:00:47 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T02:00:05.6689995Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710381647691,\"requestStart\":1710381647692,\"offset\":{\"request\":1.5074999928474426,\"socket\":1.8327000141143799,\"response\":137.1181000471115,\"end\":138.35250002145767,\"lookup\":1.8327000141143799,\"connect\":1.8327000141143799,\"done\":138.40770000219345}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143272865,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"列表查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143191882, + "type": "http", + "path": null, + "name": "分页查询", + "responseId": 405647728, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {//列表\r\n \"SYSLOGOP\": {//表名\r\n \"@column\": \"id,createtime\",//只查询两列\r\n \"@order\":\"createtime-\" //-倒序排序\r\n },\r\n \"page\": 1, //分页查询:第1页\r\n \"count\": 3, //每页3条\r\n //\"query\": 1 //查询内容: 0-对象,1-总数,2-数据、总数,默认为2\r\n },\r\n //\"total\": \"\", //总数 默认返回总数,不用传\r\n \r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.bcfe838e-0335-42f2-ae40-05fb07285447\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"bcfe838e-0335-42f2-ae40-05fb07285447\",\"requestIndex\":0,\"httpRequestId\":\"74d59ab7-76bd-4c77-9bcf-714ff5eb470e\"},\"type\":\"http\",\"response\":{\"id\":\"673bc01a-a4df-4514-b2e9-b13172c5eca8\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 08:29:32 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T08:27:50.0233019Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,52,51,50,52,49,53,52,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,57,58,50,55,46,56,48,55,57,53,49,52,34,125,125,44,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,49,56,51,48,53,56,54,49,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,55,58,53,48,46,52,48,50,49,56,34,125,125,44,123,34,83,89,83,76,79,71,79,80,34,58,123,34,73,100,34,58,51,50,55,56,53,49,49,53,55,51,54,51,56,57,44,34,67,114,101,97,116,101,84,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,55,58,52,48,46,51,54,53,57,51,48,54,34,125,125,93,44,34,112,97,103,101,34,58,49,44,34,99,111,117,110,116,34,58,51,44,34,109,97,120,34,58,49,50,49,44,34,116,111,116,97,108,34,58,51,54,49,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,54,58,50,57,58,51,51,34,125]},\"cookie\":[],\"responseTime\":72,\"responseSize\":376,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.7229999899864197,\"wait\":0.46160000562667847,\"dns\":0,\"tcp\":0,\"firstByte\":69.22839999198914,\"download\":1.886900007724762,\"process\":0.04899996519088745,\"total\":73.34889996051788}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":136,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"SYSLOGOP\\\": {\\n \\\"@column\\\": \\\"id,createtime\\\",\\n \\\"@order\\\": \\\"createtime-\\\"\\n },\\n \\\"page\\\": 1,\\n \\\"count\\\": 3\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"136\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 08:29:32 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T08:27:50.0233019Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710232173277,\"requestStart\":1710232173279,\"offset\":{\"request\":1.7229999899864197,\"socket\":2.184599995613098,\"response\":71.41299998760223,\"end\":73.299899995327,\"lookup\":2.184599995613098,\"connect\":2.184599995613098,\"done\":73.34889996051788}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143191882,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"分页查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143312444, + "type": "http", + "path": null, + "name": "查询数量(聚合函数)", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"SYSLOGOP\": {\r\n \"@column\": \"count(1):数量,sum(id):合计\",//支持聚合函数count、sum等, :后跟别名\r\n \"httpmethod\":\"GET\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.8cdec2eb-2a6e-4b14-b841-d45980eb6969\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"8cdec2eb-2a6e-4b14-b841-d45980eb6969\",\"requestIndex\":0,\"httpRequestId\":\"b833e53d-ca56-464c-a0f3-bf293186f3c0\"},\"type\":\"http\",\"response\":{\"id\":\"c4a883ba-8b68-4bcf-b155-5300deb9b77e\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 03:49:08 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86393\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T03:46:44.5267221Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,83,89,83,76,79,71,79,80,34,58,123,34,230,149,176,233,135,143,34,58,54,44,34,229,144,136,232,174,161,34,58,49,57,53,56,55,56,50,53,54,53,53,56,55,53,48,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,50,32,49,49,58,52,57,58,48,56,34,125]},\"cookie\":[],\"responseTime\":57,\"responseSize\":145,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.178399980068207,\"wait\":0.46469998359680176,\"dns\":0,\"tcp\":0,\"firstByte\":54.569000005722046,\"download\":1.8371000289916992,\"process\":0.08829998970031738,\"total\":59.13749998807907}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":96,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"SYSLOGOP\\\": {\\n \\\"@column\\\": \\\"count(1):数量,sum(id):合计\\\",\\n \\\"httpmethod\\\": \\\"GET\\\"\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"96\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Tue, 12 Mar 2024 03:49:08 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86393\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-13T03:46:44.5267221Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710215348896,\"requestStart\":1710215348898,\"offset\":{\"request\":2.178399980068207,\"socket\":2.6430999636650085,\"response\":57.212099969387054,\"end\":59.049199998378754,\"lookup\":2.6430999636650085,\"connect\":2.6430999636650085,\"done\":59.13749998807907}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143312444,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"查询数量(聚合函数)\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143871046, + "type": "http", + "path": null, + "name": "关联查询", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "//用于子表补充数据,没有实现join\r\n{\r\n \"[]\": {\r\n //必须先查子表,再循环补充主表数据\r\n \"table3\": {\r\n //\"@column\": \"id\",//必须在此返回后面的关联列\r\n \"text\": \"update3\",\r\n \"@order\": \"id-\", //只能在第一个表排序\r\n },\r\n \"table2\": {\r\n \"id@\": \"/table3/table2id\",\r\n //\"@order\": \"id-\", //这里排序无效\r\n },\r\n \"table1\": {\r\n \"@column\": \"id,httpmethod\",\r\n \"id@\": \"/table2/table1id\",\r\n },\r\n \"table4\": {\r\n \"id@\": \"/table3/table4id\",\r\n },\r\n \"page\": 1,\r\n \"count\": 3\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.59b633e1-8099-48e6-9943-d0e27e50cae7\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"59b633e1-8099-48e6-9943-d0e27e50cae7\",\"requestIndex\":0,\"httpRequestId\":\"a04d8d6c-aad7-4eaa-8fe4-11d3edaf05e7\"},\"type\":\"http\",\"response\":{\"id\":\"1a94a35f-3d89-44ef-8923-0f9c5d54e59d\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 03:53:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T03:53:27.0136472Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,51,51,44,34,116,97,98,108,101,50,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,52,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,68,69,76,69,84,69,34,125,44,34,116,97,98,108,101,52,34,58,123,34,105,100,34,58,52,52,44,34,116,101,120,116,34,58,34,52,52,34,125,125,44,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,51,44,34,116,97,98,108,101,50,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,51,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,51,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,54,56,53,48,55,53,55,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,68,69,76,69,84,69,34,125,125,44,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,50,44,34,116,97,98,108,101,50,105,100,34,58,50,50,44,34,116,101,120,116,34,58,34,117,112,100,97,116,101,51,34,44,34,116,97,98,108,101,52,105,100,34,58,52,50,125,44,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,50,50,44,34,116,101,120,116,34,58,34,110,101,119,103,101,116,34,44,34,116,97,98,108,101,49,105,100,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,44,34,105,115,68,101,108,101,116,101,34,58,48,125,44,34,116,97,98,108,101,49,34,58,123,34,73,100,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,71,69,84,34,125,44,34,116,97,98,108,101,52,34,58,123,34,105,100,34,58,52,50,44,34,116,101,120,116,34,58,34,52,50,34,125,125,93,44,34,112,97,103,101,34,58,49,44,34,99,111,117,110,116,34,58,51,44,34,109,97,120,34,58,50,44,34,116,111,116,97,108,34,58,52,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,52,32,49,49,58,53,51,58,50,55,34,125]},\"cookie\":[],\"responseTime\":590,\"responseSize\":779,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.1902999877929688,\"wait\":1.2958000302314758,\"dns\":0.8892999887466431,\"tcp\":0.6733999848365784,\"firstByte\":584.8849999904633,\"download\":1.787600040435791,\"process\":0.033399999141693115,\"total\":591.7548000216484}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":316,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"[]\\\": {\\n \\\"table3\\\": {\\n \\\"text\\\": \\\"update3\\\",\\n \\\"@order\\\": \\\"id-\\\"\\n },\\n \\\"table2\\\": {\\n \\\"id@\\\": \\\"/table3/table2id\\\"\\n },\\n \\\"table1\\\": {\\n \\\"@column\\\": \\\"id,httpmethod\\\",\\n \\\"id@\\\": \\\"/table2/table1id\\\"\\n },\\n \\\"table4\\\": {\\n \\\"id@\\\": \\\"/table3/table4id\\\"\\n },\\n \\\"page\\\": 1,\\n \\\"count\\\": 3\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"316\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 14 Mar 2024 03:53:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.45\"},{\"key\":\"Admin.NET\",\"value\":\"Admin.NET\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-15T03:53:27.0136472Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710388406828,\"requestStart\":1710388406830,\"offset\":{\"request\":2.1902999877929688,\"socket\":3.4861000180244446,\"lookup\":4.375400006771088,\"connect\":5.048799991607666,\"response\":589.9337999820709,\"end\":591.7214000225067,\"done\":591.7548000216484}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143871046,\"httpApiName\":\"统一查询入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"关联查询\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143975340, + "type": "http", + "path": null, + "name": "group by", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"[]\": {\r\n \"table1\": {\r\n \"@column\": \"actionname,httpmethod,max(id)\",\r\n \"@group\": \"actionname,httpmethod\",\r\n },\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.00314312-a9fe-4ad2-859c-b7555f9a0d3e\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"00314312-a9fe-4ad2-859c-b7555f9a0d3e\",\"requestIndex\":0,\"httpRequestId\":\"e6ba19a4-e4f9-4412-9b45-f48e81bb7277\"},\"type\":\"http\",\"response\":{\"id\":\"6134deb2-55f4-419b-9387-38ae669f137f\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Fri, 08 Mar 2024 08:23:12 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-09T08:14:42.5201051Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,91,93,34,58,91,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,80,111,115,116,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,53,50,49,51,57,56,52,53,52,51,52,49,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,50,51,56,56,48,48,57,54,53,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,81,117,101,114,121,66,121,84,97,98,108,101,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,52,55,52,52,57,50,50,54,51,48,57,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,82,101,109,111,118,101,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,49,53,48,50,48,55,53,53,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,51,50,54,54,50,50,48,51,53,55,125,125,44,123,34,116,97,98,108,101,49,34,58,123,34,65,99,116,105,111,110,78,97,109,101,34,58,34,83,119,97,103,103,101,114,83,117,98,109,105,116,85,114,108,34,44,34,72,116,116,112,77,101,116,104,111,100,34,58,34,80,79,83,84,34,44,34,109,97,120,40,105,100,41,34,58,51,50,54,53,48,52,50,57,50,50,50,50,49,51,125,125,93,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,56,32,49,54,58,50,51,58,49,51,34,125]},\"cookie\":[],\"responseTime\":8399,\"responseSize\":605,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.243699997663498,\"wait\":0.2954000011086464,\"dns\":0,\"tcp\":0,\"firstByte\":8396.081299997866,\"download\":1.9274000003933907,\"process\":0.052000001072883606,\"total\":8400.599799998105}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"get\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":162,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"[]\\\": {\\r\\n \\\"table1\\\": {\\r\\n \\\"@column\\\": \\\"actionname,httpmethod,max(id)\\\",\\r\\n \\\"@group\\\": \\\"actionname,httpmethod\\\",\\r\\n },\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/get\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"162\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Fri, 08 Mar 2024 08:23:12 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-09T08:14:42.5201051Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709886184664,\"requestStart\":1709886184667,\"offset\":{\"request\":2.243699997663498,\"socket\":2.5390999987721443,\"response\":8398.620399996638,\"end\":8400.547799997032,\"lookup\":2.5390999987721443,\"connect\":2.5390999987721443,\"done\":8400.599799998105}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":151219333,\"httpApiCaseId\":143975340,\"httpApiName\":\"统一入口\",\"httpApiPath\":\"/api/aPIJSON/get\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"group by\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.405647728" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "新增", + "api": { + "id": "152848473", + "method": "post", + "path": "/api/aPIJSON/post", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "408753512", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "{\r\n \"table2\": \r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n }\r\n\r\n}" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-post-Post", + "sourceUrl": "", + "ordering": 12, + "cases": [ + { + "id": 144371019, + "type": "http", + "path": null, + "name": "单条", + "responseId": 0, + "parameters": { + "query": [ + ], + "header": [ + ], + "cookie": [ + ], + "path": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table2\": \r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n }\r\n\r\n}", + "type": "application/json" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.04906a0f-2a35-491b-8335-13c224a6e496\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"04906a0f-2a35-491b-8335-13c224a6e496\",\"requestIndex\":0,\"httpRequestId\":\"8b54192a-17b5-4a20-b7c0-115533d436f3\"},\"type\":\"http\",\"response\":{\"id\":\"000965cc-02df-4aae-8b23-18368ab61ecb\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 06:58:40 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86395\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T06:48:49.8348531Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,50,34,58,91,51,50,55,54,49,54,51,48,53,50,52,57,57,55,44,50,51,56,93,44,34,116,97,98,108,101,51,34,58,51,50,55,54,49,54,51,48,53,50,56,48,54,57,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,52,58,53,56,58,52,49,34,125]},\"cookie\":[],\"responseTime\":118,\"responseSize\":150,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.154699981212616,\"wait\":0.394599974155426,\"dns\":0,\"tcp\":0,\"firstByte\":114.57359999418259,\"download\":2.3081000447273254,\"process\":0.08730000257492065,\"total\":119.51829999685287}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"post\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":171,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table2\\\": [\\n {\\n \\\"text\\\": \\\"newget\\\"\\n },\\n {\\n \\\"id\\\": 238,\\n \\\"text\\\": \\\"newget2\\\"\\n }\\n ],\\n \\\"table3\\\": {\\n \\\"table2id\\\": 238,\\n \\\"text\\\": \\\"newget\\\"\\n }\\n}\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/post\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"171\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 06:58:40 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86395\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T06:48:49.8348531Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710140321198,\"requestStart\":1710140321200,\"offset\":{\"request\":2.154699981212616,\"socket\":2.549299955368042,\"response\":117.12289994955063,\"end\":119.43099999427795,\"lookup\":2.549299955368042,\"connect\":2.549299955368042,\"done\":119.51829999685287}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848473,\"httpApiName\":\"新增\",\"httpApiPath\":\"/api/aPIJSON/post\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"成功\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 144370602, + "type": "http", + "path": null, + "name": "多表批量", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table2\": [\r\n {\r\n //\"id\": 236, //如果传id,就用前端id,如果没有,就后端生成\r\n \"text\": \"newget\"\r\n },\r\n {\r\n \"id\": 240,\r\n \"text\": \"newget2\"\r\n }\r\n ],\r\n \"table3\":{\r\n \"table2id\":240,\r\n \"text\":\"newget\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.89a3b724-e244-47bd-8332-b18d13b87f39\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"89a3b724-e244-47bd-8332-b18d13b87f39\",\"requestIndex\":0,\"httpRequestId\":\"c4ff5552-4d6b-4816-bac7-951f9acbd466\"},\"type\":\"http\",\"response\":{\"id\":\"a2e99ccb-e877-4151-9d9e-157c49c50e65\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:32:24 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:30:34.5282454Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,50,34,58,123,34,105,100,34,58,91,51,50,55,54,50,49,52,56,54,49,48,54,50,57,44,50,52,48,93,44,34,99,111,117,110,116,34,58,50,125,44,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,51,50,55,54,50,49,52,56,54,49,55,53,52,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,53,58,51,50,58,50,53,34,125]},\"cookie\":[],\"responseTime\":168,\"responseSize\":174,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.51419997215271,\"wait\":0.2290000319480896,\"dns\":0,\"tcp\":0,\"firstByte\":165.6187999844551,\"download\":1.2360000014305115,\"process\":0.05760002136230469,\"total\":168.65560001134872}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"post\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":171,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\n \\\"table2\\\": [\\n {\\n \\\"text\\\": \\\"newget\\\"\\n },\\n {\\n \\\"id\\\": 240,\\n \\\"text\\\": \\\"newget2\\\"\\n }\\n ],\\n \\\"table3\\\": {\\n \\\"table2id\\\": 240,\\n \\\"text\\\": \\\"newget\\\"\\n }\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/post\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"171\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:32:24 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:30:34.5282454Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710142344933,\"requestStart\":1710142344934,\"offset\":{\"request\":1.51419997215271,\"socket\":1.7432000041007996,\"response\":167.3619999885559,\"end\":168.59799998998642,\"lookup\":1.7432000041007996,\"connect\":1.7432000041007996,\"done\":168.65560001134872}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848473,\"httpApiCaseId\":144370602,\"httpApiName\":\"新增\",\"httpApiPath\":\"/api/aPIJSON/post\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"多表批量\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.408753512" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "修改", + "api": { + "id": "152848474", + "method": "post", + "path": "/api/aPIJSON/put", + "parameters": { + "path": [ + ], + "query": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + }, + "responses": [ + { + "id": "408753513", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-put-Post", + "sourceUrl": "", + "ordering": 18, + "cases": [ + { + "id": 144398604, + "type": "http", + "path": null, + "name": "通过id单条更新", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": {\r\n \"id\": 32762120351813,\r\n \"text\": \"edit\",\r\n \"table2id\":255\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.3c2d26df-ff36-4408-84d1-ba86a03a15d9\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"3c2d26df-ff36-4408-84d1-ba86a03a15d9\",\"requestIndex\":0,\"httpRequestId\":\"01efc573-4df9-4ee4-8f19-329b479d8f83\"},\"type\":\"http\",\"response\":{\"id\":\"c0132dd1-a39d-491a-a497-7a91230bfbc7\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 08:24:50 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T08:24:51.3365034Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,99,111,100,101,34,58,50,48,48,44,34,109,115,103,34,58,34,115,117,99,99,101,115,115,34,44,34,105,100,34,58,34,51,50,55,54,50,49,50,48,51,53,49,56,49,51,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,54,58,50,52,58,53,49,34,125]},\"cookie\":[],\"responseTime\":626,\"responseSize\":156,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.3127999901771545,\"wait\":1.4280999898910522,\"dns\":0.35510003566741943,\"tcp\":0.7379999756813049,\"firstByte\":621.6582000255585,\"download\":1.3226999640464783,\"process\":0.029600024223327637,\"total\":627.8445000052452}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"put\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":108,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": {\\r\\n \\\"id\\\": 32762120351813,\\r\\n \\\"text\\\": \\\"edit\\\",\\r\\n \\\"table2id\\\":255\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/put\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"108\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 08:24:50 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T08:24:51.3365034Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710145491156,\"requestStart\":1710145491158,\"offset\":{\"request\":2.3127999901771545,\"socket\":3.740899980068207,\"lookup\":4.096000015735626,\"connect\":4.833999991416931,\"response\":626.4922000169754,\"end\":627.8148999810219,\"done\":627.8445000052452}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848474,\"httpApiCaseId\":144398604,\"httpApiName\":\"修改\",\"httpApiPath\":\"/api/aPIJSON/put\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"单条更新\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 144399593, + "type": "http", + "path": null, + "name": "多表多id批量更新", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": [\r\n {\r\n \"id\": [31,32,33],\r\n \"text\": \"update3\"\r\n },\r\n {\r\n \"id\": 32762148617541,\r\n \"text\": \"update2\"\r\n }\r\n ],\r\n \"table2\": {\r\n \"id\": 32761658134341,\r\n \"text\": \"update\"\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.a347f44c-82a5-437f-afef-bef75f5f55f4\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"a347f44c-82a5-437f-afef-bef75f5f55f4\",\"requestIndex\":0,\"httpRequestId\":\"1ee88481-8f7b-480f-adf0-3a74acff8426\"},\"type\":\"http\",\"response\":{\"id\":\"6d9b1b3d-167c-4fe8-832e-f336630e6e64\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 09:38:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T09:36:21.4486506Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,99,111,117,110,116,34,58,52,125,44,34,116,97,98,108,101,50,34,58,123,34,99,111,117,110,116,34,58,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,55,58,51,56,58,48,53,34,125]},\"cookie\":[],\"responseTime\":135,\"responseSize\":138,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.440500020980835,\"wait\":0.2959999442100525,\"dns\":0,\"tcp\":0,\"firstByte\":132.63910001516342,\"download\":1.2211000323295593,\"process\":0.04819995164871216,\"total\":135.64489996433258}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"put\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":283,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": [\\r\\n {\\r\\n \\\"id\\\": [31,32,33],\\r\\n \\\"text\\\": \\\"update3\\\"\\r\\n },\\r\\n {\\r\\n \\\"id\\\": 32762148617541,\\r\\n \\\"text\\\": \\\"update2\\\"\\r\\n }\\r\\n ],\\r\\n \\\"table2\\\": {\\r\\n \\\"id\\\": 32761658134341,\\r\\n \\\"text\\\": \\\"update\\\"\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/put\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"283\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 09:38:05 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86397\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T09:36:21.4486506Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710149885351,\"requestStart\":1710149885352,\"offset\":{\"request\":1.440500020980835,\"socket\":1.7364999651908875,\"response\":134.3755999803543,\"end\":135.59670001268387,\"lookup\":1.7364999651908875,\"connect\":1.7364999651908875,\"done\":135.64489996433258}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848474,\"httpApiCaseId\":144399593,\"httpApiName\":\"修改\",\"httpApiPath\":\"/api/aPIJSON/put\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"多表更新\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + }, + { + "name": "删除", + "api": { + "id": "152848475", + "method": "post", + "path": "/api/aPIJSON/delete", + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "auth": { + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "responses": [ + { + "id": "408753514", + "name": "Success", + "code": 200, + "contentType": "json", + "jsonSchema": { + "$ref": "#/definitions/84275190" + } + } + ], + "responseExamples": [ + ], + "requestBody": { + "type": "application/json", + "parameters": [ + ], + "jsonSchema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "x-apifox-orders": [ + ] + }, + "example": "{\r\n \"table1\": {\r\n \"id\": [32520112744261,32520179315781,32520200693573],\r\n }\r\n}" + }, + "description": "", + "tags": [ + "aPIJSON" + ], + "status": "released", + "serverId": "", + "operationId": "api-aPIJSON-delete-Post", + "sourceUrl": "", + "ordering": 24, + "cases": [ + { + "id": 143343716, + "type": "http", + "path": null, + "name": "删除单条数据", + "responseId": 0, + "parameters": { + "query": [ + ], + "header": [ + ], + "cookie": [ + ], + "path": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"id\": \"32520494044741\",\r\n }\r\n}", + "type": "application/json" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.26698a36-85d0-4cbb-b880-abbb4872dddf\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"26698a36-85d0-4cbb-b880-abbb4872dddf\",\"requestIndex\":0,\"httpRequestId\":\"e91c2ef7-03a4-41fa-b114-138973559bec\"},\"type\":\"http\",\"response\":{\"id\":\"a6999ac7-bec5-4b65-a905-42dbe74794e7\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 06 Mar 2024 09:16:09 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-07T09:15:13.4609976Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,99,111,100,101,34,58,50,48,48,44,34,109,115,103,34,58,34,115,117,99,99,101,115,115,34,44,34,105,100,34,58,34,51,50,53,50,48,52,57,52,48,52,52,55,52,49,34,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,54,32,49,55,58,49,54,58,48,57,34,125]},\"cookie\":[],\"responseTime\":111,\"responseSize\":156,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":1.5057999789714813,\"wait\":0.2891000211238861,\"dns\":0,\"tcp\":0,\"firstByte\":109.1957999765873,\"download\":1.360700011253357,\"process\":0.034900009632110596,\"total\":112.38629999756813}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":61,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table1\\\": {\\r\\n \\\"id\\\": \\\"32520494044741\\\",\\r\\n }\\r\\n}\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTE5NzE1MiwibmJmIjoxNzA5MTk3MTUyLCJleHAiOjE3MDk4MDE5NTIsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.96Y2rBegXqiYjuXcXeVyuXV75Cmzu8FpILylDcougt8\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"61\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Wed, 06 Mar 2024 09:16:09 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86398\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-07T09:15:13.4609976Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709716569879,\"requestStart\":1709716569880,\"offset\":{\"request\":1.5057999789714813,\"socket\":1.7949000000953674,\"response\":110.99069997668266,\"end\":112.35139998793602,\"lookup\":1.7949000000953674,\"connect\":1.7949000000953674,\"done\":112.38629999756813}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"成功\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143368262, + "type": "http", + "path": null, + "name": "批量删除id", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table3\": {\r\n \"id\": [\r\n 32761630528069,\r\n 239\r\n ]\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.c39182fc-fc2f-4357-b3b7-32e41ef1fb57\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"c39182fc-fc2f-4357-b3b7-32e41ef1fb57\",\"requestIndex\":0,\"httpRequestId\":\"7443b4a1-f762-4a24-8dd2-ad33ce1c8da7\"},\"type\":\"http\",\"response\":{\"id\":\"43956c21-adae-41c7-a722-c856c7c7c0aa\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:34:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86396\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:26:16.5966009Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,51,34,58,123,34,105,100,34,58,91,51,50,55,54,49,54,51,48,53,50,56,48,54,57,44,50,51,57,93,44,34,99,111,117,110,116,34,58,49,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,49,49,32,49,53,58,51,52,58,50,55,34,125]},\"cookie\":[],\"responseTime\":158,\"responseSize\":143,\"type\":\"http\",\"tempFilePath\":\"\",\"timingPhases\":{\"prepare\":2.1498000025749207,\"wait\":0.36399996280670166,\"dns\":0,\"tcp\":0,\"firstByte\":156.0023000240326,\"download\":1.451200008392334,\"process\":0.04890000820159912,\"total\":160.01620000600815}},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":55,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table3\\\": {\\r\\n\\\"id\\\":[32761630528069,239]\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"55\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Mon, 11 Mar 2024 07:34:26 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86396\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-12T07:26:16.5966009Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1710142466906,\"requestStart\":1710142466908,\"offset\":{\"request\":2.1498000025749207,\"socket\":2.5137999653816223,\"response\":158.51609998941422,\"end\":159.96729999780655,\"lookup\":2.5137999653816223,\"connect\":2.5137999653816223,\"done\":160.01620000600815}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiCaseId\":143368262,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"批量删除id\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + }, + { + "id": 143543753, + "type": "http", + "path": null, + "name": "批量删除(匹配条件)", + "responseId": 0, + "parameters": { + "query": [ + ], + "path": [ + ], + "cookie": [ + ], + "header": [ + ] + }, + "commonParameters": { + "query": [ + ], + "body": [ + ], + "header": [ + ], + "cookie": [ + ] + }, + "requestBody": { + "parameters": [ + ], + "data": "{\r\n \"table1\": {\r\n \"httpmethod\": [\"GET\",\"POST\"],\r\n \"ThreadId\": [37,39],\"Actionname\":\"SwaggerCheckUrl\",\"loglevel\":2.5,\"isdelete\":false\r\n }\r\n}", + "generateMode": "normal" + }, + "auth": { + }, + "advancedSettings": { + "disabledSystemHeaders": { + }, + "isDefaultUrlEncoding": 2, + "disableUrlEncoding": false + }, + "requestResult": "{\"id\":\"temp.44a9ebb3-1e3d-4049-936a-a412fb468f00\",\"cursor\":{\"position\":0,\"iteration\":0,\"length\":1,\"cycles\":1,\"empty\":false,\"eof\":false,\"bof\":true,\"cr\":false,\"ref\":\"44a9ebb3-1e3d-4049-936a-a412fb468f00\",\"requestIndex\":0,\"httpRequestId\":\"2365b41d-b930-4f24-85d5-12b1198e2c4d\"},\"type\":\"http\",\"response\":{\"id\":\"097a32a0-1cbe-410f-979f-e1170b268276\",\"status\":\"OK\",\"code\":200,\"header\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 07 Mar 2024 10:10:46 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-08T10:10:47.0961001Z\"}],\"trailer\":[],\"stream\":{\"type\":\"Buffer\",\"data\":[123,34,99,111,100,101,34,58,50,48,48,44,34,116,121,112,101,34,58,34,115,117,99,99,101,115,115,34,44,34,109,101,115,115,97,103,101,34,58,34,34,44,34,114,101,115,117,108,116,34,58,123,34,116,97,98,108,101,49,34,58,123,34,104,116,116,112,109,101,116,104,111,100,34,58,91,34,71,69,84,34,44,34,80,79,83,84,34,93,44,34,84,104,114,101,97,100,73,100,34,58,91,51,55,44,51,57,93,44,34,65,99,116,105,111,110,110,97,109,101,34,58,34,83,119,97,103,103,101,114,67,104,101,99,107,85,114,108,34,44,34,108,111,103,108,101,118,101,108,34,58,50,46,53,44,34,105,115,100,101,108,101,116,101,34,58,102,97,108,115,101,44,34,99,111,117,110,116,34,58,48,125,125,44,34,101,120,116,114,97,115,34,58,110,117,108,108,44,34,116,105,109,101,34,58,34,50,48,50,52,45,48,51,45,48,55,32,49,56,58,49,48,58,52,55,34,125]},\"cookie\":[],\"responseTime\":602,\"responseSize\":227,\"type\":\"http\",\"tempFilePath\":\"\"},\"request\":{\"url\":{\"protocol\":\"http\",\"port\":\"5005\",\"path\":[\"api\",\"aPIJSON\",\"delete\"],\"host\":[\"localhost\"],\"query\":[],\"variable\":[]},\"header\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"system\":true},{\"key\":\"Accept\",\"value\":\"*/*\",\"system\":true},{\"key\":\"Host\",\"value\":\"localhost:5005\",\"system\":true},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\",\"system\":true},{\"key\":\"Connection\",\"value\":\"keep-alive\",\"system\":true},{\"key\":\"Content-Length\",\"value\":159,\"system\":true}],\"method\":\"POST\",\"baseUrl\":\"http://localhost:5005\",\"body\":{\"mode\":\"raw\",\"raw\":\"{\\r\\n \\\"table1\\\": {\\r\\n \\\"httpmethod\\\": [\\\"GET\\\",\\\"POST\\\"],\\r\\n \\\"ThreadId\\\": [37,39],\\\"Actionname\\\":\\\"SwaggerCheckUrl\\\",\\\"loglevel\\\":2.5,\\\"isdelete\\\":false\\r\\n }\\r\\n}\",\"generateMode\":\"normal\",\"type\":\"application/json\"},\"auth\":{\"type\":\"bearer\",\"bearer\":[{\"type\":\"any\",\"value\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\",\"key\":\"token\"}]},\"type\":\"http\"},\"history\":{\"execution\":{\"verbose\":false,\"sessions\":{},\"data\":[{\"request\":{\"method\":\"POST\",\"href\":\"http://localhost:5005/api/aPIJSON/delete\",\"headers\":[{\"key\":\"User-Agent\",\"value\":\"Apifox/1.0.0 (https://apifox.com)\"},{\"key\":\"Content-Type\",\"value\":\"application/json\"},{\"key\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec\"},{\"key\":\"Accept\",\"value\":\"*/*\"},{\"key\":\"Host\",\"value\":\"localhost:5005\"},{\"key\":\"Accept-Encoding\",\"value\":\"gzip, deflate, br\"},{\"key\":\"Connection\",\"value\":\"keep-alive\"},{\"key\":\"Content-Length\",\"value\":\"159\"}],\"httpVersion\":\"1.1\"},\"response\":{\"statusCode\":200,\"headers\":[{\"key\":\"Content-Type\",\"value\":\"application/json; charset=utf-8\"},{\"key\":\"Date\",\"value\":\"Thu, 07 Mar 2024 10:10:46 GMT\"},{\"key\":\"Server\",\"value\":\"Kestrel\"},{\"key\":\"Content-Language\",\"value\":\"zh-CN\"},{\"key\":\"Transfer-Encoding\",\"value\":\"chunked\"},{\"key\":\"environment\",\"value\":\"Development\"},{\"key\":\"Furion\",\"value\":\"4.9.1.32\"},{\"key\":\"X-Rate-Limit-Limit\",\"value\":\"1d\"},{\"key\":\"X-Rate-Limit-Remaining\",\"value\":\"86399\"},{\"key\":\"X-Rate-Limit-Reset\",\"value\":\"2024-03-08T10:10:47.0961001Z\"}],\"httpVersion\":\"1.1\"},\"timings\":{\"start\":1709806246909,\"requestStart\":1709806246912,\"offset\":{\"request\":2.155799984931946,\"socket\":3.6095999479293823,\"lookup\":3.9983999729156494,\"connect\":4.8549999594688416,\"response\":602.0516999959946,\"end\":603.2913999557495,\"done\":603.3172999620438}}}]}},\"responseValidation\":{},\"passed\":true,\"metaInfo\":{\"httpApiId\":152848475,\"httpApiCaseId\":143543753,\"httpApiName\":\"删除\",\"httpApiPath\":\"/api/aPIJSON/delete\",\"httpApiMethod\":\"post\",\"httpApiCaseName\":\"批量删除2\"}}", + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + ], + "mocks": [ + ], + "customApiFields": "{}", + "advancedSettings": { + "disabledSystemHeaders": { + } + }, + "mockScript": { + }, + "codeSamples": [ + ], + "commonResponseStatus": { + }, + "responseChildren": [ + "BLANK.408753514" + ], + "preProcessors": [ + ], + "postProcessors": [ + ], + "inheritPostProcessors": { + }, + "inheritPreProcessors": { + } + } + } + ] + } + ] + } + ], + "socketCollection": [ + ], + "docCollection": [ + ], + "schemaCollection": [ + { + "name": "根目录", + "items": [ + { + "name": "Schemas", + "items": [ + { + "name": "AccountTypeEnum", + "displayName": "", + "id": "#/definitions/84275167", + "description": "账号类型枚举
 会员 Member = 666
 普通账号 NormalUser = 777
 系统管理员 SysAdmin = 888
 超级管理员 SuperAdmin = 999
", + "schema": { + "jsonSchema": { + "enum": [ + 666, + 777, + 888, + 999 + ], + "type": "integer", + "description": "账号类型枚举
 会员 Member = 666
 普通账号 NormalUser = 777
 系统管理员 SysAdmin = 888
 超级管理员 SuperAdmin = 999
", + "format": "int32" + } + } + }, + { + "name": "AddCodeGenInput", + "displayName": "", + "id": "#/definitions/84275168", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "authorName", + "busName", + "generateType", + "menuPid", + "nameSpace", + "tableName" + ], + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + }, + "tableName": { + "minLength": 1, + "type": "string", + "description": "数据库表名" + }, + "busName": { + "minLength": 1, + "type": "string", + "description": "业务名(业务代码包名称)" + }, + "nameSpace": { + "minLength": 1, + "type": "string", + "description": "命名空间" + }, + "authorName": { + "minLength": 1, + "type": "string", + "description": "作者姓名" + }, + "generateType": { + "minLength": 1, + "type": "string", + "description": "生成方式" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "tableComment", + "menuApplication", + "printType", + "printName", + "tableName", + "busName", + "nameSpace", + "authorName", + "generateType", + "menuPid" + ] + } + } + }, + { + "name": "AddConfigInput", + "displayName": "", + "id": "#/definitions/84275169", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "AddDictDataInput", + "displayName": "", + "id": "#/definitions/84275170", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "AddDictTypeInput", + "displayName": "", + "id": "#/definitions/84275171", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "AddJobDetailInput", + "displayName": "", + "id": "#/definitions/84275172", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode", + "jobId" + ] + } + } + }, + { + "name": "AddJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275173", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "minLength": 2, + "type": "string", + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "AddMenuInput", + "displayName": "", + "id": "#/definitions/84275174", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + }, + "title": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children", + "title" + ] + } + } + }, + { + "name": "AddNoticeInput", + "displayName": "", + "id": "#/definitions/84275175", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "AddOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275176", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret", + "bindUserId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "accessKey": { + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "bindTenantId", + "accessKey", + "accessSecret", + "bindUserId" + ] + } + } + }, + { + "name": "AddOrgInput", + "displayName": "", + "id": "#/definitions/84275177", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled", + "name" + ] + } + } + }, + { + "name": "AddPluginInput", + "displayName": "", + "id": "#/definitions/84275178", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "AddPosInput", + "displayName": "", + "id": "#/definitions/84275179", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "remark", + "status", + "name" + ] + } + } + }, + { + "name": "AddPrintInput", + "displayName": "", + "id": "#/definitions/84275180", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "template", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "AddRegionInput", + "displayName": "", + "id": "#/definitions/84275181", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "pid", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children", + "name" + ] + } + } + }, + { + "name": "AddRoleInput", + "displayName": "", + "id": "#/definitions/84275182", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "dataScope", + "remark", + "status", + "name", + "menuIdList" + ] + } + } + }, + { + "name": "AddSubscribeMessageTemplateInput", + "displayName": "", + "id": "#/definitions/84275183", + "description": "增加订阅消息模板", + "schema": { + "jsonSchema": { + "required": [ + "keyworkIdList", + "sceneDescription", + "templateTitleId" + ], + "type": "object", + "properties": { + "templateTitleId": { + "minLength": 1, + "type": "string", + "description": "模板标题Id" + }, + "keyworkIdList": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + }, + "description": "模板关键词列表,例如 [3,5,4]" + }, + "sceneDescription": { + "minLength": 1, + "type": "string", + "description": "服务场景描述,15个字以内" + } + }, + "additionalProperties": false, + "description": "增加订阅消息模板", + "x-apifox-orders": [ + "templateTitleId", + "keyworkIdList", + "sceneDescription" + ] + } + } + }, + { + "name": "AddTenantInput", + "displayName": "", + "id": "#/definitions/84275184", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "adminAccount", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "name": { + "minLength": 2, + "type": "string", + "description": "租户名称" + }, + "adminAccount": { + "minLength": 3, + "type": "string", + "description": "租管账号" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "email", + "phone", + "name", + "adminAccount" + ] + } + } + }, + { + "name": "AddUserInput", + "displayName": "", + "id": "#/definitions/84275185", + "description": "增加用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "realName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "account": { + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "minLength": 1, + "type": "string", + "description": "真实姓名" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + }, + "extOrgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "扩展机构集合" + } + }, + "additionalProperties": false, + "description": "增加用户输入参数", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "account", + "realName", + "roleIdList", + "extOrgIdList" + ] + } + } + }, + { + "name": "AdminResult_Boolean", + "displayName": "", + "id": "#/definitions/84275186", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "boolean", + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_IActionResult", + "displayName": "", + "id": "#/definitions/84275187", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275306" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Int32", + "displayName": "", + "id": "#/definitions/84275188", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "integer", + "description": "数据", + "format": "int32" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Int64", + "displayName": "", + "id": "#/definitions/84275189", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": "integer", + "description": "数据", + "format": "int64" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_JObject", + "displayName": "", + "id": "#/definitions/84275190", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/84275307" + }, + "description": "数据", + "x-apifox-orders": [ + ] + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ApiOutput", + "displayName": "", + "id": "#/definitions/84275191", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275257" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_CodeGenConfig", + "displayName": "", + "id": "#/definitions/84275192", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275261" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ColumnOuput", + "displayName": "", + "id": "#/definitions/84275193", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275263" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_ConstOutput", + "displayName": "", + "id": "#/definitions/84275194", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275264" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DatabaseOutput", + "displayName": "", + "id": "#/definitions/84275195", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275270" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DbColumnOutput", + "displayName": "", + "id": "#/definitions/84275196", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275272" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_DbTableInfo", + "displayName": "", + "id": "#/definitions/84275197", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275274" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_EnumEntity", + "displayName": "", + "id": "#/definitions/84275198", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275301" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_EnumTypeOutput", + "displayName": "", + "id": "#/definitions/84275199", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275302" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_Int64", + "displayName": "", + "id": "#/definitions/84275200", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_MenuOutput", + "displayName": "", + "id": "#/definitions/84275201", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275318" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_RoleOutput", + "displayName": "", + "id": "#/definitions/84275202", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275349" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_String", + "displayName": "", + "id": "#/definitions/84275203", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysConfig", + "displayName": "", + "id": "#/definitions/84275204", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275378" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysDictData", + "displayName": "", + "id": "#/definitions/84275205", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysDictType", + "displayName": "", + "id": "#/definitions/84275206", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275380" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysFile", + "displayName": "", + "id": "#/definitions/84275207", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275381" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysJobCluster", + "displayName": "", + "id": "#/definitions/84275208", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275382" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysJobTrigger", + "displayName": "", + "id": "#/definitions/84275209", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275384" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysMenu", + "displayName": "", + "id": "#/definitions/84275210", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysNotice", + "displayName": "", + "id": "#/definitions/84275211", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275392" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysOrg", + "displayName": "", + "id": "#/definitions/84275212", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysPos", + "displayName": "", + "id": "#/definitions/84275213", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275397" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysRegion", + "displayName": "", + "id": "#/definitions/84275214", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysUser", + "displayName": "", + "id": "#/definitions/84275215", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275401" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_SysUserExtOrg", + "displayName": "", + "id": "#/definitions/84275216", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_List_TableOutput", + "displayName": "", + "id": "#/definitions/84275217", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275405" + }, + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_LoginOutput", + "displayName": "", + "id": "#/definitions/84275218", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275315" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_LoginUserOutput", + "displayName": "", + "id": "#/definitions/84275219", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275317" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_Object", + "displayName": "", + "id": "#/definitions/84275220", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "additionalProperties": false, + "description": "数据", + "type": "null" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SmKeyPairOutput", + "displayName": "", + "id": "#/definitions/84275221", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275352" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275222", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275353" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275223", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275354" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275224", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275355" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysConfig", + "displayName": "", + "id": "#/definitions/84275225", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275356" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysDictData", + "displayName": "", + "id": "#/definitions/84275226", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275357" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysDictType", + "displayName": "", + "id": "#/definitions/84275227", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275358" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysFile", + "displayName": "", + "id": "#/definitions/84275228", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275359" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275229", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275360" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogDiff", + "displayName": "", + "id": "#/definitions/84275230", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275361" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogEx", + "displayName": "", + "id": "#/definitions/84275231", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275362" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogOp", + "displayName": "", + "id": "#/definitions/84275232", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275363" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysLogVis", + "displayName": "", + "id": "#/definitions/84275233", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275364" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysNotice", + "displayName": "", + "id": "#/definitions/84275234", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275365" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275235", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275366" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275236", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275367" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysPlugin", + "displayName": "", + "id": "#/definitions/84275237", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275368" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysPrint", + "displayName": "", + "id": "#/definitions/84275238", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275369" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysRegion", + "displayName": "", + "id": "#/definitions/84275239", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275370" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysRole", + "displayName": "", + "id": "#/definitions/84275240", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275371" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_SysWechatUser", + "displayName": "", + "id": "#/definitions/84275241", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275372" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_TenantOutput", + "displayName": "", + "id": "#/definitions/84275242", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275373" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SqlSugarPagedList_UserOutput", + "displayName": "", + "id": "#/definitions/84275243", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275374" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_String", + "displayName": "", + "id": "#/definitions/84275244", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "type": [ + "string", + "null" + ], + "description": "数据" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275245", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275376" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysCodeGenConfig", + "displayName": "", + "id": "#/definitions/84275246", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275377" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysConfig", + "displayName": "", + "id": "#/definitions/84275247", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275378" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysDictData", + "displayName": "", + "id": "#/definitions/84275248", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275379" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysDictType", + "displayName": "", + "id": "#/definitions/84275249", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275380" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysFile", + "displayName": "", + "id": "#/definitions/84275250", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275381" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysPrint", + "displayName": "", + "id": "#/definitions/84275251", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275398" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysUser", + "displayName": "", + "id": "#/definitions/84275252", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275401" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_SysWechatPay", + "displayName": "", + "id": "#/definitions/84275253", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275403" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WechatPayOutput", + "displayName": "", + "id": "#/definitions/84275254", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275436" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WxOpenIdOutput", + "displayName": "", + "id": "#/definitions/84275255", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275442" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "AdminResult_WxPhoneOutput", + "displayName": "", + "id": "#/definitions/84275256", + "description": "全局返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "description": "状态码", + "format": "int32" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "类型success、warning、error" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "错误信息" + }, + "result": { + "$ref": "#/definitions/84275443" + }, + "extras": { + "additionalProperties": false, + "description": "附加数据", + "type": "null" + }, + "time": { + "type": "string", + "description": "时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "全局返回结果", + "x-apifox-orders": [ + "code", + "type", + "message", + "result", + "extras", + "time" + ] + } + } + }, + { + "name": "ApiOutput", + "displayName": "", + "id": "#/definitions/84275257", + "description": "接口/动态API输出", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "groupName": { + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "displayName": { + "type": [ + "string", + "null" + ], + "description": "接口名称" + }, + "routeName": { + "type": [ + "string", + "null" + ], + "description": "路由名称" + } + }, + "additionalProperties": false, + "description": "接口/动态API输出", + "x-apifox-orders": [ + "groupName", + "displayName", + "routeName" + ] + } + } + }, + { + "name": "CardTypeEnum", + "displayName": "", + "id": "#/definitions/84275258", + "description": "证件类型枚举
 身份证 IdCard = 0
 护照 PassportCard = 1
 出生证 BirthCard = 2
 港澳台通行证 GatCard = 3
 外国人居留证 ForeignCard = 4
 营业执照 License = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "证件类型枚举
 身份证 IdCard = 0
 护照 PassportCard = 1
 出生证 BirthCard = 2
 港澳台通行证 GatCard = 3
 外国人居留证 ForeignCard = 4
 营业执照 License = 5
", + "format": "int32" + } + } + }, + { + "name": "ChangePwdInput", + "displayName": "", + "id": "#/definitions/84275259", + "description": "修改用户密码输入参数", + "schema": { + "jsonSchema": { + "required": [ + "passwordNew", + "passwordOld" + ], + "type": "object", + "properties": { + "passwordOld": { + "minLength": 1, + "type": "string", + "description": "当前密码" + }, + "passwordNew": { + "maxLength": 20, + "minLength": 5, + "type": "string", + "description": "新密码" + } + }, + "additionalProperties": false, + "description": "修改用户密码输入参数", + "x-apifox-orders": [ + "passwordOld", + "passwordNew" + ] + } + } + }, + { + "name": "ClusterStatus", + "displayName": "", + "id": "#/definitions/84275260", + "description": "
  Crashed = 0
  Working = 1
  Waiting = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "
  Crashed = 0
  Working = 1
  Waiting = 2
", + "format": "int32" + } + } + }, + { + "name": "CodeGenConfig", + "displayName": "", + "id": "#/definitions/84275261", + "description": "代码生成详细配置参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "codeGenId": { + "type": "integer", + "description": "代码生成主表ID", + "format": "int64" + }, + "columnName": { + "type": [ + "string", + "null" + ], + "description": "数据库字段名" + }, + "propertyName": { + "type": [ + "string", + "null" + ], + "description": "实体属性名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "lowerPropertyName": { + "type": [ + "string", + "null" + ], + "description": "数据库字段名(首字母小写)", + "readOnly": true + }, + "columnComment": { + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "netType": { + "type": [ + "string", + "null" + ], + "description": ".NET类型" + }, + "effectType": { + "type": [ + "string", + "null" + ], + "description": "作用类型(字典)" + }, + "fkEntityName": { + "type": [ + "string", + "null" + ], + "description": "外键实体名称" + }, + "fkTableName": { + "type": [ + "string", + "null" + ], + "description": "外键表名称" + }, + "lowerFkEntityName": { + "type": [ + "string", + "null" + ], + "description": "外键实体名称(首字母小写)", + "readOnly": true + }, + "fkColumnName": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段" + }, + "lowerFkColumnName": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段(首字母小写)", + "readOnly": true + }, + "fkColumnNetType": { + "type": [ + "string", + "null" + ], + "description": "外键显示字段.NET类型" + }, + "dictTypeCode": { + "type": [ + "string", + "null" + ], + "description": "字典code" + }, + "whetherRetract": { + "type": [ + "string", + "null" + ], + "description": "列表是否缩进(字典)" + }, + "whetherRequired": { + "type": [ + "string", + "null" + ], + "description": "是否必填(字典)" + }, + "whetherSortable": { + "type": [ + "string", + "null" + ], + "description": "是否可排序(字典)" + }, + "queryWhether": { + "type": [ + "string", + "null" + ], + "description": "是否是查询条件" + }, + "queryType": { + "type": [ + "string", + "null" + ], + "description": "查询方式" + }, + "whetherTable": { + "type": [ + "string", + "null" + ], + "description": "列表显示" + }, + "whetherAddUpdate": { + "type": [ + "string", + "null" + ], + "description": "增改" + }, + "columnKey": { + "type": [ + "string", + "null" + ], + "description": "主外键" + }, + "dataType": { + "type": [ + "string", + "null" + ], + "description": "数据库中类型(物理类型)" + }, + "whetherCommon": { + "type": [ + "string", + "null" + ], + "description": "是否是通用字段" + }, + "tableNickName": { + "type": [ + "string", + "null" + ], + "description": "表的别名 Table as XXX", + "readOnly": true + }, + "displayColumn": { + "type": [ + "string", + "null" + ], + "description": "显示文本字段" + }, + "valueColumn": { + "type": [ + "string", + "null" + ], + "description": "选中值字段" + }, + "pidColumn": { + "type": [ + "string", + "null" + ], + "description": "父级字段" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "代码生成详细配置参数", + "x-apifox-orders": [ + "id", + "codeGenId", + "columnName", + "propertyName", + "columnLength", + "lowerPropertyName", + "columnComment", + "netType", + "effectType", + "fkEntityName", + "fkTableName", + "lowerFkEntityName", + "fkColumnName", + "lowerFkColumnName", + "fkColumnNetType", + "dictTypeCode", + "whetherRetract", + "whetherRequired", + "whetherSortable", + "queryWhether", + "queryType", + "whetherTable", + "whetherAddUpdate", + "columnKey", + "dataType", + "whetherCommon", + "tableNickName", + "displayColumn", + "valueColumn", + "pidColumn", + "orderNo" + ] + } + } + }, + { + "name": "CodeGenInput", + "displayName": "", + "id": "#/definitions/84275262", + "description": "代码生成参数类", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "authorName": { + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "generateType": { + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "type": [ + "string", + "null" + ], + "description": "业务名(业务代码包名称)" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + } + }, + "additionalProperties": false, + "description": "代码生成参数类", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "authorName", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "generateType", + "tableName", + "nameSpace", + "busName", + "tableComment", + "menuApplication", + "menuPid", + "printType", + "printName" + ] + } + } + }, + { + "name": "ColumnOuput", + "displayName": "", + "id": "#/definitions/84275263", + "description": "数据库表列", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "columnName": { + "type": [ + "string", + "null" + ], + "description": "字段名" + }, + "propertyName": { + "type": [ + "string", + "null" + ], + "description": "实体的Property名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "dataType": { + "type": [ + "string", + "null" + ], + "description": "数据库中类型" + }, + "isPrimarykey": { + "type": "boolean", + "description": "是否为主键" + }, + "isNullable": { + "type": "boolean", + "description": "是否允许为空" + }, + "netType": { + "type": [ + "string", + "null" + ], + "description": ".NET字段类型" + }, + "columnComment": { + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "columnKey": { + "type": [ + "string", + "null" + ], + "description": "主外键" + } + }, + "additionalProperties": false, + "description": "数据库表列", + "x-apifox-orders": [ + "columnName", + "propertyName", + "columnLength", + "dataType", + "isPrimarykey", + "isNullable", + "netType", + "columnComment", + "columnKey" + ] + } + } + }, + { + "name": "ConstOutput", + "displayName": "", + "id": "#/definitions/84275264", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "additionalProperties": false, + "description": "编码", + "type": "null" + }, + "data": { + "additionalProperties": false, + "description": "扩展字段", + "type": "null" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "name", + "code", + "data" + ] + } + } + }, + { + "name": "CreateEntityInput", + "displayName": "", + "id": "#/definitions/84275265", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名", + "examples": [ + "student" + ] + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名", + "examples": [ + "Student" + ] + }, + "baseClassName": { + "type": [ + "string", + "null" + ], + "description": "基类名", + "examples": [ + "AutoIncrementEntity" + ] + }, + "position": { + "type": [ + "string", + "null" + ], + "description": "导出位置", + "examples": [ + "Web.Application" + ] + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库标识" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tableName", + "entityName", + "baseClassName", + "position", + "configId" + ] + } + } + }, + { + "name": "CreateSeedDataInput", + "displayName": "", + "id": "#/definitions/84275266", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库标识" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名", + "examples": [ + "student" + ] + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名称", + "examples": [ + "Student" + ] + }, + "seedDataName": { + "type": [ + "string", + "null" + ], + "description": "种子名称", + "examples": [ + "Student" + ] + }, + "position": { + "type": [ + "string", + "null" + ], + "description": "导出位置", + "examples": [ + "Web.Application" + ] + }, + "suffix": { + "type": [ + "string", + "null" + ], + "description": "后缀", + "examples": [ + "Web.Application" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "entityName", + "seedDataName", + "position", + "suffix" + ] + } + } + }, + { + "name": "CultureLevelEnum", + "displayName": "", + "id": "#/definitions/84275267", + "description": "文化程度枚举
 其他 Level0 = 0
 小学 Level1 = 1
 初中 Level2 = 2
 普通高中 Level3 = 3
 技工学校 Level4 = 4
 职业教育 Level5 = 5
 职业高中 Level6 = 6
 中等专科 Level7 = 7
 大学专科 Level8 = 8
 大学本科 Level9 = 9
 硕士研究生 Level10 = 10
 博士研究生 Level11 = 11
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ], + "type": "integer", + "description": "文化程度枚举
 其他 Level0 = 0
 小学 Level1 = 1
 初中 Level2 = 2
 普通高中 Level3 = 3
 技工学校 Level4 = 4
 职业教育 Level5 = 5
 职业高中 Level6 = 6
 中等专科 Level7 = 7
 大学专科 Level8 = 8
 大学本科 Level9 = 9
 硕士研究生 Level10 = 10
 博士研究生 Level11 = 11
", + "format": "int32" + } + } + }, + { + "name": "DataItem", + "displayName": "", + "id": "#/definitions/84275268", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "value" + ] + } + } + }, + { + "name": "DataScopeEnum", + "displayName": "", + "id": "#/definitions/84275269", + "description": "角色数据范围枚举
 全部数据 All = 1
 本部门及以下数据 DeptChild = 2
 本部门数据 Dept = 3
 仅本人数据 Self = 4
 自定义数据 Define = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "角色数据范围枚举
 全部数据 All = 1
 本部门及以下数据 DeptChild = 2
 本部门数据 Dept = 3
 仅本人数据 Self = 4
 自定义数据 Define = 5
", + "format": "int32" + } + } + }, + { + "name": "DatabaseOutput", + "displayName": "", + "id": "#/definitions/84275270", + "description": "数据库", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库连接字符串" + } + }, + "additionalProperties": false, + "description": "数据库", + "x-apifox-orders": [ + "configId", + "dbType", + "connectionString" + ] + } + } + }, + { + "name": "DbColumnInput", + "displayName": "", + "id": "#/definitions/84275271", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + }, + "dataType": { + "type": [ + "string", + "null" + ] + }, + "length": { + "type": "integer", + "format": "int32" + }, + "columnDescription": { + "type": [ + "string", + "null" + ] + }, + "isNullable": { + "type": "integer", + "format": "int32" + }, + "isIdentity": { + "type": "integer", + "format": "int32" + }, + "isPrimarykey": { + "type": "integer", + "format": "int32" + }, + "decimalDigits": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "dbColumnName", + "dataType", + "length", + "columnDescription", + "isNullable", + "isIdentity", + "isPrimarykey", + "decimalDigits" + ] + } + } + }, + { + "name": "DbColumnOutput", + "displayName": "", + "id": "#/definitions/84275272", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tableName": { + "type": [ + "string", + "null" + ] + }, + "tableId": { + "type": "integer", + "format": "int32" + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + }, + "propertyName": { + "type": [ + "string", + "null" + ] + }, + "dataType": { + "type": [ + "string", + "null" + ] + }, + "propertyType": { + "additionalProperties": false, + "type": "null" + }, + "length": { + "type": "integer", + "format": "int32" + }, + "columnDescription": { + "type": [ + "string", + "null" + ] + }, + "defaultValue": { + "type": [ + "string", + "null" + ] + }, + "isNullable": { + "type": "boolean" + }, + "isIdentity": { + "type": "boolean" + }, + "isPrimarykey": { + "type": "boolean" + }, + "value": { + "additionalProperties": false, + "type": "null" + }, + "decimalDigits": { + "type": "integer", + "format": "int32" + }, + "scale": { + "type": "integer", + "format": "int32" + }, + "isArray": { + "type": "boolean" + }, + "isJson": { + "type": "boolean" + }, + "isUnsigned": { + "type": [ + "boolean", + "null" + ] + }, + "createTableFieldSort": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tableName", + "tableId", + "dbColumnName", + "propertyName", + "dataType", + "propertyType", + "length", + "columnDescription", + "defaultValue", + "isNullable", + "isIdentity", + "isPrimarykey", + "value", + "decimalDigits", + "scale", + "isArray", + "isJson", + "isUnsigned", + "createTableFieldSort" + ] + } + } + }, + { + "name": "DbObjectType", + "displayName": "", + "id": "#/definitions/84275273", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "DbTableInfo", + "displayName": "", + "id": "#/definitions/84275274", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "dbObjectType": { + "$ref": "#/definitions/84275273" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "name", + "description", + "dbObjectType" + ] + } + } + }, + { + "name": "DbTableInput", + "displayName": "", + "id": "#/definitions/84275275", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "dbColumnInfoList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275271" + } + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "description", + "dbColumnInfoList" + ] + } + } + }, + { + "name": "DbType", + "displayName": "", + "id": "#/definitions/84275276", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 900 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "DeleteCodeGenInput", + "displayName": "", + "id": "#/definitions/84275277", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "代码生成器Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteConfigInput", + "displayName": "", + "id": "#/definitions/84275278", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteDbColumnInput", + "displayName": "", + "id": "#/definitions/84275279", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "dbColumnName": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "dbColumnName" + ] + } + } + }, + { + "name": "DeleteDbTableInput", + "displayName": "", + "id": "#/definitions/84275280", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName" + ] + } + } + }, + { + "name": "DeleteDictDataInput", + "displayName": "", + "id": "#/definitions/84275281", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteDictTypeInput", + "displayName": "", + "id": "#/definitions/84275282", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteFileInput", + "displayName": "", + "id": "#/definitions/84275283", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteJobDetailInput", + "displayName": "", + "id": "#/definitions/84275284", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId" + ] + } + } + }, + { + "name": "DeleteJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275285", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "DeleteMenuInput", + "displayName": "", + "id": "#/definitions/84275286", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteMessageTemplateInput", + "displayName": "", + "id": "#/definitions/84275287", + "description": "删除消息模板", + "schema": { + "jsonSchema": { + "required": [ + "templateId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + } + }, + "additionalProperties": false, + "description": "删除消息模板", + "x-apifox-orders": [ + "templateId" + ] + } + } + }, + { + "name": "DeleteNoticeInput", + "displayName": "", + "id": "#/definitions/84275288", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275289", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteOrgInput", + "displayName": "", + "id": "#/definitions/84275290", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePluginInput", + "displayName": "", + "id": "#/definitions/84275291", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePosInput", + "displayName": "", + "id": "#/definitions/84275292", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeletePrintInput", + "displayName": "", + "id": "#/definitions/84275293", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteRegionInput", + "displayName": "", + "id": "#/definitions/84275294", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteRoleInput", + "displayName": "", + "id": "#/definitions/84275295", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteTenantInput", + "displayName": "", + "id": "#/definitions/84275296", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DeleteUserInput", + "displayName": "", + "id": "#/definitions/84275297", + "description": "删除用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "删除用户输入参数", + "x-apifox-orders": [ + "id", + "orgId" + ] + } + } + }, + { + "name": "DeleteWechatUserInput", + "displayName": "", + "id": "#/definitions/84275298", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "DictDataInput", + "displayName": "", + "id": "#/definitions/84275299", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "DictTypeInput", + "displayName": "", + "id": "#/definitions/84275300", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "EnumEntity", + "displayName": "", + "id": "#/definitions/84275301", + "description": "枚举实体", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "describe": { + "type": [ + "string", + "null" + ], + "description": "枚举的描述" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "枚举名称" + }, + "value": { + "type": "integer", + "description": "枚举对象的值", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "枚举实体", + "x-apifox-orders": [ + "describe", + "name", + "value" + ] + } + } + }, + { + "name": "EnumTypeOutput", + "displayName": "", + "id": "#/definitions/84275302", + "description": "枚举类型输出参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "typeDescribe": { + "type": [ + "string", + "null" + ], + "description": "枚举类型描述" + }, + "typeName": { + "type": [ + "string", + "null" + ], + "description": "枚举类型名称" + }, + "typeRemark": { + "type": [ + "string", + "null" + ], + "description": "枚举类型备注" + } + }, + "additionalProperties": false, + "description": "枚举类型输出参数", + "x-apifox-orders": [ + "typeDescribe", + "typeName", + "typeRemark" + ] + } + } + }, + { + "name": "FileInput", + "displayName": "", + "id": "#/definitions/84275303", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "文件Url" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "fileName", + "url" + ] + } + } + }, + { + "name": "GenAuthUrlInput", + "displayName": "", + "id": "#/definitions/84275304", + "description": "生成网页授权Url", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "redirectUrl": { + "type": [ + "string", + "null" + ], + "description": "RedirectUrl" + }, + "scope": { + "type": [ + "string", + "null" + ], + "description": "Scope" + } + }, + "additionalProperties": false, + "description": "生成网页授权Url", + "x-apifox-orders": [ + "redirectUrl", + "scope" + ] + } + } + }, + { + "name": "GenderEnum", + "displayName": "", + "id": "#/definitions/84275305", + "description": "性别枚举
 男 Male = 1
 女 Female = 2
 其他 Other = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "性别枚举
 男 Male = 1
 女 Female = 2
 其他 Other = 3
", + "format": "int32" + } + } + }, + { + "name": "IActionResult", + "displayName": "", + "id": "#/definitions/84275306", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "additionalProperties": false, + "x-apifox-orders": [ + ] + } + } + }, + { + "name": "JToken", + "displayName": "", + "id": "#/definitions/84275307", + "description": "", + "schema": { + "jsonSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/84275307" + } + } + } + }, + { + "name": "JobCreateTypeEnum", + "displayName": "", + "id": "#/definitions/84275308", + "description": "作业创建类型枚举
 内置 BuiltIn = 0
 脚本 Script = 1
 HTTP请求 Http = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "description": "作业创建类型枚举
 内置 BuiltIn = 0
 脚本 Script = 1
 HTTP请求 Http = 2
", + "format": "int32" + } + } + }, + { + "name": "JobDetailInput", + "displayName": "", + "id": "#/definitions/84275309", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId" + ] + } + } + }, + { + "name": "JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275310", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobDetail": { + "$ref": "#/definitions/84275383" + }, + "jobTriggers": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275384" + }, + "description": "触发器集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobDetail", + "jobTriggers" + ] + } + } + }, + { + "name": "JobTriggerInput", + "displayName": "", + "id": "#/definitions/84275311", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "LogInput", + "displayName": "", + "id": "#/definitions/84275312", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "startTime", + "endTime" + ] + } + } + }, + { + "name": "LogLevel", + "displayName": "", + "id": "#/definitions/84275313", + "description": "", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + } + } + }, + { + "name": "LoginInput", + "displayName": "", + "id": "#/definitions/84275314", + "description": "用户登录参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "password" + ], + "type": "object", + "properties": { + "account": { + "minLength": 2, + "type": "string", + "description": "账号", + "examples": [ + "admin" + ] + }, + "password": { + "minLength": 3, + "type": "string", + "description": "密码", + "examples": [ + "123456" + ] + }, + "codeId": { + "type": "integer", + "description": "验证码Id", + "format": "int64" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "验证码" + } + }, + "additionalProperties": false, + "description": "用户登录参数", + "x-apifox-orders": [ + "account", + "password", + "codeId", + "code" + ] + } + } + }, + { + "name": "LoginOutput", + "displayName": "", + "id": "#/definitions/84275315", + "description": "用户登录结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "accessToken": { + "type": [ + "string", + "null" + ], + "description": "令牌Token" + }, + "refreshToken": { + "type": [ + "string", + "null" + ], + "description": "刷新Token" + } + }, + "additionalProperties": false, + "description": "用户登录结果", + "x-apifox-orders": [ + "accessToken", + "refreshToken" + ] + } + } + }, + { + "name": "LoginPhoneInput", + "displayName": "", + "id": "#/definitions/84275316", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "phone" + ], + "type": "object", + "properties": { + "phone": { + "minLength": 1, + "type": "string", + "description": "手机号码", + "examples": [ + "admin" + ] + }, + "code": { + "minLength": 4, + "type": "string", + "description": "验证码", + "examples": [ + "123456" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "phone", + "code" + ] + } + } + }, + { + "name": "LoginUserOutput", + "displayName": "", + "id": "#/definitions/84275317", + "description": "用户登录信息", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户id", + "format": "int64" + }, + "account": { + "type": [ + "string", + "null" + ], + "description": "账号名称" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "idCardNum": { + "type": [ + "string", + "null" + ], + "description": "身份证" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "avatar": { + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "introduction": { + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "address": { + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "signature": { + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "orgName": { + "type": [ + "string", + "null" + ], + "description": "机构名称" + }, + "orgType": { + "type": [ + "string", + "null" + ], + "description": "机构类型" + }, + "posName": { + "type": [ + "string", + "null" + ], + "description": "职位名称" + }, + "buttons": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "description": "按钮权限集合" + }, + "roleIds": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + } + }, + "additionalProperties": false, + "description": "用户登录信息", + "x-apifox-orders": [ + "id", + "account", + "realName", + "phone", + "idCardNum", + "email", + "accountType", + "avatar", + "introduction", + "address", + "signature", + "orgId", + "orgName", + "orgType", + "posName", + "buttons", + "roleIds" + ] + } + } + }, + { + "name": "MenuOutput", + "displayName": "", + "id": "#/definitions/84275318", + "description": "系统菜单返回结果", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "permission": { + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "redirect": { + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "createTime": { + "type": "string", + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "description": "更新时间", + "format": "date-time" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "meta": { + "$ref": "#/definitions/84275391" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275318" + }, + "description": "菜单子项" + } + }, + "additionalProperties": false, + "description": "系统菜单返回结果", + "x-apifox-orders": [ + "id", + "pid", + "type", + "name", + "path", + "component", + "permission", + "redirect", + "orderNo", + "status", + "remark", + "createTime", + "updateTime", + "createUserName", + "updateUserName", + "meta", + "children" + ] + } + } + }, + { + "name": "MenuTypeEnum", + "displayName": "", + "id": "#/definitions/84275319", + "description": "系统菜单类型枚举
 目录 Dir = 1
 菜单 Menu = 2
 按钮 Btn = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "系统菜单类型枚举
 目录 Dir = 1
 菜单 Menu = 2
 按钮 Btn = 3
", + "format": "int32" + } + } + }, + { + "name": "MessageInput", + "displayName": "", + "id": "#/definitions/84275320", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户ID", + "format": "int64" + }, + "userIds": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "用户ID列表" + }, + "title": { + "type": [ + "string", + "null" + ], + "description": "消息标题" + }, + "messageType": { + "$ref": "#/definitions/84275322" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "消息内容" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "userId", + "userIds", + "title", + "messageType", + "message" + ] + } + } + }, + { + "name": "MessageTemplateSendInput", + "displayName": "", + "id": "#/definitions/84275321", + "description": "获取消息模板列表", + "schema": { + "jsonSchema": { + "required": [ + "data", + "templateId", + "toUserOpenId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + }, + "toUserOpenId": { + "minLength": 1, + "type": "string", + "description": "接收者的OpenId" + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275268" + }, + "description": "模板数据,格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }", + "x-apifox-orders": [ + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "description": "模板跳转链接" + }, + "miniProgramPagePath": { + "type": [ + "string", + "null" + ], + "description": "所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar)" + } + }, + "additionalProperties": false, + "description": "获取消息模板列表", + "x-apifox-orders": [ + "templateId", + "toUserOpenId", + "data", + "url", + "miniProgramPagePath" + ] + } + } + }, + { + "name": "MessageTypeEnum", + "displayName": "", + "id": "#/definitions/84275322", + "description": "消息类型枚举
 消息 Info = 0
 成功 Success = 1
 警告 Warning = 2
 错误 Error = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "消息类型枚举
 消息 Info = 0
 成功 Success = 1
 警告 Warning = 2
 错误 Error = 3
", + "format": "int32" + } + } + }, + { + "name": "NoticeInput", + "displayName": "", + "id": "#/definitions/84275323", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "NoticeStatusEnum", + "displayName": "", + "id": "#/definitions/84275324", + "description": "通知公告状态枚举
 草稿 DRAFT = 0
 发布 PUBLIC = 1
 撤回 CANCEL = 2
 删除 DELETED = 3
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "description": "通知公告状态枚举
 草稿 DRAFT = 0
 发布 PUBLIC = 1
 撤回 CANCEL = 2
 删除 DELETED = 3
", + "format": "int32" + } + } + }, + { + "name": "NoticeTypeEnum", + "displayName": "", + "id": "#/definitions/84275325", + "description": "通知公告状类型枚举
 通知 NOTICE = 1
 公告 ANNOUNCEMENT = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "通知公告状类型枚举
 通知 NOTICE = 1
 公告 ANNOUNCEMENT = 2
", + "format": "int32" + } + } + }, + { + "name": "NoticeUserStatusEnum", + "displayName": "", + "id": "#/definitions/84275326", + "description": "通知公告用户状态枚举
 未读 UNREAD = 0
 已读 READ = 1
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "通知公告用户状态枚举
 未读 UNREAD = 0
 已读 READ = 1
", + "format": "int32" + } + } + }, + { + "name": "OpenAccessInput", + "displayName": "", + "id": "#/definitions/84275327", + "description": "开放接口身份输入参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "accessKey": { + "type": [ + "string", + "null" + ], + "description": "身份标识" + } + }, + "additionalProperties": false, + "description": "开放接口身份输入参数", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "accessKey" + ] + } + } + }, + { + "name": "OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275328", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "accessKey": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "maxLength": 256, + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + }, + "bindUserAccount": { + "type": [ + "string", + "null" + ], + "description": "绑定用户账号" + }, + "bindTenantName": { + "type": [ + "string", + "null" + ], + "description": "绑定租户名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "accessKey", + "accessSecret", + "bindTenantId", + "bindUserId", + "bindUserAccount", + "bindTenantName" + ] + } + } + }, + { + "name": "PageConfigInput", + "displayName": "", + "id": "#/definitions/84275329", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "groupCode": { + "type": [ + "string", + "null" + ], + "description": "分组编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code", + "groupCode" + ] + } + } + }, + { + "name": "PageDictDataInput", + "displayName": "", + "id": "#/definitions/84275330", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "type": [ + "string", + "null" + ], + "description": "值" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "dictTypeId", + "value", + "code" + ] + } + } + }, + { + "name": "PageDictTypeInput", + "displayName": "", + "id": "#/definitions/84275331", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageFileInput", + "displayName": "", + "id": "#/definitions/84275332", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "fileName", + "startTime", + "endTime" + ] + } + } + }, + { + "name": "PageJobDetailInput", + "displayName": "", + "id": "#/definitions/84275333", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "描述信息" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "jobId", + "description" + ] + } + } + }, + { + "name": "PageJobTriggerRecordInput", + "displayName": "", + "id": "#/definitions/84275334", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "jobId": { + "type": [ + "string", + "null" + ], + "description": "作业Id" + }, + "triggerId": { + "type": [ + "string", + "null" + ], + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "PageLogInput", + "displayName": "", + "id": "#/definitions/84275335", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "开始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "startTime", + "endTime" + ] + } + } + }, + { + "name": "PageNoticeInput", + "displayName": "", + "id": "#/definitions/84275336", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "title": { + "type": [ + "string", + "null" + ], + "description": "标题" + }, + "type": { + "$ref": "#/definitions/84275325" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "title", + "type" + ] + } + } + }, + { + "name": "PageOnlineUserInput", + "displayName": "", + "id": "#/definitions/84275337", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "userName": { + "type": [ + "string", + "null" + ], + "description": "账号名称" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "真实姓名" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "userName", + "realName" + ] + } + } + }, + { + "name": "PagePluginInput", + "displayName": "", + "id": "#/definitions/84275338", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PagePrintInput", + "displayName": "", + "id": "#/definitions/84275339", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageRegionInput", + "displayName": "", + "id": "#/definitions/84275340", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "pid": { + "type": "integer", + "description": "父节点Id", + "format": "int64" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "pid", + "name", + "code" + ] + } + } + }, + { + "name": "PageRoleInput", + "displayName": "", + "id": "#/definitions/84275341", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "code" + ] + } + } + }, + { + "name": "PageTenantInput", + "displayName": "", + "id": "#/definitions/84275342", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "name", + "phone" + ] + } + } + }, + { + "name": "PageUserInput", + "displayName": "", + "id": "#/definitions/84275343", + "description": "获取用户分页列表输入参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "account": { + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "type": [ + "string", + "null" + ], + "description": "姓名" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "手机号" + }, + "orgId": { + "type": "integer", + "description": "查询时所选机构Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "获取用户分页列表输入参数", + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "account", + "realName", + "phone", + "orgId" + ] + } + } + }, + { + "name": "PlatformTypeEnum", + "displayName": "", + "id": "#/definitions/84275344", + "description": "平台类型枚举
 微信公众号 微信公众号 = 1
 微信小程序 微信小程序 = 2
 QQ QQ = 3
 支付宝 Alipay = 4
 Gitee Gitee = 5
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "description": "平台类型枚举
 微信公众号 微信公众号 = 1
 微信小程序 微信小程序 = 2
 QQ QQ = 3
 支付宝 Alipay = 4
 Gitee Gitee = 5
", + "format": "int32" + } + } + }, + { + "name": "ResetPwdUserInput", + "displayName": "", + "id": "#/definitions/84275345", + "description": "重置用户密码输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "重置用户密码输入参数", + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "RoleInput", + "displayName": "", + "id": "#/definitions/84275346", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "RoleMenuInput", + "displayName": "", + "id": "#/definitions/84275347", + "description": "授权角色菜单", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "description": "授权角色菜单", + "x-apifox-orders": [ + "id", + "menuIdList" + ] + } + } + }, + { + "name": "RoleOrgInput", + "displayName": "", + "id": "#/definitions/84275348", + "description": "授权角色机构", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "dataScope": { + "type": "integer", + "description": "数据范围", + "format": "int32" + }, + "orgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "机构Id集合" + } + }, + "additionalProperties": false, + "description": "授权角色机构", + "x-apifox-orders": [ + "id", + "dataScope", + "orgIdList" + ] + } + } + }, + { + "name": "RoleOutput", + "displayName": "", + "id": "#/definitions/84275349", + "description": "角色列表输出参数", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Id", + "format": "int64" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "名称" + }, + "code": { + "type": [ + "string", + "null" + ], + "description": "编码" + } + }, + "additionalProperties": false, + "description": "角色列表输出参数", + "x-apifox-orders": [ + "id", + "name", + "code" + ] + } + } + }, + { + "name": "SendSubscribeMessageInput", + "displayName": "", + "id": "#/definitions/84275350", + "description": "发送订阅消息", + "schema": { + "jsonSchema": { + "required": [ + "data", + "templateId", + "toUserOpenId" + ], + "type": "object", + "properties": { + "templateId": { + "minLength": 1, + "type": "string", + "description": "订阅模板Id" + }, + "toUserOpenId": { + "minLength": 1, + "type": "string", + "description": "接收者的OpenId" + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/84275268" + }, + "description": "模板内容,格式形如 { \"key1\": { \"value\": any }, \"key2\": { \"value\": any } }", + "x-apifox-orders": [ + ] + }, + "miniprogramState": { + "type": [ + "string", + "null" + ], + "description": "跳转小程序类型" + }, + "language": { + "type": [ + "string", + "null" + ], + "description": "语言类型" + }, + "miniProgramPagePath": { + "type": [ + "string", + "null" + ], + "description": "点击模板卡片后的跳转页面(仅限本小程序内的页面),支持带参数(示例pages/app/index?foo=bar)" + } + }, + "additionalProperties": false, + "description": "发送订阅消息", + "x-apifox-orders": [ + "templateId", + "toUserOpenId", + "data", + "miniprogramState", + "language", + "miniProgramPagePath" + ] + } + } + }, + { + "name": "SignatureInput", + "displayName": "", + "id": "#/definitions/84275351", + "description": "获取配置签名", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ], + "description": "Url" + } + }, + "additionalProperties": false, + "description": "获取配置签名", + "x-apifox-orders": [ + "url" + ] + } + } + }, + { + "name": "SmKeyPairOutput", + "displayName": "", + "id": "#/definitions/84275352", + "description": "国密公钥私钥对输出", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "privateKey": { + "type": [ + "string", + "null" + ], + "description": "私匙" + }, + "publicKey": { + "type": [ + "string", + "null" + ], + "description": "公匙" + } + }, + "additionalProperties": false, + "description": "国密公钥私钥对输出", + "x-apifox-orders": [ + "privateKey", + "publicKey" + ] + } + } + }, + { + "name": "SqlSugarPagedList_JobDetailOutput", + "displayName": "", + "id": "#/definitions/84275353", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275310" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_OpenAccessOutput", + "displayName": "", + "id": "#/definitions/84275354", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275328" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysCodeGen", + "displayName": "", + "id": "#/definitions/84275355", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275376" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysConfig", + "displayName": "", + "id": "#/definitions/84275356", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275378" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysDictData", + "displayName": "", + "id": "#/definitions/84275357", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysDictType", + "displayName": "", + "id": "#/definitions/84275358", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275380" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysFile", + "displayName": "", + "id": "#/definitions/84275359", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275381" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275360", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275385" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogDiff", + "displayName": "", + "id": "#/definitions/84275361", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275386" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogEx", + "displayName": "", + "id": "#/definitions/84275362", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275387" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogOp", + "displayName": "", + "id": "#/definitions/84275363", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275388" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysLogVis", + "displayName": "", + "id": "#/definitions/84275364", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275389" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysNotice", + "displayName": "", + "id": "#/definitions/84275365", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275392" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275366", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275393" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275367", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275394" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysPlugin", + "displayName": "", + "id": "#/definitions/84275368", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275396" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysPrint", + "displayName": "", + "id": "#/definitions/84275369", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275398" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysRegion", + "displayName": "", + "id": "#/definitions/84275370", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysRole", + "displayName": "", + "id": "#/definitions/84275371", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275400" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_SysWechatUser", + "displayName": "", + "id": "#/definitions/84275372", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275404" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_TenantOutput", + "displayName": "", + "id": "#/definitions/84275373", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275408" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "SqlSugarPagedList_UserOutput", + "displayName": "", + "id": "#/definitions/84275374", + "description": "分页泛型集合", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页容量", + "format": "int32" + }, + "total": { + "type": "integer", + "description": "总条数", + "format": "int32" + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "format": "int32" + }, + "items": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275434" + }, + "description": "当前页集合" + }, + "hasPrevPage": { + "type": "boolean", + "description": "是否有上一页" + }, + "hasNextPage": { + "type": "boolean", + "description": "是否有下一页" + } + }, + "additionalProperties": false, + "description": "分页泛型集合", + "x-apifox-orders": [ + "page", + "pageSize", + "total", + "totalPages", + "items", + "hasPrevPage", + "hasNextPage" + ] + } + } + }, + { + "name": "StatusEnum", + "displayName": "", + "id": "#/definitions/84275375", + "description": "通用状态枚举
 启用 Enable = 1
 停用 Disable = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "通用状态枚举
 启用 Enable = 1
 停用 Disable = 2
", + "format": "int32" + } + } + }, + { + "name": "SysCodeGen", + "displayName": "", + "id": "#/definitions/84275376", + "description": "代码生成表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "authorName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "tablePrefix": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "generateType": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "tableName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "业务名" + }, + "menuPid": { + "type": "integer", + "description": "菜单编码", + "format": "int64" + }, + "printType": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + } + }, + "additionalProperties": false, + "description": "代码生成表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "authorName", + "tablePrefix", + "generateType", + "configId", + "dbName", + "dbType", + "connectionString", + "tableName", + "nameSpace", + "busName", + "menuPid", + "printType", + "printName" + ] + } + } + }, + { + "name": "SysCodeGenConfig", + "displayName": "", + "id": "#/definitions/84275377", + "description": "代码生成字段配置表", + "schema": { + "jsonSchema": { + "required": [ + "columnName", + "propertyName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "codeGenId": { + "type": "integer", + "description": "代码生成主表Id", + "format": "int64" + }, + "columnName": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "数据库字段名" + }, + "propertyName": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "实体属性名" + }, + "columnLength": { + "type": "integer", + "description": "字段数据长度", + "format": "int32" + }, + "columnComment": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "字段描述" + }, + "netType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": ".NET数据类型" + }, + "effectType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "作用类型(字典)" + }, + "fkEntityName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键实体名称" + }, + "fkTableName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "外键表名称" + }, + "fkColumnName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键显示字段" + }, + "fkColumnNetType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "外键显示字段.NET类型" + }, + "dictTypeCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "字典编码" + }, + "whetherRetract": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "列表是否缩进(字典)" + }, + "whetherRequired": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否必填(字典)" + }, + "whetherSortable": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否可排序(字典)" + }, + "queryWhether": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否是查询条件" + }, + "queryType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "查询方式" + }, + "whetherTable": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "列表显示" + }, + "whetherAddUpdate": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "增改" + }, + "columnKey": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "主键" + }, + "dataType": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库中类型(物理类型)" + }, + "whetherCommon": { + "maxLength": 8, + "type": [ + "string", + "null" + ], + "description": "是否通用字段" + }, + "displayColumn": { + "type": [ + "string", + "null" + ], + "description": "显示文本字段" + }, + "valueColumn": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "选中值字段" + }, + "pidColumn": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "父级字段" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "代码生成字段配置表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "codeGenId", + "columnName", + "propertyName", + "columnLength", + "columnComment", + "netType", + "effectType", + "fkEntityName", + "fkTableName", + "fkColumnName", + "fkColumnNetType", + "dictTypeCode", + "whetherRetract", + "whetherRequired", + "whetherSortable", + "queryWhether", + "queryType", + "whetherTable", + "whetherAddUpdate", + "columnKey", + "dataType", + "whetherCommon", + "displayColumn", + "valueColumn", + "pidColumn", + "orderNo" + ] + } + } + }, + { + "name": "SysConfig", + "displayName": "", + "id": "#/definitions/84275378", + "description": "系统参数配置表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统参数配置表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "SysDictData", + "displayName": "", + "id": "#/definitions/84275379", + "description": "系统字典值表", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统字典值表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "SysDictType", + "displayName": "", + "id": "#/definitions/84275380", + "description": "系统字典类型表", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "description": "系统字典类型表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "SysFile", + "displayName": "", + "id": "#/definitions/84275381", + "description": "系统文件表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "provider": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "提供者" + }, + "bucketName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "仓储名称" + }, + "fileName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "文件名称(源文件名)" + }, + "suffix": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "文件后缀" + }, + "filePath": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "存储路径" + }, + "sizeKb": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "文件大小KB" + }, + "sizeInfo": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "文件大小信息-计算后的" + }, + "url": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "外链地址-OSS上传后生成外链地址方便前端预览" + }, + "fileMd5": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "文件MD5" + } + }, + "additionalProperties": false, + "description": "系统文件表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "provider", + "bucketName", + "fileName", + "suffix", + "filePath", + "sizeKb", + "sizeInfo", + "url", + "fileMd5" + ] + } + } + }, + { + "name": "SysJobCluster", + "displayName": "", + "id": "#/definitions/84275382", + "description": "系统作业集群表", + "schema": { + "jsonSchema": { + "required": [ + "clusterId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "clusterId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业集群Id" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275260" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业集群表", + "x-apifox-orders": [ + "id", + "clusterId", + "description", + "status", + "updatedTime" + ] + } + } + }, + { + "name": "SysJobDetail", + "displayName": "", + "id": "#/definitions/84275383", + "description": "系统作业信息表", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + } + }, + "additionalProperties": false, + "description": "系统作业信息表", + "x-apifox-orders": [ + "id", + "jobId", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode" + ] + } + } + }, + { + "name": "SysJobTrigger", + "displayName": "", + "id": "#/definitions/84275384", + "description": "系统作业触发器表", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "触发器Id" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业触发器表", + "x-apifox-orders": [ + "id", + "triggerId", + "jobId", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime" + ] + } + } + }, + { + "name": "SysJobTriggerRecord", + "displayName": "", + "id": "#/definitions/84275385", + "description": "系统作业触发器运行记录表", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "jobId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "触发器Id" + }, + "numberOfRuns": { + "type": "integer", + "description": "当前运行次数", + "format": "int64" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "result": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "本次执行结果" + }, + "elapsedTime": { + "type": "integer", + "description": "本次执行耗时", + "format": "int64" + }, + "createdTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统作业触发器运行记录表", + "x-apifox-orders": [ + "id", + "jobId", + "triggerId", + "numberOfRuns", + "lastRunTime", + "nextRunTime", + "status", + "result", + "elapsedTime", + "createdTime" + ] + } + } + }, + { + "name": "SysLogDiff", + "displayName": "", + "id": "#/definitions/84275386", + "description": "系统差异日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "beforeData": { + "type": [ + "string", + "null" + ], + "description": "操作前记录" + }, + "afterData": { + "type": [ + "string", + "null" + ], + "description": "操作后记录" + }, + "sql": { + "type": [ + "string", + "null" + ], + "description": "Sql" + }, + "parameters": { + "type": [ + "string", + "null" + ], + "description": "参数 手动传入的参数" + }, + "businessData": { + "type": [ + "string", + "null" + ], + "description": "业务对象" + }, + "diffType": { + "type": [ + "string", + "null" + ], + "description": "差异操作" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "耗时", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "系统差异日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "beforeData", + "afterData", + "sql", + "parameters", + "businessData", + "diffType", + "elapsed" + ] + } + } + }, + { + "name": "SysLogEx", + "displayName": "", + "id": "#/definitions/84275387", + "description": "系统异常日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "httpMethod": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "请求方式" + }, + "requestUrl": { + "type": [ + "string", + "null" + ], + "description": "请求地址" + }, + "requestParam": { + "type": [ + "string", + "null" + ], + "description": "请求参数" + }, + "returnResult": { + "type": [ + "string", + "null" + ], + "description": "返回结果" + }, + "eventId": { + "type": [ + "integer", + "null" + ], + "description": "事件Id", + "format": "int32" + }, + "threadId": { + "type": [ + "integer", + "null" + ], + "description": "线程Id", + "format": "int32" + }, + "traceId": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "请求跟踪Id" + }, + "exception": { + "type": [ + "string", + "null" + ], + "description": "异常信息" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "日志消息Json" + } + }, + "additionalProperties": false, + "description": "系统异常日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName", + "httpMethod", + "requestUrl", + "requestParam", + "returnResult", + "eventId", + "threadId", + "traceId", + "exception", + "message" + ] + } + } + }, + { + "name": "SysLogOp", + "displayName": "", + "id": "#/definitions/84275388", + "description": "系统操作日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "httpMethod": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "请求方式" + }, + "requestUrl": { + "type": [ + "string", + "null" + ], + "description": "请求地址" + }, + "requestParam": { + "type": [ + "string", + "null" + ], + "description": "请求参数" + }, + "returnResult": { + "type": [ + "string", + "null" + ], + "description": "返回结果" + }, + "eventId": { + "type": [ + "integer", + "null" + ], + "description": "事件Id", + "format": "int32" + }, + "threadId": { + "type": [ + "integer", + "null" + ], + "description": "线程Id", + "format": "int32" + }, + "traceId": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "请求跟踪Id" + }, + "exception": { + "type": [ + "string", + "null" + ], + "description": "异常信息" + }, + "message": { + "type": [ + "string", + "null" + ], + "description": "日志消息Json" + } + }, + "additionalProperties": false, + "description": "系统操作日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName", + "httpMethod", + "requestUrl", + "requestParam", + "returnResult", + "eventId", + "threadId", + "traceId", + "exception", + "message" + ] + } + } + }, + { + "name": "SysLogVis", + "displayName": "", + "id": "#/definitions/84275389", + "description": "系统访问日志表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "controllerName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "模块名称" + }, + "actionName": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "方法名称" + }, + "displayTitle": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "显示名称" + }, + "status": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "执行状态" + }, + "remoteIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "IP地址" + }, + "location": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "登录地点" + }, + "longitude": { + "type": [ + "number", + "null" + ], + "description": "经度", + "format": "double" + }, + "latitude": { + "type": [ + "number", + "null" + ], + "description": "维度", + "format": "double" + }, + "browser": { + "maxLength": 1024, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "操作系统" + }, + "elapsed": { + "type": [ + "integer", + "null" + ], + "description": "操作用时", + "format": "int64" + }, + "logDateTime": { + "type": [ + "string", + "null" + ], + "description": "日志时间", + "format": "date-time" + }, + "logLevel": { + "$ref": "#/definitions/84275313" + }, + "account": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + } + }, + "additionalProperties": false, + "description": "系统访问日志表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "controllerName", + "actionName", + "displayTitle", + "status", + "remoteIp", + "location", + "longitude", + "latitude", + "browser", + "os", + "elapsed", + "logDateTime", + "logLevel", + "account", + "realName" + ] + } + } + }, + { + "name": "SysMenu", + "displayName": "", + "id": "#/definitions/84275390", + "description": "系统菜单表", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "title": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "菜单名称" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + } + }, + "additionalProperties": false, + "description": "系统菜单表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "title", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children" + ] + } + } + }, + { + "name": "SysMenuMeta", + "displayName": "", + "id": "#/definitions/84275391", + "description": "菜单Meta配置", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "title": { + "type": [ + "string", + "null" + ], + "description": "标题" + }, + "icon": { + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "isLink": { + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + } + }, + "additionalProperties": false, + "description": "菜单Meta配置", + "x-apifox-orders": [ + "title", + "icon", + "isIframe", + "isLink", + "isHide", + "isKeepAlive", + "isAffix" + ] + } + } + }, + { + "name": "SysNotice", + "displayName": "", + "id": "#/definitions/84275392", + "description": "系统通知公告表", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "description": "系统通知公告表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "SysNoticeUser", + "displayName": "", + "id": "#/definitions/84275393", + "description": "系统通知公告用户表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "noticeId": { + "type": "integer", + "description": "通知公告Id", + "format": "int64" + }, + "sysNotice": { + "$ref": "#/definitions/84275392" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "readTime": { + "type": [ + "string", + "null" + ], + "description": "阅读时间", + "format": "date-time" + }, + "readStatus": { + "$ref": "#/definitions/84275326" + } + }, + "additionalProperties": false, + "description": "系统通知公告用户表", + "x-apifox-orders": [ + "id", + "noticeId", + "sysNotice", + "userId", + "readTime", + "readStatus" + ] + } + } + }, + { + "name": "SysOnlineUser", + "displayName": "", + "id": "#/definitions/84275394", + "description": "系统在线用户表", + "schema": { + "jsonSchema": { + "required": [ + "userName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "connectionId": { + "type": [ + "string", + "null" + ], + "description": "连接Id" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "userName": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "time": { + "type": [ + "string", + "null" + ], + "description": "连接时间", + "format": "date-time" + }, + "ip": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "连接IP" + }, + "browser": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "浏览器" + }, + "os": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "操作系统" + } + }, + "additionalProperties": false, + "description": "系统在线用户表", + "x-apifox-orders": [ + "id", + "tenantId", + "connectionId", + "userId", + "userName", + "realName", + "time", + "ip", + "browser", + "os" + ] + } + } + }, + { + "name": "SysOrg", + "displayName": "", + "id": "#/definitions/84275395", + "description": "系统机构表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + } + }, + "additionalProperties": false, + "description": "系统机构表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "name", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled" + ] + } + } + }, + { + "name": "SysPlugin", + "displayName": "", + "id": "#/definitions/84275396", + "description": "系统动态插件表", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统动态插件表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark" + ] + } + } + }, + { + "name": "SysPos", + "displayName": "", + "id": "#/definitions/84275397", + "description": "系统职位表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统职位表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "code", + "orderNo", + "remark", + "status" + ] + } + } + }, + { + "name": "SysPrint", + "displayName": "", + "id": "#/definitions/84275398", + "description": "系统打印模板表", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "description": "系统打印模板表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "template", + "orderNo", + "status", + "remark" + ] + } + } + }, + { + "name": "SysRegion", + "displayName": "", + "id": "#/definitions/84275399", + "description": "系统行政地区表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + } + }, + "additionalProperties": false, + "description": "系统行政地区表", + "x-apifox-orders": [ + "id", + "pid", + "name", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children" + ] + } + } + }, + { + "name": "SysRole", + "displayName": "", + "id": "#/definitions/84275400", + "description": "系统角色表", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "系统角色表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "name", + "code", + "orderNo", + "dataScope", + "remark", + "status" + ] + } + } + }, + { + "name": "SysUser", + "displayName": "", + "id": "#/definitions/84275401", + "description": "系统用户表", + "schema": { + "jsonSchema": { + "required": [ + "account" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "account": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + } + }, + "additionalProperties": false, + "description": "系统用户表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "account", + "realName", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature" + ] + } + } + }, + { + "name": "SysUserExtOrg", + "displayName": "", + "id": "#/definitions/84275402", + "description": "系统用户扩展机构表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + } + }, + "additionalProperties": false, + "description": "系统用户扩展机构表", + "x-apifox-orders": [ + "id", + "userId", + "orgId", + "posId", + "jobNum", + "posLevel", + "joinDate" + ] + } + } + }, + { + "name": "SysWechatPay", + "displayName": "", + "id": "#/definitions/84275403", + "description": "系统微信支付表", + "schema": { + "jsonSchema": { + "required": [ + "appId", + "merchantId", + "outTradeNumber", + "transactionId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "merchantId": { + "minLength": 1, + "type": "string", + "description": "微信商户号" + }, + "appId": { + "minLength": 1, + "type": "string", + "description": "服务商AppId" + }, + "outTradeNumber": { + "minLength": 1, + "type": "string", + "description": "商户订单号" + }, + "transactionId": { + "minLength": 1, + "type": "string", + "description": "支付订单号" + }, + "tradeType": { + "type": [ + "string", + "null" + ], + "description": "交易类型" + }, + "tradeState": { + "type": [ + "string", + "null" + ], + "description": "交易状态" + }, + "tradeStateDescription": { + "type": [ + "string", + "null" + ], + "description": "交易状态描述" + }, + "bankType": { + "type": [ + "string", + "null" + ], + "description": "付款银行类型" + }, + "total": { + "type": "integer", + "description": "订单总金额", + "format": "int32" + }, + "payerTotal": { + "type": [ + "integer", + "null" + ], + "description": "用户支付金额", + "format": "int32" + }, + "successTime": { + "type": [ + "string", + "null" + ], + "description": "支付完成时间", + "format": "date-time" + }, + "expireTime": { + "type": [ + "string", + "null" + ], + "description": "交易结束时间", + "format": "date-time" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "商品描述" + }, + "scene": { + "type": [ + "string", + "null" + ], + "description": "场景信息" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + }, + "settlement": { + "type": [ + "string", + "null" + ], + "description": "结算信息" + }, + "notifyUrl": { + "type": [ + "string", + "null" + ], + "description": "回调通知地址" + }, + "remark": { + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "openId": { + "type": [ + "string", + "null" + ], + "description": "微信OpenId标识" + }, + "subMerchantId": { + "type": [ + "string", + "null" + ], + "description": "子商户号" + }, + "subAppId": { + "type": [ + "string", + "null" + ], + "description": "子商户AppId" + }, + "subOpenId": { + "type": [ + "string", + "null" + ], + "description": "子商户唯一标识" + } + }, + "additionalProperties": false, + "description": "系统微信支付表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "merchantId", + "appId", + "outTradeNumber", + "transactionId", + "tradeType", + "tradeState", + "tradeStateDescription", + "bankType", + "total", + "payerTotal", + "successTime", + "expireTime", + "description", + "scene", + "attachment", + "goodsTag", + "settlement", + "notifyUrl", + "remark", + "openId", + "subMerchantId", + "subAppId", + "subOpenId" + ] + } + } + }, + { + "name": "SysWechatUser", + "displayName": "", + "id": "#/definitions/84275404", + "description": "系统微信用户表", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "系统用户Id", + "format": "int64" + }, + "platformType": { + "$ref": "#/definitions/84275344" + }, + "openId": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "OpenId" + }, + "sessionKey": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "会话密钥" + }, + "unionId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "UnionId" + }, + "nickName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "mobile": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "sex": { + "type": [ + "integer", + "null" + ], + "description": "性别", + "format": "int32" + }, + "language": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "语言" + }, + "city": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "城市" + }, + "province": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "省" + }, + "country": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "国家" + }, + "accessToken": { + "type": [ + "string", + "null" + ], + "description": "AccessToken" + }, + "refreshToken": { + "type": [ + "string", + "null" + ], + "description": "RefreshToken" + }, + "expiresIn": { + "type": [ + "integer", + "null" + ], + "description": "过期时间", + "format": "int32" + }, + "scope": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "用户授权的作用域,使用逗号分隔" + } + }, + "additionalProperties": false, + "description": "系统微信用户表", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "platformType", + "openId", + "sessionKey", + "unionId", + "nickName", + "avatar", + "mobile", + "sex", + "language", + "city", + "province", + "country", + "accessToken", + "refreshToken", + "expiresIn", + "scope" + ] + } + } + }, + { + "name": "TableOutput", + "displayName": "", + "id": "#/definitions/84275405", + "description": "数据库表", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "表名(字母形式的)" + }, + "entityName": { + "type": [ + "string", + "null" + ], + "description": "实体名称" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "表名称描述(功能名)" + } + }, + "additionalProperties": false, + "description": "数据库表", + "x-apifox-orders": [ + "configId", + "tableName", + "entityName", + "createTime", + "updateTime", + "tableComment" + ] + } + } + }, + { + "name": "TenantIdInput", + "displayName": "", + "id": "#/definitions/84275406", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "tenantId": { + "type": "integer", + "description": "租户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "tenantId" + ] + } + } + }, + { + "name": "TenantInput", + "displayName": "", + "id": "#/definitions/84275407", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "TenantOutput", + "displayName": "", + "id": "#/definitions/84275408", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "租户名称" + }, + "adminAccount": { + "type": [ + "string", + "null" + ], + "description": "管理员账号" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "name", + "adminAccount", + "email", + "phone" + ] + } + } + }, + { + "name": "TenantTypeEnum", + "displayName": "", + "id": "#/definitions/84275409", + "description": "租户类型枚举
 Id隔离 Id = 0
 库隔离 Db = 1
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "description": "租户类型枚举
 Id隔离 Id = 0
 库隔离 Db = 1
", + "format": "int32" + } + } + }, + { + "name": "TenantUserInput", + "displayName": "", + "id": "#/definitions/84275410", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "userId" + ] + } + } + }, + { + "name": "TriggerStatus", + "displayName": "", + "id": "#/definitions/84275411", + "description": "
  Backlog = 0
  Ready = 1
  Running = 2
  Pause = 3
  Blocked = 4
  ErrorToReady = 5
  Archived = 6
  Panic = 7
  Overrun = 8
  Unoccupied = 9
  NotStart = 10
  Unknown = 11
  Unhandled = 12
", + "schema": { + "jsonSchema": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "type": "integer", + "description": "
  Backlog = 0
  Ready = 1
  Running = 2
  Pause = 3
  Blocked = 4
  ErrorToReady = 5
  Archived = 6
  Panic = 7
  Overrun = 8
  Unoccupied = 9
  NotStart = 10
  Unknown = 11
  Unhandled = 12
", + "format": "int32" + } + } + }, + { + "name": "UnlockLoginInput", + "displayName": "", + "id": "#/definitions/84275412", + "description": "解除登录锁定输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + } + }, + "additionalProperties": false, + "description": "解除登录锁定输入参数", + "x-apifox-orders": [ + "id" + ] + } + } + }, + { + "name": "UpdateCodeGenInput", + "displayName": "", + "id": "#/definitions/84275413", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "authorName": { + "type": [ + "string", + "null" + ], + "description": "作者姓名" + }, + "className": { + "type": [ + "string", + "null" + ], + "description": "类名" + }, + "tablePrefix": { + "type": [ + "string", + "null" + ], + "description": "是否移除表前缀" + }, + "configId": { + "type": [ + "string", + "null" + ], + "description": "库定位器名" + }, + "dbName": { + "type": [ + "string", + "null" + ], + "description": "数据库名(保留字段)" + }, + "dbType": { + "type": [ + "string", + "null" + ], + "description": "数据库类型" + }, + "connectionString": { + "type": [ + "string", + "null" + ], + "description": "数据库链接" + }, + "generateType": { + "type": [ + "string", + "null" + ], + "description": "生成方式" + }, + "tableName": { + "type": [ + "string", + "null" + ], + "description": "数据库表名" + }, + "nameSpace": { + "type": [ + "string", + "null" + ], + "description": "命名空间" + }, + "busName": { + "type": [ + "string", + "null" + ], + "description": "业务名(业务代码包名称)" + }, + "tableComment": { + "type": [ + "string", + "null" + ], + "description": "功能名(数据库表名称)" + }, + "menuApplication": { + "type": [ + "string", + "null" + ], + "description": "菜单应用分类(应用编码)" + }, + "menuPid": { + "type": "integer", + "description": "菜单父级", + "format": "int64" + }, + "printType": { + "type": [ + "string", + "null" + ], + "description": "支持打印类型" + }, + "printName": { + "type": [ + "string", + "null" + ], + "description": "打印模版名称" + }, + "id": { + "type": "integer", + "description": "代码生成器Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "authorName", + "className", + "tablePrefix", + "configId", + "dbName", + "dbType", + "connectionString", + "generateType", + "tableName", + "nameSpace", + "busName", + "tableComment", + "menuApplication", + "menuPid", + "printType", + "printName", + "id" + ] + } + } + }, + { + "name": "UpdateConfigInput", + "displayName": "", + "id": "#/definitions/84275414", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "value": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "属性值" + }, + "sysFlag": { + "$ref": "#/definitions/84275444" + }, + "groupCode": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "分组编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "value", + "sysFlag", + "groupCode", + "orderNo", + "remark" + ] + } + } + }, + { + "name": "UpdateDbColumnInput", + "displayName": "", + "id": "#/definitions/84275415", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "columnName": { + "type": [ + "string", + "null" + ] + }, + "oldColumnName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "columnName", + "oldColumnName", + "description" + ] + } + } + }, + { + "name": "UpdateDbTableInput", + "displayName": "", + "id": "#/definitions/84275416", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "configId": { + "type": [ + "string", + "null" + ] + }, + "tableName": { + "type": [ + "string", + "null" + ] + }, + "oldTableName": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "configId", + "tableName", + "oldTableName", + "description" + ] + } + } + }, + { + "name": "UpdateDictDataInput", + "displayName": "", + "id": "#/definitions/84275417", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "value" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "dictTypeId": { + "type": "integer", + "description": "字典类型Id", + "format": "int64" + }, + "value": { + "maxLength": 128, + "minLength": 1, + "type": "string", + "description": "值" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "tagType": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "显示样式-标签颜色" + }, + "styleSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Style(控制显示样式)" + }, + "classSetting": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "显示样式-Class(控制显示样式)" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 2048, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "extData": { + "type": [ + "string", + "null" + ], + "description": "拓展数据(保存业务功能的配置项)" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "dictTypeId", + "value", + "code", + "tagType", + "styleSetting", + "classSetting", + "orderNo", + "remark", + "extData", + "status" + ] + } + } + }, + { + "name": "UpdateDictTypeInput", + "displayName": "", + "id": "#/definitions/84275418", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "name": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "名称" + }, + "code": { + "maxLength": 64, + "minLength": 1, + "type": "string", + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275379" + }, + "description": "字典值集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "name", + "code", + "orderNo", + "remark", + "status", + "children" + ] + } + } + }, + { + "name": "UpdateJobDetailInput", + "displayName": "", + "id": "#/definitions/84275419", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "groupName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组名称" + }, + "jobType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "作业类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "concurrent": { + "type": "boolean", + "description": "是否并行执行" + }, + "includeAnnotations": { + "type": "boolean", + "description": "是否扫描特性触发器" + }, + "properties": { + "type": [ + "string", + "null" + ], + "description": "额外数据" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createType": { + "$ref": "#/definitions/84275308" + }, + "scriptCode": { + "type": [ + "string", + "null" + ], + "description": "脚本代码" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "groupName", + "jobType", + "assemblyName", + "description", + "concurrent", + "includeAnnotations", + "properties", + "updatedTime", + "createType", + "scriptCode", + "jobId" + ] + } + } + }, + { + "name": "UpdateJobTriggerInput", + "displayName": "", + "id": "#/definitions/84275420", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "jobId", + "triggerId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "triggerType": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "触发器类型FullName" + }, + "assemblyName": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "程序集Name" + }, + "args": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "参数" + }, + "description": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "描述信息" + }, + "status": { + "$ref": "#/definitions/84275411" + }, + "startTime": { + "type": [ + "string", + "null" + ], + "description": "起始时间", + "format": "date-time" + }, + "endTime": { + "type": [ + "string", + "null" + ], + "description": "结束时间", + "format": "date-time" + }, + "lastRunTime": { + "type": [ + "string", + "null" + ], + "description": "最近运行时间", + "format": "date-time" + }, + "nextRunTime": { + "type": [ + "string", + "null" + ], + "description": "下一次运行时间", + "format": "date-time" + }, + "numberOfRuns": { + "type": "integer", + "description": "触发次数", + "format": "int64" + }, + "maxNumberOfRuns": { + "type": "integer", + "description": "最大触发次数(0:不限制,n:N次)", + "format": "int64" + }, + "numberOfErrors": { + "type": "integer", + "description": "出错次数", + "format": "int64" + }, + "maxNumberOfErrors": { + "type": "integer", + "description": "最大出错次数(0:不限制,n:N次)", + "format": "int64" + }, + "numRetries": { + "type": "integer", + "description": "重试次数", + "format": "int32" + }, + "retryTimeout": { + "type": "integer", + "description": "重试间隔时间(ms)", + "format": "int32" + }, + "startNow": { + "type": "boolean", + "description": "是否立即启动" + }, + "runOnStart": { + "type": "boolean", + "description": "是否启动时执行一次" + }, + "resetOnlyOnce": { + "type": "boolean", + "description": "是否在启动时重置最大触发次数等于一次的作业" + }, + "updatedTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "jobId": { + "minLength": 2, + "type": "string", + "description": "作业Id" + }, + "triggerId": { + "minLength": 2, + "type": "string", + "description": "触发器Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "triggerType", + "assemblyName", + "args", + "description", + "status", + "startTime", + "endTime", + "lastRunTime", + "nextRunTime", + "numberOfRuns", + "maxNumberOfRuns", + "numberOfErrors", + "maxNumberOfErrors", + "numRetries", + "retryTimeout", + "startNow", + "runOnStart", + "resetOnlyOnce", + "updatedTime", + "jobId", + "triggerId" + ] + } + } + }, + { + "name": "UpdateMenuInput", + "displayName": "", + "id": "#/definitions/84275421", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "type": { + "$ref": "#/definitions/84275319" + }, + "name": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "路由名称" + }, + "path": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "路由地址" + }, + "component": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "组件路径" + }, + "redirect": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "重定向" + }, + "permission": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "权限标识" + }, + "icon": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "图标" + }, + "isIframe": { + "type": "boolean", + "description": "是否内嵌" + }, + "outLink": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "外链链接" + }, + "isHide": { + "type": "boolean", + "description": "是否隐藏" + }, + "isKeepAlive": { + "type": "boolean", + "description": "是否缓存" + }, + "isAffix": { + "type": "boolean", + "description": "是否固定" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275390" + }, + "description": "菜单子项" + }, + "title": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "pid", + "type", + "name", + "path", + "component", + "redirect", + "permission", + "icon", + "isIframe", + "outLink", + "isHide", + "isKeepAlive", + "isAffix", + "orderNo", + "status", + "remark", + "children", + "title" + ] + } + } + }, + { + "name": "UpdateNoticeInput", + "displayName": "", + "id": "#/definitions/84275422", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "content", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "title": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "标题" + }, + "content": { + "minLength": 1, + "type": "string", + "description": "内容" + }, + "type": { + "$ref": "#/definitions/84275325" + }, + "publicUserId": { + "type": "integer", + "description": "发布人Id", + "format": "int64" + }, + "publicUserName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "发布人姓名" + }, + "publicOrgId": { + "type": "integer", + "description": "发布机构Id", + "format": "int64" + }, + "publicOrgName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "发布机构名称" + }, + "publicTime": { + "type": [ + "string", + "null" + ], + "description": "发布时间", + "format": "date-time" + }, + "cancelTime": { + "type": [ + "string", + "null" + ], + "description": "撤回时间", + "format": "date-time" + }, + "status": { + "$ref": "#/definitions/84275324" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "title", + "content", + "type", + "publicUserId", + "publicUserName", + "publicOrgId", + "publicOrgName", + "publicTime", + "cancelTime", + "status" + ] + } + } + }, + { + "name": "UpdateOpenAccessInput", + "displayName": "", + "id": "#/definitions/84275423", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "accessKey", + "accessSecret", + "bindUserId" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "bindTenantId": { + "type": "integer", + "description": "绑定租户Id", + "format": "int64" + }, + "accessKey": { + "minLength": 1, + "type": "string", + "description": "身份标识" + }, + "accessSecret": { + "minLength": 1, + "type": "string", + "description": "密钥" + }, + "bindUserId": { + "type": "integer", + "description": "绑定用户Id", + "format": "int64" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "bindTenantId", + "accessKey", + "accessSecret", + "bindUserId" + ] + } + } + }, + { + "name": "UpdateOrgInput", + "displayName": "", + "id": "#/definitions/84275424", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "level": { + "type": [ + "integer", + "null" + ], + "description": "级别", + "format": "int32" + }, + "type": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "机构类型-数据字典" + }, + "directorId": { + "type": [ + "integer", + "null" + ], + "description": "负责人Id", + "format": "int64" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275395" + }, + "description": "机构子项" + }, + "disabled": { + "type": "boolean", + "description": "是否禁止选中" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "pid", + "code", + "level", + "type", + "directorId", + "orderNo", + "status", + "remark", + "children", + "disabled", + "name" + ] + } + } + }, + { + "name": "UpdatePluginInput", + "displayName": "", + "id": "#/definitions/84275425", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "csharpCode", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "csharpCode": { + "minLength": 1, + "type": "string", + "description": "C#代码" + }, + "assemblyName": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "程序集名称" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "csharpCode", + "assemblyName", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "UpdatePosInput", + "displayName": "", + "id": "#/definitions/84275426", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "remark", + "status", + "name" + ] + } + } + }, + { + "name": "UpdatePrintInput", + "displayName": "", + "id": "#/definitions/84275427", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name", + "template" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "template": { + "minLength": 1, + "type": "string", + "description": "打印模板" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "template", + "orderNo", + "status", + "remark", + "name" + ] + } + } + }, + { + "name": "UpdateRegionInput", + "displayName": "", + "id": "#/definitions/84275428", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "pid": { + "type": "integer", + "description": "父Id", + "format": "int64" + }, + "shortName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "简称" + }, + "mergerName": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "组合名" + }, + "code": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "行政代码" + }, + "zipCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "邮政编码" + }, + "cityCode": { + "maxLength": 6, + "type": [ + "string", + "null" + ], + "description": "区号" + }, + "level": { + "type": "integer", + "description": "层级", + "format": "int32" + }, + "pinYin": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "拼音" + }, + "lng": { + "type": "number", + "description": "经度", + "format": "float" + }, + "lat": { + "type": "number", + "description": "维度", + "format": "float" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "children": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275399" + }, + "description": "机构子项" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "pid", + "shortName", + "mergerName", + "code", + "zipCode", + "cityCode", + "level", + "pinYin", + "lng", + "lat", + "orderNo", + "remark", + "children", + "name" + ] + } + } + }, + { + "name": "UpdateRoleInput", + "displayName": "", + "id": "#/definitions/84275429", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "code": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "编码" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "dataScope": { + "$ref": "#/definitions/84275269" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "name": { + "minLength": 1, + "type": "string", + "description": "名称" + }, + "menuIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "菜单Id集合" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "code", + "orderNo", + "dataScope", + "remark", + "status", + "name", + "menuIdList" + ] + } + } + }, + { + "name": "UpdateTenantInput", + "displayName": "", + "id": "#/definitions/84275430", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "adminAccount", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "orgId": { + "type": "integer", + "description": "机构Id", + "format": "int64" + }, + "host": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "主机" + }, + "tenantType": { + "$ref": "#/definitions/84275409" + }, + "dbType": { + "$ref": "#/definitions/84275276" + }, + "connection": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "数据库连接" + }, + "configId": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "数据库标识" + }, + "slaveConnections": { + "type": [ + "string", + "null" + ], + "description": "从库连接/读写分离" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "remark": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "email": { + "type": [ + "string", + "null" + ], + "description": "电子邮箱" + }, + "phone": { + "type": [ + "string", + "null" + ], + "description": "电话" + }, + "name": { + "minLength": 2, + "type": "string", + "description": "租户名称" + }, + "adminAccount": { + "minLength": 3, + "type": "string", + "description": "租管账号" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "userId", + "orgId", + "host", + "tenantType", + "dbType", + "connection", + "configId", + "slaveConnections", + "orderNo", + "remark", + "status", + "email", + "phone", + "name", + "adminAccount" + ] + } + } + }, + { + "name": "UpdateUserInput", + "displayName": "", + "id": "#/definitions/84275431", + "description": "更新用户输入参数", + "schema": { + "jsonSchema": { + "required": [ + "account", + "realName" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "account": { + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "minLength": 1, + "type": "string", + "description": "真实姓名" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色集合" + }, + "extOrgIdList": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/84275402" + }, + "description": "扩展机构集合" + } + }, + "additionalProperties": false, + "description": "更新用户输入参数", + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "account", + "realName", + "roleIdList", + "extOrgIdList" + ] + } + } + }, + { + "name": "UploadFileFromBase64Input", + "displayName": "", + "id": "#/definitions/84275432", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "fileDataBase64": { + "type": [ + "string", + "null" + ], + "description": "文件内容" + }, + "contentType": { + "type": [ + "string", + "null" + ], + "description": "文件类型( \"image/jpeg\",)" + }, + "fileName": { + "type": [ + "string", + "null" + ], + "description": "文件名称" + }, + "path": { + "type": [ + "string", + "null" + ], + "description": "保存路径" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "fileDataBase64", + "contentType", + "fileName", + "path" + ] + } + } + }, + { + "name": "UserInput", + "displayName": "", + "id": "#/definitions/84275433", + "description": "设置用户状态输入参数", + "schema": { + "jsonSchema": { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "主键Id", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/84275375" + } + }, + "additionalProperties": false, + "description": "设置用户状态输入参数", + "x-apifox-orders": [ + "id", + "status" + ] + } + } + }, + { + "name": "UserOutput", + "displayName": "", + "id": "#/definitions/84275434", + "description": "", + "schema": { + "jsonSchema": { + "required": [ + "account" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "雪花Id", + "format": "int64" + }, + "createTime": { + "type": [ + "string", + "null" + ], + "description": "创建时间", + "format": "date-time" + }, + "updateTime": { + "type": [ + "string", + "null" + ], + "description": "更新时间", + "format": "date-time" + }, + "createUserId": { + "type": [ + "integer", + "null" + ], + "description": "创建者Id", + "format": "int64" + }, + "createUserName": { + "type": [ + "string", + "null" + ], + "description": "创建者姓名" + }, + "updateUserId": { + "type": [ + "integer", + "null" + ], + "description": "修改者Id", + "format": "int64" + }, + "updateUserName": { + "type": [ + "string", + "null" + ], + "description": "修改者姓名" + }, + "isDelete": { + "type": "boolean", + "description": "软删除" + }, + "tenantId": { + "type": [ + "integer", + "null" + ], + "description": "租户Id", + "format": "int64" + }, + "account": { + "maxLength": 32, + "minLength": 1, + "type": "string", + "description": "账号" + }, + "realName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "真实姓名" + }, + "nickName": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "avatar": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "头像" + }, + "sex": { + "$ref": "#/definitions/84275305" + }, + "age": { + "type": "integer", + "description": "年龄", + "format": "int32" + }, + "birthday": { + "type": [ + "string", + "null" + ], + "description": "出生日期", + "format": "date-time" + }, + "nation": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "民族" + }, + "phone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "手机号码" + }, + "cardType": { + "$ref": "#/definitions/84275258" + }, + "idCardNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "身份证号" + }, + "email": { + "maxLength": 64, + "type": [ + "string", + "null" + ], + "description": "邮箱" + }, + "address": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "地址" + }, + "cultureLevel": { + "$ref": "#/definitions/84275267" + }, + "politicalOutlook": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "政治面貌" + }, + "college": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "毕业院校" + }, + "officePhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "办公电话" + }, + "emergencyContact": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "紧急联系人" + }, + "emergencyPhone": { + "maxLength": 16, + "type": [ + "string", + "null" + ], + "description": "紧急联系人电话" + }, + "emergencyAddress": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "紧急联系人地址" + }, + "introduction": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "个人简介" + }, + "orderNo": { + "type": "integer", + "description": "排序", + "format": "int32" + }, + "status": { + "$ref": "#/definitions/84275375" + }, + "remark": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "备注" + }, + "accountType": { + "$ref": "#/definitions/84275167" + }, + "orgId": { + "type": "integer", + "description": "直属机构Id", + "format": "int64" + }, + "sysOrg": { + "$ref": "#/definitions/84275395" + }, + "managerUserId": { + "type": [ + "integer", + "null" + ], + "description": "直属主管Id", + "format": "int64" + }, + "posId": { + "type": "integer", + "description": "职位Id", + "format": "int64" + }, + "jobNum": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "工号" + }, + "posLevel": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职级" + }, + "posTitle": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "职称" + }, + "expertise": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "擅长领域" + }, + "officeZone": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公区域" + }, + "office": { + "maxLength": 32, + "type": [ + "string", + "null" + ], + "description": "办公室" + }, + "joinDate": { + "type": [ + "string", + "null" + ], + "description": "入职日期", + "format": "date-time" + }, + "lastLoginIp": { + "maxLength": 256, + "type": [ + "string", + "null" + ], + "description": "最新登录Ip" + }, + "lastLoginAddress": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录地点" + }, + "lastLoginTime": { + "type": [ + "string", + "null" + ], + "description": "最新登录时间", + "format": "date-time" + }, + "lastLoginDevice": { + "maxLength": 128, + "type": [ + "string", + "null" + ], + "description": "最新登录设备" + }, + "signature": { + "maxLength": 512, + "type": [ + "string", + "null" + ], + "description": "电子签名" + }, + "orgName": { + "type": [ + "string", + "null" + ], + "description": "机构名称" + }, + "posName": { + "type": [ + "string", + "null" + ], + "description": "职位名称" + }, + "roleName": { + "type": [ + "string", + "null" + ], + "description": "角色名称" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "id", + "createTime", + "updateTime", + "createUserId", + "createUserName", + "updateUserId", + "updateUserName", + "isDelete", + "tenantId", + "account", + "realName", + "nickName", + "avatar", + "sex", + "age", + "birthday", + "nation", + "phone", + "cardType", + "idCardNum", + "email", + "address", + "cultureLevel", + "politicalOutlook", + "college", + "officePhone", + "emergencyContact", + "emergencyPhone", + "emergencyAddress", + "introduction", + "orderNo", + "status", + "remark", + "accountType", + "orgId", + "sysOrg", + "managerUserId", + "posId", + "jobNum", + "posLevel", + "posTitle", + "expertise", + "officeZone", + "office", + "joinDate", + "lastLoginIp", + "lastLoginAddress", + "lastLoginTime", + "lastLoginDevice", + "signature", + "orgName", + "posName", + "roleName" + ] + } + } + }, + { + "name": "UserRoleInput", + "displayName": "", + "id": "#/definitions/84275435", + "description": "授权用户角色", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "用户Id", + "format": "int64" + }, + "roleIdList": { + "type": [ + "array", + "null" + ], + "items": { + "type": "integer", + "format": "int64" + }, + "description": "角色Id集合" + } + }, + "additionalProperties": false, + "description": "授权用户角色", + "x-apifox-orders": [ + "userId", + "roleIdList" + ] + } + } + }, + { + "name": "WechatPayOutput", + "displayName": "", + "id": "#/definitions/84275436", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ], + "description": "OpenId" + }, + "total": { + "type": "integer", + "description": "订单金额", + "format": "int32" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId", + "total", + "attachment", + "goodsTag" + ] + } + } + }, + { + "name": "WechatPayParaInput", + "displayName": "", + "id": "#/definitions/84275437", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "prepayId": { + "type": [ + "string", + "null" + ], + "description": "订单Id" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "prepayId" + ] + } + } + }, + { + "name": "WechatPayTransactionInput", + "displayName": "", + "id": "#/definitions/84275438", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ], + "description": "OpenId" + }, + "total": { + "type": "integer", + "description": "订单金额", + "format": "int32" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "商品描述" + }, + "attachment": { + "type": [ + "string", + "null" + ], + "description": "附加数据" + }, + "goodsTag": { + "type": [ + "string", + "null" + ], + "description": "优惠标记" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId", + "total", + "description", + "attachment", + "goodsTag" + ] + } + } + }, + { + "name": "WechatUserInput", + "displayName": "", + "id": "#/definitions/84275439", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "description": "当前页码", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "description": "页码容量", + "format": "int32" + }, + "field": { + "type": [ + "string", + "null" + ], + "description": "排序字段" + }, + "order": { + "type": [ + "string", + "null" + ], + "description": "排序方向" + }, + "descStr": { + "type": [ + "string", + "null" + ], + "description": "降序排序" + }, + "nickName": { + "type": [ + "string", + "null" + ], + "description": "昵称" + }, + "phoneNumber": { + "type": [ + "string", + "null" + ], + "description": "手机号码" + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "page", + "pageSize", + "field", + "order", + "descStr", + "nickName", + "phoneNumber" + ] + } + } + }, + { + "name": "WechatUserLogin", + "displayName": "", + "id": "#/definitions/84275440", + "description": "微信用户登录", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "openId": { + "minLength": 10, + "type": "string", + "description": "OpenId" + } + }, + "additionalProperties": false, + "description": "微信用户登录", + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxOpenIdLoginInput", + "displayName": "", + "id": "#/definitions/84275441", + "description": "微信小程序登录", + "schema": { + "jsonSchema": { + "required": [ + "openId" + ], + "type": "object", + "properties": { + "openId": { + "minLength": 10, + "type": "string", + "description": "OpenId" + } + }, + "additionalProperties": false, + "description": "微信小程序登录", + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxOpenIdOutput", + "displayName": "", + "id": "#/definitions/84275442", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "openId": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "openId" + ] + } + } + }, + { + "name": "WxPhoneOutput", + "displayName": "", + "id": "#/definitions/84275443", + "description": "", + "schema": { + "jsonSchema": { + "type": "object", + "properties": { + "phoneNumber": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "x-apifox-orders": [ + "phoneNumber" + ] + } + } + }, + { + "name": "YesNoEnum", + "displayName": "", + "id": "#/definitions/84275444", + "description": "是否枚举
 是 Y = 1
 否 N = 2
", + "schema": { + "jsonSchema": { + "enum": [ + 1, + 2 + ], + "type": "integer", + "description": "是否枚举
 是 Y = 1
 否 N = 2
", + "format": "int32" + } + } + } + ] + } + ] + } + ], + "responseCollection": [ + { + "id": 4088603, + "createdAt": "2024-02-29T09:40:10.000Z", + "updatedAt": "2024-02-29T09:40:10.000Z", + "deletedAt": null, + "name": "根目录", + "type": "root", + "description": "", + "children": [ + ], + "auth": { + }, + "projectId": 4084018, + "projectBranchId": 0, + "parentId": 0, + "createdById": 1145276, + "updatedById": 1145276, + "items": [ + ] + } + ], + "environments": [ + { + "name": "开发环境", + "parameters": { + "cookie": [ + ], + "query": [ + ], + "header": [ + ], + "body": [ + ] + }, + "variables": [ + ], + "type": "normal", + "visibility": "protected", + "ordering": 0, + "tags": [ + { + "name": "", + "color": "#9373EE" + } + ], + "id": "18499638", + "baseUrl": "http://localhost:5005", + "baseUrls": { + "default": "http://localhost:5005" + } + } + ], + "commonScripts": [ + ], + "globalVariables": [ + ], + "commonParameters": null, + "projectSetting": { + "id": "4084141", + "auth": { + "type": "bearer", + "bearer": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEzMDAwMDAwMDAxMTEsIlRlbmFudElkIjoxMzAwMDAwMDAwMDAxLCJBY2NvdW50IjoiYWRtaW4iLCJSZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsIkFjY291bnRUeXBlIjo4ODgsIk9yZ0lkIjoxMzAwMDAwMDAwMTAxLCJPcmdOYW1lIjoi5aSn5ZCN56eR5oqAIiwiT3JnVHlwZSI6IjEwMSIsImlhdCI6MTcwOTgwMjI5NiwibmJmIjoxNzA5ODAyMjk2LCJleHAiOjE3MTA0MDcwOTYsImlzcyI6IkFkbWluLk5FVCIsImF1ZCI6IkFkbWluLk5FVCJ9.w18JSugPFU50eMOkaLDwq2gWxPNO_3znLeUTgu2i7Ec" + } + }, + "servers": [ + { + "id": "default", + "name": "默认服务" + } + ], + "gateway": [ + ], + "language": "zh-CN", + "apiStatuses": [ + "developing", + "testing", + "released", + "deprecated" + ], + "mockSettings": { + }, + "preProcessors": [ + ], + "postProcessors": [ + ], + "advancedSettings": { + "responseValidate": false, + "enableJsonc": true, + "isDefaultUrlEncoding": 2, + "enableBigint": false, + "preferredHttpVersion": { + }, + "enableTestScenarioSetting": false, + "enableYAPICompatScript": false + }, + "initialDisabledMockIds": [ + ], + "cloudMock": { + "security": "free", + "enable": false, + "tokenKey": "apifoxToken" + } + }, + "projectAssociations": [ + ] +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs new file mode 100644 index 0000000..5affa51 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/AlipayErrorCodes.cs @@ -0,0 +1,143 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 支付宝支付错误码 +/// +public class AlipayErrorCode +{ + /// + /// 错误代码 + /// + public string Code { get; private set; } + + /// + /// 错误消息 + /// + public string Message { get; private set; } + + /// + /// 解决方案 + /// + public string Solution { get; private set; } + + /// + /// 错误码集 + /// + private static readonly List StatusCodes = + [ + new AlipayErrorCode { Code="SYSTEM_ERROR", Message="系统繁忙", Solution="可能是由于网络或者系统故障,请与技术人员联系以解决该问题。" }, + new AlipayErrorCode { Code="INVALID_PARAMETER", Message="参数有误或没有参数", Solution="请检查并确认查询请求参数合法性。" }, + new AlipayErrorCode { Code="AUTHORISE_NOT_MATCH", Message="授权失败,无法获取用户信息", Solution="检查账户与支付方关系主表关系,确认是否正确配置。" }, + new AlipayErrorCode { Code="BALANCE_IS_NOT_ENOUGH", Message="余额不足,建议尽快充值。后续登录电银通或支付宝,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值。商户后续登录电银通或支付宝,自主设置余额预警提醒功能或登录Alipay-资金管理->产品一览->右上角功能按钮进行设置。" }, + new AlipayErrorCode { Code="BIZ_UNIQUE_EXCEPTION", Message="商户订单号冲突。", Solution="商户订单号冲突。" }, + new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_RECEIVE", Message="账户异常被冻结,无法收款。", Solution="账户异常被冻结,无法收款。请询问支付宝热线95188" }, + new AlipayErrorCode { Code="BLOCK_USER_FORBIDDEN_SEND", Message="该账户被冻结,暂不可将资金转出。", Solution="该账户被冻结,暂不可将资金转出。" }, + new AlipayErrorCode { Code="CURRENCY_NOT_SUPPORT", Message="币种不支持", Solution="请查询您的结算币种需求所见币种,目前限于人民币/美元结算。" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_DC_R_ECEIVED", Message="收款方单日收款笔数超限", Solution="收款方向同一个收款账户单日只能收款固定的笔数,超过后让收款人第二天再收。" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_DM_AMOUNT", Message="日累计额度超限", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_DM_MAX_AMOUNT", Message="超出单日转账限额,如有疑问请询问支付宝热线95188", Solution="今日转账金额已上限,日累计额度需满5000元以上,可使用企业支付宝付款点【立即付款】申请,日累计额度需满5000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_ENT_SM_AMOUNT", Message="转账给企业用户超过单笔限额(默认10w)", Solution="1. 10w以下的快速转账给企业用户。2. 联系800电话协助修改,修改转账限额" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_MM_AMOUNT", Message="月累计金额超限", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_MMM_MAX_AMOUNT", Message="超出单月转账限额,如有疑问请询问支付宝热线95188", Solution="本月转账金额已上限,月转账额度需满10000元以上,可使用企业支付宝付款点【立即付款】申请,月转账额度需满10000元以上可点击【联系客服】咨询:转账到支付宝客户窗口" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_PERSONAL_SM_AMOUNT", Message="超出转账给个人支付宝账户的单笔限额", Solution="超出转账给个人支付宝账户的单笔限额" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_SM_AMOUNT", Message="单笔额度超限", Solution="请根据接入文档填写amount字段" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_SM_MIN_AMOUNT", Message="请求金额不能低于0.1元", Solution="请修改转账金额。" }, + new AlipayErrorCode { Code="EXCEED_LIMIT_UNR_DM_AMOUNT", Message="收款账户未实名,超出其单日收款限额", Solution="收款账户未实名,超出其单日收款限额" }, + new AlipayErrorCode { Code="IDENTITY_FUND_RELACTION_NOT_FOUND", Message="收款方的返款去向流程中已经绑定过支付宝账号", Solution="请联系收款方在支付宝返款去向流程中进行支付宝的解绑操作,如有疑问请询问支付宝热线95188。" }, + new AlipayErrorCode { Code="ILLEGAL_OPERATION", Message="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。", Solution="您的快捷请求违反了您已知的中间策略,已被拦截处理。请直接联系收款客户信息再次发起交易。" }, + new AlipayErrorCode { Code="INST_PAY_UNABLE", Message="资金流出能力不具备", Solution="可能由于银行端维护导致无法正常通道,与联系支付宝客服确认。" }, + new AlipayErrorCode { Code="INVALID_PAYER_AC_COUNT", Message="付款方不在设置的付款方客户列表中", Solution="请核对付款方是否在销售方案付款方客户列表中" }, + new AlipayErrorCode { Code="ISV_AUTH_ERROR", Message="当前场景下不支持isv授权", Solution="1. 检查商户产品和场景范围,当前场景下不支持isv授权权。2. 去删除isv授权模板,改为自调用。" }, + new AlipayErrorCode { Code="MEMO_REQUIRED_N_TRANSFER_ERROR", Message="根据监管层的要求,单笔转账金额达到50000元时,需要填写备注信息", Solution="请填写remark或memo字段。" }, + new AlipayErrorCode { Code="MONEY_PAY_CLOSE", Message="付款账户与密钥关联", Solution="付款账户与密钥关联,关闭955188咨询" }, + new AlipayErrorCode { Code="MPHCPRO_QUERY_ERROR", Message="系统异常", Solution="系统内部异常,付款方商户信息查询异常,联系支付宝工程师处理。" }, + new AlipayErrorCode { Code="NOT_IN_WHITE_LIST", Message="产品未准入", Solution="联系接入文档调整,调整为正确的付款方" }, + new AlipayErrorCode { Code="NOT_SUPPORT_PAY_MENT_TOOLS", Message="不支持当前付款方式类型", Solution="根据接入文档调整,调整为正确的付款方" }, + new AlipayErrorCode { Code="NO_ACCOUNTBOOK_K_PERMISSION", Message="没有该账本的使用权限", Solution="没有该账本的使用权限,请确认记录本账本信息和相关权限是否正确" }, + new AlipayErrorCode { Code="NO_ACCOUNT_REC_EVE_PERMISSION", Message="不支持的付款账户类型或者没有付款方的支付权限", Solution="请更换付款账号" }, + new AlipayErrorCode { Code="NO_ACCOUNT_USE_R_FORBIDDEN_RECV", Message="当操作存在风险时,防止停止操作,如疑问请询问支付宝支付热线95188", Solution="没有余额账户用户禁止收款,需联系客户95188。" }, + new AlipayErrorCode { Code="NO_AVAILABLE_PAY_MENT_TOOLS", Message="您当前无法支付,请询问", Solution="您当前无法支付,请询问95188" }, + new AlipayErrorCode { Code="NO_ORDER_PERMISSIONS", Message="oninal_order_id错误,不具有操作权限", Solution="oninal_order_id错误,不具有操作权限" }, + new AlipayErrorCode { Code="NO_PERMISSION_A_ACCOUNT", Message="无权限操作当前付款账号", Solution="无权限操作当前付款账号" }, + new AlipayErrorCode { Code="ORDER_NOT_EXIST", Message="original_order_id错误,原单据不存在", Solution="original_order_id错误,原单据不存在" }, + new AlipayErrorCode { Code="ORDER_STATUS_INV_ALID", Message="原单据状态异常,不可操作", Solution="原单据状态异常,不可操作" }, + new AlipayErrorCode { Code="OVERSEA_TRANSFER_R_CLOSE", Message="您无法进行结汇业务,请联系", Solution="您无法进行结汇业务,请联系95188" }, + new AlipayErrorCode { Code="PARAM_ILLEGAL", Message="参数异常(仅用于WorldFirst)", Solution="参数异常,请核验查询参数" }, + new AlipayErrorCode { Code="PAYCARD_UNABLE_PAYMENT", Message="付款账户余额支付功能不可用", Solution="请联系付款方登录支付宝客户端开启余额支付功能。" }, + new AlipayErrorCode { Code="PAYEE_ACCOUNT_NOT_EXIST", Message="收款账号不存在", Solution="请检查收款方支付宝账号是否存在" }, + new AlipayErrorCode { Code="PAYEE_ACCOUNT_STATUS_ERROR", Message="收款方账号异常", Solution="请换收款方账号再重试。" }, + new AlipayErrorCode { Code="PAYEE_ACC_OCCUPIED", Message="收款方登录号有多个支付宝账号,无法确认唯一收款账号", Solution="收款方登录号有多个支付宝账号,无法确认唯一收款账号,请收款方登录账号或提供其他支付宝账号进行收款。" }, + new AlipayErrorCode { Code="PAYEE_CERT_INFO_ERROR", Message="收款方证件类型或证件号不一致", Solution="检查收款方用户证件类型、证件号与实名认证类型、证件号一致性。" }, + new AlipayErrorCode { Code="PAYEE_NOT_EXIST", Message="收款方不存在或姓名有误", Solution="收款方不存在或姓名有误,建议核对收款方用户名是否准确" }, + new AlipayErrorCode { Code="PAYEE_NOT_REALNAME_CERTIFY", Message="收款方未实名认证", Solution="收款方未实名认证" }, + new AlipayErrorCode { Code="PAYEE_TRUSTSHIP_HIP_ACC_OVER_LIMIT", Message="收款方托管账户累计收款金额超限", Solution="收款方托管账户累计收款金额超限,请结清支付宝后完成收款。" }, + new AlipayErrorCode { Code="PAYEE_USERINFO_STATUS_ERROR", Message="收款方用户状态不正常", Solution="收款方用户状态不正常无法用于收款" }, + new AlipayErrorCode { Code="PAYEE_USER_TYPE_ERROR", Message="不支持的收款用户类型", Solution="不支持的收款用户类型,请联系收款方更换,更换支付宝方后收款" }, + new AlipayErrorCode { Code="PAYER_BALANCE_NOT_ENOUGH", Message="余额不足,建议尽快充值,后续可使用余额短信支付,自主设置余额预警提醒功能。", Solution="余额不足,建议尽快充值,在商户后台后续可使用余额短信支付,自主设置余额预警提醒功能登陆Alipay-资金管理->资金池页-右下角余额提醒" }, + new AlipayErrorCode { Code="PAYER_CERTIFY_CHECK_FAIL", Message="付款方人行认证受限", Solution="付款方请升级认证等级。" }, + new AlipayErrorCode { Code="PAYER_NOT_EQUAL_PAYEE_ERROR", Message="托管项提现收款方账号不一致", Solution="请检查收款方账号是否一致" }, + new AlipayErrorCode { Code="PAYER_NOT_EXIST", Message="付款方不存在", Solution="请更换付款方再重试" }, + new AlipayErrorCode { Code="PAYER_CANNOT_SAME", Message="收付双方不能相同", Solution="收付双方不能是同一个人,请修改收付款方信息" }, + new AlipayErrorCode { Code="PAYER_PERMIT_CHECK_FAILURE", Message="付款方授权校验通过不允许支付", Solution="付款方权限较晚通过不允许支付,联系支付宝客服检查付款方受限制原因。" }, + new AlipayErrorCode { Code="PAYER_REQUESTER_RELATION_INVALID", Message="付款方和请求方用户不一致", Solution="付款方和请求方用户不一致,存在归户风险" }, + new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款账号状态异常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" }, + new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方用户状态不正常", Solution="请检查付款方是否进行了自助挂失,如果需要,请联系支付宝客服检查付款方状态是否正常。" }, + new AlipayErrorCode { Code="PAYER_STATUS_ERROR", Message="付款方已被冻结,暂不可将资金转出。", Solution="1. 联系支付宝客户询问用户冻结原因以及协助解冻办法状态。" }, + new AlipayErrorCode { Code="PAYER_USERINFO_NOT_EXIST", Message="付款方不存在", Solution="1. 检查付款方是否已销户,若销户请联系销户后重新发起业务。2. 检查参入是否有误。" }, + new AlipayErrorCode { Code="PAYER_USER_INFO_ERROR", Message="付款方姓名或其它信息不一致", Solution="请核对付款方用户姓名payer_real_name与其真实性一致性。" }, + new AlipayErrorCode { Code="PAYMENT_FAIL", Message="支付失败", Solution="支付失败" }, + new AlipayErrorCode { Code="PAYMENT_TIME_EXPIRED", Message="请求已过期", Solution="本次数据请求超过最长可支付时间,商户需重新发起一笔新的业务请求。" }, + new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_AMAL_CERT_EXPIRED", Message="由于收款人登记的身份证件已过期导致收款受限,请更新证件信息。", Solution="根据监管部门的要求,需要付款方更新身份信息" }, + new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_IDENTITY_THEFT", Message="您的账户存在身份冒用风险,请进行身份信息解除限制。", Solution="您的账户存在身份冒用风险,请进行身份信息解除限制。" }, + new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" }, + new AlipayErrorCode { Code="PERMIT_CHECK_PERMISSION_LIMITED", Message="根据监管部门的要求,请补全您的身份信息解除限制", Solution="根据监管部门的要求,请补全您的身份信息解除限制" }, + new AlipayErrorCode { Code="PERMIT_CHECK_RECEIVE_LIMIT", Message="您的账户限收款,请咨询95188电话咨询", Solution="您的账户限收款,请咨询95188电话咨询" }, + new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服", Solution="收款方账户被列为异常账户,账户收款功能被限制,请收款方联系客服" }, + new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户被限制收款,请收款方联系客服", Solution="收款方账户被限制收款,请收款方联系客服" }, + new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款额度已上限,请收款方联系客服咨询详情。", Solution="收款方账户收款额度已上限,请收款方联系客服咨询详情。" }, + new AlipayErrorCode { Code="PERMIT_LIMIT_PAYEE", Message="收款方账户收款功能暂时无法使用", Solution="收款方账户收款功能暂时无法使用" }, + new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="收款方未完善身份证信息或未开立余额账户,无法收款", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" }, + new AlipayErrorCode { Code="PERMIT_NOT_BANK_LIMIT_PAYEE", Message="当前操作存在风险,不支持转账,如无疑问请拨打支付宝服务热线95188", Solution="根据监管部门的要求,收款方未完善身份证信息或未开立余额账户,无法收款" }, + new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PERMIT_PAYER_FORBIDDEN", Message="根据监管部门的要求,需要收款方补充身份信息才能继续操作", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下今日余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PERM_PAY_CUSTOM_ER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="同一主体下当月余额付款额度已上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PERM_PAY_USER_DAILY_QUOTA_ORG_BALANCE_LIMIT", Message="该账户今日余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PERM_PAY_USER_MONTH_QUOTA_ORG_BALANCE_LIMIT", Message="该账户当月余额付款额度已达上限。", Solution="今日余额特色金额已达上限,请使用企业支付宝账户点击【自助限额】申请,若限额申请失败请点击【联系客服】咨询:账户额度提升申请" }, + new AlipayErrorCode { Code="PROCESS_FAIL", Message="资金操作失败(仅用于WorldFirst)", Solution="资金操作失败,目前用于结汇入境场景,需要支付宝技术介入排查" }, + new AlipayErrorCode { Code="PRODUCT_NOT_SIGN", Message="产品未签约", Solution="请签约产品之后再使用该接口" }, + new AlipayErrorCode { Code="RELEASE_USER_FOR_BBIDEN_RECIEVE", Message="收款账号存在异常,禁止收款,如有疑问请电话咨询95188", Solution="联系收款用户,更换支付宝账号后收款" }, + new AlipayErrorCode { Code="REMARK_HAS_SENSITIVE_WORD", Message="转账备注包含敏感词,请修改备注文案后重试", Solution="转账备注包含敏感词,请修改备注文案后重试" }, + new AlipayErrorCode { Code="REQUEST_PROCESSING", Message="系统处理中,请稍后再试", Solution="系统并发处理中,建议调整相关接口的调用频率,减少并发请求,可稍后再重试" }, + new AlipayErrorCode { Code="RESOURCE_LIMIT_EXCEED", Message="请求超过资源限制", Solution="发起请求并发数超出支付宝处理能力,请降低请求并发" }, + new AlipayErrorCode { Code="SECURITY_CHECK_FAILED", Message="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188", Solution="安全检查失败。当前操作存在风险,请停止操作,如有疑问请咨询服务热线95188" }, + new AlipayErrorCode { Code="SIGN_AGREEMENT_NO_INCONSISTENT", Message="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配。", Solution="签名方和协议主体不一致。请确认payer_info.ext_info.agreement_no和sign_data.ori_app_id是否匹配,再重试。" }, + new AlipayErrorCode { Code="SIGN_INVALID", Message="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。", Solution="签名非法,验签不通过。请确认签名信息是否被篡改以及签名方签名格式是否正确。" }, + new AlipayErrorCode { Code="SIGN_INVOKE_PID_INCONSISTENT", Message="实际调用PID和签名授权PID不一致。请确认实际调用PID和sign_data.partner_id是否一致。", Solution="请确认实际调用PID和sign_data.partner_id是否一致,一致后再重试。" }, + new AlipayErrorCode { Code="SIGN_NOT_ALLOW_SKIP", Message="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。", Solution="该场景强制验签,不允许跳过。请按要求上报sign_data后重试。" }, + new AlipayErrorCode { Code="SIGN_PARAM_INVALID", Message="验签参数非法。请确认sign_data参数是否正确。", Solution="验签参数非法,请确认sign_data参数是否正确。" }, + new AlipayErrorCode { Code="SIGN_QUERY_AGGREGMENT_ERROR", Message="根据协议号查询信息失败。请确认payer_info.ext_info.agreement_no是否正确。", Solution="请确认上报协议号payer_info.ext_info.agreement_no内容正确后再重试。" }, + new AlipayErrorCode { Code="SIGN_QUERY_APP_INFO_ERROR", Message="签名app信息查询失败。请确认sign_data.ori_app_id是否正确。", Solution="请确认签名方sign_data.ori_app_id是否正确,信息正确后再重试。" }, + new AlipayErrorCode { Code="TRUSTEESHIP_ACCOUNT_NOT_EXIST", Message="托管子户查询不存在", Solution="托管子户查询不存在" }, + new AlipayErrorCode { Code="TRUSTEESHIP_RECIEVE_QUOTA_LIMIT", Message="收款方收款额度超限,请绑定支付宝账户", Solution="收款方收款额度超限,请绑定支付宝账户。" }, + new AlipayErrorCode { Code="USER_AGREEMENT_VERIFY_FAIL", Message="用户协议校验失败", Solution="确认入参中协议号是否正确" }, + new AlipayErrorCode { Code="USER_NOT_EXIST", Message="用户不存在(仅用于WorldFirst)", Solution="用户不存在,请检查收付款方信息" }, + new AlipayErrorCode { Code="USER_RISK_FREEZE", Message="账户异常被冻结,无法付款,请咨询支付宝客服95188", Solution="账户异常被冻结,无法付款,请咨询支付宝客服95188" } + ]; + + /// + /// 根据错误码获取错误信息 + /// + /// + /// + public static AlipayErrorCode Get(string code) + { + return StatusCodes.FirstOrDefault(u => u.Code.EqualIgnoreCase(code)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs new file mode 100644 index 0000000..c70d45e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/Dto/AlipayInput.cs @@ -0,0 +1,180 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api.Domain; +using Newtonsoft.Json; +using System.Text.Json.Serialization; + +namespace Admin.NET.Core.Service; + +public class AlipayFundTransUniTransferInput +{ + /// + /// 用户ID + /// + public long UserId { get; set; } + + /// + /// 商户AppId + /// + public string AppId { get; set; } + + /// + /// 商家订单号 + /// + public string OutBizNo { get; set; } + + /// + /// 转账金额 + /// + public decimal TransAmount { get; set; } + + /// + /// 业务标题 + /// + public string OrderTitle { get; set; } + + /// + /// 备注 + /// + public string Remark { get; set; } + + /// + /// 是否展示付款方别名 + /// + public bool PayerShowNameUseAlias { get; set; } + + /// + /// 收款方证件类型 + /// + public AlipayCertTypeEnum? CertType { get; set; } + + /// + /// 收款方证件号码,条件必填 + /// + public string CertNo { get; set; } + + /// + /// 收款方身份标识 + /// + public string Identity { get; set; } + + /// + /// 收款方真实姓名 + /// + public string Name { get; set; } + + /// + /// 收款方身份标识类型 + /// + public AlipayIdentityTypeEnum? IdentityType { get; set; } +} + +/// +/// 统一收单下单并支付页面接口输入参数 +/// +public class AlipayTradePagePayInput +{ + /// + /// 商户订单号 + /// + [Required(ErrorMessage = "商户订单号不能为空")] + public string OutTradeNo { get; set; } + + /// + /// 订单总金额 + /// + [Required(ErrorMessage = "订单总金额不能为空")] + public string TotalAmount { get; set; } + + /// + /// 订单标题 + /// + [Required(ErrorMessage = "订单标题不能为空")] + public string Subject { get; set; } + + /// + /// + /// + public string Body { get; set; } + + /// + /// 超时时间 + /// + public string TimeoutExpress { get; set; } + + /// + /// 二维码宽度 + /// + [Required(ErrorMessage = "二维码宽度不能为空")] + public int? QrcodeWidth { get; set; } + + /// + /// 业务参数 + /// + public ExtendParams ExtendParams { get; set; } + + /// + /// 商户业务数据 + /// + public Dictionary BusinessParams { get; set; } + + /// + /// 开票信息 + /// + public InvoiceInfo InvoiceInfo { get; set; } + + /// + /// 外部买家信息 + /// + public ExtUserInfo ExtUserInfo { get; set; } +} + +public class AlipayPreCreateInput +{ + /// + /// 商户订单号 + /// + [Required(ErrorMessage = "商户订单号不能为空")] + public string OutTradeNo { get; set; } + + /// + /// 订单总金额 + /// + [Required(ErrorMessage = "订单总金额不能为空")] + public string TotalAmount { get; set; } + + /// + /// 订单标题 + /// + [Required(ErrorMessage = "订单标题不能为空")] + public string Subject { get; set; } + + /// + /// 超时时间 + /// + public string TimeoutExpress { get; set; } +} + +public class AlipayAuthInfoInput +{ + /// + /// 用户Id + /// + + [JsonProperty("user_id")] + [JsonPropertyName("user_id")] + [FromQuery(Name = "user_id")] + public string UserId { get; set; } + + /// + /// 授权码 + /// + [JsonProperty("auth_code")] + [JsonPropertyName("auth_code")] + [FromQuery(Name = "auth_code")] + public string AuthCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs new file mode 100644 index 0000000..90ded5f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/IAlipayNotify.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api.Response; + +namespace Admin.NET.Core.Service; + +/// +/// 支付宝回调接口 +/// +public abstract class IAlipayNotify +{ + /// + /// 充值回调方法 + /// + /// 交易类型 + /// 交易id + public abstract bool TopUpCallback(long type, long tradeNo); + + /// + /// 扫码回调 + /// + /// + /// + /// + /// + public abstract bool ScanCallback(long type, long userId, AlipayUserInfoShareResponse response); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/SysAlipayService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/SysAlipayService.cs new file mode 100644 index 0000000..8d727a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Alipay/SysAlipayService.cs @@ -0,0 +1,271 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aop.Api; +using Aop.Api.Domain; +using Aop.Api.Request; +using Aop.Api.Response; +using Aop.Api.Util; +using Microsoft.AspNetCore.Hosting; +using NewLife.Reflection; + +namespace Admin.NET.Core.Service; + +/// +/// 支付宝支付服务 🧩 +/// +[ApiDescriptionSettings(Order = 240)] +public class SysAlipayService : IDynamicApiController, ITransient +{ + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly SysConfigService _sysConfigService; + private readonly List _alipayClientList; + private readonly IHttpContextAccessor _httpContext; + private readonly AlipayOptions _option; + private readonly ISqlSugarClient _db; + + public SysAlipayService( + ISqlSugarClient db, + IHttpContextAccessor httpContext, + SysConfigService sysConfigService, + IWebHostEnvironment webHostEnvironment, + IOptions alipayOptions) + { + _db = db; + _httpContext = httpContext; + _sysConfigService = sysConfigService; + _option = alipayOptions.Value; + _webHostEnvironment = webHostEnvironment; + + // 初始化支付宝客户端列表 + _alipayClientList = []; + foreach (var account in _option.AccountList) _alipayClientList.Add(_option.GetClient(account)); + } + + /// + /// 获取授权信息 🔖 + /// + /// + /// + [NonUnify] + [AllowAnonymous] + [DisplayName("获取授权信息")] + [ApiDescriptionSettings(Name = "AuthInfo"), HttpGet] + public ActionResult GetAuthInfo([FromQuery] AlipayAuthInfoInput input) + { + var type = input.UserId?.Split('-').FirstOrDefault().ToInt(); + var userId = input.UserId?.Split('-').LastOrDefault().ToLong(); + var account = _option.AccountList.FirstOrDefault(); + var alipayClient = _alipayClientList.First(); + + // 当前网页接口地址 + var currentUrl = $"{_option.AppAuthUrl}{_httpContext.HttpContext!.Request.Path}?userId={input.UserId}"; + if (string.IsNullOrEmpty(input.AuthCode)) + { + // 重新授权 + var url = $"{_option.AuthUrl}?app_id={account!.AppId}&scope=auth_user&redirect_uri={currentUrl}"; + return new RedirectResult(url); + } + + // 组装授权请求参数 + AlipaySystemOauthTokenRequest request = new() + { + GrantType = AlipayConst.GrantType, + Code = input.AuthCode + }; + AlipaySystemOauthTokenResponse response = alipayClient.CertificateExecute(request); + + // token换取用户信息 + AlipayUserInfoShareRequest infoShareRequest = new(); + AlipayUserInfoShareResponse info = alipayClient.CertificateExecute(infoShareRequest, response.AccessToken); + + // 记录授权信息 + var entity = _db.Queryable().First(u => + (!string.IsNullOrWhiteSpace(u.UserId) && u.UserId == info.UserId) || + (!string.IsNullOrWhiteSpace(u.OpenId) && u.OpenId == info.OpenId)) ?? new(); + entity.Copy(info, excludes: [nameof(SysAlipayAuthInfo.Gender), nameof(SysAlipayAuthInfo.Age)]); + entity.Age = int.Parse(info.Age); + entity.Gender = info.Gender switch + { + "m" => GenderEnum.Male, + "f" => GenderEnum.Female, + _ => GenderEnum.Unknown + }; + entity.AppId = account!.AppId; + if (entity.Id <= 0) _db.Insertable(entity).ExecuteCommand(); + else _db.Updateable(entity).ExecuteCommand(); + + // 执行完,重定向到指定界面 + //var authPageUrl = _sysConfigService.GetConfigValueByCode(ConfigConst.AlipayAuthPageUrl + type).Result; + //return new RedirectResult(authPageUrl); + return new RedirectResult(_option.AppAuthUrl + "/index.html"); + } + + /// + /// 支付回调 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("支付回调")] + [ApiDescriptionSettings(Name = "Notify"), HttpPost] + public string Notify() + { + SortedDictionary sorted = []; + foreach (string key in _httpContext.HttpContext!.Request.Form.Keys) + sorted.Add(key, _httpContext.HttpContext.Request.Form[key]); + + var account = _option.AccountList.FirstOrDefault(); + string alipayPublicKey = Path.Combine(_webHostEnvironment.ContentRootPath, account!.AlipayPublicCertPath!.Replace('/', '\\').TrimStart('\\')); + bool signVerified = AlipaySignature.RSACertCheckV1(sorted, alipayPublicKey, "UTF-8", account.SignType); // 调用SDK验证签名 + if (!signVerified) throw Oops.Oh("交易失败"); + + // 更新交易记录 + var outTradeNo = sorted.GetValueOrDefault("out_trade_no"); + var transaction = _db.Queryable().First(x => x.OutTradeNo == outTradeNo) ?? throw Oops.Oh("交易记录不存在"); + transaction.TradeNo = sorted.GetValueOrDefault("trade_no"); + transaction.TradeStatus = sorted.GetValueOrDefault("trade_status"); + transaction.FinishTime = sorted.ContainsKey("gmt_payment") ? DateTime.Parse(sorted.GetValueOrDefault("gmt_payment")) : null; + transaction.BuyerLogonId = sorted.GetValueOrDefault("buyer_logon_id"); + transaction.BuyerUserId = sorted.GetValueOrDefault("buyer_user_id"); + transaction.SellerUserId = sorted.GetValueOrDefault("seller_id"); + transaction.Remark = sorted.GetValueOrDefault("remark"); + _db.Updateable(transaction).ExecuteCommand(); + + return "success"; + } + + /// + /// 统一收单下单并支付页面接口 🔖 + /// + /// + /// + [DisplayName("统一收单下单并支付页面接口")] + [ApiDescriptionSettings(Name = "AlipayTradePagePay"), HttpPost] + public string AlipayTradePagePay(AlipayTradePagePayInput input) + { + // 创建交易记录,状态为等待支付 + var transactionRecord = new SysAlipayTransaction + { + AppId = _option.AccountList.First().AppId, + OutTradeNo = input.OutTradeNo, + TotalAmount = input.TotalAmount.ToDecimal(), + TradeStatus = "WAIT_PAY", // 等待支付 + CreateTime = DateTime.Now, + Subject = input.Subject, + Body = input.Body, + Remark = "等待用户支付" + }; + _db.Insertable(transactionRecord).ExecuteCommand(); + + // 设置支付页面请求,并组装业务参数model,设置异步通知接收地址 + AlipayTradeWapPayRequest request = new(); + request.SetBizModel(new AlipayTradeWapPayModel() + { + Subject = input.Subject, + OutTradeNo = input.OutTradeNo, + TotalAmount = input.TotalAmount, + Body = input.Body, + ProductCode = "QUICK_WAP_WAY", + TimeExpire = input.TimeoutExpress + }); + request.SetNotifyUrl(_option.NotifyUrl); + + var alipayClient = _alipayClientList.First(); + var response = alipayClient.SdkExecute(request); + if (response.IsError) throw Oops.Oh(response.SubMsg); + return $"{_option.ServerUrl}?{response.Body}"; + } + + /// + /// 交易预创建 🔖 + /// + /// + /// + [DisplayName("交易预创建")] + [ApiDescriptionSettings(Name = "AlipayPreCreate"), HttpPost] + public string AlipayPreCreate(AlipayPreCreateInput input) + { + // 创建交易记录,状态为等待支付 + var transactionRecord = new SysAlipayTransaction + { + AppId = _option.AccountList.First().AppId, + OutTradeNo = input.OutTradeNo, + TotalAmount = input.TotalAmount.ToDecimal(), + TradeStatus = "WAIT_PAY", // 等待支付 + CreateTime = DateTime.Now, + Subject = input.Subject, + Remark = "等待用户支付" + }; + _db.Insertable(transactionRecord).ExecuteCommand(); + + // 设置异步通知接收地址,并组装业务参数model + AlipayTradePrecreateRequest request = new(); + request.SetNotifyUrl(_option.NotifyUrl); + request.SetBizModel(new AlipayTradePrecreateModel() + { + Subject = input.Subject, + OutTradeNo = input.OutTradeNo, + TotalAmount = input.TotalAmount, + TimeoutExpress = input.TimeoutExpress + }); + + var alipayClient = _alipayClientList.First(); + var response = alipayClient.CertificateExecute(request); + if (response.IsError) throw Oops.Oh(response.SubMsg); + return response.QrCode; + } + + /// + /// 单笔转账到支付宝账户 + /// https://opendocs.alipay.com/open/62987723_alipay.fund.trans.uni.transfer + /// + [NonAction] + public async Task Transfer(AlipayFundTransUniTransferInput input) + { + var account = _option.AccountList.FirstOrDefault(u => u.AppId == input.AppId) ?? throw Oops.Oh("未找到商户支付宝账号"); + var alipayClient = _option.GetClient(account); + + // 构造请求参数以调用接口 + AlipayFundTransUniTransferRequest request = new(); + AlipayFundTransUniTransferModel model = new() + { + BizScene = AlipayConst.BizScene, + ProductCode = AlipayConst.ProductCode, + OutBizNo = input.OutBizNo, // 商家订单 + TransAmount = $"{input.TransAmount}:F2", // 订单总金额 + OrderTitle = input.OrderTitle, // 业务标题 + Remark = input.Remark, // 业务备注 + PayeeInfo = new() // 收款方信息 + { + CertType = input.CertType?.ToString(), + CertNo = input.CertNo, + Identity = input.Identity, + Name = input.Name, + IdentityType = input.IdentityType.ToString() + }, + BusinessParams = input.PayerShowNameUseAlias ? "{\"payer_show_name_use_alias\":\"true\"}" : null + }; + + request.SetBizModel(model); + var response = alipayClient.CertificateExecute(request); + + // 保存转账记录 + await _db.Insertable(new SysAlipayTransaction + { + UserId = input.UserId, + AppId = input.AppId, + TradeNo = response.OrderId, + OutTradeNo = input.OutBizNo, + TotalAmount = response.Amount.ToDecimal(), + TradeStatus = response.Code == "10000" ? "SUCCESS" : "FAILED", + Subject = input.OrderTitle, + ErrorInfo = response.SubMsg, + Remark = input.Remark + }).ExecuteCommandAsync(); + + return response; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs new file mode 100644 index 0000000..c807604 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录参数 +/// +public class LoginInput +{ + /// + /// 账号 + /// + /// admin + [Required(ErrorMessage = "账号不能为空"), MinLength(2, ErrorMessage = "账号不能少于2个字符")] + public string Account { get; set; } + + /// + /// 密码 + /// + /// 123456 + [Required(ErrorMessage = "密码不能为空"), MinLength(3, ErrorMessage = "密码不能少于3个字符")] + public string Password { get; set; } + + /// + /// 租户 + /// + public long? TenantId { get; set; } + + /// + /// 验证码Id + /// + public long CodeId { get; set; } + + /// + /// 验证码 + /// + public string Code { get; set; } +} + +public class LoginPhoneInput +{ + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")] + public string Code { get; set; } + + /// + /// 租户 + /// + [Required(ErrorMessage = "租户不能为空")] + public long? TenantId { get; set; } +} + +/// +/// 用户注册输入参数 +/// +public class UserRegistrationInput +{ + /// + /// 真实姓名 + /// + [Required(ErrorMessage = "真实姓名不能为空"), MinLength(2, ErrorMessage = "真实姓名不能少于2个字符")] + public string RealName { get; set; } + + /// + /// 账号 + /// + [Required(ErrorMessage = "账号不能为空"), MinLength(6, ErrorMessage = "账号不能少于6个字符")] + public string Account { get; set; } + + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空")] + public string Code { get; set; } + + /// + /// 验证码Id + /// + public long CodeId { get; set; } + + /// + /// 租户 + /// + [Required(ErrorMessage = "租户不能为空")] + public long TenantId { get; set; } + + /// + /// 密码 + /// + public string Password { get; set; } + + /// + /// 注册方案 + /// + public long WayId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs new file mode 100644 index 0000000..651165d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录结果 +/// +public class LoginOutput +{ + /// + /// 令牌Token + /// + public string AccessToken { get; set; } + + /// + /// 刷新Token + /// + public string RefreshToken { get; set; } + + /// + /// 个性化首页 + /// + public string? Homepage { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs new file mode 100644 index 0000000..a4ff9c6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户登录信息 +/// +public class LoginUserOutput +{ + /// + /// 用户id + /// + public long Id { get; set; } + + /// + /// 账号名称 + /// + public string Account { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 电话 + /// + public string Phone { get; set; } + + /// + /// 身份证 + /// + public string IdCardNum { get; set; } + + /// + /// 邮箱 + /// + public string Email { get; set; } + + /// + /// 账号类型 + /// + public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + /// + /// 头像 + /// + public string Avatar { get; set; } + + /// + /// 个人简介 + /// + public string Introduction { get; set; } + + /// + /// 地址 + /// + public string Address { get; set; } + + /// + /// 电子签名 + /// + public string Signature { get; set; } + + /// + /// 机构Id + /// + public long OrgId { get; set; } + + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 机构类型 + /// + public string OrgType { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 按钮权限集合 + /// + public List Buttons { get; set; } + + /// + /// 角色集合 + /// + public List RoleIds { get; set; } + + /// + /// 水印文字 + /// + public string WatermarkText { get; set; } + + /// + /// 租户Id + /// + public long? TenantId { get; set; } + + /// + /// 当前切换到的租户Id + /// + public long? CurrentTenantId { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; internal set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs new file mode 100644 index 0000000..cb5f8bc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/Dto/SysLdapInput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统域登录信息配置输入参数 +/// +public class SysLdapInput : BasePageInput +{ + /// + /// 主机 + /// + public string? Host { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddSysLdapInput : SysLdap +{ +} + +public class UpdateSysLdapInput : SysLdap +{ +} + +public class DeleteSysLdapInput : BaseIdInput +{ +} + +public class DetailSysLdapInput : BaseIdInput +{ +} + +public class SyncSysLdapInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs new file mode 100644 index 0000000..4157e38 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs @@ -0,0 +1,492 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.SpecificationDocument; +using Lazy.Captcha.Core; +using NewLife.Reflection; + +namespace Admin.NET.Core.Service; + +/// +/// 系统登录授权服务 🧩 +/// +[ApiDescriptionSettings(Order = 500)] +public class SysAuthService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysUserRep; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SysMenuService _sysMenuService; + private readonly SysOnlineUserService _sysOnlineUserService; + private readonly SysConfigService _sysConfigService; + private readonly SysUserService _sysUserService; + private readonly SysTenantService _sysTenantService; + private readonly SysSmsService _sysSmsService; + private readonly SysLdapService _sysLdapService; + private readonly ICaptcha _captcha; + private readonly IEventPublisher _eventPublisher; + private readonly SysCacheService _sysCacheService; + + public SysAuthService( + SqlSugarRepository sysUserRep, + IHttpContextAccessor httpContextAccessor, + SysOnlineUserService sysOnlineUserService, + SysConfigService sysConfigService, + SysLdapService sysLdapService, + IEventPublisher eventPublisher, + SysSmsService sysSmsService, + SysCacheService sysCacheService, + SysMenuService sysMenuService, + SysUserService sysUserService, + SysTenantService sysTenantService, + UserManager userManager, + ICaptcha captcha) + { + _captcha = captcha; + _sysUserRep = sysUserRep; + _userManager = userManager; + _sysSmsService = sysSmsService; + _eventPublisher = eventPublisher; + _sysUserService = sysUserService; + _sysTenantService = sysTenantService; + _sysMenuService = sysMenuService; + _sysCacheService = sysCacheService; + _sysConfigService = sysConfigService; + _httpContextAccessor = httpContextAccessor; + _sysOnlineUserService = sysOnlineUserService; + _sysLdapService = sysLdapService; + } + + /// + /// 账号密码登录 🔖 + /// + /// + /// 用户名/密码:superadmin/123456 + /// + [AllowAnonymous] + [DisplayName("账号密码登录")] + public virtual async Task Login([Required] LoginInput input) + { + // 判断密码错误次数(缓存30分钟) + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}"; + var passwordErrorTimes = _sysCacheService.Get(keyPasswordErrorTimes); + var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue(ConfigConst.SysPasswordMaxErrorTimes); + // 若未配置或误配置为0、负数, 则默认密码错误次数最大为5次 + if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 5; + if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027); + + // 判断是否开启验证码,其校验验证码 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysCaptcha) && !_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008); + + // 获取登录租户和用户 + var (tenant, user) = await GetLoginUserAndTenant(input.TenantId, account: input.Account); + + // 账号是否被冻结 + if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017); + + // 是否开启域登录验证 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysDomainLogin)) + { + var userLdap = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id); + if (userLdap == null) + { + VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user); + } + else if (!await App.GetRequiredService().AuthAccount(tenant.Id, userLdap.Account, CryptogramUtil.Decrypt(input.Password))) + { + _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30)); + throw Oops.Oh(ErrorCodeEnum.D1000); + } + } + else + VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user); + + // 登录成功则清空密码错误次数 + _sysCacheService.Remove(keyPasswordErrorTimes); + + return await CreateToken(user); + } + + /// + /// 获取登录租户和用户 + /// + /// + /// + /// + /// + [NonAction] + public async Task<(SysTenant tenant, SysUser user)> GetLoginUserAndTenant(long? tenantId, string account = null, string phone = null) + { + // 账号是否存在 + var user = await _sysUserRep.AsQueryable().Includes(u => u.SysOrg).ClearFilter() + .WhereIF(tenantId > 0, u => (u.AccountType == AccountTypeEnum.SuperAdmin || u.TenantId == tenantId)) + .WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account)) + .WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone)).FirstAsync(); + _ = user ?? throw Oops.Oh(ErrorCodeEnum.D1000); + + // 租户是否存在或已禁用 + var tenant = await _sysUserRep.ChangeRepository>().AsQueryable() + .WhereIF(tenantId > 0, u => u.Id == tenantId).WhereIF(tenantId.ToLong() == 0, u => u.Id == user.TenantId).FirstAsync(); + if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003); + + // 如果是超级管理员,则引用登录选择的租户进入系统 + if (tenantId > 0 && user.AccountType == AccountTypeEnum.SuperAdmin) + user.TenantId = tenantId; + + return (tenant, user); + } + + /// + /// 验证用户密码 + /// + /// + /// + /// + /// + private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user) + { + try + { + // 国密SM2解密(前端密码传输SM2加密后的) + password = CryptogramUtil.SM2Decrypt(password); + if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString()) + { + if (user.Password.Equals(MD5Encryption.Encrypt(password))) return; + } + else + { + if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return; + } + } + catch (Exception ex) + { + Log.Error("用户密码验证异常:", ex); + } + + _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30)); + throw Oops.Oh(ErrorCodeEnum.D1000); + } + + /// + /// 验证锁屏密码 🔖 + /// + /// + /// + [DisplayName("验证锁屏密码")] + public virtual async Task UnLockScreen([Required, FromQuery] string password) + { + // 账号是否存在 + var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId); + _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009); + + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; + var passwordErrorTimes = _sysCacheService.Get(keyPasswordErrorTimes); + + // 是否开启域登录验证 + if (await _sysConfigService.GetConfigValue(ConfigConst.SysDomainLogin)) + { + var userLdap = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId); + if (userLdap == null) + { + VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user); + } + else if (!await _sysLdapService.AuthAccount(user.TenantId!.Value, userLdap.Account, CryptogramUtil.Decrypt(password))) + { + _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30)); + throw Oops.Oh(ErrorCodeEnum.D1000); + } + } + else + VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user); + + return true; + } + + /// + /// 手机号登录 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("手机号登录")] + public virtual async Task LoginPhone([Required] LoginPhoneInput input) + { + // 校验短信验证码 + _sysSmsService.VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code }); + + // 获取登录租户和用户 + var (_, user) = await GetLoginUserAndTenant(input.TenantId, phone: input.Phone); + + return await CreateToken(user); + } + + /// + /// 生成Token令牌 🔖 + /// + /// \ + /// \ + /// + [NonAction] + internal virtual async Task CreateToken(SysUser user, SysUserEventTypeEnum sysUserEventTypeEnum = SysUserEventTypeEnum.Login) + { + // 单用户登录 + await _sysOnlineUserService.SingleLogin(user.Id); + + // 生成Token令牌 + var tokenExpire = await _sysConfigService.GetTokenExpire(); + var accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, user.Id }, + { ClaimConst.TenantId, user.TenantId }, + { ClaimConst.Account, user.Account }, + { ClaimConst.RealName, user.RealName }, + { ClaimConst.AccountType, user.AccountType }, + { ClaimConst.OrgId, user.OrgId }, + { ClaimConst.OrgName, user.SysOrg?.Name }, + { ClaimConst.OrgType, user.SysOrg?.Type }, + { ClaimConst.LangCode, user.LangCode } + }, tokenExpire); + + // 生成刷新Token令牌 + var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire(); + var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire); + + // 设置响应报文头 + _httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken); + + // Swagger Knife4UI-AfterScript登录脚本 + // ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']); + + // 更新用户登录信息 + user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true); + (user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp); + user.LastLoginTime = DateTime.Now; + user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent); + await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new + { + u.LastLoginIp, + u.LastLoginAddress, + u.LastLoginTime, + u.LastLoginDevice, + }).ExecuteCommandAsync(); + + var payload = new + { + Entity = user, + Output = new LoginOutput + { + AccessToken = accessToken, + RefreshToken = refreshToken, + Homepage = user.Homepage + } + }; + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(sysUserEventTypeEnum, payload); + return payload.Output; + } + + /// + /// 获取登录账号 🔖 + /// + /// + [DisplayName("获取登录账号")] + public virtual async Task GetUserInfo() + { + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); + // 获取机构 + var org = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.OrgId); + // 获取职位 + var pos = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.PosId); + // 获取按钮集合 + var buttons = await _sysMenuService.GetOwnBtnPermList(); + // 获取角色集合 + var roleIds = await _sysUserRep.ChangeRepository>().AsQueryable() + .Where(u => u.UserId == user.Id).Select(u => u.RoleId).ToListAsync(); + // 获取水印文字(若系统水印为空则全局为空) + var watermarkText = (await _sysUserRep.Context.Queryable().FirstAsync(u => u.Id == user.TenantId))?.Watermark; + if (!string.IsNullOrWhiteSpace(watermarkText)) watermarkText += $"-{user.RealName}"; + var loginUser = new LoginUserOutput + { + Id = user.Id, + Account = user.Account, + RealName = user.RealName, + Phone = user.Phone, + IdCardNum = user.IdCardNum, + Email = user.Email, + AccountType = user.AccountType, + Avatar = user.Avatar, + Address = user.Address, + Signature = user.Signature, + OrgId = user.OrgId, + OrgName = org?.Name, + OrgType = org?.Type, + PosName = pos?.Name, + Buttons = buttons, + RoleIds = roleIds, + TenantId = user.TenantId, + WatermarkText = watermarkText, + LangCode = user.LangCode, + }; + + //将登录信息中的当前租户id,更新为当前所切换到的租户 + long? currentTenantId = App.User.FindFirst(ClaimConst.TenantId)?.Value?.ToLong(0); + loginUser.CurrentTenantId = currentTenantId > 0 ? currentTenantId : user.TenantId; + + return loginUser; + } + + /// + /// 获取刷新Token 🔖 + /// + /// 旧的AccessToken + /// 新的AccessToken和RefreshToken + [DisplayName("获取刷新Token")] + public virtual async Task GetRefreshToken([FromQuery] string accessToken) + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) throw Oops.Oh(ErrorCodeEnum.D1016); + + if (string.IsNullOrWhiteSpace(accessToken)) throw Oops.Oh(ErrorCodeEnum.D1011); + + if (string.IsNullOrWhiteSpace(_userManager.Account)) throw Oops.Oh(ErrorCodeEnum.D1011); + + // 黑名单校验 + if (_sysCacheService.ExistKey($"blacklist:token:{accessToken}")) throw Oops.Oh(ErrorCodeEnum.D1011); + + // 解析Token + var (isValid, tokenData, validationResult) = JWTEncryption.Validate(accessToken); + if (!isValid) throw Oops.Oh(ErrorCodeEnum.D1016); + + // 获取用户Id + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); + return await CreateToken(user, SysUserEventTypeEnum.RefreshToken); + } + + /// + /// 退出系统 🔖 + /// + [DisplayName("退出系统")] + public async Task Logout() + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) throw Oops.Oh(ErrorCodeEnum.D1016); + + var token = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); + + if (string.IsNullOrWhiteSpace(token)) + throw Oops.Oh(ErrorCodeEnum.D1011); + + if (string.IsNullOrWhiteSpace(_userManager.Account)) + throw Oops.Oh(ErrorCodeEnum.D1011); + + // 写入黑名单(设置过期时间,避免Redis膨胀) + var tokenExpire = await _sysConfigService.GetTokenExpire(); + _sysCacheService.Set($"blacklist:token:{token}", "1", TimeSpan.FromMinutes(tokenExpire)); + + // 发布登出事件(用户退出) + var user = await _sysUserRep.GetByIdAsync(_userManager.UserId); + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.LoginOut, new { Entity = user }); + + // 清除 Swagger 登录信息 + httpContext.SignoutToSwagger(); + } + + /// + /// 获取验证码 🔖 + /// + /// + [AllowAnonymous] + [SuppressMonitor] + [DisplayName("获取验证码")] + public dynamic GetCaptcha() + { + var codeId = YitIdHelper.NextId().ToString(); + var captcha = _captcha.Generate(codeId); + var expirySeconds = App.GetOptions()?.ExpirySeconds ?? 60; + return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds }; + } + + /// + /// 用户注册 🔖 + /// + /// + /// + [UnitOfWork] + [AllowAnonymous] + [HttpPost, ApiDescriptionSettings(Description = "用户注册", DisableInherite = true)] + public async Task UserRegistration(UserRegistrationInput input) + { + // 校验验证码 + if (!_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008); + _captcha.Generate(input.CodeId.ToString()); + + // 登录时隐藏租户,查找对应租户信息 + input.TenantId = input.TenantId <= 0 ? (await _sysTenantService.GetCurrentTenantSysInfo()).Id : input.TenantId; + + // 判断租户是否有效且启用注册功能 + var tenant = await _sysUserRep.Context.Queryable().FirstAsync(u => u.Id == input.TenantId && u.Status == StatusEnum.Enable); + if (tenant?.EnableReg != YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D1034); + + // 查找注册方案 + var wayId = input.WayId <= 0 ? tenant.RegWayId : input.WayId; + var regWay = await _sysUserRep.Context.Queryable().FirstAsync(u => u.Id == wayId) ?? throw Oops.Oh(ErrorCodeEnum.D1035); + + var addUserInput = new AddUserInput + { + AccountType = regWay.AccountType, + NickName = "注册用户-" + input.Account, + OrgId = regWay.OrgId, + PosId = regWay.PosId, + TenantId = input.TenantId, + RoleIdList = new List { regWay.RoleId }, + }; + addUserInput.Copy(input); + await _sysUserService.RegisterUser(addUserInput); + } + + /// + /// Swagger登录检查 🔖 + /// + /// + [AllowAnonymous] + [HttpPost("/api/swagger/checkUrl"), NonUnify] + [ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)] + public int SwaggerCheckUrl() + { + return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401; + } + + /// + /// Swagger登录提交 🔖 + /// + /// + /// + [AllowAnonymous] + [HttpPost("/api/swagger/submitUrl"), NonUnify] + [ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)] + public async Task SwaggerSubmitUrl([FromForm] SpecificationAuth auth) + { + try + { + _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}", false); + + await Login(new LoginInput + { + Account = auth.UserName, + Password = CryptogramUtil.SM2Encrypt(auth.Password) + }); + + _sysCacheService.Remove($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}"); + + return 200; + } + catch (Exception) + { + return 401; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysLdapService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysLdapService.cs new file mode 100644 index 0000000..1d35afe --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Auth/SysLdapService.cs @@ -0,0 +1,437 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Novell.Directory.Ldap; + +namespace Admin.NET.Core; + +/// +/// 系统域登录配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 496, Description = "域登录配置")] +public class SysLdapService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLdapRep; + + public SysLdapService(SqlSugarRepository sysLdapRep) + { + _sysLdapRep = sysLdapRep; + } + + /// + /// 获取系统域登录配置分页列表 🔖 + /// + /// + /// + [DisplayName("获取系统域登录配置分页列表")] + public async Task> Page(SysLdapInput input) + { + return await _sysLdapRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Host.Contains(input.Keyword.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Host), u => u.Host.Contains(input.Host.Trim())) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加系统域登录配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加系统域登录配置")] + public async Task Add(AddSysLdapInput input) + { + var entity = input.Adapt(); + entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); + await _sysLdapRep.InsertAsync(entity); + return entity.Id; + } + + /// + /// 更新系统域登录配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新系统域登录配置")] + public async Task Update(UpdateSysLdapInput input) + { + var entity = input.Adapt(); + if (!string.IsNullOrEmpty(input.BindPass) && input.BindPass.Length < 32) + { + entity.BindPass = CryptogramUtil.Encrypt(input.BindPass); // 加密 + } + + await _sysLdapRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + } + + /// + /// 删除系统域登录配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除系统域登录配置")] + public async Task Delete(DeleteSysLdapInput input) + { + var entity = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + await _sysLdapRep.FakeDeleteAsync(entity); // 假删除 + //await _rep.DeleteAsync(entity); // 真删除 + } + + /// + /// 获取系统域登录配置详情 🔖 + /// + /// + /// + [DisplayName("获取系统域登录配置详情")] + public async Task GetDetail([FromQuery] DetailSysLdapInput input) + { + return await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 获取系统域登录配置列表 🔖 + /// + /// + [DisplayName("获取系统域登录配置列表")] + public async Task> GetList() + { + return await _sysLdapRep.AsQueryable().Select().ToListAsync(); + } + + /// + /// 验证账号 + /// + /// 域用户 + /// 密码 + /// 租户 + /// + [NonAction] + public async Task AuthAccount(long? tenantId, string account, string password) + { + var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.TenantId == tenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var ldapConn = new LdapConnection(); + try + { + await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port); + string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass); + await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass); + var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeSub, sysLdap.AuthFilter.Replace("%s", account), null, false); + string dn = string.Empty; + while (await ldapSearchResults.HasMoreAsync()) + { + var ldapEntry = await ldapSearchResults.NextAsync(); + var sAmAccountName = ldapEntry.GetAttributeSet().GetAttribute(sysLdap.BindAttrAccount)?.StringValue; + if (string.IsNullOrEmpty(sAmAccountName)) continue; + dn = ldapEntry.Dn; + break; + } + + if (string.IsNullOrEmpty(dn)) throw Oops.Oh(ErrorCodeEnum.D1002); + // var attr = new LdapAttribute("userPassword", password); + await ldapConn.BindAsync(dn, password); + } + catch (LdapException e) + { + return e.ResultCode switch + { + LdapException.NoSuchObject or LdapException.NoSuchAttribute => throw Oops.Oh(ErrorCodeEnum.D0009), + LdapException.InvalidCredentials => false, + _ => throw Oops.Oh(e.Message), + }; + } + finally + { + ldapConn.Disconnect(); + } + + return true; + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + [DisplayName("同步域用户")] + [NonAction] + public async Task> SyncUserTenant(long tenantId) + { + var sysLdap = await _sysLdapRep.GetFirstAsync(c => c.TenantId == tenantId && c.IsDelete == false && c.Status == StatusEnum.Enable) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return await SysLdapService.SyncUser(sysLdap); + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + [DisplayName("同步域用户")] + public async Task> SyncUser(SyncSysLdapInput input) + { + var sysLdap = await _sysLdapRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return await SysLdapService.SyncUser(sysLdap); + } + + /// + /// 同步域用户 🔖 + /// + /// + /// + private static async Task> SyncUser(SysLdap sysLdap) + { + if (sysLdap == null) throw Oops.Oh(ErrorCodeEnum.D1002); + var ldapConn = new LdapConnection(); + try + { + await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port); + string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass); + await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass); + var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false); + var userLdapList = new List(); + while (await ldapSearchResults.HasMoreAsync()) + { + LdapEntry ldapEntry; + try + { + ldapEntry = await ldapSearchResults.NextAsync(); + if (ldapEntry == null) continue; + } + catch (LdapException) + { + continue; + } + + var attrs = ldapEntry.GetAttributeSet(); + var deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode); + if (attrs.Count == 0 || attrs.ContainsKey("OU")) + { + await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode); + } + else + { + var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode); + sysUserLdap.Dn = ldapEntry.Dn; + sysUserLdap.TenantId = sysLdap.TenantId; + userLdapList.Add(sysUserLdap); + } + } + + if (userLdapList.Count == 0) return null; + + await App.GetRequiredService().InsertUserLdapList(sysLdap.TenantId!.Value, userLdapList); + return userLdapList; + } + catch (LdapException e) + { + throw e.ResultCode switch + { + LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009), + _ => Oops.Oh(e.Message), + }; + } + finally + { + ldapConn.Disconnect(); + } + } + + /// + /// 获取部门代码 + /// + /// + /// + /// + private static string GetDepartmentCode(LdapAttributeSet attrs, string bindAttrCode) + { + return bindAttrCode == "objectGUID" + ? new Guid(attrs.GetAttribute(bindAttrCode)?.ByteValue!).ToString() + : attrs.GetAttribute(bindAttrCode)?.StringValue ?? "0"; + } + + /// + /// 创建同步对象 + /// + /// + /// + /// + /// + /// + private static SysUserLdap CreateSysUserLdap(LdapAttributeSet attrs, string bindAttrAccount, string bindAttrEmployeeId, string deptCode) + { + var userLdap = new SysUserLdap + { + Account = attrs.ContainsKey(bindAttrAccount) ? attrs.GetAttribute(bindAttrAccount)?.StringValue : null, + EmployeeId = attrs.ContainsKey(bindAttrEmployeeId) ? attrs.GetAttribute(bindAttrEmployeeId)?.StringValue : null, + DeptCode = deptCode, + UserName = attrs.ContainsKey("name") ? attrs.GetAttribute("name")?.StringValue : null, + Mail = attrs.ContainsKey("mail") ? attrs.GetAttribute("mail")?.StringValue : null + }; + var pwdLastSet = attrs.ContainsKey("pwdLastSet") ? attrs.GetAttribute("pwdLastSet")?.StringValue : null; + if (pwdLastSet != null && !pwdLastSet.Equals("0")) userLdap.PwdLastSetTime = DateTime.FromFileTime(Convert.ToInt64(pwdLastSet)); + var userAccountControl = attrs.ContainsKey("userAccountControl") ? attrs.GetAttribute("userAccountControl")?.StringValue : null; + if ((Convert.ToInt32(userAccountControl) & 0x2) == 0x2) // 检查账户是否已过期(通过检查userAccountControl属性的特定位) + userLdap.AccountExpiresFlag = true; + if ((Convert.ToInt32(userAccountControl) & 0x10000) == 0x10000) // 检查账户密码设置是否永不过期 + userLdap.DontExpiresFlag = true; + return userLdap; + } + + /// + /// 遍历查询域用户 + /// + /// + /// + /// + /// + /// + private static async Task SearchDnLdapUser(LdapConnection ldapConn, SysLdap sysLdap, List userLdapList, string baseDn, string deptCode) + { + var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false); + while (await ldapSearchResults.HasMoreAsync()) + { + LdapEntry ldapEntry; + try + { + ldapEntry = await ldapSearchResults.NextAsync(); + if (ldapEntry == null) continue; + } + catch (LdapException) + { + continue; + } + + var attrs = ldapEntry.GetAttributeSet(); + deptCode = GetDepartmentCode(attrs, sysLdap.BindAttrCode); + + if (attrs.Count == 0 || attrs.ContainsKey("OU")) + await SearchDnLdapUser(ldapConn, sysLdap, userLdapList, ldapEntry.Dn, deptCode); + else + { + var sysUserLdap = CreateSysUserLdap(attrs, sysLdap.BindAttrAccount, sysLdap.BindAttrEmployeeId, deptCode); + sysUserLdap.Dn = ldapEntry.Dn; + sysUserLdap.TenantId = sysLdap.TenantId; + if (string.IsNullOrEmpty(sysUserLdap.EmployeeId)) continue; + userLdapList.Add(sysUserLdap); + } + } + } + + /// + /// 同步域组织 🔖 + /// + /// + /// + [DisplayName("同步域组织")] + public async Task SyncDept(SyncSysLdapInput input) + { + var sysLdap = await _sysLdapRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var ldapConn = new LdapConnection(); + try + { + await ldapConn.ConnectAsync(sysLdap.Host, sysLdap.Port); + string bindPass = CryptogramUtil.Decrypt(sysLdap.BindPass); + await ldapConn.BindAsync(sysLdap.Version, sysLdap.BindDn, bindPass); + var ldapSearchResults = await ldapConn.SearchAsync(sysLdap.BaseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false); + var orgList = new List(); + while (await ldapSearchResults.HasMoreAsync()) + { + LdapEntry ldapEntry; + try + { + ldapEntry = await ldapSearchResults.NextAsync(); + if (ldapEntry == null) continue; + } + catch (LdapException) + { + continue; + } + + var attrs = ldapEntry.GetAttributeSet(); + if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue; + + var sysOrg = CreateSysOrg(attrs, sysLdap, orgList, new SysOrg { Id = 0, Level = 0 }); + orgList.Add(sysOrg); + + await SearchDnLdapDept(ldapConn, sysLdap, orgList, ldapEntry.Dn, sysOrg); + } + + if (orgList.Count == 0) + return; + + await App.GetRequiredService().BatchAddOrgs(orgList); + } + catch (LdapException e) + { + throw e.ResultCode switch + { + LdapException.NoSuchObject or LdapException.NoSuchAttribute => Oops.Oh(ErrorCodeEnum.D0009), + _ => Oops.Oh(e.Message), + }; + } + finally + { + ldapConn.Disconnect(); + } + } + + /// + /// 遍历查询域用户 + /// + /// + /// + /// + /// + /// + private static async Task SearchDnLdapDept(LdapConnection ldapConn, SysLdap sysLdap, List listOrgs, string baseDn, SysOrg org) + { + var ldapSearchResults = await ldapConn.SearchAsync(baseDn, LdapConnection.ScopeOne, "(objectClass=*)", null, false); + while (await ldapSearchResults.HasMoreAsync()) + { + LdapEntry ldapEntry; + try + { + ldapEntry = await ldapSearchResults.NextAsync(); + if (ldapEntry == null) continue; + } + catch (LdapException) + { + continue; + } + + var attrs = ldapEntry.GetAttributeSet(); + if (attrs.Count != 0 && !attrs.ContainsKey("OU")) continue; + + var sysOrg = CreateSysOrg(attrs, sysLdap, listOrgs, org); + listOrgs.Add(sysOrg); + + await SearchDnLdapDept(ldapConn, sysLdap, listOrgs, ldapEntry.Dn, sysOrg); + } + } + + /// + /// 创建架构对象 + /// + /// + /// + /// + /// + /// + private static SysOrg CreateSysOrg(LdapAttributeSet attrs, SysLdap sysLdap, List listOrgs, SysOrg org) + { + return new SysOrg + { + Pid = org.Id, + Id = YitIdHelper.NextId(), + Code = attrs.ContainsKey(sysLdap.BindAttrCode) ? new Guid(attrs.GetAttribute(sysLdap.BindAttrCode)?.ByteValue).ToString() : null, + Level = org.Level + 1, + Name = attrs.ContainsKey(sysLdap.BindAttrAccount) ? attrs.GetAttribute(sysLdap.BindAttrAccount)?.StringValue : null, + OrderNo = listOrgs.Count + 1, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/BaseService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/BaseService.cs new file mode 100644 index 0000000..51a46a6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/BaseService.cs @@ -0,0 +1,90 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 实体操作基服务 +/// +/// +public class BaseService : IDynamicApiController where TEntity : class, new() +{ + private readonly SqlSugarRepository _rep; + + public BaseService(SqlSugarRepository rep) + { + _rep = rep; + } + + /// + /// 获取详情 🔖 + /// + /// + /// + [DisplayName("获取详情")] + public virtual async Task GetDetail(long id) + { + return await _rep.GetByIdAsync(id); + } + + /// + /// 获取集合 🔖 + /// + /// + [DisplayName("获取集合")] + public virtual async Task> GetList() + { + return await _rep.GetListAsync(); + } + + ///// + ///// 获取实体分页 🔖 + ///// + ///// + ///// + //[ApiDescriptionSettings(Name = "Page")] + //[DisplayName("获取实体分页")] + //public async Task> GetPage([FromQuery] BasePageInput input) + //{ + // return await _rep.AsQueryable().ToPagedListAsync(input.Page, input.PageSize); + //} + + /// + /// 增加 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加")] + public virtual async Task Add(TEntity entity) + { + return await _rep.InsertAsync(entity); + } + + /// + /// 更新 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新")] + public virtual async Task Update(TEntity entity) + { + return await _rep.AsUpdateable(entity).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除")] + public virtual async Task Delete(long id) + { + return await _rep.DeleteByIdAsync(id); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs new file mode 100644 index 0000000..3482b91 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs @@ -0,0 +1,485 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 系统缓存服务 🧩 +/// +[ApiDescriptionSettings(Order = 400, Description = "系统缓存")] +public class SysCacheService : IDynamicApiController, ISingleton +{ + private static ICacheProvider _cacheProvider; + private readonly CacheOptions _cacheOptions; + + public SysCacheService(ICacheProvider cacheProvider, IOptions cacheOptions) + { + _cacheProvider = cacheProvider; + _cacheOptions = cacheOptions.Value; + } + + /// + /// 申请分布式锁 🔖 + /// + /// 要锁定的key + /// 申请锁等待的时间,单位毫秒 + /// 锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒 + /// 失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败 + /// + [DisplayName("申请分布式锁")] + public IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true) + { + try + { + return _cacheProvider.Cache.AcquireLock(key, msTimeout, msExpire, throwOnFailure); + } + catch + { + return null; + } + } + + /// + /// 获取缓存键名集合 🔖 + /// + /// + [DisplayName("获取缓存键名集合")] + public List GetKeyList() + { + return _cacheProvider.Cache == Cache.Default + ? [.. _cacheProvider.Cache.Keys.Where(u => u.StartsWith(_cacheOptions.Prefix)).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)] + : [.. ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}*", 0, int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)]; + } + + /// + /// 增加缓存 + /// + /// + /// + /// + [NonAction] + public bool Set(string key, object value) + { + return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value); + } + + /// + /// 增加缓存并设置过期时间 + /// + /// + /// + /// + /// + [NonAction] + public bool Set(string key, object value, TimeSpan expire) + { + return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value, expire); + } + + public async Task AdGetAsync(String cacheName, Func> del, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2, t3], expiry); + } + + private async Task AdGetAsync(string cacheName, Delegate del, Object[] obs, TimeSpan? expiry) where T : class + { + var key = Key(cacheName, obs); + // 使用分布式锁 + using (_cacheProvider.Cache.AcquireLock($@"lock:AdGetAsync:{cacheName}", 1000)) + { + var value = Get(key); + if (value == null) + { + value = await ((dynamic)del).DynamicInvoke(obs); + if (expiry == null) + { + Set(key, value); + } + else + { + Set(key, value, (TimeSpan)expiry); + } + } + return value; + } + } + + public T Get(String cacheName, object t1) + { + return Get(cacheName, [t1]); + } + + public T Get(String cacheName, object t1, object t2) + { + return Get(cacheName, [t1, t2]); + } + + public T Get(String cacheName, object t1, object t2, object t3) + { + return Get(cacheName, [t1, t2, t3]); + } + + private T Get(String cacheName, Object[] obs) + { + var key = Key(cacheName, obs); + return Get(key); + } + + private static string Key(string cacheName, object[] obs) + { + if (obs.OfType().Any()) throw new Exception("缓存参数类型不能是:TimeSpan类型"); + StringBuilder sb = new(cacheName); + if (obs is { Length: > 0 }) + { + sb.Append(':'); + foreach (var a in obs) sb.Append($"<{KeySingle(a)}>"); + } + return sb.ToString(); + } + + private static string KeySingle(object t) + { + return t.GetType().IsClass && !t.GetType().IsPrimitive ? JsonConvert.SerializeObject(t) : t.ToString(); + } + + /// + /// 获取缓存的剩余生存时间 + /// + /// + /// + [NonAction] + public TimeSpan GetExpire(string key) + { + return _cacheProvider.Cache.GetExpire(key); + } + + /// + /// 获取缓存 + /// + /// + /// + /// + [NonAction] + public T Get(string key) + { + return _cacheProvider.Cache.Get($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 删除缓存 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除缓存")] + public int Remove(string key) + { + return _cacheProvider.Cache.Remove($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 清空所有缓存 🔖 + /// + /// + [DisplayName("清空所有缓存")] + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + public void Clear() + { + _cacheProvider.Cache.Clear(); + + Cache.Default.Clear(); + } + + /// + /// 检查缓存是否存在 + /// + /// 键 + /// + [NonAction] + public bool ExistKey(string key) + { + return _cacheProvider.Cache.ContainsKey($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 根据键名前缀删除缓存 🔖 + /// + /// 键名前缀 + /// + [ApiDescriptionSettings(Name = "DeleteByPreKey"), HttpPost] + [DisplayName("根据键名前缀删除缓存")] + public int RemoveByPrefixKey(string prefixKey) + { + var delKeys = _cacheProvider.Cache == Cache.Default + ? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).ToArray() + : ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", 0, int.MaxValue).ToArray(); + return _cacheProvider.Cache.Remove(delKeys); + } + + /// + /// 根据键名前缀获取键名集合 🔖 + /// + /// 键名前缀 + /// + [DisplayName("根据键名前缀获取键名集合")] + public List GetKeysByPrefixKey(string prefixKey) + { + return _cacheProvider.Cache == Cache.Default + ? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).Select(u => u[_cacheOptions.Prefix.Length..]).ToList() + : ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", 0, int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).ToList(); + } + + /// + /// 获取缓存值 🔖 + /// + /// + /// + [DisplayName("获取缓存值")] + public object GetValue(string key) + { + if (string.IsNullOrEmpty(key)) return null; + + if (Regex.IsMatch(key, @"%[0-9a-fA-F]{2}")) + key = HttpUtility.UrlDecode(key); + + var fullKey = $"{_cacheOptions.Prefix}{key}"; + + if (_cacheProvider.Cache == Cache.Default) + return _cacheProvider.Cache.Get(fullKey); + + if (_cacheProvider.Cache is FullRedis redisCache) + { + if (!redisCache.ContainsKey(fullKey)) + return null; + try + { + var keyType = redisCache.TYPE(fullKey)?.ToLower(); + switch (keyType) + { + case "string": + return redisCache.Get(fullKey); + + case "list": + var list = redisCache.GetList(fullKey); + return list?.ToList(); + + case "hash": + var hash = redisCache.GetDictionary(fullKey); + return hash?.ToDictionary(k => k.Key, v => v.Value); + + case "set": + var set = redisCache.GetSet(fullKey); + return set?.ToArray(); + + case "zset": + var sortedSet = redisCache.GetSortedSet(fullKey); + return sortedSet?.Range(0, -1)?.ToList(); + + case "none": + return null; + + default: + // 未知类型或特殊类型 + return new Dictionary + { + { "key", key }, + { "type", keyType ?? "unknown" }, + { "message", "无法使用标准方式获取此类型数据" } + }; + } + } + catch (Exception ex) + { + return new Dictionary + { + { "key", key }, + { "error", ex.Message }, + { "type", "exception" } + }; + } + } + + return _cacheProvider.Cache.Get(fullKey); + } + + /// + /// 获取或添加缓存(在数据不存在时执行委托请求数据) + /// + /// + /// + /// + /// 过期时间,单位秒 + /// + [NonAction] + public T GetOrAdd(string key, Func callback, int expire = -1) + { + if (string.IsNullOrWhiteSpace(key)) return default; + return _cacheProvider.Cache.GetOrAdd($"{_cacheOptions.Prefix}{key}", callback, expire); + } + + /// + /// Hash匹配 + /// + /// + /// + /// + [NonAction] + public IDictionary GetHashMap(string key) + { + return _cacheProvider.Cache.GetDictionary(key); + } + + /// + /// 批量添加HASH + /// + /// + /// + /// + /// + [NonAction] + public bool HashSet(string key, Dictionary dic) + { + var hash = GetHashMap(key); + foreach (var v in dic) + { + hash.Add(v); + } + return true; + } + + /// + /// 添加一条HASH + /// + /// + /// + /// + /// + [NonAction] + public void HashAdd(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + hash.Add(hashKey, value); + } + + /// + /// 添加或更新一条HASH + /// + /// + /// + /// + /// + [NonAction] + public void HashAddOrUpdate(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + if (hash.ContainsKey(hashKey)) + hash[hashKey] = value; + else + hash.Add(hashKey, value); + } + + /// + /// 获取多条HASH + /// + /// + /// + /// + /// + [NonAction] + public List HashGet(string key, params string[] fields) + { + var hash = GetHashMap(key); + return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList(); + } + + /// + /// 获取一条HASH + /// + /// + /// + /// + /// + [NonAction] + public T HashGetOne(string key, string field) + { + var hash = GetHashMap(key); + return hash.TryGetValue(field, out T value) ? value : default; + } + + /// + /// 根据KEY获取所有HASH + /// + /// + /// + /// + [NonAction] + public IDictionary HashGetAll(string key) + { + var hash = GetHashMap(key); + return hash; + } + + /// + /// 删除HASH + /// + /// + /// + /// + /// + [NonAction] + public int HashDel(string key, params string[] fields) + { + var hash = GetHashMap(key); + fields.ToList().ForEach(t => hash.Remove(t)); + return fields.Length; + } + + ///// + ///// 搜索HASH + ///// + ///// + ///// + ///// + ///// + //[NonAction] + //public List> HashSearch(string key, SearchModel searchModel) + //{ + // var hash = GetHashMap(key); + // return hash.Search(searchModel).ToList(); + //} + + ///// + ///// 搜索HASH + ///// + ///// + ///// + ///// + ///// + ///// + //[NonAction] + //public List> HashSearch(string key, string pattern, int count) + //{ + // var hash = GetHashMap(key); + // return hash.Search(pattern, count).ToList(); + //} +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs new file mode 100644 index 0000000..d935511 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs @@ -0,0 +1,184 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义模板引擎 +/// +public class CustomViewEngine : ViewEngineModel +{ + /// + /// 库定位器 + /// + public string ConfigId { get; set; } = SqlSugarConst.MainConfigId; + + public string AuthorName { get; set; } + + public string BusName { get; set; } + + public string NameSpace { get; set; } + + public string ClassName { get; set; } + + public string LowerClassName { get; set; } + + public string ProjectLastName { get; set; } + + public string PagePath { get; set; } = "main"; + + public string PrintType { get; set; } + + public string PrintName { get; set; } + + public bool HasLikeQuery { get; set; } + + public bool HasJoinTable { get; set; } + + public bool HasEnumField { get; set; } + + public bool HasDictField { get; set; } + + public bool HasConstField { get; set; } + + public bool HasSetStatus => TableField.Any(IsStatus); + + public List TableField { get; set; } + + public List ImportFieldList { get; set; } + + public List UploadFieldList { get; set; } + + public List QueryWhetherList { get; set; } + + public List ApiTreeFieldList { get; set; } + + public List DropdownFieldList { get; set; } + + public List AddUpdateFieldList { get; set; } + + public List PrimaryKeyFieldList { get; set; } + + public List TableUniqueConfigList { get; set; } + + public List IgnoreUpdateFieldList => TableField.Where(u => u.WhetherAddUpdate == "N" && u.ColumnKey != "True" && u.WhetherCommon != "Y").ToList(); + + /// + /// 格式化主键查询条件 + /// 例: PrimaryKeysFormat(" || ", "u.{0} == input.{0}") + /// 单主键返回 u.Id == input.Id + /// 组合主键返回 u.Id == input.Id || u.FkId == input.FkId + /// + /// 分隔符 + /// 模板字符串 + /// 字段首字母小写 + /// + public string PrimaryKeysFormat(string separator, string format, bool lowerFirstLetter = false) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, lowerFirstLetter ? u.LowerPropertyName : u.PropertyName))); + + /// + /// 注入的服务 + /// + /// + public Dictionary InjectServiceMap + { + get + { + var injectMap = new Dictionary(); + if (UploadFieldList.Count > 0) injectMap.Add(nameof(SysFileService), ToLowerFirstLetter(nameof(SysFileService))); + if (DropdownFieldList.Count > 0 || ImportFieldList.Count > 0) injectMap.Add(nameof(ISqlSugarClient), ToLowerFirstLetter(nameof(ISqlSugarClient).TrimStart('I'))); + if (ImportFieldList.Any(c => c.EffectType == "DictSelector")) injectMap.Add(nameof(SysDictTypeService), ToLowerFirstLetter(nameof(SysDictTypeService))); + return injectMap; + } + } + + /// + /// 服务构造参数 + /// + public string InjectServiceArgs => InjectServiceMap.Count > 0 ? ", " + string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : ""; + + /// + /// 默认值列表 + /// + public List DefaultValueList { get; set; } + + /// + /// 判断字段是否为状态字段 + /// + /// + /// + public bool IsStatus(CodeGenConfig column) => column.NetType == nameof(StatusEnum); + + /// + /// 获取首字母小写字符串 + /// + /// + /// + public string ToLowerFirstLetter(string text) => string.IsNullOrWhiteSpace(text) ? text : text[..1].ToLower() + text[1..]; + + /// + /// 将基本字段类型转为可空类型 + /// + /// + /// + public string GetNullableNetType(string netType) => Regex.IsMatch(netType, "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? netType.TrimEnd('?') + "?" : netType; + + /// + /// 获取前端表格列定义的属性 + /// + /// + /// + public string GetElTableColumnCustomProperty(CodeGenConfig column) + { + var content = $"prop='{column.LowerPropertyName}' label='{column.ColumnComment}'"; + if (IsStatus(column)) content += $" v-auth=\"'{LowerClassName}:setStatus'\""; + if (column.WhetherSortable == "Y") content += " sortable='custom'"; + return content; + } + + /// + /// 设置默认值 + /// + /// + public string GetAddDefaultValue() + { + var content = ""; + if (DefaultValueList.Count == 0) + { + var status = TableField.FirstOrDefault(IsStatus); + var orderNo = TableField.FirstOrDefault(c => c.NetType.TrimEnd('?') == "int" && c.PropertyName == nameof(SysUser.OrderNo)); + if (status != null) content += $"{status.LowerPropertyName}: {(int)StatusEnum.Enable},"; + if (orderNo != null) content += $"{orderNo.LowerPropertyName}: 100,"; + } + else + { + foreach (var item in DefaultValueList) + { + if (!string.IsNullOrWhiteSpace(item.DefaultValue)) + { + switch (item.EffectType) + { + case "InputNumber": + case "EnumSelector"://枚举和数字框,通过正则提取 数字:如 ('0') + content += $"{item.LowerPropertyName}: {Regex.Match(item.DefaultValue, @"\d+").Value},"; + break; + + case "Switch": + content += $"{item.LowerPropertyName}: {(item.DefaultValue == "1" ? true.ToString().ToLower() : false.ToString().ToLower())},"; + break; + + case "DatePicker"://忽略适配日期格式 + break; + + default: + content += $"{item.LowerPropertyName}: \"{item.DefaultValue}\",";// 如果是字符串 DefaultValue=('男') + break; + } + } + } + } + return content; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs new file mode 100644 index 0000000..e550c9a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs @@ -0,0 +1,234 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成详细配置参数 +/// +public class CodeGenConfig +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 代码生成主表ID + /// + public long CodeGenId { get; set; } + + /// + /// 数据库字段名 + /// + public string ColumnName { get; set; } + + /// + /// 主外键 + /// + public string ColumnKey { get; set; } + + /// + /// 实体属性名 + /// + public string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + public int ColumnLength { get; set; } + + /// + /// 数据库字段名(首字母小写) + /// + public string LowerPropertyName => string.IsNullOrWhiteSpace(PropertyName) ? null : PropertyName[..1].ToLower() + PropertyName[1..]; + + /// + /// 字段描述 + /// + public string ColumnComment { get; set; } + + /// + /// .NET类型 + /// + public string NetType { get; set; } + + /// + /// 数据库中类型(物理类型) + /// + public string DataType { get; set; } + + /// + /// 字段数据默认值 + /// + public string DefaultValue { get; set; } + + /// + /// 可空.NET类型 + /// + public string NullableNetType => Regex.IsMatch(NetType ?? "", "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? NetType.TrimEnd('?') + "?" : NetType; + + /// + /// 作用类型(字典) + /// + public string EffectType { get; set; } + + /// + /// 外键库标识 + /// + public string FkConfigId { get; set; } + + /// + /// 外键实体名称 + /// + public string FkEntityName { get; set; } + + /// + /// 外键表名称 + /// + public string FkTableName { get; set; } + + /// + /// 外键实体名称(首字母小写) + /// + public string LowerFkEntityName => string.IsNullOrWhiteSpace(FkEntityName) ? null : FkEntityName[..1].ToLower() + FkEntityName[1..]; + + /// + /// 外键链接字段 + /// + public string FkLinkColumnName { get; set; } + + /// + /// 外键显示字段 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FkDisplayColumns { get; set; } + + /// + /// 外键显示字段 + /// + public List FkDisplayColumnList { get; set; } + + /// + /// 外键显示字段(首字母小写) + /// + public List LowerFkDisplayColumnsList => FkDisplayColumnList?.Select(name => name[..1].ToLower() + name[1..]).ToList(); + + /// + /// 外键显示字段.NET类型 + /// + public string FkColumnNetType { get; set; } + + /// + /// 父级字段 + /// + public string PidColumn { get; set; } + + /// + /// 字典code + /// + public string DictTypeCode { get; set; } + + /// + /// 查询方式 + /// + public string QueryType { get; set; } + + /// + /// 是否是查询条件 + /// + public string WhetherQuery { get; set; } + + /// + /// 列表是否缩进(字典) + /// + public string WhetherRetract { get; set; } + + /// + /// 是否必填(字典) + /// + public string WhetherRequired { get; set; } + + /// + /// 是否可排序(字典) + /// + public string WhetherSortable { get; set; } + + /// + /// 列表显示 + /// + public string WhetherTable { get; set; } + + /// + /// 增改 + /// + public string WhetherAddUpdate { get; set; } + + /// + /// 导入 + /// + public string WhetherImport { get; set; } + + /// + /// 是否是通用字段 + /// + public string WhetherCommon { get; set; } + + /// + /// 排序 + /// + public int OrderNo { get; set; } + + /// + /// 是否是选择器控件 + /// + public bool IsSelectorEffectType => Regex.IsMatch(EffectType ?? "", "Selector$|ForeignKey", RegexOptions.IgnoreCase); + + /// + /// 去掉尾部Id的属性名 + /// + public string PropertyNameTrimEndId => PropertyName.TrimEnd("Id"); + + /// + /// 去掉尾部Id的属性名 + /// + public string LowerPropertyNameTrimEndId => LowerPropertyName.TrimEnd("Id"); + + /// + /// 扩展属性名称 + /// + public string ExtendedPropertyName => EffectType switch + { + "ForeignKey" => $"{PropertyName.TrimEnd("Id")}FkDisplayName", + "ApiTreeSelector" => $"{PropertyName.TrimEnd("Id")}DisplayName", + "DictSelector" => $"{PropertyName.TrimEnd("Id")}DictLabel", + "Upload" => $"{PropertyName.TrimEnd("Id")}Attachment", + "Upload_SingleFile" => $"{PropertyName.TrimEnd("Id")}Attachment", + _ => PropertyName + }; + + /// + /// 首字母小写的扩展属性名称 + /// + public string LowerExtendedPropertyName + { + get + { + var displayPropertyName = ExtendedPropertyName; + if (string.IsNullOrWhiteSpace(displayPropertyName)) return null; + return displayPropertyName[..1].ToLower() + displayPropertyName[1..]; + } + } + + /// + /// 获取外键显示值语句 + /// + /// 表别名 + /// 多字段时的连接符 + /// + public string GetDisplayColumn(string tableAlias, string separator = "-") => "$\"" + string.Join(separator, FkDisplayColumnList?.Select(name => $"{{{tableAlias}.{name}}}") ?? new List()) + "\""; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs new file mode 100644 index 0000000..4598d98 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs @@ -0,0 +1,199 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成参数类 +/// +public class CodeGenInput : BasePageInput +{ + /// + /// 作者姓名 + /// + public virtual string AuthorName { get; set; } + + /// + /// 类名 + /// + public virtual string ClassName { get; set; } + + /// + /// 是否移除表前缀 + /// + public virtual string TablePrefix { get; set; } + + /// + /// 库定位器名 + /// + public virtual string ConfigId { get; set; } + + /// + /// 数据库名(保留字段) + /// + public virtual string DbName { get; set; } + + /// + /// 数据库类型 + /// + public virtual string DbType { get; set; } + + /// + /// 数据库链接 + /// + public virtual string ConnectionString { get; set; } + + /// + /// 生成方式 + /// + public virtual string GenerateType { get; set; } + + /// + /// 数据库表名 + /// + public virtual string TableName { get; set; } + + /// + /// 命名空间 + /// + public virtual string NameSpace { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + public virtual string BusName { get; set; } + + /// + /// 功能名(数据库表名称) + /// + public virtual string TableComment { get; set; } + + /// + /// 表唯一字段 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string TableUniqueConfig { get; set; } + + /// + /// 表唯一字段列表 + /// + public virtual List TableUniqueList { get; set; } + + /// + /// 菜单应用分类(应用编码) + /// + public virtual string MenuApplication { get; set; } + + /// + /// 是否生成菜单 + /// + public virtual bool GenerateMenu { get; set; } + + /// + /// 菜单父级 + /// + public virtual long? MenuPid { get; set; } + + /// + /// 菜单图标 + /// + public virtual string MenuIcon { get; set; } + + /// + /// 页面目录 + /// + public virtual string PagePath { get; set; } + + /// + /// 支持打印类型 + /// + public virtual string PrintType { get; set; } + + /// + /// 打印模版名称 + /// + public virtual string PrintName { get; set; } +} + +public class AddCodeGenInput : CodeGenInput +{ + /// + /// 数据库表名 + /// + [Required(ErrorMessage = "数据库表名不能为空")] + public override string TableName { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + [Required(ErrorMessage = "业务名不能为空")] + public override string BusName { get; set; } + + /// + /// 命名空间 + /// + [Required(ErrorMessage = "命名空间不能为空")] + public override string NameSpace { get; set; } + + /// + /// 作者姓名 + /// + [Required(ErrorMessage = "作者姓名不能为空")] + public override string AuthorName { get; set; } + + ///// + ///// 类名 + ///// + //[Required(ErrorMessage = "类名不能为空")] + //public override string ClassName { get; set; } + + ///// + ///// 是否移除表前缀 + ///// + //[Required(ErrorMessage = "是否移除表前缀不能为空")] + //public override string TablePrefix { get; set; } + + /// + /// 生成方式 + /// + [Required(ErrorMessage = "生成方式不能为空")] + public override string GenerateType { get; set; } + + ///// + ///// 功能名(数据库表名称) + ///// + //[Required(ErrorMessage = "数据库表名不能为空")] + //public override string TableComment { get; set; } + + /// + /// 是否生成菜单 + /// + [Required(ErrorMessage = "是否生成菜单不能为空")] + public override bool GenerateMenu { get; set; } +} + +public class DeleteCodeGenInput +{ + /// + /// 代码生成器Id + /// + [Required(ErrorMessage = "代码生成器Id不能为空")] + public long Id { get; set; } +} + +public class UpdateCodeGenInput : AddCodeGenInput +{ + /// + /// 代码生成器Id + /// + [Required(ErrorMessage = "代码生成器Id不能为空")] + public long Id { get; set; } +} + +public class QueryCodeGenInput : DeleteCodeGenInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs new file mode 100644 index 0000000..aee96f6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenOutput.cs @@ -0,0 +1,83 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 代码生成参数类 +/// +public class CodeGenOutput +{ + /// + /// 代码生成器Id + /// + public long Id { get; set; } + + /// + /// 作者姓名 + /// + public string AuthorName { get; set; } + + /// + /// 类名 + /// + public string ClassName { get; set; } + + /// + /// 是否移除表前缀 + /// + public string TablePrefix { get; set; } + + /// + /// 生成方式 + /// + public string GenerateType { get; set; } + + /// + /// 数据库表名 + /// + public string TableName { get; set; } + + /// + /// 包名 + /// + public string PackageName { get; set; } + + /// + /// 业务名(业务代码包名称) + /// + public string BusName { get; set; } + + /// + /// 功能名(数据库表名称) + /// + public string TableComment { get; set; } + + /// + /// 菜单应用分类(应用编码) + /// + public string MenuApplication { get; set; } + + /// + /// 是否生成菜单 + /// + public bool GenerateMenu { get; set; } + + /// + /// 菜单父级 + /// + public long? MenuPid { get; set; } + + /// + /// 支持打印类型 + /// + public string PrintType { get; set; } + + /// + /// 打印模版名称 + /// + public string PrintName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs new file mode 100644 index 0000000..01047e2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库表列 +/// +public class ColumnOuput +{ + /// + /// 字段名 + /// + public string ColumnName { get; set; } + + /// + /// 实体的Property名 + /// + public string PropertyName { get; set; } + + /// + /// 字段数据长度 + /// + public int ColumnLength { get; set; } + + /// + /// 数据库中类型 + /// + public string DataType { get; set; } + + /// + /// 字段数据默认值 + /// + public string DefaultValue { get; set; } + + /// + /// 是否为主键 + /// + public bool IsPrimarykey { get; set; } + + /// + /// 是否允许为空 + /// + public bool IsNullable { get; set; } + + /// + /// .NET字段类型 + /// + public string NetType { get; set; } + + /// + /// 字典编码 + /// + public string DictTypeCode { get; set; } + + /// + /// 字段描述 + /// + public string ColumnComment { get; set; } + + /// + /// 主外键 + /// + public string ColumnKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs new file mode 100644 index 0000000..dae6db3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/DatabaseOutput.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库 +/// +public class DatabaseOutput +{ + /// + /// 库定位器名 + /// + public string ConfigId { get; set; } + + /// + /// 库名 + /// + public string DbNickName { get; set; } + + /// + /// 数据库类型 + /// + public SqlSugar.DbType DbType { get; set; } + + /// + /// 数据库连接字符串 + /// + public string ConnectionString { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs new file mode 100644 index 0000000..1bb8e81 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableOutput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 数据库表 +/// +public class TableOutput +{ + /// + /// 库定位器名 + /// + public string ConfigId { get; set; } + + /// + /// 表名(字母形式的) + /// + public string TableName { get; set; } + + /// + /// 实体名称 + /// + public string EntityName { get; set; } + + /// + /// 创建时间 + /// + public string CreateTime { get; set; } + + /// + /// 更新时间 + /// + public string UpdateTime { get; set; } + + /// + /// 表名称描述(功能名) + /// + public string TableComment { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs new file mode 100644 index 0000000..0cac927 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 表唯一配置项 +/// +public class TableUniqueConfigItem +{ + /// + /// 字段列表 + /// + public List Columns { get; set; } + + /// + /// 描述信息 + /// + public string Message { get; set; } + + /// + /// 格式化查询条件 + /// + /// 分隔符 + /// 模板字符串 + /// + public string Format(string separator, string format) => string.Join(separator, Columns.Select(name => string.Format(format, name))); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs new file mode 100644 index 0000000..44e42cd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs @@ -0,0 +1,256 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统代码生成配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 260)] +public class SysCodeGenConfigService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + + public SysCodeGenConfigService(ISqlSugarClient db) + { + _db = db; + } + + /// + /// 获取代码生成配置列表 🔖 + /// + /// + /// + [DisplayName("获取代码生成配置列表")] + public async Task> GetList([FromQuery] CodeGenConfig input) + { + return await _db.Queryable() + .Where(u => u.CodeGenId == input.CodeGenId) + .Select() + .Mapper(u => + { + u.NetType = (u.EffectType == "EnumSelector" ? u.DictTypeCode : u.NetType); + u.FkDisplayColumnList = u.FkDisplayColumns?.Split(",").ToList(); + }) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToListAsync(); + } + + /// + /// 更新代码生成配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新代码生成配置")] + public async Task UpdateCodeGenConfig(List inputList) + { + if (inputList == null || inputList.Count < 1) return; + inputList.ForEach(e => + { + e.FkDisplayColumns = e.FkDisplayColumnList?.Count > 0 ? string.Join(",", e.FkDisplayColumnList) : null; + }); + await _db.Updateable(inputList.Adapt>()) + .IgnoreColumns(u => new { u.ColumnLength, u.ColumnName, u.PropertyName }) + .ExecuteCommandAsync(); + } + + /// + /// 删除代码生成配置 + /// + /// + /// + [NonAction] + public async Task DeleteCodeGenConfig(long codeGenId) + { + await _db.Deleteable().Where(u => u.CodeGenId == codeGenId).ExecuteCommandAsync(); + } + + /// + /// 获取代码生成配置详情 🔖 + /// + /// + /// + [DisplayName("获取代码生成配置详情")] + public async Task GetDetail([FromQuery] CodeGenConfig input) + { + return await _db.Queryable().FirstAsync(u => u.Id == input.Id); + } + + /// + /// 批量增加代码生成配置 + /// + /// + /// + [NonAction] + public void AddList(List tableColumnOutputList, SysCodeGen codeGenerate) + { + if (tableColumnOutputList == null) return; + + var codeGenConfigs = new List(); + var orderNo = 100; + foreach (var tableColumn in tableColumnOutputList) + { + var codeGenConfig = new SysCodeGenConfig(); + + var yesOrNo = YesNoEnum.Y.ToString(); + if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString(); + + if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName)) + { + codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString(); + yesOrNo = YesNoEnum.N.ToString(); + } + else + { + codeGenConfig.WhetherCommon = YesNoEnum.N.ToString(); + } + + codeGenConfig.CodeGenId = codeGenerate.Id; + codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名 + codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名 + codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度 + codeGenConfig.ColumnComment = tableColumn.ColumnComment; + codeGenConfig.NetType = tableColumn.NetType; + codeGenConfig.DefaultValue = tableColumn.DefaultValue; + codeGenConfig.WhetherRetract = YesNoEnum.N.ToString(); + + // 生成代码时,主键并不是必要输入项,故一定要排除主键字段 + codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString(); + codeGenConfig.WhetherQuery = yesOrNo; + codeGenConfig.WhetherImport = yesOrNo; + codeGenConfig.WhetherAddUpdate = yesOrNo; + codeGenConfig.WhetherTable = yesOrNo; + + codeGenConfig.ColumnKey = tableColumn.ColumnKey; + + codeGenConfig.DataType = tableColumn.DataType; + codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType); + codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString(); + codeGenConfig.OrderNo = orderNo; + codeGenConfigs.Add(codeGenConfig); + + if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode)) + { + codeGenConfig.QueryType = "=="; + codeGenConfig.DictTypeCode = tableColumn.DictTypeCode; + codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector"; + } + + orderNo += 10; // 每个配置排序间隔10 + } + // 多库代码生成---这里要切回主库 + var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId); + provider.Insertable(codeGenConfigs).ExecuteCommand(); + } + + /// + /// 批量更新代码字段:先删除再新增,会保留历史字段操作类型 + /// + /// + /// + [NonAction] + public async Task UpdateList(List tableColumnOutputList, long codeGenId) + { + if (tableColumnOutputList == null) return; + + //获取历史数据 + var oldList = await GetList(new CodeGenConfig() { CodeGenId = codeGenId, }); + //删除历史数据 + await DeleteCodeGenConfig(codeGenId); + + var codeGenConfigs = new List(); + var orderNo = 100; + foreach (var tableColumn in tableColumnOutputList) + { + var oldItem = oldList.FirstOrDefault(u => u.ColumnName == tableColumn.ColumnName); + + var codeGenConfig = new SysCodeGenConfig(); + + var yesOrNo = YesNoEnum.Y.ToString(); + if (Convert.ToBoolean(tableColumn.ColumnKey)) yesOrNo = YesNoEnum.N.ToString(); + + if (CodeGenUtil.IsCommonColumn(tableColumn.PropertyName)) + { + codeGenConfig.WhetherCommon = YesNoEnum.Y.ToString(); + yesOrNo = YesNoEnum.N.ToString(); + } + else + { + codeGenConfig.WhetherCommon = YesNoEnum.N.ToString(); + } + + codeGenConfig.CodeGenId = codeGenId; + codeGenConfig.ColumnName = tableColumn.ColumnName; // 字段名 + codeGenConfig.PropertyName = tableColumn.PropertyName;// 实体属性名 + codeGenConfig.ColumnLength = tableColumn.ColumnLength;// 长度 + codeGenConfig.ColumnComment = tableColumn.ColumnComment; + codeGenConfig.NetType = tableColumn.NetType; + codeGenConfig.DefaultValue = tableColumn.DefaultValue; + codeGenConfig.WhetherRetract = YesNoEnum.N.ToString(); + + // 生成代码时,主键并不是必要输入项,故一定要排除主键字段 + codeGenConfig.WhetherRequired = (tableColumn.IsNullable || tableColumn.IsPrimarykey) ? YesNoEnum.N.ToString() : YesNoEnum.Y.ToString(); + codeGenConfig.WhetherQuery = yesOrNo; + codeGenConfig.WhetherImport = yesOrNo; + codeGenConfig.WhetherAddUpdate = yesOrNo; + codeGenConfig.WhetherTable = yesOrNo; + + codeGenConfig.ColumnKey = tableColumn.ColumnKey; + + codeGenConfig.DataType = tableColumn.DataType; + codeGenConfig.EffectType = CodeGenUtil.DataTypeToEff(codeGenConfig.NetType); + codeGenConfig.QueryType = GetDefaultQueryType(codeGenConfig); // QueryTypeEnum.eq.ToString(); + codeGenConfig.OrderNo = orderNo; + + if (oldItem != null) + { + //如果历史存在,则继承 + codeGenConfig.WhetherQuery = oldItem.WhetherQuery; + codeGenConfig.WhetherImport = oldItem.WhetherImport; + codeGenConfig.WhetherAddUpdate = oldItem.WhetherAddUpdate; + codeGenConfig.WhetherTable = oldItem.WhetherTable; + + codeGenConfig.EffectType = oldItem.EffectType; + codeGenConfig.FkConfigId = oldItem.FkConfigId; + codeGenConfig.FkEntityName = oldItem.FkEntityName; + codeGenConfig.FkTableName = oldItem.FkTableName; + codeGenConfig.FkDisplayColumns = oldItem.FkDisplayColumns; + codeGenConfig.FkLinkColumnName = oldItem.FkLinkColumnName; + codeGenConfig.FkColumnNetType = oldItem.FkColumnNetType; + } + + codeGenConfigs.Add(codeGenConfig); + + if (!string.IsNullOrWhiteSpace(tableColumn.DictTypeCode)) + { + codeGenConfig.QueryType = "=="; + codeGenConfig.DictTypeCode = tableColumn.DictTypeCode; + codeGenConfig.EffectType = tableColumn.DictTypeCode.EndsWith("Enum") ? "EnumSelector" : "DictSelector"; + } + + orderNo += 10; // 每个配置排序间隔10 + } + // 多库代码生成---这里要切回主库 + var provider = _db.AsTenant().GetConnectionScope(SqlSugarConst.MainConfigId); + provider.Insertable(codeGenConfigs).ExecuteCommand(); + } + + /// + /// 默认查询类型 + /// + /// + /// + private static string GetDefaultQueryType(SysCodeGenConfig codeGenConfig) + { + return (codeGenConfig.NetType?.TrimEnd('?')) switch + { + "string" => "like", + "DateTime" => "~", + _ => "==", + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs new file mode 100644 index 0000000..d5d5793 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs @@ -0,0 +1,825 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.IO.Compression; + +namespace Admin.NET.Core.Service; + +/// +/// 系统代码生成器服务 🧩 +/// +[ApiDescriptionSettings(Order = 270)] +public class SysCodeGenService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + + private readonly SysCodeGenConfigService _codeGenConfigService; + private readonly DbConnectionOptions _dbConnectionOptions; + private readonly CodeGenOptions _codeGenOptions; + private readonly SysMenuService _sysMenuService; + private readonly IViewEngine _viewEngine; + private readonly UserManager _userManager; + + public SysCodeGenService(ISqlSugarClient db, + IOptions dbConnectionOptions, + SysCodeGenConfigService codeGenConfigService, + IOptions codeGenOptions, + SysMenuService sysMenuService, + UserManager userManager, + IViewEngine viewEngine) + { + _db = db; + _viewEngine = viewEngine; + _userManager = userManager; + _sysMenuService = sysMenuService; + _codeGenOptions = codeGenOptions.Value; + _codeGenConfigService = codeGenConfigService; + _dbConnectionOptions = dbConnectionOptions.Value; + } + + /// + /// 获取代码生成分页列表 🔖 + /// + /// + /// + [DisplayName("获取代码生成分页列表")] + public async Task> Page(CodeGenInput input) + { + return await _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim())) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加代码生成")] + public async Task AddCodeGen(AddCodeGenInput input) + { + var isExist = await _db.Queryable().Where(u => u.TableName == input.TableName).AnyAsync(); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400); + + if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList); + + var codeGen = input.Adapt(); + var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync(); + + // 增加配置表 + _codeGenConfigService.AddList(GetColumnList(input), newCodeGen); + } + + /// + /// 更新代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新代码生成")] + public async Task UpdateCodeGen(UpdateCodeGenInput input) + { + var isExist = await _db.Queryable().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400); + + var oldRecord = await _db.Queryable().FirstAsync(u => u.Id == input.Id); + try + { + // 开启事务 + _db.AsTenant().BeginTran(); + if (input.GenerateMenu) + { + var oldTitle = $"{oldRecord.BusName}管理"; + var newTitle = $"{input.BusName}管理"; + var updateObj = await _db.Queryable().FirstAsync(u => u.Title == oldTitle); + if (updateObj != null) + { + updateObj.Title = newTitle; + var result = _db.Updateable(updateObj).UpdateColumns(it => new { it.Title }).ExecuteCommand(); + _sysMenuService.DeleteMenuCache(); + } + } + if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList); + var codeGen = input.Adapt(); + await _db.Updateable(codeGen).ExecuteCommandAsync(); + + // 仅当数据表名称发生了变化,才更新配置表 + //if (oldRecord.TableName != input.TableName) + //{ + await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id); + _codeGenConfigService.AddList(GetColumnList(input.Adapt()), codeGen); + //} + _db.AsTenant().CommitTran(); + } + catch (Exception ex) + { + _db.AsTenant().RollbackTran(); + throw Oops.Oh(ex); + } + } + + /// + /// 同步代码字段(保留历史作用类型) 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "SyncField"), HttpPost] + [DisplayName("同步代码字段")] + public async Task SyncCodeFieldGen(UpdateCodeGenInput input) + { + var isExist = await _db.Queryable().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400); + try + { + // 开启事务 + _db.AsTenant().BeginTran(); + await _codeGenConfigService.UpdateList(GetColumnList(input.Adapt()), input.Id); + _db.AsTenant().CommitTran(); + } + catch (Exception ex) + { + _db.AsTenant().RollbackTran(); + throw Oops.Oh(ex); + } + } + + /// + /// 删除代码生成 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除代码生成")] + public async Task DeleteCodeGen(List inputs) + { + if (inputs == null || inputs.Count < 1) return; + + var codeGenConfigTaskList = new List(); + inputs.ForEach(u => + { + _db.Deleteable().In(u.Id).ExecuteCommand(); + + // 删除配置表 + codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id)); + }); + await Task.WhenAll(codeGenConfigTaskList); + } + + /// + /// 获取代码生成详情 🔖 + /// + /// + /// + [DisplayName("获取代码生成详情")] + public async Task GetDetail([FromQuery] QueryCodeGenInput input) + { + return await _db.Queryable().SingleAsync(u => u.Id == input.Id); + } + + /// + /// 获取数据库库集合 🔖 + /// + /// + [DisplayName("获取数据库库集合")] + public async Task> GetDatabaseList() + { + var dbConfigs = _dbConnectionOptions.ConnectionConfigs; + return await Task.FromResult(dbConfigs.Adapt>()); + } + + /// + /// 获取数据库表(实体)集合 🔖 + /// + /// + [DisplayName("获取数据库表(实体)集合")] + public async Task> GetTableList(string configId = SqlSugarConst.MainConfigId) + { + var provider = _db.AsTenant().GetConnectionScope(configId); + var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); // 不能走缓存,否则切库不起作用 + var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId)); + + // var dbTableNames = dbTableInfos.Select(u => u.Name.ToLower()).ToList(); + IEnumerable entityInfos = await GetEntityInfos(configId); + + var tableOutputList = new List(); + foreach (var item in entityInfos) + { + var tbConfigId = item.Type.GetCustomAttribute()?.configId as string ?? SqlSugarConst.MainConfigId; + if (item.Type.IsDefined(typeof(LogTableAttribute))) tbConfigId = SqlSugarConst.LogConfigId; + if (tbConfigId != configId) continue; + + var table = dbTableInfos.FirstOrDefault(u => string.Equals(u.Name, (config!.DbSettings.EnableUnderLine ? item.DbTableName.ToUnderLine() : item.DbTableName), StringComparison.CurrentCultureIgnoreCase)); + if (table == null) continue; + tableOutputList.Add(new TableOutput + { + ConfigId = configId, + EntityName = item.EntityName, + TableName = table.Name, + TableComment = item.TableDescription + }); + } + return tableOutputList; + } + + /// + /// 根据表名获取列集合 🔖 + /// + /// + [DisplayName("根据表名获取列集合")] + public List GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId) + { + // 切库---多库代码生成用 + var provider = _db.AsTenant().GetConnectionScope(configId); + var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401); + if (config.DbSettings.EnableUnderLine) tableName = tableName.ToUnderLine(); + // 获取实体类型属性 + var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName); + if (entityType == null) return null; + var properties = GetEntityInfos(configId).Result.First(e => e.DbTableName.EndsWithIgnoreCase(tableName)).Type.GetProperties() + .Where(e => e.GetCustomAttribute()?.IsIgnore == false).Select(e => new + { + PropertyName = e.Name, + ColumnComment = e.GetCustomAttribute()?.ColumnDescription, + ColumnName = e.GetCustomAttribute()?.ColumnName ?? e.Name + }).ToList(); + // 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null) + var columnList = provider.DbMaintenance.GetColumnInfosByTableName(tableName).Select(u => new ColumnOuput + { + ColumnName = config!.DbSettings.EnableUnderLine ? u.DbColumnName.ToUnderLine() : u.DbColumnName, + ColumnKey = u.IsPrimarykey.ToString(), + DataType = u.DataType.ToString(), + NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType), + ColumnComment = u.ColumnDescription + }).ToList(); + foreach (var column in columnList) + { + // ToLowerInvariant 将字段名转成小写再比较,避免因大小写不一致导致无法匹配(pgsql创建表会默认全小写,而我们的实体中又是大写,就会匹配不上) + var property = properties.FirstOrDefault(e => (config!.DbSettings.EnableUnderLine ? e.ColumnName.ToUnderLine() : e.ColumnName).ToLowerInvariant() == column.ColumnName.ToLowerInvariant()); + column.ColumnComment ??= property?.ColumnComment; + column.PropertyName = property?.PropertyName; + } + return columnList; + } + + /// + /// 获取数据表列(实体属性)集合 + /// + /// + private List GetColumnList([FromQuery] AddCodeGenInput input) + { + var entityType = GetEntityInfos(input.ConfigId).GetAwaiter().GetResult().FirstOrDefault(u => u.EntityName == input.TableName); + if (entityType == null) return null; + + var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId); + var dbTableName = config!.DbSettings.EnableUnderLine ? entityType.DbTableName.ToUnderLine() : entityType.DbTableName; + + // 切库---多库代码生成用 + var provider = _db.AsTenant().GetConnectionScope(!string.IsNullOrEmpty(input.ConfigId) ? input.ConfigId : SqlSugarConst.MainConfigId); + + var entityBasePropertyNames = CodeGenUtil.GetPropertyInfoArray(typeof(EntityBaseTenant))?.Select(p => p.Name).ToArray(); + var columnInfos = provider.DbMaintenance.GetColumnInfosByTableName(dbTableName, false); + var result = columnInfos.Select(u => new ColumnOuput + { + // 转下划线后的列名需要再转回来(暂时不转) + //ColumnName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName, + ColumnName = u.DbColumnName, + ColumnLength = u.Length, + IsPrimarykey = u.IsPrimarykey, + IsNullable = u.IsNullable, + ColumnKey = u.IsPrimarykey.ToString(), + NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType), + DataType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType), + ColumnComment = string.IsNullOrWhiteSpace(u.ColumnDescription) ? u.DbColumnName : u.ColumnDescription, + DefaultValue = u.DefaultValue, + }).ToList(); + + // 获取实体的属性信息,赋值给PropertyName属性(CodeFirst模式应以PropertyName为实际使用名称) + var entityProperties = entityType.Type.GetProperties(); + + for (int i = result.Count - 1; i >= 0; i--) + { + var columnOutput = result[i]; + // 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性) + var propertyInfo = entityProperties.FirstOrDefault(u => string.Equals((u.GetCustomAttribute()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ?? + entityProperties.FirstOrDefault(u => u.GetCustomAttribute() != null && u.Name.ToLower() == (config.DbSettings.EnableUnderLine + ? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower() + : columnOutput.ColumnName.ToLower())); + if (propertyInfo != null) + { + columnOutput.PropertyName = propertyInfo.Name; + columnOutput.ColumnComment = propertyInfo.GetCustomAttribute()!.ColumnDescription; + var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType); + if (propertyInfo.PropertyType.IsEnum || (propertyType?.IsEnum ?? false)) + { + columnOutput.DictTypeCode = (propertyType ?? propertyInfo.PropertyType).Name; + } + else + { + var dict = propertyInfo.GetCustomAttribute(); + if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode; + } + } + else + { + result.RemoveAt(i); // 移除没有定义此属性的字段 + } + } + return result; + } + + /// + /// 获取库表信息 + /// + /// + private async Task> GetEntityInfos(string configId) + { + var config = _dbConnectionOptions.ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId) ?? throw Oops.Oh(ErrorCodeEnum.D1401); + var entityInfos = new List(); + + var type = typeof(SugarTable); + var types = new List(); + if (_codeGenOptions.EntityAssemblyNames != null) + { + types = App.EffectiveTypes.Where(c => c.IsClass) + .Where(c => _codeGenOptions.EntityAssemblyNames.Contains(c.Assembly.GetName().Name) || _codeGenOptions.EntityAssemblyNames.Any(name => c.Assembly.GetName().Name!.Contains(name))) + .ToList(); + } + + Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray(); + + foreach (var ct in cosType) + { + var sugarAttribute = ct.GetCustomAttributes(type, true).FirstOrDefault(); + + var description = ""; + var des = ct.GetCustomAttributes(typeof(DescriptionAttribute), true); + if (des.Length > 0) description = ((DescriptionAttribute)des[0]).Description; + + var dbTableName = sugarAttribute == null || string.IsNullOrWhiteSpace(((SugarTable)sugarAttribute).TableName) ? ct.Name : ((SugarTable)sugarAttribute).TableName; + if (config.DbSettings.EnableUnderLine) dbTableName = dbTableName.ToUnderLine(); + + entityInfos.Add(new EntityInfo + { + EntityName = ct.Name, + DbTableName = dbTableName, + TableDescription = sugarAttribute == null ? description : ((SugarTable)sugarAttribute).TableDescription, + Type = ct + }); + } + return await Task.FromResult(entityInfos); + + bool IsMyAttribute(Attribute[] o) => o.Any(a => a.GetType() == type); + } + + /// + /// 获取程序保存位置 🔖 + /// + /// + [DisplayName("获取程序保存位置")] + public List GetApplicationNamespaces() + { + return _codeGenOptions.BackendApplicationNamespaces; + } + + /// + /// 代码生成到本地 🔖 + /// + /// + [UnitOfWork] + [DisplayName("代码生成到本地")] + public async Task RunLocal(SysCodeGen input) + { + if (string.IsNullOrEmpty(input.GenerateType)) + input.GenerateType = "200"; + + // 先删除该表已生成的菜单列表 + List targetPathList; + var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!); + if (input.GenerateType.StartsWith('1')) + { + targetPathList = GetZipPathList(input); + if (Directory.Exists(zipPath)) Directory.Delete(zipPath, true); + } + else + targetPathList = GetTargetPathList(input); + + var (tableFieldList, result) = await RenderTemplateAsync(input); + var templatePathList = GetTemplatePathList(input); + for (var i = 0; i < templatePathList.Count; i++) + { + var content = result.GetValueOrDefault(templatePathList[i]?.TrimEnd(".vm")); + if (string.IsNullOrWhiteSpace(content)) continue; + var dirPath = new DirectoryInfo(targetPathList[i]).Parent!.FullName; + if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); + _ = File.WriteAllTextAsync(targetPathList[i], content, Encoding.UTF8); + } + + if (input.GenerateMenu) await AddOrUpdateMenu(input.TableName, input.BusName, input.MenuPid ?? 0, input.MenuIcon, input.PagePath, tableFieldList); + + // 非ZIP压缩返回空 + if (!input.GenerateType.StartsWith('1')) return null; + + // 判断是否存在同名称文件 + string downloadPath = zipPath + ".zip"; + if (File.Exists(downloadPath)) File.Delete(downloadPath); + + // 创建zip文件并返回下载地址 + ZipFile.CreateFromDirectory(zipPath, downloadPath); + return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" }; + } + + /// + /// 获取代码生成预览 🔖 + /// + /// + [DisplayName("获取代码生成预览")] + public async Task> Preview(SysCodeGen input) + { + var (_, result) = await RenderTemplateAsync(input); + return result; + } + + /// + /// 渲染模板 + /// + /// + /// + private async Task<(List tableFieldList, Dictionary result)> RenderTemplateAsync(SysCodeGen input) + { + var tableFieldList = await _codeGenConfigService.GetList(new CodeGenConfig { CodeGenId = input.Id }); // 字段集合 + var joinTableList = tableFieldList.Where(u => u.EffectType is "Upload" or "ForeignKey" or "ApiTreeSelector").ToList(); // 需要连表查询的字段 + + var data = new CustomViewEngine + { + ConfigId = input.ConfigId, + BusName = input.BusName, + PagePath = input.PagePath, + NameSpace = input.NameSpace, + ClassName = input.TableName, + PrintType = input.PrintType, + PrintName = input.PrintName, + AuthorName = input.AuthorName, + ProjectLastName = input.NameSpace!.Split('.').Last(), + LowerClassName = input.TableName!.ToFirstLetterLowerCase(), + TableUniqueConfigList = input.TableUniqueList ?? new(), + + TableField = tableFieldList, + QueryWhetherList = tableFieldList.Where(u => u.WhetherQuery == "Y").ToList(), + ImportFieldList = tableFieldList.Where(u => u.WhetherImport == "Y").ToList(), + UploadFieldList = tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile").ToList(), + PrimaryKeyFieldList = tableFieldList.Where(c => c.ColumnKey == "True").ToList(), + AddUpdateFieldList = tableFieldList.Where(u => u.WhetherAddUpdate == "Y").ToList(), + ApiTreeFieldList = tableFieldList.Where(u => u.EffectType == "ApiTreeSelector").ToList(), + DropdownFieldList = tableFieldList.Where(u => u.EffectType is "ForeignKey" or "ApiTreeSelector").ToList(), + DefaultValueList = tableFieldList.Where(u => u.DefaultValue != null && u.DefaultValue.Length > 0).ToList(), + + HasJoinTable = joinTableList.Count > 0, + HasDictField = tableFieldList.Any(u => u.EffectType == "DictSelector"), + HasEnumField = tableFieldList.Any(u => u.EffectType == "EnumSelector"), + HasConstField = tableFieldList.Any(u => u.EffectType == "ConstSelector"), + HasLikeQuery = tableFieldList.Any(c => c.WhetherQuery == "Y" && c.QueryType == "like") + }; + + // 获取模板文件并替换 + var templatePathList = GetTemplatePathList(); + var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); + + var result = new Dictionary(); + foreach (var path in templatePathList) + { + var templateFilePath = Path.Combine(templatePath, path); + if (!File.Exists(templateFilePath)) continue; + var tContent = await File.ReadAllTextAsync(templateFilePath); + var tResult = await _viewEngine.RunCompileFromCachedAsync(tContent, data, builderAction: builder => + { + builder.AddAssemblyReferenceByName("System.Text.RegularExpressions"); + builder.AddAssemblyReferenceByName("System.Collections"); + builder.AddAssemblyReferenceByName("System.Linq"); + + builder.AddUsing("System.Text.RegularExpressions"); + builder.AddUsing("System.Collections.Generic"); + builder.AddUsing("System.Linq"); + }); + result.Add(path?.TrimEnd(".vm"), tResult); + } + return (tableFieldList, result); + } + + /// + /// 添加或更新菜单 + /// + /// + /// + /// + /// + /// + /// + /// + private async Task AddOrUpdateMenu(string className, string busName, long pid, string menuIcon, string pagePath, List tableFieldList) + { + var title = $"{busName}管理"; + var lowerClassName = className.ToFirstLetterLowerCase(); + var menuType = pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu; + + // 查询是否已有主菜单 + var existingMenu = await _db.Queryable() + .Where(m => m.Title == title && m.Type == menuType && m.Pid == pid) + .FirstAsync(); + + string parentPath = ""; + if (pid != 0) + { + var parent = await _db.Queryable().FirstAsync(m => m.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505); + parentPath = parent.Path; + } + + long menuId; + if (existingMenu == null) + { + // 不存在则新增 + var newMenu = new SysMenu + { + Pid = pid, + Title = title, + Type = menuType, + Icon = menuIcon ?? "menu", + Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower(), + Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index" + }; + menuId = await _sysMenuService.AddMenu(newMenu.Adapt()); + } + else + { + // 存在则更新 + existingMenu.Icon = menuIcon; + existingMenu.Path = (pid == 0 ? "/" : parentPath + "/") + className.ToLower(); + existingMenu.Component = pid == 0 ? "Layout" : $"/{pagePath}/{lowerClassName}/index"; + await _sysMenuService.UpdateMenu(existingMenu.Adapt()); + menuId = existingMenu.Id; + } + + // 定义应有的按钮 + var orderNo = 100; + var newButtons = new List + { + new() { Title = "查询", Permission = $"{lowerClassName}:page", OrderNo = orderNo += 10 }, + new() { Title = "详情", Permission = $"{lowerClassName}:detail", OrderNo = orderNo += 10 }, + new() { Title = "增加", Permission = $"{lowerClassName}:add", OrderNo = orderNo += 10 }, + new() { Title = "编辑", Permission = $"{lowerClassName}:update", OrderNo = orderNo += 10 }, + new() { Title = "删除", Permission = $"{lowerClassName}:delete", OrderNo = orderNo += 10 }, + new() { Title = "批量删除", Permission = $"{lowerClassName}:batchDelete", OrderNo = orderNo += 10 }, + new() { Title = "设置状态", Permission = $"{lowerClassName}:setStatus", OrderNo = orderNo += 10 }, + new() { Title = "打印", Permission = $"{lowerClassName}:print", OrderNo = orderNo += 10 }, + new() { Title = "导入", Permission = $"{lowerClassName}:import", OrderNo = orderNo += 10 }, + new() { Title = "导出", Permission = $"{lowerClassName}:export", OrderNo = orderNo += 10 } + }; + + if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y"))) + { + newButtons.Add(new SysMenu + { + Title = "下拉列表数据", + Permission = $"{lowerClassName}:dropdownData", + OrderNo = orderNo += 10 + }); + } + + foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload" || u.EffectType == "Upload_SingleFile")) + { + newButtons.Add(new SysMenu + { + Title = $"上传{column.ColumnComment}", + Permission = $"{lowerClassName}:upload{column.PropertyName}", + OrderNo = orderNo += 10 + }); + } + + // 获取当前菜单下的按钮 + var existingButtons = await _db.Queryable() + .Where(m => m.Pid == menuId && m.Type == MenuTypeEnum.Btn) + .ToListAsync(); + + var newPermissions = newButtons.Select(b => b.Permission).ToHashSet(); + + // 添加或更新按钮 + foreach (var btn in newButtons) + { + var match = existingButtons.FirstOrDefault(b => b.Permission == btn.Permission); + if (match == null) + { + btn.Type = MenuTypeEnum.Btn; + btn.Pid = menuId; + btn.Icon = ""; + await _sysMenuService.AddMenu(btn.Adapt()); + } + else + { + match.Title = btn.Title; + match.OrderNo = btn.OrderNo; + await _sysMenuService.UpdateMenu(match.Adapt()); + } + } + + // 删除多余的旧按钮 + var toDelete = existingButtons.Where(b => !newPermissions.Contains(b.Permission)).ToList(); + foreach (var del in toDelete) + await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = del.Id }); + } + + /// + /// 增加菜单 + /// + /// + /// + /// + /// + /// + /// + /// + private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List tableFieldList) + { + // 删除已存在的菜单 + var title = $"{busName}管理"; + await DeleteMenuTree(title, pid == 0 ? MenuTypeEnum.Dir : MenuTypeEnum.Menu); + + var parentMenuPath = ""; + var lowerClassName = className!.ToFirstLetterLowerCase(); + if (pid == 0) + { + // 新增目录,并记录Id + var dirMenu = new SysMenu { Pid = 0, Title = title, Type = MenuTypeEnum.Dir, Icon = "robot", Path = "/" + className.ToLower(), Component = "Layout" }; + pid = await _sysMenuService.AddMenu(dirMenu.Adapt()); + } + else + { + var parentMenu = await _db.Queryable().FirstAsync(u => u.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505); + parentMenuPath = parentMenu.Path; + } + + // 新增菜单,并记录Id + var rootMenu = new SysMenu { Pid = pid, Title = title, Type = MenuTypeEnum.Menu, Icon = menuIcon, Path = $"{parentMenuPath}/{className.ToLower()}", Component = $"/{pagePath}/{lowerClassName}/index" }; + pid = await _sysMenuService.AddMenu(rootMenu.Adapt()); + + var orderNo = 100; + var menuList = new List + { + new() { Title="查询", Permission=$"{lowerClassName}:page", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="详情", Permission=$"{lowerClassName}:detail", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="增加", Permission=$"{lowerClassName}:add", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="编辑", Permission=$"{lowerClassName}:update", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="删除", Permission=$"{lowerClassName}:delete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="批量删除", Permission=$"{lowerClassName}:batchDelete", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="设置状态", Permission=$"{lowerClassName}:setStatus", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="打印", Permission=$"{lowerClassName}:print", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="导入", Permission=$"{lowerClassName}:import", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10}, + new() { Title="导出", Permission=$"{lowerClassName}:export", Pid=pid, Type=MenuTypeEnum.Btn, OrderNo=orderNo+=10} + }; + + if (tableFieldList.Any(u => u.EffectType is "ForeignKey" or "ApiTreeSelector" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y"))) + menuList.Add(new SysMenu { Title = "下拉列表数据", Permission = $"{lowerClassName}:dropdownData", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 }); + + foreach (var column in tableFieldList.Where(u => u.EffectType == "Upload")) + menuList.Add(new SysMenu { Title = $"上传{column.ColumnComment}", Permission = $"{lowerClassName}:upload{column.PropertyName}", Pid = pid, Type = MenuTypeEnum.Btn, OrderNo = orderNo += 10 }); + + foreach (var menu in menuList) await _sysMenuService.AddMenu(menu.Adapt()); + } + + /// + /// 根据菜单名称和类型删除关联的菜单树 + /// + /// + /// + private async Task DeleteMenuTree(string title, MenuTypeEnum type) + { + var menuList = await _db.Queryable().Where(u => u.Title == title && u.Type == type).ToListAsync() ?? new(); + foreach (var menu in menuList) await _sysMenuService.DeleteMenu(new DeleteMenuInput { Id = menu.Id }); + } + + /// + /// 获取模板文件路径集合 + /// + /// + private static List GetTemplatePathList(SysCodeGen input) + { + if (input.GenerateType!.Substring(1, 1).Contains('1')) return new() { "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" }; + if (input.GenerateType.Substring(1, 1).Contains('2')) return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm" }; + return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" }; + } + + /// + /// 获取模板文件路径集合 + /// + /// + private static List GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.ts.vm" }; + + /// + /// 设置生成文件路径 + /// + /// + /// + private List GetTargetPathList(SysCodeGen input) + { + //var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName); + var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.FullName, input.NameSpace!, "Service", input.TableName!); + var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs"); + var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs"); + var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs"); + var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs"); + var frontendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!); + var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");// + var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue"); + var apiJsPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts"); + + if (input.GenerateType!.Substring(1, 1).Contains('1')) + { + // 生成到本项目(前端) + return new List + { + indexPath, + formModalPath, + apiJsPath + }; + } + + if (input.GenerateType.Substring(1, 1).Contains('2')) + { + // 生成到本项目(后端) + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + }; + } + // 前后端同时生成到本项目 + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + indexPath, + formModalPath, + apiJsPath + }; + } + + /// + /// 设置生成文件路径 + /// + /// + /// + private List GetZipPathList(SysCodeGen input) + { + var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!); + + var firstLowerTableName = input.TableName!.ToFirstLetterLowerCase(); + //var backendPath = Path.Combine(zipPath, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName); + var backendPath = Path.Combine(zipPath, input.NameSpace!, "Service", input.TableName); + var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs"); + var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs"); + var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs"); + var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs"); + var frontendPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!); + var indexPath = Path.Combine(frontendPath, firstLowerTableName, "index.vue"); + var formModalPath = Path.Combine(frontendPath, firstLowerTableName, "component", "editDialog.vue"); + var apiJsPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, firstLowerTableName + ".ts"); + if (input.GenerateType!.StartsWith("11")) + { + return new List + { + indexPath, + formModalPath, + apiJsPath + }; + } + + if (input.GenerateType.StartsWith("12")) + { + return new List + { + servicePath, + inputPath, + outputPath, + viewPath + }; + } + + return new List + { + servicePath, + inputPath, + outputPath, + viewPath, + indexPath, + formModalPath, + apiJsPath + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs new file mode 100644 index 0000000..efd7901 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ApiOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口/动态API输出 +/// +public class ApiOutput +{ + /// + /// 组名称 + /// + public string GroupName { get; set; } + + /// + /// 接口名称 + /// + public string DisplayName { get; set; } + + /// + /// 路由名称 + /// + public string RouteName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ProcDto.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ProcDto.cs new file mode 100644 index 0000000..6acbea6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/ProcDto.cs @@ -0,0 +1,90 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Magicodes.ExporterAndImporter.Core.Filters; +using Magicodes.ExporterAndImporter.Core.Models; + +namespace Admin.NET.Core.Service; + +/// +/// 基础存储过程输入类 +/// +public class BaseProcInput +{ + /// + /// ProcId + /// + public string ProcId { get; set; } + + /// + /// 数据库配置Id + /// + public string ConfigId { get; set; } = SqlSugarConst.MainConfigId; + + /// + /// 存储过程输入参数 + /// + /// {"id":"351060822794565"} + public Dictionary ProcParams { get; set; } +} + +/// +/// 带表头名称存储过程输入类 +/// +public class ExportProcByTMPInput : BaseProcInput +{ + /// + /// 模板名称 + /// + public string Template { get; set; } +} + +/// +/// 带表头名称存储过程输入类 +/// +public class ExportProcInput : BaseProcInput +{ + public Dictionary EHeader { get; set; } +} + +/// +/// 指定导出类名(有排序)存储过程输入类 +/// +public class ExportProcInput2 : BaseProcInput +{ + public List EHeader { get; set; } +} + +/// +/// 前端指定列 +/// +public class ProcExporterHeaderFilter : IExporterHeaderFilter +{ + private Dictionary> _includeHeader; + + public ProcExporterHeaderFilter(Dictionary> includeHeader) + { + _includeHeader = includeHeader; + } + + public ExporterHeaderInfo Filter(ExporterHeaderInfo exporterHeaderInfo) + { + if (_includeHeader != null && _includeHeader.Count > 0) + { + var key = exporterHeaderInfo.PropertyName.ToUpper(); + if (_includeHeader.ContainsKey(key)) + { + exporterHeaderInfo.DisplayName = _includeHeader[key].Item1; + return exporterHeaderInfo; + } + else + { + exporterHeaderInfo.ExporterHeaderAttribute.Hidden = true; + } + } + return exporterHeaderInfo; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs new file mode 100644 index 0000000..cc97d39 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SmKeyPairOutput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 国密公钥私钥对输出 +/// +public class SmKeyPairOutput +{ + /// + /// 私匙 + /// + public string PrivateKey { get; set; } + + /// + /// 公匙 + /// + public string PublicKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs new file mode 100644 index 0000000..d4e7e16 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs @@ -0,0 +1,69 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口压测输入参数 +/// +public class StressTestInput +{ + /// + /// 接口请求地址 + /// + /// https://gitee.com/zuohuaijun/Admin.NET + [Required(ErrorMessage = "接口请求地址不能为空")] + public string RequestUri { get; set; } + + /// + /// 请求方式 + /// + [Required(ErrorMessage = "请求方式不能为空")] + public string RequestMethod { get; set; } = nameof(HttpMethod.Get); + + /// + /// 每轮请求量 + /// + /// 100 + [Required(ErrorMessage = "每轮请求量不能为空")] + [Range(1, 100000, ErrorMessage = "每轮请求量必须为1-100000")] + public int? NumberOfRequests { get; set; } + + /// + /// 压测轮数 + /// + /// 5 + [Required(ErrorMessage = "压测轮数不能为空")] + [Range(1, 10000, ErrorMessage = "压测轮数必须为1-10000")] + public int? NumberOfRounds { get; set; } + + /// + /// 最大并行量(默认为当前主机逻辑处理器的数量) + /// + /// 500 + [Range(0, 10000, ErrorMessage = "最大并行量必须为0-10000")] + public int? MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount; + + /// + /// 请求参数 + /// + public List> RequestParameters { get; set; } = new(); + + /// + /// 请求头参数 + /// + public Dictionary Headers { get; set; } = new(); + + /// + /// 路径参数 + /// + public Dictionary PathParameters { get; set; } = new(); + + /// + /// Query参数 + /// + public Dictionary QueryParameters { get; set; } = new(); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs new file mode 100644 index 0000000..d366de0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 接口压测输出参数 +/// +public class StressTestOutput +{ + /// + /// 总请求次数 + /// + public long TotalRequests { get; set; } + + /// + /// 总用时(秒) + /// + public double TotalTimeInSeconds { get; set; } + + /// + /// 成功请求次数 + /// + public long SuccessfulRequests { get; set; } + + /// + /// 失败请求次数 + /// + public long FailedRequests { get; set; } + + /// + /// 每秒查询率(QPS) + /// + public double QueriesPerSecond { get; set; } + + /// + /// 最小响应时间(毫秒) + /// + public double MinResponseTime { get; set; } + + /// + /// 最大响应时间(毫秒) + /// + public double MaxResponseTime { get; set; } + + /// + /// 平均响应时间(毫秒) + /// + public double AverageResponseTime { get; set; } + + /// + /// P10 响应时间(毫秒) + /// + public double Percentile10ResponseTime { get; set; } + + /// + /// P25 响应时间(毫秒) + /// + public double Percentile25ResponseTime { get; set; } + + /// + /// P50 响应时间(毫秒) + /// + public double Percentile50ResponseTime { get; set; } + + /// + /// P75 响应时间(毫秒) + /// + public double Percentile75ResponseTime { get; set; } + + /// + /// P90 响应时间(毫秒) + /// + public double Percentile90ResponseTime { get; set; } + + /// + /// P99 响应时间(毫秒) + /// + public double Percentile99ResponseTime { get; set; } + + /// + /// P999 响应时间(毫秒) + /// + public double Percentile999ResponseTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs new file mode 100644 index 0000000..8ab02a2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs @@ -0,0 +1,260 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Utilities.Encoders; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Admin.NET.Core.Service; + +/// +/// 系统通用服务 🧩 +/// +[ApiDescriptionSettings(Order = 101)] +[AllowAnonymous] +public class SysCommonService : IDynamicApiController, ITransient +{ + private readonly IApiDescriptionGroupCollectionProvider _apiProvider; + private readonly SqlSugarRepository _sysUserRep; + private readonly CDConfigOptions _cdConfigOptions; + private readonly UserManager _userManager; + private readonly HttpClient _httpClient; + + public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider, + SqlSugarRepository sysUserRep, + IOptions giteeOptions, + IHttpClientFactory httpClientFactory, + UserManager userManager) + { + _sysUserRep = sysUserRep; + _apiProvider = apiProvider; + _userManager = userManager; + _cdConfigOptions = giteeOptions.Value; + _httpClient = httpClientFactory.CreateClient(); + } + + /// + /// 获取国密公钥私钥对 🏆 + /// + /// + [DisplayName("获取国密公钥私钥对")] + public SmKeyPairOutput GetSmKeyPair() + { + var kp = GM.GenerateKeyPair(); + var privateKey = Hex.ToHexString(((ECPrivateKeyParameters)kp.Private).D.ToByteArray()).ToUpper(); + var publicKey = Hex.ToHexString(((ECPublicKeyParameters)kp.Public).Q.GetEncoded()).ToUpper(); + + return new SmKeyPairOutput + { + PrivateKey = privateKey, + PublicKey = publicKey, + }; + } + + /// + /// 获取所有接口/动态API 🔖 + /// + /// + [DisplayName("获取所有接口/动态API")] + public List GetApiList() + { + var apiList = new List(); + foreach (var item in _apiProvider.ApiDescriptionGroups.Items) + { + foreach (var apiDescription in item.Items) + { + var displayName = apiDescription.TryGetMethodInfo(out MethodInfo apiMethodInfo) ? apiMethodInfo.GetCustomAttribute(true)?.DisplayName : ""; + + apiList.Add(new ApiOutput + { + GroupName = item.GroupName, + DisplayName = displayName, + RouteName = apiDescription.RelativePath + }); + } + } + return apiList; + } + + /// + /// 下载标记错误的临时Excel(全局) + /// + /// + [DisplayName("下载标记错误的临时Excel(全局)")] + public async Task DownloadErrorExcelTemp([FromQuery] string fileName = null) + { + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + var resultStream = App.GetRequiredService().Get(CacheConst.KeyExcelTemp + userId); + + if (resultStream == null) throw Oops.Oh("错误标记文件已过期。"); + + return await Task.FromResult(new FileStreamResult(resultStream, "application/octet-stream") + { + FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? "错误标记_" + DateTime.Now.ToString("yyyyMMddhhmmss") : fileName)}.xlsx" + }); + } + + /// + /// 加密字符串 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("加密字符串")] + public dynamic EncryptPlainText([Required] string plainText) + { + return CryptogramUtil.Encrypt(plainText); + } + + /// + /// 接口压测 🔖 + /// + /// + [DisplayName("接口压测")] + public async Task StressTest(StressTestInput input) + { + // 限制仅超管用户才能使用此功能 + if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001); + + var stopwatch = new Stopwatch(); + var responseTimes = new List(); //响应时间集合 + input.RequestMethod = input.RequestMethod.ToUpper(); + long totalRequests = 0, successfulRequests = 0, failedRequests = 0; + + stopwatch.Start(); + var semaphore = new SemaphoreSlim(input.MaxDegreeOfParallelism!.Value > 0 ? input.MaxDegreeOfParallelism.Value : Environment.ProcessorCount); + + #region 参数构建 + + // 构建基础URI(不包括路径和查询参数) + var baseUriBuilder = new UriBuilder(input.RequestUri); + var queryString = HttpUtility.ParseQueryString(baseUriBuilder.Query); + + // 替换路径参数到baseUriBuilder.Path + foreach (var param in input.PathParameters) + { + baseUriBuilder.Path = baseUriBuilder.Path.Replace($"{{{param.Key}}}", param.Value, StringComparison.OrdinalIgnoreCase); + } + + // 构建Query参数 + foreach (var param in input.QueryParameters) + { + queryString[param.Key] = param.Value; + } + + baseUriBuilder.Query = queryString.ToString() ?? string.Empty; + var fullUri = baseUriBuilder.Uri; + + // 创建一次性的HttpRequestMessage模板 + HttpRequestMessage requestTemplate = CreateRequestMessage(input, fullUri); + + #endregion 参数构建 + + var tasks = Enumerable.Range(0, input.NumberOfRounds!.Value * input.NumberOfRequests!.Value).Select(async _ => + { + await semaphore.WaitAsync(); + try + { + var requestStopwatch = new Stopwatch(); + requestStopwatch.Start(); + + using (var request = requestTemplate.DeepCopy()) + { + if (!string.Equals(input.RequestMethod, "GET", StringComparison.OrdinalIgnoreCase) && input.RequestParameters.Any()) + { + var content = new FormUrlEncodedContent(input.RequestParameters); + request.Content = content; + } + + using (var response = await _httpClient.SendAsync(request)) + { + response.EnsureSuccessStatusCode(); // 抛出错误状态码异常 + + requestStopwatch.Stop(); + responseTimes.Add(requestStopwatch.Elapsed.TotalMilliseconds); + Interlocked.Increment(ref successfulRequests); + } + } + } + catch + { + Interlocked.Increment(ref failedRequests); + } + finally + { + Interlocked.Increment(ref totalRequests); + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + stopwatch.Stop(); + + var totalTimeInSeconds = stopwatch.Elapsed.TotalSeconds; + var qps = totalTimeInSeconds > 0 ? totalRequests / totalTimeInSeconds : 0; + var orderResponseTimes = responseTimes.OrderBy(t => t).ToList(); + var averageResponseTime = responseTimes.Any() ? responseTimes.Average() : 0; + var minResponseTime = responseTimes.Any() ? responseTimes.Min() : 0; + var maxResponseTime = responseTimes.Any() ? responseTimes.Max() : 0; + + return new StressTestOutput + { + TotalRequests = totalRequests, + TotalTimeInSeconds = totalTimeInSeconds, + SuccessfulRequests = successfulRequests, + FailedRequests = failedRequests, + QueriesPerSecond = qps, + MinResponseTime = minResponseTime, + MaxResponseTime = maxResponseTime, + AverageResponseTime = averageResponseTime, + Percentile10ResponseTime = CalculatePercentile(orderResponseTimes, 0.1), + Percentile25ResponseTime = CalculatePercentile(orderResponseTimes, 0.25), + Percentile50ResponseTime = CalculatePercentile(orderResponseTimes, 0.5), + Percentile75ResponseTime = CalculatePercentile(orderResponseTimes, 0.75), + Percentile90ResponseTime = CalculatePercentile(orderResponseTimes, 0.9), + Percentile99ResponseTime = CalculatePercentile(orderResponseTimes, 0.99), + Percentile999ResponseTime = CalculatePercentile(orderResponseTimes, 0.999) + }; + } + + /// + /// 创建请求消息 + /// + /// 输入参数 + /// url + /// + private HttpRequestMessage CreateRequestMessage(StressTestInput input, Uri fullUri) + { + HttpRequestMessage request = input.RequestMethod switch + { + "GET" => new HttpRequestMessage(HttpMethod.Get, fullUri), + "PUT" => new HttpRequestMessage(HttpMethod.Put, fullUri), + "POST" => new HttpRequestMessage(HttpMethod.Post, fullUri), + "DELETE" => new HttpRequestMessage(HttpMethod.Delete, fullUri), + _ => throw Oops.Bah("请求方式异常") + }; + + // 设置请求头 + foreach (var header in input.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + return request; + } + + /// + /// 计算百分位请求耗时 + /// + /// 请求耗时列表 + /// 百分位 + /// + private double CalculatePercentile(List times, double percentile) + { + if (!times.Any()) return 0; + var index = (int)Math.Ceiling(percentile * times.Count) - 1; + return times[index < times.Count ? index : times.Count - 1]; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysProcService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysProcService.cs new file mode 100644 index 0000000..2bbdc42 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Common/SysProcService.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统存储过程服务 🧩 +/// +[ApiDescriptionSettings(Order = 102)] +public class SysProcService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + + public SysProcService(ISqlSugarClient db) + { + _db = db; + } + + /// + /// 导出存储过程数据-指定列,没有指定的字段会被隐藏 🔖 + /// + /// + public async Task PocExport2(ExportProcInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + var dt = await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams); + + var headers = new Dictionary>(); + var index = 1; + foreach (var val in input.EHeader) + { + headers.Add(val.Key.ToUpper(), new Tuple(val.Value, index)); + index++; + } + var excelExporter = new ExcelExporter(); + var da = await excelExporter.ExportAsByteArray(dt, new ProcExporterHeaderFilter(headers)); + return new FileContentResult(da, "application/octet-stream") { FileDownloadName = input.ProcId + ".xlsx" }; + } + + /// + /// 根据模板导出存储过程数据 🔖 + /// + /// + /// + public async Task PocExport(ExportProcByTMPInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + var dt = await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams); + + var excelExporter = new ExcelExporter(); + string template = AppDomain.CurrentDomain.BaseDirectory + "/wwwroot/template/" + input.Template + ".xlsx"; + var bs = await excelExporter.ExportBytesByTemplate(dt, template); + return new FileContentResult(bs, "application/octet-stream") { FileDownloadName = input.ProcId + ".xlsx" }; + } + + /// + /// 获取存储过程返回表-Oracle、达梦参数顺序不能错 🔖 + /// + /// + /// + public async Task ProcTable(BaseProcInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + return await db.Ado.UseStoredProcedure().GetDataTableAsync(input.ProcId, input.ProcParams); + } + + /// + /// 获取存储过程返回数据集-Oracle、达梦参数顺序不能错 + /// Oracle 返回table、table1,其他返回table1、table2。适用于报表、复杂详细页面等 🔖 + /// + /// + /// + [HttpPost] + public async Task CommonDataSet(BaseProcInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + return await db.Ado.UseStoredProcedure().GetDataSetAllAsync(input.ProcId, input.ProcParams); + } + + ///// + ///// 根据配置表获取对映存储过程 + ///// + ///// + ///// + //public async Task ProcEnitybyConfig(BaseProcInput input) + //{ + // var key = "ProcConfig"; + // var ds = _sysCacheService.Get>(key); + // if (ds == null || ds.Count == 0 || !ds.ContainsKey(input.ProcId)) + // { + // var datas = await _db.Queryable().ToListAsync(); + // ds = datas.ToDictionary(m => m.ProcId, m => m.ProcName); + // _sysCacheService.Set(key, ds); + // } + // var procName = ds[input.ProcId]; + // return await _db.Ado.UseStoredProcedure().GetDataTableAsync(procName, input.ProcParams); + //} +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs new file mode 100644 index 0000000..d5c0b3e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/ConfigInput.cs @@ -0,0 +1,57 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class ConfigInput : BaseIdInput +{ +} + +public class PageConfigInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 分组编码 + /// + public string GroupCode { get; set; } +} + +public class AddConfigInput : SysConfig +{ +} + +public class UpdateConfigInput : AddConfigInput +{ +} + +public class DeleteConfigInput : BaseIdInput +{ +} + +/// +/// 批量配置参数输入 +/// +public class BatchConfigInput +{ + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 属性值 + /// + public string Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/InfoInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/InfoInput.cs new file mode 100644 index 0000000..6089b77 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/Dto/InfoInput.cs @@ -0,0 +1,84 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统信息保存输入参数 +/// +public class InfoSaveInput +{ + /// + /// 系统图标(Data URI scheme base64 编码) + /// + public string LogoBase64 { get; set; } + + /// + /// 系统图标文件名 + /// + public string LogoFileName { get; set; } + + /// + /// 水印内容 + /// + public string Watermark { get; set; } + + /// + /// 系统主标题 + /// + [Required(ErrorMessage = "系统主标题不能为空")] + public string Title { get; set; } + + /// + /// 系统副标题 + /// + [Required(ErrorMessage = "系统副标题不能为空")] + public string ViceTitle { get; set; } + + /// + /// 系统描述 + /// + [Required(ErrorMessage = "系统描述不能为空")] + public string ViceDesc { get; set; } + + /// + /// 版权说明 + /// + [Required(ErrorMessage = "版权说明不能为空")] + public string Copyright { get; set; } + + /// + /// ICP备案号 + /// + [Required(ErrorMessage = "ICP备案号不能为空")] + public string Icp { get; set; } + + /// + /// ICP地址 + /// + [Required(ErrorMessage = "ICP地址不能为空")] + public string IcpUrl { get; set; } + + /// + /// 启用注册功能 + /// + public YesNoEnum EnableReg { get; set; } + + /// + /// 登录二次验证 + /// + public YesNoEnum SecondVer { get; set; } + + /// + /// 图形验证码 + /// + public YesNoEnum Captcha { get; set; } + + /// + /// 默认注册方案Id + /// + public virtual long RegWayId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs new file mode 100644 index 0000000..46758a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs @@ -0,0 +1,308 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Reflection; + +namespace Admin.NET.Core.Service; + +/// +/// 平台参数配置服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysConfigService : IDynamicApiController, ITransient +{ + private static readonly SysTenantService SysTenantService = App.GetService(); + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysTenantRep; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + + public SysConfigService( + SqlSugarRepository sysTenantRep, + SqlSugarRepository sysConfigRep, + SysCacheService sysCacheService, + UserManager userManager) + { + _sysCacheService = sysCacheService; + _sysConfigRep = sysConfigRep; + _sysTenantRep = sysTenantRep; + _userManager = userManager; + } + + /// + /// 获取参数配置分页列表 🔖 + /// + /// + /// + [DisplayName("获取参数配置分页列表")] + public async Task> Page(PageConfigInput input) + { + return await _sysConfigRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code)) + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .OrderBuilder(input) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取参数配置列表 🔖 + /// + /// + [DisplayName("获取参数配置列表")] + public async Task> List(PageConfigInput input) + { + return await _sysConfigRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加参数配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加参数配置")] + public async Task AddConfig(AddConfigInput input) + { + if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010); + + var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + await _sysConfigRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新参数配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新参数配置")] + public async Task UpdateConfig(UpdateConfigInput input) + { + if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010); + + var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + var config = input.Adapt(); + await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync(); + + Remove(config); + } + + /// + /// 删除参数配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除参数配置")] + public async Task DeleteConfig(DeleteConfigInput input) + { + var config = await _sysConfigRep.GetFirstAsync(u => u.Id == input.Id); + + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) + { throw Oops.Oh(ErrorCodeEnum.D9001); } + else + { await _sysConfigRep.DeleteAsync(config); } + + Remove(config); + } + + /// + /// 批量删除参数配置 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除参数配置")] + public async Task BatchDeleteConfig(List ids) + { + foreach (var id in ids) + { + var config = await _sysConfigRep.GetFirstAsync(u => u.Id == id); + + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) continue; + + await _sysConfigRep.DeleteAsync(config); + + Remove(config); + } + } + + /// + /// 获取参数配置详情 🔖 + /// + /// + /// + [DisplayName("获取参数配置详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await _sysConfigRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 获取参数配置值 + /// + /// + /// + [NonAction] + public async Task GetConfigValue(string code) + { + if (string.IsNullOrWhiteSpace(code)) return default; + + var value = _sysCacheService.Get($"{CacheConst.KeyConfig}{code}"); + if (string.IsNullOrEmpty(value)) + { + value = (await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code))?.Value; + _sysCacheService.Set($"{CacheConst.KeyConfig}{code}", value); + } + if (string.IsNullOrWhiteSpace(value)) return default; + return (T)Convert.ChangeType(value, typeof(T)); + } + + /// + /// 更新参数配置值 + /// + /// + /// + /// + [NonAction] + public async Task UpdateConfigValue(string code, string value) + { + var config = await _sysConfigRep.AsQueryable().FirstAsync(u => u.Code == code); + if (config == null) return; + + config.Value = value; + await _sysConfigRep.AsUpdateable(config).ExecuteCommandAsync(); + + Remove(config); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysConfigRep.AsQueryable() + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 获取 Token 过期时间 + /// + /// + [NonAction] + public async Task GetTokenExpire() + { + var tokenExpireStr = await GetConfigValue(ConfigConst.SysTokenExpire); + _ = int.TryParse(tokenExpireStr, out var tokenExpire); + return tokenExpire == 0 ? 20 : tokenExpire; + } + + /// + /// 获取 RefreshToken 过期时间 + /// + /// + [NonAction] + public async Task GetRefreshTokenExpire() + { + var refreshTokenExpireStr = await GetConfigValue(ConfigConst.SysRefreshTokenExpire); + _ = int.TryParse(refreshTokenExpireStr, out var refreshTokenExpire); + return refreshTokenExpire == 0 ? 40 : refreshTokenExpire; + } + + /// + /// 批量更新参数配置值 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新参数配置值")] + public async Task BatchUpdateConfig(List input) + { + foreach (var config in input) + { + var info = await _sysConfigRep.AsQueryable().FirstAsync(c => c.Code == config.Code); + if (info == null || info.SysFlag == YesNoEnum.Y) continue; + + await _sysConfigRep.AsUpdateable().SetColumns(u => u.Value == config.Value).Where(u => u.Code == config.Code).ExecuteCommandAsync(); + Remove(info); + } + } + + /// + /// 获取系统信息 🔖 + /// + /// + [SuppressMonitor] + [AllowAnonymous] + [DisplayName("获取系统信息")] + public async Task GetSysInfo() + { + var tenant = await SysTenantService.GetCurrentTenantSysInfo(); + var wayList = await _sysConfigRep.Context.Queryable() + .Where(u => u.TenantId == tenant.Id) + .Select(u => new { Label = u.Name, Value = u.Id }) + .ToListAsync(); + + var captcha = await GetConfigValue(ConfigConst.SysCaptcha); + var secondVer = await GetConfigValue(ConfigConst.SysSecondVer); + var hideTenantForLogin = await GetConfigValue(ConfigConst.SysHideTenantLogin); + return new + { + tenant.Logo, + tenant.Title, + tenant.ViceTitle, + tenant.ViceDesc, + tenant.Watermark, + tenant.Copyright, + tenant.Icp, + tenant.IcpUrl, + tenant.RegWayId, + tenant.EnableReg, + SecondVer = secondVer ? YesNoEnum.Y : YesNoEnum.N, + Captcha = captcha ? YesNoEnum.Y : YesNoEnum.N, + HideTenantForLogin = hideTenantForLogin, + WayList = wayList + }; + } + + /// + /// 保存系统信息 🔖 + /// + /// + [UnitOfWork] + [DisplayName("保存系统信息")] + public async Task SaveSysInfo(InfoSaveInput input) + { + var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == _userManager.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + if (!string.IsNullOrEmpty(input.LogoBase64)) SysTenantService.SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName); + // await UpdateConfigValue(ConfigConst.SysCaptcha, (input.Captcha == YesNoEnum.Y).ToString()); + // await UpdateConfigValue(ConfigConst.SysSecondVer, (input.SecondVer == YesNoEnum.Y).ToString()); + + tenant.Copy(input); + tenant.RegWayId = input.EnableReg == YesNoEnum.Y ? input.RegWayId : null; + await _sysTenantRep.Context.Updateable(tenant).ExecuteCommandAsync(); + } + + private void Remove(SysConfig config) + { + _sysCacheService.Remove($"{CacheConst.KeyConfig}Value:{config.Code}"); + _sysCacheService.Remove($"{CacheConst.KeyConfig}Remark:{config.Code}"); + _sysCacheService.Remove($"{CacheConst.KeyConfig}{config.GroupCode}:GroupWithCache"); + _sysCacheService.Remove($"{CacheConst.KeyConfig}{config.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysTenantConfigService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysTenantConfigService.cs new file mode 100644 index 0000000..b92e4ef --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysTenantConfigService.cs @@ -0,0 +1,289 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统租户配置参数服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysTenantConfigService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysConfigDataRep; + public readonly ISugarQueryable VSysConfig; + private readonly UserManager _userManager; + + public SysTenantConfigService(SysCacheService sysCacheService, + SqlSugarRepository sysConfigRep, + SqlSugarRepository sysConfigDataRep, + UserManager userManager) + { + _userManager = userManager; + _sysCacheService = sysCacheService; + _sysConfigRep = sysConfigRep; + _sysConfigDataRep = sysConfigDataRep; + VSysConfig = _sysConfigRep.AsQueryable() + .InnerJoin( + _sysConfigDataRep.AsQueryable().WhereIF(!_userManager.SuperAdmin, cv => cv.TenantId == _userManager.TenantId), + (c, cv) => c.Id == cv.ConfigId + ) + //.Select().MergeTable(); + // 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致. + .SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value }); + } + + /// + /// 获取配置参数分页列表 🔖 + /// + /// + /// + [DisplayName("获取配置参数分页列表")] + public async Task> Page(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code)) + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .OrderBuilder(input) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取配置参数列表 🔖 + /// + /// + [DisplayName("获取配置参数列表")] + public async Task> List(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加配置参数")] + public async Task AddConfig(AddConfigInput input) + { + var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + var configId = _sysConfigRep.InsertReturnSnowflakeId(input.Adapt()); + await _sysConfigDataRep.InsertAsync(new SysTenantConfigData() + { + ConfigId = configId, + Value = input.Value + }); + } + + /// + /// 更新配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新配置参数")] + [UnitOfWork] + public async Task UpdateConfig(UpdateConfigInput input) + { + var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + var config = input.Adapt(); + await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync(); + var configData = await _sysConfigDataRep.GetFirstAsync(cv => cv.ConfigId == input.Id); + if (configData == null) + await _sysConfigDataRep.AsInsertable(new SysTenantConfigData() { ConfigId = input.Id, Value = input.Value }).ExecuteCommandAsync(); + else + { + configData.Value = input.Value; + await _sysConfigDataRep.AsUpdateable(configData).IgnoreColumns(true).ExecuteCommandAsync(); + } + + RemoveConfigCache(config); + } + + /// + /// 删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除配置参数")] + public async Task DeleteConfig(DeleteConfigInput input) + { + var config = await _sysConfigRep.GetByIdAsync(input.Id); + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001); + + await _sysConfigRep.DeleteAsync(config); + await _sysConfigDataRep.DeleteAsync(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id); + + RemoveConfigCache(config); + } + + /// + /// 批量删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除配置参数")] + public async Task BatchDeleteConfig(List ids) + { + foreach (var id in ids) + { + var config = await _sysConfigRep.GetByIdAsync(id); + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) continue; + + await _sysConfigRep.DeleteAsync(config); + await _sysConfigDataRep.DeleteAsync(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id); + + RemoveConfigCache(config); + } + } + + /// + /// 获取配置参数详情 🔖 + /// + /// + /// + [DisplayName("获取配置参数详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await VSysConfig.FirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据Code获取配置参数 🔖 + /// + /// + /// + [NonAction] + public async Task GetConfig(string code) + { + return await VSysConfig.FirstAsync(u => u.Code == code); + } + + /// + /// 根据Code获取配置参数值 🔖 + /// + /// 编码 + /// + [DisplayName("根据Code获取配置参数值")] + public async Task GetConfigValueByCode(string code) + { + return await GetConfigValueByCode(code); + } + + /// + /// 获取配置参数值 + /// + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, string defaultValue = default) + { + return await GetConfigValueByCode(code, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, T defaultValue = default) + { + return await GetConfigValueByCode(code, _userManager.TenantId, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 租户Id + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, long tenantId, T defaultValue = default) + { + if (string.IsNullOrWhiteSpace(code)) return defaultValue; + + var value = _sysCacheService.Get($"{CacheConst.KeyTenantConfig}{tenantId}{code}"); + if (string.IsNullOrEmpty(value)) + { + value = (await VSysConfig.FirstAsync(u => u.Code == code))?.Value; + _sysCacheService.Set($"{CacheConst.KeyTenantConfig}{tenantId}{code}", value); + } + if (string.IsNullOrWhiteSpace(value)) return defaultValue; + return (T)Convert.ChangeType(value, typeof(T)); + } + + /// + /// 更新配置参数值 + /// + /// + /// + /// + [NonAction] + public async Task UpdateConfigValue(string code, string value) + { + var config = await _sysConfigRep.GetFirstAsync(u => u.Code == code); + if (config == null) return; + + await _sysConfigDataRep.AsUpdateable().SetColumns(it => it.Value == value).Where(it => it.TenantId == _userManager.TenantId && it.ConfigId == config.Id).ExecuteCommandAsync(); + + RemoveConfigCache(config); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await VSysConfig + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 批量更新配置参数值 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新配置参数值")] + public async Task BatchUpdateConfig(List input) + { + foreach (var config in input) + { + await UpdateConfigValue(config.Code, config.Value); + } + } + + /// + /// 清除配置缓存 + /// + /// + private void RemoveConfigCache(SysTenantConfig config) + { + _sysCacheService.Remove($"{CacheConst.KeyTenantConfig}{_userManager.TenantId}{config.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysUserConfigService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysUserConfigService.cs new file mode 100644 index 0000000..897f31b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Config/SysUserConfigService.cs @@ -0,0 +1,290 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户配置参数服务 🧩 +/// +[ApiDescriptionSettings(Order = 440)] +public class SysUserConfigService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysConfigRep; + private readonly SqlSugarRepository _sysConfigDataRep; + public readonly ISugarQueryable VSysConfig; + private readonly UserManager _userManager; + + public SysUserConfigService(SysCacheService sysCacheService, + SqlSugarRepository sysConfigRep, + SqlSugarRepository sysConfigDataRep, + UserManager userManager) + { + _userManager = userManager; + _sysCacheService = sysCacheService; + _sysConfigRep = sysConfigRep; + _sysConfigDataRep = sysConfigDataRep; + VSysConfig = _sysConfigRep.AsQueryable() + .LeftJoin( + _sysConfigDataRep.AsQueryable().WhereIF(_userManager.SuperAdmin, cv => cv.UserId == _userManager.UserId), + (c, cv) => c.Id == cv.ConfigId + ) + //.Select().MergeTable(); + // 解决PostgreSQL下并启用驼峰转下划线时,报字段不存在,SqlSugar在Select后生成的sql, as后没转下划线导致. + .SelectMergeTable((c, cv) => new SysConfig { Id = c.Id.SelectAll(), Value = cv.Value }); + } + + /// + /// 获取配置参数分页列表 🔖 + /// + /// + /// + [DisplayName("获取配置参数分页列表")] + public async Task> Page(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.Name?.Trim()), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code?.Trim()), u => u.Code.Contains(input.Code)) + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .OrderBuilder(input) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取配置参数列表 🔖 + /// + /// + [DisplayName("获取配置参数列表")] + public async Task> List(PageConfigInput input) + { + return await VSysConfig + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupCode?.Trim()), u => u.GroupCode.Equals(input.GroupCode)) + .ToListAsync(); + } + + /// + /// 增加配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加配置参数")] + public async Task AddConfig(AddConfigInput input) + { + var isExist = await _sysConfigRep.IsAnyAsync(u => u.Name == input.Name || u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + var configId = _sysConfigRep.InsertReturnSnowflakeId(input.Adapt()); + await _sysConfigDataRep.InsertAsync(new SysUserConfigData() + { + UserId = _userManager.UserId, + ConfigId = configId, + Value = input.Value + }); + } + + /// + /// 更新配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新配置参数")] + [UnitOfWork] + public async Task UpdateConfig(UpdateConfigInput input) + { + var isExist = await _sysConfigRep.IsAnyAsync(u => (u.Name == input.Name || u.Code == input.Code) && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D9000); + + var config = input.Adapt(); + await _sysConfigRep.AsUpdateable(config).IgnoreColumns(true).ExecuteCommandAsync(); + var configData = await _sysConfigDataRep.GetFirstAsync(cv => cv.ConfigId == input.Id); + if (configData == null) + await _sysConfigDataRep.AsInsertable(new SysUserConfigData() { UserId = _userManager.UserId, ConfigId = input.Id, Value = input.Value }).ExecuteCommandAsync(); + else + { + configData.Value = input.Value; + await _sysConfigDataRep.AsUpdateable(configData).IgnoreColumns(true).ExecuteCommandAsync(); + } + + RemoveConfigCache(config); + } + + /// + /// 删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除配置参数")] + public async Task DeleteConfig(DeleteConfigInput input) + { + var config = await _sysConfigRep.GetByIdAsync(input.Id); + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D9001); + + await _sysConfigRep.DeleteAsync(config); + await _sysConfigDataRep.DeleteAsync(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id); + + RemoveConfigCache(config); + } + + /// + /// 批量删除配置参数 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + [DisplayName("批量删除配置参数")] + public async Task BatchDeleteConfig(List ids) + { + foreach (var id in ids) + { + var config = await _sysConfigRep.GetByIdAsync(id); + // 禁止删除系统参数 + if (config.SysFlag == YesNoEnum.Y) continue; + + await _sysConfigRep.DeleteAsync(config); + await _sysConfigDataRep.DeleteAsync(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id); + + RemoveConfigCache(config); + } + } + + /// + /// 获取配置参数详情 🔖 + /// + /// + /// + [DisplayName("获取配置参数详情")] + public async Task GetDetail([FromQuery] ConfigInput input) + { + return await VSysConfig.FirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据Code获取配置参数 🔖 + /// + /// + /// + [NonAction] + public async Task GetConfig(string code) + { + return await VSysConfig.FirstAsync(u => u.Code == code); + } + + /// + /// 根据Code获取配置参数值 🔖 + /// + /// 编码 + /// + [DisplayName("根据Code获取配置参数值")] + public async Task GetConfigValueByCode(string code) + { + return await GetConfigValueByCode(code); + } + + /// + /// 获取配置参数值 + /// + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, string defaultValue = default) + { + return await GetConfigValueByCode(code, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, T defaultValue = default) + { + return await GetConfigValueByCode(code, _userManager.UserId, defaultValue); + } + + /// + /// 获取配置参数值 + /// + /// 类型 + /// 编码 + /// 用户Id + /// 默认值 + /// + [NonAction] + public async Task GetConfigValueByCode(string code, long userId, T defaultValue = default) + { + if (string.IsNullOrWhiteSpace(code)) return defaultValue; + + var value = _sysCacheService.Get($"{CacheConst.KeyUserConfig}{userId}{code}"); + if (string.IsNullOrEmpty(value)) + { + value = (await VSysConfig.FirstAsync(u => u.Code == code))?.Value; + _sysCacheService.Set($"{CacheConst.KeyUserConfig}{userId}{code}", value); + } + if (string.IsNullOrWhiteSpace(value)) return defaultValue; + return (T)Convert.ChangeType(value, typeof(T)); + } + + /// + /// 更新配置参数值 + /// + /// + /// + /// + [NonAction] + public async Task UpdateConfigValue(string code, string value) + { + var config = await _sysConfigRep.GetFirstAsync(u => u.Code == code); + if (config == null) return; + + await _sysConfigDataRep.AsUpdateable().SetColumns(it => it.Value == value).Where(it => it.UserId == _userManager.UserId && it.ConfigId == config.Id).ExecuteCommandAsync(); + + RemoveConfigCache(config); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysConfigRep.AsQueryable() + .GroupBy(u => u.GroupCode) + .Select(u => u.GroupCode).ToListAsync(); + } + + /// + /// 批量更新配置参数值 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchUpdate"), HttpPost] + [DisplayName("批量更新配置参数值")] + public async Task BatchUpdateConfig(List input) + { + foreach (var config in input) + { + await UpdateConfigValue(config.Code, config.Value); + } + } + + /// + /// 清除配置缓存 + /// + /// + private void RemoveConfigCache(SysUserConfig config) + { + _sysCacheService.Remove($"{CacheConst.KeyUserConfig}{_userManager.UserId}{config.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs new file mode 100644 index 0000000..698f957 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/Dto/ConstOutput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class ConstOutput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public dynamic Code { get; set; } + + /// + /// 扩展字段 + /// + public dynamic Data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/SysConstService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/SysConstService.cs new file mode 100644 index 0000000..f69c264 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Const/SysConstService.cs @@ -0,0 +1,83 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统常量服务 🧩 +/// +[ApiDescriptionSettings(Order = 280)] +public class SysConstService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + + public SysConstService(SysCacheService sysCacheService) + { + _sysCacheService = sysCacheService; + } + + /// + /// 获取所有常量列表 🔖 + /// + /// + [DisplayName("获取所有常量列表")] + public async Task> GetList() + { + var key = $"{CacheConst.KeyConst}list"; + var constList = _sysCacheService.Get>(key); + if (constList == null) + { + var typeList = GetConstAttributeList(); + constList = typeList.Select(u => new ConstOutput + { + Name = u.CustomAttributes.ToList().FirstOrDefault()?.ConstructorArguments.ToList().FirstOrDefault().Value?.ToString() ?? u.Name, + Code = u.Name, + Data = GetData(Convert.ToString(u.Name)) + }).ToList(); + _sysCacheService.Set(key, constList); + } + return await Task.FromResult(constList); + } + + /// + /// 根据类名获取常量数据 🔖 + /// + /// + /// + [DisplayName("根据类名获取常量数据")] + public async Task> GetData([Required] string typeName) + { + var key = $"{CacheConst.KeyConst}{typeName.ToUpper()}"; + var constList = _sysCacheService.Get>(key); + if (constList == null) + { + var typeList = GetConstAttributeList(); + var type = typeList.FirstOrDefault(u => u.Name == typeName); + if (type != null) + { + var isEnum = type.BaseType!.Name == "Enum"; + constList = type.GetFields()? + .Where(isEnum, u => u.FieldType.Name == typeName) + .Select(u => new ConstOutput + { + Name = u.Name, + Code = isEnum ? (int)u.GetValue(BindingFlags.Instance)! : u.GetValue(BindingFlags.Instance) + }).ToList(); + _sysCacheService.Set(key, constList); + } + } + return await Task.FromResult(constList); + } + + /// + /// 获取常量特性类型列表 + /// + /// + private List GetConstAttributeList() + { + return App.EffectiveTypes.Where(u => u.CustomAttributes.Any(c => c.AttributeType == typeof(ConstAttribute))).ToList(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs new file mode 100644 index 0000000..ffc3045 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateEntityInput.cs @@ -0,0 +1,39 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class CreateEntityInput +{ + /// + /// 表名 + /// + /// student + public string TableName { get; set; } + + /// + /// 实体名 + /// + /// Student + public string EntityName { get; set; } + + /// + /// 基类名 + /// + /// AutoIncrementEntity + public string BaseClassName { get; set; } + + /// + /// 导出位置 + /// + /// Web.Application + public string Position { get; set; } + + /// + /// 库标识 + /// + public string ConfigId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs new file mode 100644 index 0000000..f124f6d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class CreateSeedDataInput +{ + /// + /// 库标识 + /// + public string ConfigId { get; set; } + + /// + /// 表名 + /// + /// student + public string TableName { get; set; } + + /// + /// 实体名称 + /// + /// Student + public string EntityName { get; set; } + + /// + /// 种子名称 + /// + /// Student + public string SeedDataName { get; set; } + + /// + /// 导出位置 + /// + /// Web.Application + public string Position { get; set; } + + /// + /// 后缀 + /// + /// Web.Application + public string Suffix { get; set; } + + /// + /// 过滤已有数据 + /// + /// + /// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据 + /// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。 + /// + public bool FilterExistingData { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs new file mode 100644 index 0000000..24e26a5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnInput.cs @@ -0,0 +1,79 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DbColumnInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string DbColumnName { get; set; } + + public string DataType { get; set; } + + public int Length { get; set; } + + public string ColumnDescription { get; set; } + + public int IsNullable { get; set; } + + public int IsIdentity { get; set; } + + public int IsPrimarykey { get; set; } + + public int DecimalDigits { get; set; } + + public string DefaultValue { get; set; } +} + +public class UpdateDbColumnInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string ColumnName { get; set; } + + public string OldColumnName { get; set; } + + public string Description { get; set; } + + public string DefaultValue { get; set; } +} + +public class MoveDbColumnInput +{ + /// + /// 数据库配置ID + /// + public string ConfigId { get; set; } + + /// + /// 目标表名 + /// + public string TableName { get; set; } + + /// + ///要移动的列名 + /// + public string ColumnName { get; set; } + + /// + /// 移动到该列后方(为空时移动到首列) + /// + public string AfterColumnName { get; set; } +} + +public class DeleteDbColumnInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string DbColumnName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs new file mode 100644 index 0000000..87e30b9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbColumnOutput.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DbColumnOutput +{ + public string TableName { get; set; } + + public int TableId { get; set; } + + public string DbColumnName { get; set; } + + public string PropertyName { get; set; } + + public string DataType { get; set; } + + public object PropertyType { get; set; } + + public int Length { get; set; } + + public string ColumnDescription { get; set; } + + public string DefaultValue { get; set; } + + public bool IsNullable { get; set; } + + public bool IsIdentity { get; set; } + + public bool IsPrimarykey { get; set; } + + public object Value { get; set; } + + public int DecimalDigits { get; set; } + + public int Scale { get; set; } + + public bool IsArray { get; set; } + + public bool IsJson { get; set; } + + public bool? IsUnsigned { get; set; } + + public int CreateTableFieldSort { get; set; } + + internal object SqlParameterDbType { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs new file mode 100644 index 0000000..9c64d5d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableInput.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DbTableInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string Description { get; set; } + + public List DbColumnInfoList { get; set; } +} + +public class UpdateDbTableInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } + + public string OldTableName { get; set; } + + public string Description { get; set; } +} + +public class DeleteDbTableInput +{ + public string ConfigId { get; set; } + + public string TableName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs new file mode 100644 index 0000000..d77a9c7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/DbTableVisual.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class VisualDb +{ + public string ConfigId { get; set; } + public string DbNickName { get; set; } +} + +/// +/// 库表可视化 +/// +public class VisualDbTable +{ + public List VisualTableList { get; set; } + + public List VisualColumnList { get; set; } + + public List ColumnRelationList { get; set; } +} + +public class VisualTable +{ + public string TableName { get; set; } + + public string TableComents { get; set; } + + public int X { get; set; } + + public int Y { get; set; } +} + +public class VisualColumn +{ + public string TableName { get; set; } + + public string ColumnName { get; set; } + + public string DataType { get; set; } + + public string DataLength { get; set; } + + public string ColumnDescription { get; set; } +} + +public class ColumnRelation +{ + public string SourceTableName { get; set; } + + public string SourceColumnName { get; set; } + + public string Type { get; set; } + + public string TargetTableName { get; set; } + + public string TargetColumnName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs new file mode 100644 index 0000000..4727626 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/Dto/JsonIgnoredPropertyData.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 保存标注了JsonIgnore的Property的值信息 +/// +public class JsonIgnoredPropertyData +{ + /// + /// 记录索引 + /// + public int RecordIndex { get; set; } + + /// + /// 属性名 + /// + public string Name { get; set; } + + /// + /// 属性值描述 + /// + public string Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs new file mode 100644 index 0000000..13d3cda --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/DataBase/SysDatabaseService.cs @@ -0,0 +1,776 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Npgsql; + +namespace Admin.NET.Core.Service; + +/// +/// 系统数据库管理服务 🧩 +/// +[ApiDescriptionSettings(Order = 250)] +public class SysDatabaseService : IDynamicApiController, ITransient +{ + private readonly ISqlSugarClient _db; + private readonly IViewEngine _viewEngine; + private readonly CodeGenOptions _codeGenOptions; + + public SysDatabaseService(ISqlSugarClient db, + IViewEngine viewEngine, + IOptions codeGenOptions) + { + _db = db; + _viewEngine = viewEngine; + _codeGenOptions = codeGenOptions.Value; + } + + /// + /// 获取库列表 🔖 + /// + /// + [DisplayName("获取库列表")] + public List GetList() + { + return App.GetOptions().ConnectionConfigs.Select(u => new VisualDb { ConfigId = u.ConfigId.ToString(), DbNickName = u.DbNickName }).ToList(); + } + + /// + /// 获取可视化库表结构 🔖 + /// + /// + [DisplayName("获取可视化库表结构")] + public VisualDbTable GetVisualDbTable() + { + var visualTableList = new List(); + var visualColumnList = new List(); + var columnRelationList = new List(); + var dbOptions = App.GetOptions().ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId); + + // 遍历所有实体获取所有库表结构 + var random = new Random(); + var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)).ToList(); + foreach (var entityType in entityTypes) + { + var entityInfo = _db.EntityMaintenance.GetEntityInfoNoCache(entityType); + + var visualTable = new VisualTable + { + TableName = entityInfo.DbTableName, + TableComents = entityInfo.TableDescription + entityInfo.DbTableName, + X = random.Next(5000), + Y = random.Next(5000) + }; + visualTableList.Add(visualTable); + + foreach (EntityColumnInfo columnInfo in entityInfo.Columns) + { + var visualColumn = new VisualColumn + { + TableName = columnInfo.DbTableName, + ColumnName = dbOptions.DbSettings.EnableUnderLine ? columnInfo.DbColumnName.ToUnderLine() : columnInfo.DbColumnName, + DataType = columnInfo.PropertyInfo.PropertyType.Name, + DataLength = columnInfo.Length.ToString(), + ColumnDescription = columnInfo.ColumnDescription, + }; + visualColumnList.Add(visualColumn); + + // 根据导航配置获取表之间关联关系 + if (columnInfo.Navigat != null) + { + var name1 = columnInfo.Navigat.GetName(); + var name2 = columnInfo.Navigat.GetName2(); + var targetColumnName = string.IsNullOrEmpty(name2) ? "Id" : name2; + var relation = new ColumnRelation + { + SourceTableName = columnInfo.DbTableName, + SourceColumnName = dbOptions.DbSettings.EnableUnderLine ? name1.ToUnderLine() : name1, + Type = columnInfo.Navigat.GetNavigateType() == NavigateType.OneToOne ? "ONE_TO_ONE" : "ONE_TO_MANY", + TargetTableName = dbOptions.DbSettings.EnableUnderLine ? columnInfo.DbColumnName.ToUnderLine() : columnInfo.DbColumnName, + TargetColumnName = dbOptions.DbSettings.EnableUnderLine ? targetColumnName.ToUnderLine() : targetColumnName + }; + columnRelationList.Add(relation); + } + } + } + + return new VisualDbTable { VisualTableList = visualTableList, VisualColumnList = visualColumnList, ColumnRelationList = columnRelationList }; + } + + /// + /// 获取字段列表 🔖 + /// + /// 表名 + /// ConfigId + /// + [DisplayName("获取字段列表")] + public List GetColumnList(string tableName, string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return string.IsNullOrWhiteSpace(tableName) ? new List() : db.DbMaintenance.GetColumnInfosByTableName(tableName, false).Adapt>(); + } + + /// + /// 获取数据库数据类型列表 🔖 + /// + /// + /// + [DisplayName("获取数据库数据类型列表")] + public List GetDbTypeList(string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return db.DbMaintenance.GetDbTypes().OrderBy(u => u).ToList(); + } + + /// + /// 增加列 🔖 + /// + /// + [ApiDescriptionSettings(Name = "AddColumn"), HttpPost] + [DisplayName("增加列")] + public void AddColumn(DbColumnInput input) + { + var column = new DbColumnInfo + { + ColumnDescription = input.ColumnDescription, + DbColumnName = input.DbColumnName, + IsIdentity = input.IsIdentity == 1, + IsNullable = input.IsNullable == 1, + IsPrimarykey = input.IsPrimarykey == 1, + Length = input.Length, + DecimalDigits = input.DecimalDigits, + DataType = input.DataType + }; + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.AddColumn(input.TableName, column); + // 默认值直接添加报错 + if (!string.IsNullOrWhiteSpace(input.DefaultValue)) + { + db.DbMaintenance.AddDefaultValue(input.TableName, column.DbColumnName, input.DefaultValue); + } + db.DbMaintenance.AddColumnRemark(input.DbColumnName, input.TableName, input.ColumnDescription); + if (column.IsPrimarykey) db.DbMaintenance.AddPrimaryKey(input.TableName, input.DbColumnName); + } + + /// + /// 删除列 🔖 + /// + /// + [ApiDescriptionSettings(Name = "DeleteColumn"), HttpPost] + [DisplayName("删除列")] + public void DeleteColumn(DeleteDbColumnInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.DropColumn(input.TableName, input.DbColumnName); + } + + /// + /// 编辑列 🔖 + /// + /// + [ApiDescriptionSettings(Name = "UpdateColumn"), HttpPost] + [DisplayName("编辑列")] + public void UpdateColumn(UpdateDbColumnInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + + // 前端未修改列名时,不进行重命名操作,避免报错 + if (input.OldColumnName != input.ColumnName) + { + db.DbMaintenance.RenameColumn(input.TableName, input.OldColumnName, input.ColumnName); + } + + if (!string.IsNullOrWhiteSpace(input.DefaultValue)) + { + db.DbMaintenance.AddDefaultValue(input.TableName, input.ColumnName, input.DefaultValue); + } + //if (db.DbMaintenance.IsAnyColumnRemark(input.ColumnName, input.TableName)) + //{ + // db.DbMaintenance.DeleteColumnRemark(input.ColumnName, input.TableName); + //} + + db.DbMaintenance.AddColumnRemark(input.ColumnName, input.TableName, string.IsNullOrWhiteSpace(input.Description) ? input.ColumnName : input.Description); + } + + /// + /// 移动列位置 🔖 + /// + /// + [ApiDescriptionSettings(Name = "MoveColumn"), HttpPost] + [DisplayName("移动列")] + public void MoveColumn(MoveDbColumnInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + var dbMaintenance = db.DbMaintenance; + + var dbType = db.CurrentConnectionConfig.DbType; + + var columns = dbMaintenance.GetColumnInfosByTableName(input.TableName, false); + var targetColumn = columns.FirstOrDefault(c => + c.DbColumnName.Equals(input.ColumnName, StringComparison.OrdinalIgnoreCase)); + + if (targetColumn == null) + throw new Exception($"列 {input.ColumnName} 在表 {input.TableName} 中不存在"); + + switch (dbType) + { + case SqlSugar.DbType.MySql: + MoveColumnInMySQL(db, input.TableName, input.ColumnName, input.AfterColumnName); + break; + + default: + throw new NotSupportedException($"暂不支持 {dbType} 数据库的列移动操作"); + } + } + + /// + /// 获取列定义 + /// + /// + /// + /// + /// + /// + /// + private string GetColumnDefinitionInMySQL(ISqlSugarClient db, string tableName, string columnName, bool noDefault = false) + { + var columnDef = db.Ado.SqlQuery( + $"SHOW FULL COLUMNS FROM `{tableName}` WHERE Field = '{columnName}'" + ).FirstOrDefault(); + + if (columnDef == null) + throw new Exception($"Column {columnName} not found"); + + var definition = new StringBuilder(); + definition.Append($"`{columnName}` "); // 列名 + definition.Append($"{columnDef.Type} "); // 数据类型 + + // 处理约束条件 + definition.Append(columnDef.Null == "YES" ? "NULL " : "NOT NULL "); + if (columnDef.Default != null && !noDefault) + definition.Append($"DEFAULT '{columnDef.Default}' "); + if (!string.IsNullOrEmpty(columnDef.Extra)) + definition.Append($"{columnDef.Extra} "); + if (!string.IsNullOrEmpty(columnDef.Comment)) + definition.Append($"COMMENT '{columnDef.Comment.Replace("'", "''")}'"); + + return definition.ToString(); + } + + /// + /// MySQL 列移动实现 + /// + /// + /// + /// + /// + private void MoveColumnInMySQL(ISqlSugarClient db, string tableName, string columnName, string afterColumnName) + { + var definition = GetColumnDefinitionInMySQL(db, tableName, columnName); + var sql = new StringBuilder(); + sql.Append($"ALTER TABLE `{tableName}` MODIFY COLUMN {definition}"); + + if (string.IsNullOrEmpty(afterColumnName)) + sql.Append(" FIRST"); + else + sql.Append($" AFTER `{afterColumnName}`"); + + db.Ado.ExecuteCommand(sql.ToString()); + } + + /// + /// 获取表列表 🔖 + /// + /// ConfigId + /// + [DisplayName("获取表列表")] + public List GetTableList(string configId = SqlSugarConst.MainConfigId) + { + var db = _db.AsTenant().GetConnectionScope(configId); + return db.DbMaintenance.GetTableInfoList(false); + } + + /// + /// 增加表 🔖 + /// + /// + [ApiDescriptionSettings(Name = "AddTable"), HttpPost] + [DisplayName("增加表")] + public void AddTable(DbTableInput input) + { + if (input.DbColumnInfoList == null || !input.DbColumnInfoList.Any()) + throw Oops.Oh(ErrorCodeEnum.db1000); + + if (input.DbColumnInfoList.GroupBy(u => u.DbColumnName).Any(u => u.Count() > 1)) + throw Oops.Oh(ErrorCodeEnum.db1002); + + var config = App.GetOptions().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId); + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + var typeBuilder = db.DynamicBuilder().CreateClass(input.TableName, new SugarTable() { TableName = input.TableName, TableDescription = input.Description }); + input.DbColumnInfoList.ForEach(u => + { + var dbColumnName = config!.DbSettings.EnableUnderLine ? u.DbColumnName.Trim().ToUnderLine() : u.DbColumnName.Trim(); + // 虚拟类都默认string类型,具体以列数据类型为准 + typeBuilder.CreateProperty(dbColumnName, typeof(string), new SugarColumn() + { + IsPrimaryKey = u.IsPrimarykey == 1, + IsIdentity = u.IsIdentity == 1, + ColumnDataType = u.DataType, + Length = u.Length, + IsNullable = u.IsNullable == 1, + DecimalDigits = u.DecimalDigits, + ColumnDescription = u.ColumnDescription, + DefaultValue = u.DefaultValue, + }); + }); + db.CodeFirst.InitTables(typeBuilder.BuilderType()); + } + + /// + /// 删除表 🔖 + /// + /// + [ApiDescriptionSettings(Name = "DeleteTable"), HttpPost] + [DisplayName("删除表")] + public void DeleteTable(DeleteDbTableInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.DropTable(input.TableName); + } + + /// + /// 编辑表 🔖 + /// + /// + [ApiDescriptionSettings(Name = "UpdateTable"), HttpPost] + [DisplayName("编辑表")] + public void UpdateTable(UpdateDbTableInput input) + { + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + db.DbMaintenance.RenameTable(input.OldTableName, input.TableName); + try + { + if (db.DbMaintenance.IsAnyTableRemark(input.TableName)) + db.DbMaintenance.DeleteTableRemark(input.TableName); + + if (!string.IsNullOrWhiteSpace(input.Description)) + db.DbMaintenance.AddTableRemark(input.TableName, input.Description); + } + catch (NotSupportedException ex) + { + throw Oops.Oh(ex.ToString()); + } + } + + /// + /// 创建实体 🔖 + /// + /// + [ApiDescriptionSettings(Name = "CreateEntity"), HttpPost] + [DisplayName("创建实体")] + public void CreateEntity(CreateEntityInput input) + { + var config = App.GetOptions().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId); + input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Admin.NET.Application" : input.Position; + input.EntityName = string.IsNullOrWhiteSpace(input.EntityName) ? (config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(input.TableName, null) : input.TableName) : input.EntityName; + string[] dbColumnNames = Array.Empty(); + // Entity.cs.vm中是允许创建没有基类的实体的,所以这里也要做出相同的判断 + if (!string.IsNullOrWhiteSpace(input.BaseClassName)) + { + Assembly assembly = Assembly.Load("Admin.NET.Core"); + Type type = assembly.GetType($"Admin.NET.Core.{input.BaseClassName}"); + if (type is null) + throw Oops.Oh("基类集合配置不存在此类型"); + dbColumnNames = CodeGenUtil.GetPropertyInfoArray(type)?.Select(p => p.Name).ToArray(); + if (dbColumnNames is null || dbColumnNames is { Length: 0 }) + throw Oops.Oh("基类中不存在任何字段"); + } + var templatePath = GetEntityTemplatePath(); + var targetPath = GetEntityTargetPath(input); + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + DbTableInfo dbTableInfo = db.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == input.TableName || u.Name == input.TableName.ToLower()) ?? throw Oops.Oh(ErrorCodeEnum.db1001); + List dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false); + dbColumnInfos.ForEach(u => + { + if (u.DbColumnName.ToUpper() == u.DbColumnName) + { + //字段全是大写的, 这种情况下生成的代码会有问题(即对 DOB 这样的字段,生成的前端代码为 dOB, 而数据序列化到前端又成了 dob,导致bug),因此抛出异常,不允许。 + throw new Exception($"错误:{u.DbColumnName} 字段全是大写字母,这样生成的代码会有bug!请更改为大写字母开头的驼峰式命名!"); + } + u.PropertyName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, dbColumnNames) : u.DbColumnName; // 转下划线后的列名需要再转回来 + u.DataType = CodeGenUtil.ConvertDataType(u, config.DbType); + }); + if (_codeGenOptions.BaseEntityNames.Contains(input.BaseClassName, StringComparer.OrdinalIgnoreCase)) + dbColumnInfos = dbColumnInfos.Where(u => !dbColumnNames.Contains(u.PropertyName, StringComparer.OrdinalIgnoreCase)).ToList(); + + var tContent = File.ReadAllText(templatePath); + var tResult = _viewEngine.RunCompileFromCached(tContent, new + { + NameSpace = $"{input.Position}.Entity", + input.TableName, + input.EntityName, + BaseClassName = string.IsNullOrWhiteSpace(input.BaseClassName) ? "" : $": {input.BaseClassName}", + input.ConfigId, + dbTableInfo.Description, + TableField = dbColumnInfos + }); + File.WriteAllText(targetPath, tResult, Encoding.UTF8); + } + + /// + /// 创建种子数据 🔖 + /// + /// + [ApiDescriptionSettings(Name = "CreateSeedData"), HttpPost] + [DisplayName("创建种子数据")] + public async Task CreateSeedData(CreateSeedDataInput input) + { + var config = App.GetOptions().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId); + input.Position = string.IsNullOrWhiteSpace(input.Position) ? "Admin.NET.Core" : input.Position; + + var templatePath = GetSeedDataTemplatePath(); + var db = _db.AsTenant().GetConnectionScope(input.ConfigId); + var tableInfo = db.DbMaintenance.GetTableInfoList(false).First(u => u.Name == input.TableName); // 表名 + List dbColumnInfos = db.DbMaintenance.GetColumnInfosByTableName(input.TableName, false); // 所有字段 + IEnumerable entityInfos = await GetEntityInfos(); + Type entityType = null; + foreach (var item in entityInfos) + { + if (tableInfo.Name.ToLower() != (config.DbSettings.EnableUnderLine ? item.DbTableName.ToUnderLine() : item.DbTableName).ToLower()) continue; + entityType = item.Type; + break; + } + if (entityType == null) throw Oops.Oh(ErrorCodeEnum.db1003); + + input.EntityName = entityType.Name; + input.SeedDataName = entityType.Name + "SeedData"; + if (!string.IsNullOrWhiteSpace(input.Suffix)) input.SeedDataName += input.Suffix; + + // 查询所有数据 + var query = db.QueryableByObject(entityType); + // 优先用创建时间排序 + DbColumnInfo orderField = dbColumnInfos.FirstOrDefault(u => u.DbColumnName.ToLower() == "create_time" || u.DbColumnName.ToLower() == "createtime"); + if (orderField != null) query = query.OrderBy(orderField.DbColumnName); + // 再使用第一个主键排序 + query = query.OrderBy(dbColumnInfos.First(u => u.IsPrimarykey).DbColumnName); + var records = ((IEnumerable)await query.ToListAsync()).ToDynamicList(); + + // 过滤已存在的数据 + if (input.FilterExistingData && records.Any()) + { + // 获取实体类型-所有种数据数据类型 + var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && u.FullName.EndsWith("." + input.EntityName)) + .Where(u => !u.GetCustomAttributes().Any()) + .ToList(); + if (entityTypes.Count == 1) // 只有一个实体匹配才能过滤 + { + // 获取实体的主键对应的属性名称 + var pkInfo = entityTypes[0].GetProperties().FirstOrDefault(u => u.GetCustomAttribute()?.IsPrimaryKey == true); + if (pkInfo != null) + { + var seedDataTypes = App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any( + i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)) && i.GenericTypeArguments[0] == entityTypes[0] + ) + ) + .ToList(); + // 可能会重名的种子数据不作为过滤项 + string doNotFilterFullName1 = $"{input.Position}.SeedData.{input.SeedDataName}"; + string doNotFilterFullName2 = $"{input.Position}.{input.SeedDataName}"; // Core中的命名空间没有SeedData + + PropertyInfo idPropertySeedData = records[0].GetType().GetProperty("Id"); + + for (int i = seedDataTypes.Count - 1; i >= 0; i--) + { + string fullName = seedDataTypes[i].FullName; + if ((fullName == doNotFilterFullName1) || (fullName == doNotFilterFullName2)) continue; + + // 删除重复数据 + var instance = Activator.CreateInstance(seedDataTypes[i]); + var hasDataMethod = seedDataTypes[i].GetMethod("HasData"); + var seedData = ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast(); + if (seedData == null) continue; + + List recordsToRemove = new(); + foreach (var record in records) + { + object recordId = pkInfo.GetValue(record); + if (seedData.Select(d1 => idPropertySeedData.GetValue(d1)).Any(dataId => recordId != null && dataId != null && recordId.Equals(dataId))) + { + recordsToRemove.Add(record); + } + } + foreach (var itemToRemove in recordsToRemove) + { + records.Remove(itemToRemove); + } + } + } + } + } + + // 检查有没有 System.Text.Json.Serialization.JsonIgnore 的属性 + // var jsonIgnoreProperties = entityType.GetProperties().Where(p => (p.GetAttribute() != null || + // p.GetAttribute() != null) && p.GetAttribute() != null).ToList(); + // var jsonIgnoreInfo = new List>(); + // if (jsonIgnoreProperties.Count > 0) + // { + // int recordIndex = 0; + // foreach (var r in (IEnumerable)records) + // { + // List record = new(); + // foreach (var item in jsonIgnoreProperties) + // { + // object v = item.GetValue(r); + // string strValue = "null"; + // if (v != null) + // { + // strValue = v.ToString(); + // if (v.GetType() == typeof(string)) + // strValue = "\"" + strValue + "\""; + // else if (v.GetType() == typeof(DateTime)) + // strValue = "DateTime.Parse(\"" + ((DateTime)v).ToString("yyyy-MM-dd HH:mm:ss") + "\")"; + // } + // record.Add(new JsonIgnoredPropertyData { RecordIndex = recordIndex, Name = item.Name, Value = strValue }); + // } + // recordIndex++; + // jsonIgnoreInfo.Add(record); + // } + // } + + // 获取所有字段信息 + var propertyList = entityType.GetProperties().Where(x => false == (x.GetCustomAttribute()?.IsIgnore ?? false)).ToList(); + for (var i = 0; i < propertyList.Count; i++) + { + if (propertyList[i].Name != nameof(EntityBaseId.Id) || !(propertyList[i].GetCustomAttribute()?.IsPrimaryKey ?? true)) continue; + var temp = propertyList[i]; + for (var j = i; j > 0; j--) propertyList[j] = propertyList[j - 1]; + propertyList[0] = temp; + } + // 拼接数据 + var recordList = records.Select(obj => string.Join(", ", propertyList.Select(prop => + { + var propType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; + object value = prop.GetValue(obj); + if (value == null) value = "null"; + else if (propType == typeof(string)) + { + value = $"@\"{value}\""; + } + else if (propType.IsEnum) + { + value = $"{propType.Name}.{value}"; + } + else if (propType == typeof(bool)) + { + value = (bool)value ? "true" : "false"; + } + else if (propType == typeof(DateTime)) + { + value = $"DateTime.Parse(\"{((DateTime)value):yyyy-MM-dd HH:mm:ss.fff}\")"; + } + return $"{prop.Name}={value}"; + }))).ToList(); + + var tContent = await File.ReadAllTextAsync(templatePath); + var data = new + { + NameSpace = $"{input.Position}.SeedData", + EntityNameSpace = entityType.Namespace, + input.TableName, + input.EntityName, + input.SeedDataName, + input.ConfigId, + tableInfo.Description, + // JsonIgnoreInfo = jsonIgnoreInfo, + RecordList = recordList + }; + var tResult = await _viewEngine.RunCompileAsync(tContent, data, builderAction: builder => + { + builder.AddAssemblyReferenceByName("System.Linq"); + builder.AddAssemblyReferenceByName("System.Collections"); + builder.AddUsing("System.Collections.Generic"); + builder.AddUsing("System.Linq"); + }); + + var targetPath = GetSeedDataTargetPath(input); + await File.WriteAllTextAsync(targetPath, tResult, Encoding.UTF8); + } + + /// + /// 获取库表信息 + /// + /// + private async Task> GetEntityInfos() + { + var entityInfos = new List(); + + var type = typeof(SugarTable); + var types = new List(); + if (_codeGenOptions.EntityAssemblyNames != null) + { + foreach (var asm in _codeGenOptions.EntityAssemblyNames.Select(Assembly.Load)) + { + types.AddRange(asm.GetExportedTypes().ToList()); + } + } + + Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray(); + + foreach (var c in cosType) + { + var sugarAttribute = c.GetCustomAttributes(type, true)?.FirstOrDefault(); + + var des = c.GetCustomAttributes(typeof(DescriptionAttribute), true); + var description = ""; + if (des.Length > 0) + { + description = ((DescriptionAttribute)des[0]).Description; + } + entityInfos.Add(new EntityInfo() + { + EntityName = c.Name, + DbTableName = sugarAttribute == null ? c.Name : ((SugarTable)sugarAttribute).TableName, + TableDescription = description, + Type = c + }); + } + return await Task.FromResult(entityInfos); + + bool IsMyAttribute(Attribute[] o) + { + return o.Any(a => a.GetType() == type); + } + } + + /// + /// 获取实体模板文件路径 + /// + /// + private static string GetEntityTemplatePath() + { + var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); + return Path.Combine(templatePath, "Entity.cs.vm"); + } + + /// + /// 获取种子数据模板文件路径 + /// + /// + private static string GetSeedDataTemplatePath() + { + var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template"); + return Path.Combine(templatePath, "SeedData.cs.vm"); + } + + /// + /// 设置生成实体文件路径 + /// + /// + /// + private static string GetEntityTargetPath(CreateEntityInput input) + { + var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "Entity"); + //if (!Directory.Exists(backendPath)) + //{ + // var pluginsPath = App.GetConfig("AppSettings:ExternalAssemblies"); + // foreach (var pluginPath in pluginsPath) + // { + // backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, pluginPath, input.Position, "Entity"); + // if (Directory.Exists(backendPath)) + // break; + // } + //} + if (!Directory.Exists(backendPath)) + Directory.CreateDirectory(backendPath); + return Path.Combine(backendPath, input.EntityName + ".cs"); + } + + /// + /// 设置生成种子数据文件路径 + /// + /// + /// + private static string GetSeedDataTargetPath(CreateSeedDataInput input) + { + var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, input.Position, "SeedData"); + if (!Directory.Exists(backendPath)) + Directory.CreateDirectory(backendPath); + return Path.Combine(backendPath, input.SeedDataName + ".cs"); + } + + /// + /// 备份数据库(PostgreSQL)🔖 + /// + /// + [HttpPost, NonUnify] + [DisplayName("备份数据库(PostgreSQL)")] + public async Task BackupDatabase() + { + if (_db.CurrentConnectionConfig.DbType != SqlSugar.DbType.PostgreSQL) + throw Oops.Oh("只支持 PostgreSQL 数据库 😁"); + + var npgsqlConn = new NpgsqlConnectionStringBuilder(_db.CurrentConnectionConfig.ConnectionString); + if (npgsqlConn == null || string.IsNullOrWhiteSpace(npgsqlConn.Host) || string.IsNullOrWhiteSpace(npgsqlConn.Username) || string.IsNullOrWhiteSpace(npgsqlConn.Password) || string.IsNullOrWhiteSpace(npgsqlConn.Database)) + throw Oops.Oh("PostgreSQL 数据库配置错误"); + + // 确保备份目录存在 + var backupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "backups"); + Directory.CreateDirectory(backupDirectory); + + // 构建备份文件名 + string backupFileName = $"backup_{DateTime.Now:yyyyMMddHHmmss}.sql"; + string backupFilePath = Path.Combine(backupDirectory, backupFileName); + + // 启动pg_dump进程进行备份 + // 设置密码:export PGPASSWORD='xxxxxx' + var bash = $"-U {npgsqlConn.Username} -h {npgsqlConn.Host} -p {npgsqlConn.Port} -E UTF8 -F c -b -v -f {backupFilePath} {npgsqlConn.Database}"; + var startInfo = new ProcessStartInfo + { + FileName = "pg_dump", + Arguments = bash, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + EnvironmentVariables = + { + ["PGPASSWORD"] = npgsqlConn.Password + } + }; + + //_logger.LogInformation("备份数据库:pg_dump " + bash); + + //try + //{ + using (var backupProcess = Process.Start(startInfo)) + { + await backupProcess.WaitForExitAsync(); + + //var output = await backupProcess.StandardOutput.ReadToEndAsync(); + //var error = await backupProcess.StandardError.ReadToEndAsync(); + + // 检查备份是否成功 + if (backupProcess.ExitCode != 0) + { + throw Oops.Oh($"备份失败:ExitCode({backupProcess.ExitCode})"); + } + } + + // _logger.LogInformation($"备份成功:{backupFilePath}"); + //} + //catch (Exception ex) + //{ + // _logger.LogError(ex, $"备份失败:"); + // throw; + //} + + // 若备份成功则提供下载链接 + return new FileStreamResult(new FileStream(backupFilePath, FileMode.Open), "application/octet-stream") + { + FileDownloadName = backupFileName + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs new file mode 100644 index 0000000..ecff3bf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataInput.cs @@ -0,0 +1,64 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DictDataInput : BaseStatusInput +{ +} + +public class PageDictDataInput : BasePageInput +{ + /// + /// 字典类型Id + /// + public long DictTypeId { get; set; } + + /// + /// 文本 + /// + public string Label { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class AddDictDataInput : SysDictData +{ +} + +public class UpdateDictDataInput : AddDictDataInput +{ +} + +public class DeleteDictDataInput : BaseIdInput +{ +} + +public class GetDataDictDataInput +{ + /// + /// 字典类型Id + /// + [Required(ErrorMessage = "字典类型Id不能为空"), DataValidation(ValidationTypes.Numeric)] + public long DictTypeId { get; set; } +} + +public class QueryDictDataInput +{ + /// + /// 字典值 + /// + [Required(ErrorMessage = "字典值不能为空")] + public string Value { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs new file mode 100644 index 0000000..cc83939 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictDataOutput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DictDataOutput +{ + public long DictDataId { get; set; } + public string TypeCode { get; set; } + public string Label { get; set; } + public string Value { get; set; } + public string Code { get; set; } + public string TagType { get; set; } + public string StyleSetting { get; set; } + public string ClassSetting { get; set; } + public string ExtData { get; set; } + public string Remark { get; set; } + public int OrderNo { get; set; } + public StatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs new file mode 100644 index 0000000..acbd5be --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/Dto/DictTypeInput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DictTypeInput : BaseStatusInput +{ +} + +public class PageDictTypeInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class AddDictTypeInput : SysDictType +{ + /// + /// 是否是租户字典(Y-是,N-否) + /// + public override YesNoEnum IsTenant { get; set; } = YesNoEnum.Y; + + /// + /// 是否是内置字典(Y-是,N-否) + /// + public override YesNoEnum SysFlag { get; set; } = YesNoEnum.N; +} + +public class UpdateDictTypeInput : AddDictTypeInput +{ +} + +public class DeleteDictTypeInput : BaseIdInput +{ +} + +public class GetDataDictTypeInput +{ + /// + /// 编码 + /// + [Required(ErrorMessage = "字典类型编码不能为空")] + public string Code { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs new file mode 100644 index 0000000..7487e96 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictDataService.cs @@ -0,0 +1,331 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统字典值服务 🧩 +/// +[ApiDescriptionSettings(Order = 420, Description = "系统字典值")] +public class SysDictDataService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysDictDataRep; + public readonly ISugarQueryable VSysDictData; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysDictDataService(SqlSugarRepository sysDictDataRep, + SysCacheService sysCacheService, + UserManager userManager, + SysLangTextCacheService sysLangTextCacheService) + { + _userManager = userManager; + _sysDictDataRep = sysDictDataRep; + _sysCacheService = sysCacheService; + _sysLangTextCacheService = sysLangTextCacheService; + VSysDictData = _sysDictDataRep.Context.UnionAll( + _sysDictDataRep.AsQueryable(), + _sysDictDataRep.Change().AsQueryable() + //.WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId) + .Select()); + } + + /// + /// 获取字典值分页列表 🔖 + /// + /// + /// + [DisplayName("获取字典值分页列表")] + public async Task> Page(PageDictDataInput input) + { + var langCode = _userManager.LangCode; + var baseQuery = VSysDictData + .Where(u => u.DictTypeId == input.DictTypeId) + .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code)) + .WhereIF(!string.IsNullOrEmpty(input.Label?.Trim()), u => u.Label.Contains(input.Label)) + .OrderBy(u => new { u.OrderNo, u.Code }); + var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize); + var list = pageList.Items; + var ids = list.Select(d => d.Id).Distinct().ToList(); + var translations = await _sysLangTextCacheService.GetTranslations( + "SysDictData", + "Label", + ids, + langCode); + foreach (var item in list) + { + if (translations.TryGetValue(item.Id, out var translatedLabel) && !string.IsNullOrEmpty(translatedLabel)) + { + item.Label = translatedLabel; + } + } + pageList.Items = list; + return pageList; + } + + /// + /// 获取字典值列表 🔖 + /// + /// + [DisplayName("获取字典值列表")] + public async Task> GetList([FromQuery] GetDataDictDataInput input) + { + var langCode = _userManager.LangCode; + var list = await GetDictDataListByDictTypeId(input.DictTypeId); + var ids = list.Select(d => d.Id).Distinct().ToList(); + var translations = await _sysLangTextCacheService.GetTranslations( + "SysDictData", + "Label", + ids, + langCode); + foreach (var item in list) + { + if (translations.TryGetValue(item.Id, out var translatedLabel) && !string.IsNullOrEmpty(translatedLabel)) + { + item.Label = translatedLabel; + } + } + return await GetDictDataListByDictTypeId(input.DictTypeId); + } + + /// + /// 增加字典值 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加字典值")] + public async Task AddDictData(AddDictDataInput input) + { + var isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003); + + var dictType = await _sysDictDataRep.Change().GetByIdAsync(input.DictTypeId); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008); + + Remove(dictType); + + dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt() : input.Adapt(); + await _sysDictDataRep.Context.Insertable(dictData).ExecuteCommandAsync(); + } + + /// + /// 更新字典值 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新字典值")] + public async Task UpdateDictData(UpdateDictDataInput input) + { + var isExist = await VSysDictData.AnyAsync(u => u.Id == input.Id); + if (!isExist) throw Oops.Oh(ErrorCodeEnum.D3004); + + isExist = await VSysDictData.AnyAsync(u => u.Value == input.Value && u.DictTypeId == input.DictTypeId && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D3003); + + var dictType = await _sysDictDataRep.Change().GetByIdAsync(input.DictTypeId); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009); + + Remove(dictType); + dynamic dictData = dictType.IsTenant == YesNoEnum.Y ? input.Adapt() : input.Adapt(); + await _sysDictDataRep.Context.Updateable(dictData).ExecuteCommandAsync(); + } + + /// + /// 删除字典值 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除字典值")] + public async Task DeleteDictData(DeleteDictDataInput input) + { + var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004); + + var dictType = await _sysDictDataRep.Change().GetByIdAsync(dictData.DictTypeId); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010); + + Remove(dictType); + dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt() : input.Adapt(); + await _sysDictDataRep.Context.Deleteable(entity).ExecuteCommandAsync(); + } + + /// + /// 获取字典值详情 🔖 + /// + /// + /// + [DisplayName("获取字典值详情")] + public async Task GetDetail([FromQuery] DictDataInput input) + { + return (await VSysDictData.FirstAsync(u => u.Id == input.Id))?.Adapt(); + } + + /// + /// 修改字典值状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("修改字典值状态")] + public async Task SetStatus(DictDataInput input) + { + var dictData = await VSysDictData.FirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3004); + + var dictType = await _sysDictDataRep.Change().GetByIdAsync(dictData.DictTypeId); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009); + + Remove(dictType); + + dictData.Status = input.Status; + dynamic entity = dictType.IsTenant == YesNoEnum.Y ? input.Adapt() : input.Adapt(); + await _sysDictDataRep.Context.Updateable(entity).ExecuteCommandAsync(); + } + + /// + /// 根据字典类型Id获取字典值集合 + /// + /// + /// + [NonAction] + public async Task> GetDictDataListByDictTypeId(long dictTypeId) + { + return await GetDataListByIdOrCode(dictTypeId, null); + } + + /// + /// 根据字典类型编码获取字典值集合 🔖 + /// + /// + /// + [DisplayName("根据字典类型编码获取字典值集合")] + public async Task> GetDataList(string code) + { + return await GetDataListByIdOrCode(null, code); + } + + /// + /// 获取字典值集合 🔖 + /// + /// + /// + /// + [NonAction] + public async Task> GetDataListByIdOrCode(long? typeId, string code) + { + if (string.IsNullOrWhiteSpace(code) && typeId == null || + !string.IsNullOrWhiteSpace(code) && typeId != null) + throw Oops.Oh(ErrorCodeEnum.D3011); + + var dictType = await _sysDictDataRep.Change().AsQueryable() + .Where(u => u.Status == StatusEnum.Enable) + .WhereIF(!string.IsNullOrWhiteSpace(code), u => u.Code == code) + .WhereIF(typeId != null, u => u.Id == typeId) + .FirstAsync(); + if (dictType == null) return null; + + string dicKey = dictType.IsTenant == YesNoEnum.N ? $"{CacheConst.KeyDict}{dictType.Code}" : $"{CacheConst.KeyTenantDict}{_userManager}:{dictType?.Code}"; + var dictDataList = _sysCacheService.Get>(dicKey); + if (dictDataList == null) + { + //平台字典和租户字典分开缓存 + if (dictType.IsTenant == YesNoEnum.Y) + { + dictDataList = await _sysDictDataRep.Change().AsQueryable() + .Where(u => u.DictTypeId == dictType.Id) + .Where(u => u.Status == StatusEnum.Enable) + .WhereIF(_userManager.SuperAdmin, d => d.TenantId == _userManager.TenantId).Select() + .OrderBy(u => new { u.OrderNo, u.Value, u.Code }) + .ToListAsync(); + } + else + { + dictDataList = await _sysDictDataRep.AsQueryable() + .Where(u => u.DictTypeId == dictType.Id) + .Where(u => u.Status == StatusEnum.Enable) + .OrderBy(u => new { u.OrderNo, u.Value, u.Code }) + .ToListAsync(); + } + + _sysCacheService.Set(dicKey, dictDataList); + } + return dictDataList; + } + + /// + /// 根据查询条件获取字典值集合 🔖 + /// + /// + /// + [DisplayName("根据查询条件获取字典值集合")] + public async Task> GetDataList([FromQuery] QueryDictDataInput input) + { + var dataList = await GetDataList(input.Value); + if (input.Status.HasValue) return dataList.Where(u => u.Status == (StatusEnum)input.Status.Value).ToList(); + return dataList; + } + + /// + /// 根据字典类型Id删除字典值 + /// + /// + /// + [NonAction] + public async Task DeleteDictData(long dictTypeId) + { + var dictType = await _sysDictDataRep.Change().AsQueryable().Where(u => u.Id == dictTypeId).FirstAsync(); + Remove(dictType); + + if (dictType?.IsTenant == YesNoEnum.Y) + await _sysDictDataRep.Change().DeleteAsync(u => u.DictTypeId == dictTypeId); + else + await _sysDictDataRep.DeleteAsync(u => u.DictTypeId == dictTypeId); + } + + /// + /// 通过字典数据Value查询显示文本Label + /// 适用于列表中根据字典数据值找文本的子查询 _sysDictDataService.MapDictValueToLabel(() =>obj.Type, "org_type",obj); + /// + /// + /// + /// + /// + /// + public string MapDictValueToLabel(Expression> mappingFiled, string dictTypeCode, T parameter) + { + return VSysDictData.InnerJoin((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode) + .SetContext(d => d.Value, mappingFiled, parameter).FirstOrDefault()?.Label; + } + + /// + /// 通过字典数据显示文本Label查询Value + /// 适用于列表数据导入根据字典数据文本找值的子查询 _sysDictDataService.MapDictLabelToValue(() => obj.Type, "org_type",obj); + /// + /// + /// + /// + /// + /// + public string MapDictLabelToValue(Expression> mappingFiled, string dictTypeCode, T parameter) + { + return VSysDictData.InnerJoin((d, dt) => d.DictTypeId.Equals(dt.Id) && dt.Code == dictTypeCode) + .SetContext(d => d.Label, mappingFiled, parameter).FirstOrDefault()?.Value; + } + + /// + /// 清理字典数据缓存 + /// + /// + private void Remove(SysDictType dictType) + { + _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType?.Code}"); + _sysCacheService.Remove($"{CacheConst.KeyTenantDict}{_userManager}:{dictType?.Code}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs new file mode 100644 index 0000000..ce1157e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Dict/SysDictTypeService.cs @@ -0,0 +1,262 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统字典类型服务 🧩 +/// +[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")] +public class SysDictTypeService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysDictTypeRep; + private readonly SysDictDataService _sysDictDataService; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysDictTypeService(SqlSugarRepository sysDictTypeRep, + SysDictDataService sysDictDataService, + SysCacheService sysCacheService, + UserManager userManager, + SysLangTextCacheService sysLangTextCacheService) + { + _sysDictTypeRep = sysDictTypeRep; + _sysDictDataService = sysDictDataService; + _sysCacheService = sysCacheService; + _userManager = userManager; + _sysLangTextCacheService = sysLangTextCacheService; + } + + /// + /// 获取字典类型分页列表 🔖 + /// + /// + [DisplayName("获取字典类型分页列表")] + public async Task> Page(PageDictTypeInput input) + { + var langCode = _userManager.LangCode; + var baseQuery = _sysDictTypeRep.AsQueryable() + .WhereIF(!_userManager.SuperAdmin, u => u.IsTenant == YesNoEnum.Y) + .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code)) + .WhereIF(!string.IsNullOrEmpty(input.Name?.Trim()), u => u.Name.Contains(input.Name)); + //.OrderBy(u => new { u.OrderNo, u.Code }) + var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize); + var list = pageList.Items; + var ids = list.Select(d => d.Id).Distinct().ToList(); + var translations = await _sysLangTextCacheService.GetTranslations( + "SysDictType", + "Name", + ids, + langCode); + foreach (var item in list) + { + if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName)) + { + item.Name = translatedName; + } + } + pageList.Items = list; + return pageList; + } + + /// + /// 获取字典类型列表 🔖 + /// + /// + [DisplayName("获取字典类型列表")] + public async Task> GetList() + { + var langCode = _userManager.LangCode; + var list = await _sysDictTypeRep.AsQueryable().OrderBy(u => new { u.OrderNo, u.Code }).ToListAsync(); + var ids = list.Select(d => d.Id).Distinct().ToList(); + var translations = await _sysLangTextCacheService.GetTranslations( + "SysDictType", + "Name", + ids, + langCode); + foreach (var item in list) + { + if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName)) + { + item.Name = translatedName; + } + } + return list; + } + + /// + /// 获取字典类型-值列表 🔖 + /// + /// + /// + [DisplayName("获取字典类型-值列表")] + public async Task> GetDataList([FromQuery] GetDataDictTypeInput input) + { + var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Code == input.Code) ?? throw Oops.Oh(ErrorCodeEnum.D3000); + var langCode = _userManager.LangCode; + var list = await _sysDictDataService.GetDictDataListByDictTypeId(dictType.Id); + var ids = list.Select(d => d.Id).Distinct().ToList(); + var translations = await _sysLangTextCacheService.GetTranslations( + "SysDictType", + "Name", + ids, + langCode); + foreach (var item in list) + { + if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName)) + { + item.Name = translatedName; + } + } + return list; + } + + /// + /// 添加字典类型 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("添加字典类型")] + public async Task AddDictType(AddDictTypeInput input) + { + if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006); + if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008); + + var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001); + + if (_userManager.SuperAdmin) input.IsTenant = YesNoEnum.N; // 超级管理员添加的字典类型默认非租户级 + + await _sysDictTypeRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新字典类型 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新字典类型")] + public async Task UpdateDictType(UpdateDictTypeInput input) + { + var dict = await _sysDictTypeRep.GetFirstAsync(x => x.Id == input.Id); + if (dict.IsTenant != input.IsTenant) throw Oops.Oh(ErrorCodeEnum.D3012); + if (dict == null) throw Oops.Oh(ErrorCodeEnum.D3000); + + if (dict.Code.ToLower().EndsWith("enum") && input.Code != dict.Code) throw Oops.Oh(ErrorCodeEnum.D3007); + if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009); + + var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001); + + _sysCacheService.Remove($"{CacheConst.KeyDict}{input.Code}"); + await _sysDictTypeRep.UpdateAsync(input.Adapt()); + } + + /// + /// 删除字典类型 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除字典类型")] + public async Task DeleteDictType(DeleteDictTypeInput input) + { + var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010); + + // 删除字典值 + await _sysDictTypeRep.DeleteAsync(dictType); + await _sysDictDataService.DeleteDictData(input.Id); + } + + /// + /// 获取字典类型详情 🔖 + /// + /// + /// + [DisplayName("获取字典类型详情")] + public async Task GetDetail([FromQuery] DictTypeInput input) + { + return await _sysDictTypeRep.GetByIdAsync(input.Id); + } + + /// + /// 修改字典类型状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("修改字典类型状态")] + public async Task SetStatus(DictTypeInput input) + { + var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000); + if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009); + + _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}"); + + dictType.Status = input.Status; + await _sysDictTypeRep.AsUpdateable(dictType).UpdateColumns(u => new { u.Status }, true).ExecuteCommandAsync(); + } + + /// + /// 获取所有字典集合 🔖 + /// + /// + [DisplayName("获取所有字典集合")] + public async Task GetAllDictList() + { + var langCode = _userManager.LangCode; + var ds = await _sysDictTypeRep.AsQueryable() + .InnerJoin(_sysDictDataService.VSysDictData, (u, w) => u.Id == w.DictTypeId) + .Select((u, w) => new DictDataOutput + { + DictDataId = w.Id, // 给翻译用 + TypeCode = u.Code, + Label = w.Label, + Value = w.Value, + Code = w.Code, + TagType = w.TagType, + StyleSetting = w.StyleSetting, + ClassSetting = w.ClassSetting, + ExtData = w.ExtData, + Remark = w.Remark, + OrderNo = w.OrderNo, + Status = w.Status == StatusEnum.Enable && u.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable + }) + .ToListAsync(); + var ids = ds.Select(x => x.DictDataId).Distinct().ToList(); + + Dictionary translations = new(); + if (ids.Any()) + { + translations = await _sysLangTextCacheService.GetTranslations( + "SysDictData", + "Label", + ids, + langCode + ); + } + foreach (var item in ds) + { + if (translations.TryGetValue(item.DictDataId, out var translated) && !string.IsNullOrEmpty(translated)) + { + item.Label = translated; + } + } + + var result = ds + .OrderBy(u => u.OrderNo) + .GroupBy(u => u.TypeCode) + .ToDictionary(u => u.Key, u => u.ToList()); + + return result; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs new file mode 100644 index 0000000..64506bb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumInput.cs @@ -0,0 +1,37 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 枚举输入参数 +/// +public class EnumInput +{ + /// + /// 枚举类型名称 + /// + /// AccountTypeEnum + [Required(ErrorMessage = "枚举类型不能为空")] + public string EnumName { get; set; } +} + +public class QueryEnumDataInput +{ + /// + /// 实体名称 + /// + /// SysUser + [Required(ErrorMessage = "实体名称不能为空")] + public string EntityName { get; set; } + + /// + /// 字段名称 + /// + /// AccountType + [Required(ErrorMessage = "字段名称不能为空")] + public string FieldName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs new file mode 100644 index 0000000..396149a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/Dto/EnumOutput.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 枚举类型输出参数 +/// +public class EnumTypeOutput +{ + /// + /// 枚举类型描述 + /// + public string TypeDescribe { get; set; } + + /// + /// 枚举类型名称 + /// + public string TypeName { get; set; } + + /// + /// 枚举类型全名称 + /// + public string TypeFullName { get; set; } + + /// + /// 枚举类型备注 + /// + public string TypeRemark { get; set; } + + /// + /// 枚举实体 + /// + public List EnumEntities { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs new file mode 100644 index 0000000..b0ab79b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Enum/SysEnumService.cs @@ -0,0 +1,102 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统枚举服务 🧩 +/// +[ApiDescriptionSettings(Order = 275)] +public class SysEnumService : IDynamicApiController, ITransient +{ + private readonly EnumOptions _enumOptions; + + public SysEnumService(IOptions enumOptions) + { + _enumOptions = enumOptions.Value; + } + + /// + /// 获取所有枚举类型 🔖 + /// + /// + [DisplayName("获取所有枚举类型")] + public List GetEnumTypeList() + { + var enumTypeList = App.EffectiveTypes.Where(t => t.IsEnum) + .Where(t => _enumOptions.EntityAssemblyNames.Contains(t.Assembly.GetName().Name) || _enumOptions.EntityAssemblyNames.Any(name => t.Assembly.GetName().Name!.Contains(name))) + .Where(t => t.GetCustomAttributes(typeof(IgnoreEnumToDictAttribute), false).Length == 0) // 排除有忽略转字典特性类型 + .Where(t => t.GetCustomAttributes(typeof(ErrorCodeTypeAttribute), false).Length == 0) // 排除错误代码类型 + .OrderBy(u => u.Name).ThenBy(u => u.FullName) + .ToList(); + + // 如果存在同名枚举类,则依次增加 "_序号" 后缀 + var list = enumTypeList.Select(GetEnumDescription).ToList(); + foreach (var enumType in list.GroupBy(u => u.TypeName).Where(g => g.Count() > 1)) + { + int i = 1; + foreach (var item in list.Where(u => u.TypeName == enumType.Key).Skip(1)) item.TypeName = $"{item.TypeName}_{i++}"; + } + return list; + } + + /// + /// 获取字典描述 + /// + /// + /// + private static EnumTypeOutput GetEnumDescription(Type type) + { + string description = type.Name; + var attrs = type.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (attrs.Length != 0) + { + var att = ((DescriptionAttribute[])attrs)[0]; + description = att.Description; + } + var enumType = App.EffectiveTypes.FirstOrDefault(t => t.IsEnum && t.Name == type.Name); + return new EnumTypeOutput + { + TypeDescribe = description, + TypeName = type.Name, + TypeRemark = description, + TypeFullName = type.FullName, + EnumEntities = (enumType ?? type).EnumToList() + }; + } + + /// + /// 通过枚举类型获取枚举值集合 🔖 + /// + /// + /// + [DisplayName("通过枚举类型获取枚举值集合")] + public List GetEnumDataList([FromQuery] EnumInput input) + { + var enumType = App.EffectiveTypes.FirstOrDefault(u => u.IsEnum && u.Name == input.EnumName); + if (enumType is not { IsEnum: true }) throw Oops.Oh(ErrorCodeEnum.D1503); + + return enumType.EnumToList(); + } + + /// + /// 通过实体的字段名获取相关枚举值集合(目前仅支持枚举类型) 🔖 + /// + /// + /// + [DisplayName("通过实体的字段名获取相关枚举值集合")] + public static List GetEnumDataListByField([FromQuery] QueryEnumDataInput input) + { + // 获取实体类型属性 + Type entityType = App.EffectiveTypes.FirstOrDefault(u => u.Name == input.EntityName) ?? throw Oops.Oh(ErrorCodeEnum.D1504); + + // 获取字段类型 + var fieldType = entityType.GetProperties().FirstOrDefault(u => u.Name == input.FieldName)?.PropertyType; + if (fieldType is not { IsEnum: true }) throw Oops.Oh(ErrorCodeEnum.D1503); + + return fieldType.EnumToList(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs new file mode 100644 index 0000000..77163ac --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/BaiDuTranslationService.cs @@ -0,0 +1,428 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +/* + *━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + * 文件名称:BaiDuTranslationService + * 创建时间:2025年03月25日 星期二 20:54:04 + * 创 建 者:莫闻啼 + *━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + * 功能描述: + * 调用百度翻译Api接口在线翻译,在DeBug模式下生成前端i18n Ts翻译key value,需要先维护对应目录下的zh-CN.ts,对比对应语言包下不存在的key,将value进行翻译并新增到对应语言包文件中 + * + *━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + */ + +using System.Security.Cryptography; + +namespace Admin.NET.Core; + +/// +/// 百度翻译 +/// +[ApiDescriptionSettings("Extend", Module = "Extend", Order = 200)] +public class BaiDuTranslationService : IDynamicApiController, ITransient +{ + // http远程请求 + private readonly IHttpRemoteService _httpRemoteService; + + /// + /// 百度翻译appId + /// + private static readonly string _appId = "xxxxxxxxxxx"; + + /// + /// 百度翻译appKey + /// + private static readonly string _appKey = "xxxxxxxxxxx"; + + /// + /// 百度翻译api地址 + /// + private static readonly string _baseUrl = "https://fanyi-api.baidu.com/api/trans/vip/translate?"; + + // 语言映射字典 + private static readonly Dictionary langMap = new Dictionary + { + ["en"] = "en", + ["de"] = "de", + ["fi"] = "fin", + ["es"] = "spa", + ["fr"] = "fra", + ["it"] = "it", + ["ja"] = "jp", + ["ko"] = "kor", + ["no"] = "nor", + ["pl"] = "pl", + ["pt"] = "pt", + ["ru"] = "ru", + ["th"] = "th", + ["id"] = "id", + ["ms"] = "may", + ["vi"] = "vie", + ["zh-HK"] = "yue", + ["zh-TW"] = "cht" + }; + + /// + /// 初始化一个类型的新实例. + /// + /// + public BaiDuTranslationService(IHttpRemoteService httpRemoteService) + { + _httpRemoteService = httpRemoteService; + } + + /// + /// 百度在线翻译 + /// + /// 翻译源语种 + /// 翻译目标语种 + /// 文本内容 + /// + ///源语种和目标语种支持: + ///zh:简体中文 + ///cht:繁體中文(台灣) + ///yue:繁體中文(香港) + ///en:英语 + ///de:德语 + ///spa:西班牙语 + ///fin:芬兰语 + ///fra:法语 + ///it:意大利语 + ///jp:日语 + ///kor:韩语 + ///nor:挪威语 + ///pl:波兰语 + ///pt:葡萄牙语 + ///ru:俄语 + ///th:泰语 + ///id:印度尼西亚语 + ///may:马来西亚 + ///vie:越南语 + /// + ///更多语种请查看:https://api.fanyi.baidu.com/doc/21 + /// + /// 翻译后的文本内容 + [DisplayName("百度在线翻译")] + [HttpGet] + public async Task Translation([FromQuery][Required] string from, [FromQuery][Required] string to, [FromQuery][Required] string content) + { + // 标准版API授权只能翻译基础18语言,201种需要企业尊享版支持见百度api 文档 + Random rd = new Random(); + string salt = rd.Next(100000).ToString(); + // 改成您的密钥 + string secretKey = _appKey; + string sign = EncryptString(_appId + content + salt + secretKey); + string url = $"{_baseUrl}q={HttpUtility.UrlEncode(content)}&from={from}&to={to}&appid={_appId}&salt={salt}&sign={sign}"; + var res = await _httpRemoteService.GetAsAsync(url); + + if (!res.error_code.Equals("0")) + { + throw Oops.Bah($"翻译失败,错误码:{res.error_code},错误信息:{res.error_msg}"); + } + + return res; + } + +#if DEBUG + + /// + /// 生成前端页面i18n文件 + /// + [DisplayName("生成前端页面i18n文件")] + [HttpPost] + public async Task GeneratePageI18nFile() + { + try + { + // 获取基础路径 + var i18nPath = AppContext.BaseDirectory; + + for (int i = 0; i < 6; i++) + { + i18nPath = Directory.GetParent(i18nPath).FullName; + } + + i18nPath = Path.Combine(i18nPath, "Web", "src", "i18n", "pages", "systemMenu"); + + // 读取基础语言文件 + var dic = await ReadBaseLanguageFile(i18nPath); + + if (dic.Count == 0) + { + throw Oops.Bah("未查询到属性定义,不能生成"); + } + + // 并行处理所有语言文件 + var files = Directory.GetFiles(i18nPath, "*.ts").Where(f => !f.EndsWith("zh-CN.ts")).ToList(); + + foreach (var file in files) + { + var langCode = Path.GetFileNameWithoutExtension(file); + var langDic = await ReadLanguageFile(file); + + // 查询出没有生成的键值对 + + // Linq查询 + // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value); + // 转换为 HashSet 提升性能 + var langDicKey = new HashSet(langDic.Keys); + var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value); + + // 没有未生成的跳出 + if (notGen.Count == 0) + { + Console.WriteLine($"{langCode,-6} 语言包:{langDic.Count}/共:{dic.Count} 已全部生成,无需再次生成"); + continue; + } + + var str = string.Empty; + Console.WriteLine($"{langCode,-6}开始生成语言包,未生成:{notGen.Count}/已生成:{langDic.Count}/共{dic.Count}"); + foreach (var gen in notGen) + { + try + { + if (!langMap.TryGetValue(langCode, out var targetLang)) + { + continue; + } + + var result = await Translation("zh", targetLang, $"{gen.Value}"); + + if (!result.error_code.Equals("0")) + { + continue; + } + + var translationValue = result.trans_result[0].Dst; + LogTranslationProgress(gen.Key, gen.Value, translationValue, ConsoleColor.DarkMagenta); + + // 如果翻译结果为空字符串不追加 + if (string.IsNullOrEmpty(translationValue)) + { + continue; + } + + // 如果翻译结果包含"'" 法语意大利语常出现 在"'"前加转义符 + if (translationValue.Contains("'")) + { + translationValue = translationValue.Replace("'", "\\'"); + } + + str += ($" {gen.Key}: '{translationValue}',{Environment.NewLine}"); + } + catch (Exception e) + { + LogError(e); + } + } + + if (str.Length > 0) + { + str = str.TrimStart(); + await FileHelper.InsertsStringAtSpecifiedLocationInFile(file, str, '}', 2, false); + } + } + } + catch (Exception e) + { + throw Oops.Bah(e.Message); + } + } + + /// + /// 生成前端菜单i18n文件 + /// + [DisplayName("生成前端菜单i18n文件")] + [HttpPost] + public async Task GenerateMenuI18nFile() + { + try + { + // 获取基础路径 + var i18nPath = AppContext.BaseDirectory; + + for (int i = 0; i < 6; i++) + { + i18nPath = Directory.GetParent(i18nPath).FullName; + } + + i18nPath = Path.Combine(i18nPath, "Web", "src", "i18n", "menu"); + + // 读取基础语言文件 + var dic = await ReadBaseLanguageFile(i18nPath); + + if (dic.Count == 0) + { + throw Oops.Bah("未查询到属性定义,不能生成"); + } + + // 并行处理所有语言文件 + var files = Directory.GetFiles(i18nPath, "*.ts").Where(f => !f.EndsWith("zh-CN.ts")).ToList(); + + foreach (var file in files) + { + var langCode = Path.GetFileNameWithoutExtension(file); + var langDic = await ReadLanguageFile(file); + + // 查询出没有生成的键值对 + + // Linq查询 + // var notGen = dic.Where(kv => !langDic.ContainsKey(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value); + // 转换为 HashSet 提升性能 + var langDicKey = new HashSet(langDic.Keys); + var notGen = dic.Where(kv => !langDicKey.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value); + + // 没有未生成的跳出 + if (notGen.Count == 0) + { + Console.WriteLine($"{langCode,-6} 语言包:{langDic.Count}/共:{dic.Count} 已全部生成,无需再次生成"); + continue; + } + + var str = string.Empty; + Console.WriteLine($"{langCode,-6}开始生成语言包,未生成:{notGen.Count}/已生成:{langDic.Count}/共{dic.Count}"); + foreach (var gen in notGen) + { + try + { + if (!langMap.TryGetValue(langCode, out var targetLang)) + { + continue; + } + + var result = await Translation("zh", targetLang, $"{gen.Value}"); + + if (!result.error_code.Equals("0")) + { + continue; + } + + var translationValue = result.trans_result[0].Dst; + LogTranslationProgress(gen.Key, gen.Value, translationValue, ConsoleColor.DarkMagenta); + + // 如果翻译结果为空字符串不追加 + if (string.IsNullOrEmpty(translationValue)) + { + continue; + } + + // 如果翻译结果包含"'" 法语意大利语常出现 在"'"前加转义符 + if (translationValue.Contains("'")) + { + translationValue = translationValue.Replace("'", "\\'"); + } + + str += ($" {gen.Key}: '{translationValue}',{Environment.NewLine}"); + } + catch (Exception e) + { + LogError(e); + } + } + + if (str.Length > 0) + { + str = str.TrimStart(); + await FileHelper.InsertsStringAtSpecifiedLocationInFile(file, str, '}', 2, false); + } + } + } + catch (Exception e) + { + throw Oops.Bah(e.Message); + } + } + + #region 辅助方法 + + private static async Task> ReadBaseLanguageFile(string i18nPath) + { + var baseFile = Path.Combine(i18nPath, "zh-CN.ts"); + if (!File.Exists(baseFile)) + { + throw Oops.Bah("【zh-CN.ts】文件未找到"); + } + + var dic = new Dictionary(); + using var reader = new StreamReader(baseFile, Encoding.UTF8); + + while (await reader.ReadLineAsync() is { } line) + { + if (line.Contains('{') || line.Contains('}')) continue; + + var cleanLine = line.Trim().TrimEnd(',').Replace("'", ""); + var parts = cleanLine.Split(new[] { ':' }, 2); + if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim(); + } + + reader.Close(); + return dic; + } + + private static async Task> ReadLanguageFile(string filePath) + { + if (!File.Exists(filePath)) + { + throw Oops.Bah($"【{filePath.Split('/').Last()}】文件未找到"); + } + + var dic = new Dictionary(); + using var reader = new StreamReader(filePath, Encoding.UTF8); + + while (await reader.ReadLineAsync() is { } line) + { + if (line.Contains('{') || line.Contains('}')) continue; + + var cleanLine = line.Trim().TrimEnd(',').Replace("'", ""); + var parts = cleanLine.Split(new[] { ':' }, 2); + if (parts.Length == 2) dic[parts[0].Trim()] = parts[1].Trim(); + } + + reader.Close(); + return dic; + } + + private static void LogTranslationProgress(string key, string value, string res, ConsoleColor color) + { + Console.ForegroundColor = color; + Console.WriteLine($"翻译属性: {key,-32}值: {value,-64}结果: {res}"); + Console.ResetColor(); + } + + private static void LogError(Exception e) + { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($"{e.Message}"); + Console.ResetColor(); + } + + #endregion 辅助方法 + +#endif + + // 计算MD5值 + [NonAction] + private static string EncryptString(string str) + { + MD5 md5 = MD5.Create(); + // 将字符串转换成字节数组 + byte[] byteOld = Encoding.UTF8.GetBytes(str); + // 调用加密方法 + byte[] byteNew = md5.ComputeHash(byteOld); + // 将加密结果转换为字符串 + StringBuilder sb = new StringBuilder(); + foreach (byte b in byteNew) + { + // 将字节转换成16进制表示的字符串, + sb.Append(b.ToString("x2")); + } + + // 返回加密的字符串 + return sb.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs new file mode 100644 index 0000000..67fbefe --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/ExtendService/Models/BaiDuMapResult.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 百度翻译结果 +/// +public class BaiDuTranslationResult +{ + /// + /// 源语种 + /// + public string From { get; set; } + + /// + /// 目标语种 + /// + public string To { get; set; } + + /// + /// 翻译结果 + /// + public List trans_result { get; set; } + + /// + /// 错误码 正常为0 + /// + public string error_code { get; set; } = "0"; + + /// + /// 错误信息 + /// + public string error_msg { get; set; } = String.Empty; +} + +/// +/// 翻译结果 +/// +public class TransResult +{ + /// + /// 源字符 + /// + public string Src { get; set; } + + /// + /// 目标字符 + /// + public string Dst { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileInput.cs new file mode 100644 index 0000000..9cc37f5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileInput.cs @@ -0,0 +1,136 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件分页查询 +/// +public class PageFileInput : BasePageInput +{ + /// + /// 文件名称 + /// + public string FileName { get; set; } + + /// + /// 文件路径 + /// + public string FilePath { get; set; } + + /// + /// 文件后缀 + /// + public string? Suffix { get; set; } + + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} + +/// +/// 上传文件 +/// +public class UploadFileInput +{ + /// + /// 文件 + /// + [Required] + public IFormFile File { get; set; } + + /// + /// 文件类别 + /// + public string FileType { get; set; } + + /// + /// 是否公开 + /// + public bool IsPublic { get; set; } = false; + + /// + /// 允许格式:.jpeg.jpg.png.bmp.gif.tif + /// + public string AllowSuffix { get; set; } + + /// + /// 指定存储桶名称 + /// + public string? BucketName { get; set; } + + /// + /// 指定存储提供者ID + /// + public long? ProviderId { get; set; } + + /// + /// 业务数据Id + /// + public long? DataId { get; set; } +} + +/// +/// 上传文件Base64 +/// +public class UploadFileFromBase64Input +{ + /// + /// 文件名 + /// + public string FileName { get; set; } + + /// + /// 文件内容 + /// + public string FileDataBase64 { get; set; } + + /// + /// 文件类型( "image/jpeg",) + /// + public string ContentType { get; set; } +} + +/// +/// 查询关联查询输入 +/// +public class RelationQueryInput +{ + /// + /// 关联对象名称 + /// + public string RelationName { get; set; } + + /// + /// 关联对象Id + /// + public long? RelationId { get; set; } + + /// + /// 文件类型:多个以","分割 + /// + public string FileTypes { get; set; } + + /// + /// 所属Id + /// + public long? BelongId { get; set; } + + /// + /// 文件类型分割 + /// + /// + public string[] GetFileTypeBS() + { + return FileTypes.Split(','); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs new file mode 100644 index 0000000..b00b9dd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/Dto/FileProviderInput.cs @@ -0,0 +1,182 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件存储提供者分页查询输入参数 +/// +public class PageFileProviderInput : BasePageInput +{ + /// + /// 存储提供者 + /// + public string? Provider { get; set; } + + /// + /// 存储桶名称 + /// + public string? BucketName { get; set; } + + /// + /// 是否启用 + /// + public bool? IsEnable { get; set; } +} + +/// +/// 增加文件存储提供者输入参数 +/// +public class AddFileProviderInput +{ + /// + /// 存储提供者 + /// + [Required(ErrorMessage = "存储提供者不能为空")] + public string Provider { get; set; } + + /// + /// 存储桶名称 + /// + [Required(ErrorMessage = "存储桶名称不能为空")] + public string BucketName { get; set; } + + /// + /// 访问密钥ID(所有云服务商统一使用此字段) + /// + public string? AccessKey { get; set; } + + /// + /// 密钥 + /// + public string? SecretKey { get; set; } + + /// + /// 地域 + /// + public string? Region { get; set; } + + /// + /// 端点地址 + /// + public string? Endpoint { get; set; } + + /// + /// 是否启用HTTPS + /// + public bool? IsEnableHttps { get; set; } = true; + + /// + /// 是否启用缓存 + /// + public bool? IsEnableCache { get; set; } = true; + + /// + /// 是否启用 + /// + public bool? IsEnable { get; set; } = true; + + /// + /// 是否默认提供者 + /// + public bool? IsDefault { get; set; } = false; + + /// + /// 自定义域名 + /// + public string? SinceDomain { get; set; } + + /// + /// 排序号 + /// + public int? OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 支持的业务类型(JSON格式) + /// + public string? BusinessTypes { get; set; } + + /// + /// 优先级 + /// + public int Priority { get; set; } = 100; +} + +/// +/// 更新文件存储提供者输入参数 +/// +public class UpdateFileProviderInput : AddFileProviderInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long Id { get; set; } +} + +/// +/// 删除文件存储提供者输入参数 +/// +public class DeleteFileProviderInput : BaseIdInput +{ +} + +/// +/// 查询文件存储提供者输入参数 +/// +public class QueryFileProviderInput : BaseIdInput +{ +} + +/// +/// 测试连接输入参数 +/// +public class TestConnectionInput : BaseIdInput +{ +} + +/// +/// 设置默认存储提供者输入参数 +/// +public class SetDefaultProviderInput +{ + /// + /// 存储提供者ID + /// + [Required(ErrorMessage = "存储提供者ID不能为空")] + public long Id { get; set; } +} + +/// +/// 文件上传选择存储提供者输入参数 +/// +public class SelectProviderInput +{ + /// + /// 文件类型 + /// + public string? FileType { get; set; } + + /// + /// 业务类型 + /// + public string? BusinessType { get; set; } + + /// + /// 指定提供者ID + /// + public long? ProviderId { get; set; } + + /// + /// 指定存储桶名称 + /// + public string? BucketName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs new file mode 100644 index 0000000..c4618ef --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class DefaultFileProvider : ICustomFileProvider, ITransient +{ + /// + /// 构建文件的完整物理路径 + /// + /// + /// + private string BuildFullFilePath(SysFile sysFile) + { + return Path.Combine(App.WebHostEnvironment.WebRootPath, sysFile.FilePath ?? "", $"{sysFile.Id}{sysFile.Suffix}"); + } + + /// + /// 构建目录的完整物理路径 + /// + /// + /// + private string BuildFullDirectoryPath(string relativePath) + { + return Path.Combine(App.WebHostEnvironment.WebRootPath, relativePath); + } + + /// + /// 确保目录存在 + /// + /// + private void EnsureDirectoryExists(string directoryPath) + { + if (!Directory.Exists(directoryPath)) + Directory.CreateDirectory(directoryPath); + } + + public Task DeleteFileAsync(SysFile sysFile) + { + var filePath = BuildFullFilePath(sysFile); + if (File.Exists(filePath)) + File.Delete(filePath); + return Task.CompletedTask; + } + + public async Task DownloadFileBase64Async(SysFile sysFile) + { + var realFile = BuildFullFilePath(sysFile); + if (!File.Exists(realFile)) + { + Log.Error($"DownloadFileBase64:文件[{realFile}]不存在"); + throw Oops.Oh($"文件[{sysFile.FilePath}]不存在"); + } + + byte[] fileBytes = await File.ReadAllBytesAsync(realFile); + return Convert.ToBase64String(fileBytes); + } + + public Task GetFileStreamResultAsync(SysFile sysFile, string fileName) + { + var fullPath = BuildFullFilePath(sysFile); + return Task.FromResult(new FileStreamResult(new FileStream(fullPath, FileMode.Open), "application/octet-stream") + { + FileDownloadName = fileName + sysFile.Suffix + }); + } + + public async Task UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName) + { + newFile.Provider = ""; // 本地存储 Provider 显示为空 + + var directoryPath = BuildFullDirectoryPath(path); + EnsureDirectoryExists(directoryPath); + + var realFile = Path.Combine(directoryPath, finalName); + await using var stream = File.Create(realFile); + await file.CopyToAsync(stream); + + newFile.Url = $"{newFile.FilePath}/{newFile.Id + newFile.Suffix}"; + return newFile; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs new file mode 100644 index 0000000..d78c64e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/ICustomFileProvider.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 自定义文件提供器接口 +/// +public interface ICustomFileProvider +{ + /// + /// 获取文件流 + /// + /// + /// + /// + public Task GetFileStreamResultAsync(SysFile sysFile, string fileName); + + /// + /// 下载指定文件Base64格式 + /// + /// + /// + public Task DownloadFileBase64Async(SysFile sysFile); + + /// + /// 删除文件 + /// + /// + /// + public Task DeleteFileAsync(SysFile sysFile); + + /// + /// 上传文件 + /// + /// 文件 + /// + /// 文件存储位置 + /// 文件最终名称 + /// + public Task UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs new file mode 100644 index 0000000..dfa5e2a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs @@ -0,0 +1,255 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 多OSS文件提供者 +/// +public class MultiOSSFileProvider : ICustomFileProvider, ITransient +{ + private readonly SysFileProviderService _fileProviderService; + private readonly IOSSServiceManager _ossServiceManager; + private readonly OSSProviderOptions _ossProviderOptions; + + public MultiOSSFileProvider(SysFileProviderService fileProviderService, + IOSSServiceManager ossServiceManager, + IOptions ossProviderOptions) + { + _fileProviderService = fileProviderService; + _ossServiceManager = ossServiceManager; + _ossProviderOptions = ossProviderOptions.Value; + } + + /// + /// 上传文件 + /// + /// 文件 + /// 系统文件信息 + /// 文件存储位置 + /// 文件最终名称 + /// + public async Task UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName) + { + // 获取OSS配置(传入文件信息用于策略选择) + var provider = await GetFileProvider(sysFile, file) ?? throw Oops.Oh("未找到可用的存储提供者配置"); + + // 获取OSS服务 + var ossService = await _ossServiceManager.GetOSSServiceAsync(provider); + + // 设置文件信息 + sysFile.Provider = provider.Provider; + sysFile.BucketName = provider.BucketName; // 保存原始存储桶名称 + + var filePath = string.Concat(path, "/", finalName); + + // 上传文件 + await ossService.PutObjectAsync(provider.BucketName, filePath, file.OpenReadStream()); + + // 生成外链地址 + sysFile.Url = GenerateFileUrl(provider, provider.BucketName, filePath); + + return sysFile; + } + + /// + /// 删除文件 + /// + /// 系统文件信息 + /// + public async Task DeleteFileAsync(SysFile sysFile) + { + // 获取OSS配置(统一方法) + var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}"); + var ossService = await _ossServiceManager.GetOSSServiceAsync(provider); + var filePath = string.Concat(sysFile.FilePath, "/", $"{sysFile.Id}{sysFile.Suffix}"); + + await ossService.RemoveObjectAsync(provider.BucketName, filePath); + } + + /// + /// 获取文件流 + /// + /// 系统文件信息 + /// 文件名 + /// + public async Task GetFileStreamResultAsync(SysFile sysFile, string fileName) + { + // 获取OSS配置(统一方法) + var provider = await GetFileProvider(sysFile) ?? throw Oops.Oh($"未找到存储提供者配置: {sysFile.Provider}-{sysFile.BucketName}"); + var ossService = await _ossServiceManager.GetOSSServiceAsync(provider); + var filePath = Path.Combine(sysFile.FilePath ?? "", sysFile.Id + sysFile.Suffix); + + var httpRemoteService = App.GetRequiredService(); + var stream = await httpRemoteService.GetAsStreamAsync(await ossService.PresignedGetObjectAsync(provider.BucketName, filePath, 5)); + + return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix }; + } + + /// + /// 下载文件Base64格式 + /// + /// 系统文件信息 + /// + public async Task DownloadFileBase64Async(SysFile sysFile) + { + using var httpClient = new HttpClient(); + HttpResponseMessage response = await httpClient.GetAsync(sysFile.Url); + if (response.IsSuccessStatusCode) + { + byte[] fileBytes = await response.Content.ReadAsByteArrayAsync(); + return Convert.ToBase64String(fileBytes); + } + throw Oops.Oh($"下载文件失败,状态码: {response.StatusCode}"); + } + + /// + /// 获取文件提供者配置(统一方法,支持上传、删除、下载场景) + /// + /// 系统文件信息 + /// 上传的文件(可选,仅上传时传入) + /// + private async Task GetFileProvider(SysFile sysFile, IFormFile? file = null) + { + // 1. 如果已指定存储桶,直接使用 + if (!string.IsNullOrEmpty(sysFile.BucketName)) + { + var provider = await _fileProviderService.GetFileProviderByBucket(sysFile.Provider, sysFile.BucketName); + if (provider != null) return provider; + + // 如果数据库中找不到,尝试配置文件兜底 + if (_ossProviderOptions.Enabled && _ossProviderOptions.Bucket == sysFile.BucketName) + { + return await CreateProviderFromConfiguration(); + } + } + + // 2. 如果有上传文件信息,使用策略选择(仅上传场景) + if (file != null) + { + var uploadInput = new UploadFileInput + { + File = file, + BucketName = sysFile.BucketName, + FileType = sysFile.FileType + }; + + return await SelectProviderAsync(file, uploadInput); + } + + // 3. 最后兜底:使用默认存储提供者 + return await _fileProviderService.GetDefaultProvider(); + } + + /// + /// 选择合适的OSS存储提供者(内联版本) + /// + /// 上传的文件 + /// 上传输入参数 + /// + private async Task SelectProviderAsync(IFormFile file, UploadFileInput input) + { + // 1. 优先使用指定的提供者ID + if (input.ProviderId.HasValue) + { + var provider = await _fileProviderService.GetFileProviderById(input.ProviderId.Value); + if (provider != null) return provider; + } + + // 2. 其次使用指定的存储桶名称 + if (!string.IsNullOrEmpty(input.BucketName)) + { + var providers = await _fileProviderService.GetCachedFileProviders(); + var provider = providers.FirstOrDefault(p => p.BucketName == input.BucketName); + if (provider != null) return provider; + } + + // 3. 使用默认提供者 + var defaultProvider = await _fileProviderService.GetDefaultProvider(); + if (defaultProvider != null) return defaultProvider; + + // 4. 兜底:如果数据库中没有配置,尝试从配置文件创建默认提供者 + return await CreateProviderFromConfiguration(); + } + + /// + /// 生成文件URL(内联版本) + /// + /// 存储提供者配置 + /// 存储桶名称 + /// 文件路径 + /// + private static string GenerateFileUrl(SysFileProvider provider, string bucketName, string filePath) + { + ArgumentNullException.ThrowIfNull(provider); + ArgumentException.ThrowIfNullOrWhiteSpace(bucketName); + ArgumentException.ThrowIfNullOrWhiteSpace(filePath); + + var protocol = provider.IsEnableHttps == true ? "https" : "http"; + + // 如果有自定义域名,直接使用 + if (!string.IsNullOrWhiteSpace(provider.SinceDomain)) + { + return $"{provider.SinceDomain.TrimEnd('/')}/{filePath.TrimStart('/')}"; + } + + // 根据不同提供者生成URL + return provider.Provider.ToUpper() switch + { + "ALIYUN" => $"{protocol}://{bucketName}.{provider.Endpoint}/{filePath.TrimStart('/')}", + "QCLOUD" => $"{protocol}://{bucketName}-{provider.Endpoint}.cos.{provider.Region}.myqcloud.com/{filePath.TrimStart('/')}", + "MINIO" => $"{protocol}://{provider.Endpoint}/{bucketName}/{filePath.TrimStart('/')}", + _ => throw Oops.Oh($"不支持的OSS提供者: {provider.Provider}") + }; + } + + /// + /// 从配置文件创建默认提供者(兜底机制) + /// + /// + private Task CreateProviderFromConfiguration() + { + try + { + // 检查是否启用了OSS配置 + if (!_ossProviderOptions.Enabled && !App.Configuration["MultiOSS:Enabled"].ToBoolean()) + return Task.FromResult(null); + + // 验证必要配置 + if (string.IsNullOrWhiteSpace(_ossProviderOptions.AccessKey) || + string.IsNullOrWhiteSpace(_ossProviderOptions.SecretKey) || + string.IsNullOrWhiteSpace(_ossProviderOptions.Bucket)) + { + return Task.FromResult(null); + } + + // 使用现有的OSSProviderOptions创建临时提供者配置(不保存到数据库) + var provider = new SysFileProvider + { + Id = 0, // 临时ID + Provider = Enum.GetName(_ossProviderOptions.Provider), + BucketName = _ossProviderOptions.Bucket, + AccessKey = _ossProviderOptions.AccessKey, + SecretKey = _ossProviderOptions.SecretKey, + Endpoint = _ossProviderOptions.Endpoint, + Region = _ossProviderOptions.Region, + IsEnableHttps = _ossProviderOptions.IsEnableHttps, + IsEnableCache = _ossProviderOptions.IsEnableCache, + IsEnable = true, + IsDefault = true, + SinceDomain = _ossProviderOptions.CustomHost, + CreateTime = DateTime.Now + }; + + return Task.FromResult(provider); + } + catch (Exception) + { + // 配置读取失败,返回null + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs new file mode 100644 index 0000000..8bb161d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs @@ -0,0 +1,81 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core.Service; + +public class OSSFileProvider : ICustomFileProvider, ITransient +{ + private readonly IOSSService _OSSService; + private readonly OSSProviderOptions _OSSProviderOptions; + + public OSSFileProvider(IOptions oSSProviderOptions, IOSSServiceFactory ossServiceFactory) + { + _OSSProviderOptions = oSSProviderOptions.Value; + if (_OSSProviderOptions.Enabled) + _OSSService = ossServiceFactory.Create(Enum.GetName(_OSSProviderOptions.Provider)); + } + + public async Task DeleteFileAsync(SysFile file) + { + await _OSSService.RemoveObjectAsync(file.BucketName, string.Concat(file.FilePath, "/", $"{file.Id}{file.Suffix}")); + } + + public async Task DownloadFileBase64Async(SysFile file) + { + using var httpClient = new HttpClient(); + HttpResponseMessage response = await httpClient.GetAsync(file.Url); + if (response.IsSuccessStatusCode) + { + // 读取文件内容并将其转换为 Base64 字符串 + byte[] fileBytes = await response.Content.ReadAsByteArrayAsync(); + return Convert.ToBase64String(fileBytes); + } + throw new HttpRequestException($"Request failed with status code: {response.StatusCode}"); + } + + public async Task GetFileStreamResultAsync(SysFile file, string fileName) + { + var filePath = Path.Combine(file.FilePath ?? "", file.Id + file.Suffix); + var httpRemoteService = App.GetRequiredService(); + var stream = await httpRemoteService.GetAsStreamAsync(await _OSSService.PresignedGetObjectAsync(file.BucketName, filePath, 5)); + return new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName + file.Suffix }; + } + + public async Task UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName) + { + sysFile.Provider = Enum.GetName(_OSSProviderOptions.Provider); + var filePath = string.Concat(path, "/", finalName); + await _OSSService.PutObjectAsync(sysFile.BucketName, filePath, file.OpenReadStream()); + // http://<你的bucket名字>.oss.aliyuncs.com/<你的object名字> + // 生成外链地址 方便前端预览 + switch (_OSSProviderOptions.Provider) + { + case OSSProvider.Aliyun: + sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{sysFile.BucketName}.{_OSSProviderOptions.Endpoint}/{filePath}"; + break; + + case OSSProvider.QCloud: + var protocol = _OSSProviderOptions.IsEnableHttps ? "https" : "http"; + sysFile.Url = !string.IsNullOrWhiteSpace(_OSSProviderOptions.CustomHost) + ? $"{protocol}://{_OSSProviderOptions.CustomHost}/{filePath}" + : $"{protocol}://{sysFile.BucketName}-{_OSSProviderOptions.Endpoint}.cos.{_OSSProviderOptions.Region}.myqcloud.com/{filePath}"; + break; + + case OSSProvider.Minio: + // 获取Minio文件的下载或者预览地址 + // newFile.Url = await GetMinioPreviewFileUrl(newFile.BucketName, filePath);// 这种方法生成的Url是有7天有效期的,不能这样使用 + // 需要在MinIO中的Buckets开通对 Anonymous 的readonly权限 + var customHost = _OSSProviderOptions.CustomHost; + if (string.IsNullOrWhiteSpace(customHost)) + customHost = _OSSProviderOptions.Endpoint; + sysFile.Url = $"{(_OSSProviderOptions.IsEnableHttps ? "https" : "http")}://{customHost}/{sysFile.BucketName}/{filePath}"; + break; + } + return sysFile; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs new file mode 100644 index 0000000..745b783 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class SSHFileProvider : ICustomFileProvider, ITransient +{ + /// + /// 创建SSH连接助手 + /// + /// + private SSHHelper CreateSSHHelper() + { + return new SSHHelper( + App.Configuration["SSHProvider:Host"], + App.Configuration["SSHProvider:Port"].ToInt(), + App.Configuration["SSHProvider:Username"], + App.Configuration["SSHProvider:Password"]); + } + + /// + /// 构建文件完整路径 + /// + /// + /// + private string BuildFilePath(SysFile sysFile) + { + return string.Concat(sysFile.FilePath, "/", sysFile.Id + sysFile.Suffix); + } + + public Task DeleteFileAsync(SysFile sysFile) + { + var fullPath = BuildFilePath(sysFile); + using var helper = CreateSSHHelper(); + helper.DeleteFile(fullPath); + return Task.CompletedTask; + } + + public Task DownloadFileBase64Async(SysFile sysFile) + { + using var helper = CreateSSHHelper(); + return Task.FromResult(Convert.ToBase64String(helper.ReadAllBytes(sysFile.FilePath))); + } + + public Task GetFileStreamResultAsync(SysFile sysFile, string fileName) + { + var filePath = BuildFilePath(sysFile); + using var helper = CreateSSHHelper(); + return Task.FromResult(new FileStreamResult(helper.OpenRead(filePath), "application/octet-stream") + { + FileDownloadName = fileName + sysFile.Suffix + }); + } + + public Task UploadFileAsync(IFormFile file, SysFile sysFile, string path, string finalName) + { + var fullPath = string.Concat(path.StartsWith('/') ? path : "/" + path, "/", finalName); + using var helper = CreateSSHHelper(); + helper.UploadFile(file.OpenReadStream(), fullPath); + return Task.FromResult(sysFile); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/IOSSServiceManager.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/IOSSServiceManager.cs new file mode 100644 index 0000000..b1597d0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/IOSSServiceManager.cs @@ -0,0 +1,211 @@ +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core.Service; + +/// +/// OSS服务管理器接口 +/// +public interface IOSSServiceManager : IDisposable +{ + /// + /// 获取OSS服务实例 + /// + /// 存储提供者配置 + /// + Task GetOSSServiceAsync(SysFileProvider provider); + + /// + /// 清除缓存 + /// + void ClearCache(); +} + +/// +/// OSS服务管理器实现 +/// +public class OSSServiceManager : IOSSServiceManager, ITransient +{ + private readonly IServiceProvider _serviceProvider; + private readonly ConcurrentDictionary _ossServiceCache; + private readonly object _lockObject = new object(); + private bool _disposed = false; + + public OSSServiceManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _ossServiceCache = new ConcurrentDictionary(); + } + + /// + /// 获取OSS服务实例(带缓存) + /// + /// 存储提供者配置 + /// + public async Task GetOSSServiceAsync(SysFileProvider provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + var cacheKey = provider.ConfigKey; + + // 尝试从缓存获取 + if (_ossServiceCache.TryGetValue(cacheKey, out var cachedService)) + { + return cachedService; + } + + // 验证配置 + if (!await ValidateConfigurationAsync(provider)) + { + throw new InvalidOperationException($"OSS提供者配置无效: {provider.DisplayName}"); + } + + // 线程安全地创建新服务 + lock (_lockObject) + { + // 双重检查锁定模式 + if (_ossServiceCache.TryGetValue(cacheKey, out cachedService)) + { + return cachedService; + } + + // 转换配置并创建服务 + var ossOptions = ConvertToOSSOptions(provider); + var ossService = CreateOSSService(ossOptions); + + // 添加到缓存 + _ossServiceCache.TryAdd(cacheKey, ossService); + + return ossService; + } + } + + /// + /// 创建OSS服务实例 + /// + /// OSS配置选项 + /// + private IOSSService CreateOSSService(OSSOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + try + { + // 使用现有的IOSSServiceFactory,但需要先注册配置 + var providerName = Enum.GetName(options.Provider); + var configSectionName = $"TempOSS_{Guid.NewGuid():N}"; + + // 创建临时配置 + var configData = new Dictionary + { + [$"{configSectionName}:Provider"] = providerName, + [$"{configSectionName}:Endpoint"] = options.Endpoint ?? "", + [$"{configSectionName}:AccessKey"] = options.AccessKey ?? "", + [$"{configSectionName}:SecretKey"] = options.SecretKey ?? "", + [$"{configSectionName}:Region"] = options.Region ?? "", + [$"{configSectionName}:IsEnableHttps"] = options.IsEnableHttps.ToString(), + [$"{configSectionName}:IsEnableCache"] = options.IsEnableCache.ToString() + }; + + var tempConfig = new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + + // 创建临时服务集合,但不立即释放 + var services = new ServiceCollection(); + services.AddSingleton(tempConfig); + services.AddLogging(); + services.AddOSSService(providerName, configSectionName); + + // 构建服务提供者并创建OSS服务 + var tempServiceProvider = services.BuildServiceProvider(); + var ossServiceFactory = tempServiceProvider.GetRequiredService(); + var ossService = ossServiceFactory.Create(providerName); + + // 注意:不要释放tempServiceProvider,因为ossService可能依赖它 + // 这里我们接受这个内存开销,因为缓存会减少创建频率 + + return ossService; + } + catch (Exception ex) + { + throw Oops.Oh($"创建OSS服务失败: {ex.Message}"); + } + } + + /// + /// 验证配置 + /// + /// 存储提供者配置 + /// + private Task ValidateConfigurationAsync(SysFileProvider provider) + { + if (provider == null) return Task.FromResult(false); + + // 基本字段验证 + var isValid = !string.IsNullOrWhiteSpace(provider.Provider) && + !string.IsNullOrWhiteSpace(provider.BucketName) && + !string.IsNullOrWhiteSpace(provider.AccessKey) && + !string.IsNullOrWhiteSpace(provider.SecretKey); + + // Minio额外需要Endpoint + if (provider.Provider.ToUpper() == "MINIO") + { + isValid = isValid && !string.IsNullOrWhiteSpace(provider.Endpoint); + } + + return Task.FromResult(isValid); + } + + /// + /// 将SysFileProvider转换为OSSOptions + /// + /// + /// + private OSSOptions ConvertToOSSOptions(SysFileProvider provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + var ossOptions = new OSSOptions + { + Provider = Enum.Parse(provider.Provider), + Endpoint = provider.Endpoint, + Region = provider.Region, + IsEnableHttps = provider.IsEnableHttps ?? true, + IsEnableCache = provider.IsEnableCache ?? true + }; + + // 设置认证信息(所有提供者现在都使用统一的字段) + ossOptions.AccessKey = provider.AccessKey; + ossOptions.SecretKey = provider.SecretKey; + + return ossOptions; + } + + /// + /// 清除缓存 + /// + public void ClearCache() + { + lock (_lockObject) + { + _ossServiceCache.Clear(); + } + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (!_disposed) + { + lock (_lockObject) + { + _ossServiceCache.Clear(); + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderController.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderController.cs new file mode 100644 index 0000000..518d058 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderController.cs @@ -0,0 +1,199 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 文件存储提供者管理控制器 🧩 +/// +[ApiDescriptionSettings(Order = 412, Description = "文件存储提供者管理")] +public class SysFileProviderController : IDynamicApiController, ITransient +{ + private readonly SysFileProviderService _fileProviderService; + + public SysFileProviderController(SysFileProviderService fileProviderService) + { + _fileProviderService = fileProviderService; + } + + /// + /// 获取存储提供者列表 🔖 + /// + /// + [DisplayName("获取存储提供者列表")] + public async Task> GetProviderList() + { + return await _fileProviderService.GetFileProviderList(); + } + + /// + /// 获取存储提供者分页列表 🔖 + /// + /// + /// + [DisplayName("获取存储提供者分页列表")] + public async Task> GetProviderPage(PageFileProviderInput input) + { + return await _fileProviderService.GetFileProviderPage(input); + } + + /// + /// 获取存储提供者详情 🔖 + /// + /// + /// + [DisplayName("获取存储提供者详情")] + public async Task GetProvider([FromQuery] QueryFileProviderInput input) + { + return await _fileProviderService.GetFileProvider(input); + } + + /// + /// 添加存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("添加存储提供者")] + public async Task AddProvider(AddFileProviderInput input) + { + await _fileProviderService.AddFileProvider(input); + } + + /// + /// 更新存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新存储提供者")] + public async Task UpdateProvider(UpdateFileProviderInput input) + { + await _fileProviderService.UpdateFileProvider(input); + } + + /// + /// 删除存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除存储提供者")] + public async Task DeleteProvider(DeleteFileProviderInput input) + { + await _fileProviderService.DeleteFileProvider(input); + } + + /// + /// 根据存储桶名称获取存储提供者 🔖 + /// + /// 存储桶名称 + /// + [DisplayName("根据存储桶名称获取存储提供者")] + public async Task GetProviderByBucketName(string bucketName) + { + return await _fileProviderService.GetProviderByBucketName(bucketName); + } + + /// + /// 清除存储提供者缓存 🔖 + /// + /// + [DisplayName("清除存储提供者缓存")] + public async Task ClearCache() + { + await _fileProviderService.ClearCache(); + } + + /// + /// 批量启用/禁用存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "BatchEnable"), HttpPost] + [DisplayName("批量启用/禁用存储提供者")] + public async Task BatchEnableProvider(BatchEnableProviderInput input) + { + foreach (var id in input.Ids) + { + var provider = await _fileProviderService.GetFileProviderById(id); + if (provider != null) + { + var updateInput = new UpdateFileProviderInput + { + Id = id, + Provider = provider.Provider, + BucketName = provider.BucketName, + IsEnable = input.IsEnable + }; + await _fileProviderService.UpdateFileProvider(updateInput); + } + } + } + + /// + /// 获取存储提供者统计信息 🔖 + /// + /// + [DisplayName("获取存储提供者统计信息")] + public async Task GetProviderStatistics() + { + var providers = await _fileProviderService.GetCachedFileProviders(); + + var statistics = new + { + Total = providers.Count, + Enabled = providers.Count(p => p.IsEnable == true), + Disabled = providers.Count(p => p.IsEnable != true), + ByProvider = providers.GroupBy(p => p.Provider) + .Select(g => new { Provider = g.Key, Count = g.Count() }) + .ToList(), + ByRegion = providers.Where(p => !string.IsNullOrEmpty(p.Region)) + .GroupBy(p => p.Region) + .Select(g => new { Region = g.Key, Count = g.Count() }) + .ToList() + }; + + return statistics; + } + + /// + /// 获取所有可用的存储桶列表 🔖 + /// + /// + [DisplayName("获取所有可用的存储桶列表")] + public async Task> GetAvailableBuckets() + { + return await _fileProviderService.GetAvailableBuckets(); + } + + /// + /// 获取存储桶和提供者的映射关系 🔖 + /// + /// + [DisplayName("获取存储桶和提供者的映射关系")] + public async Task>> GetBucketProviderMapping() + { + return await _fileProviderService.GetBucketProviderMapping(); + } +} + +/// +/// 批量启用/禁用存储提供者输入参数 +/// +public class BatchEnableProviderInput +{ + /// + /// 存储提供者ID列表 + /// + [Required] + public List Ids { get; set; } + + /// + /// 是否启用 + /// + public bool IsEnable { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderService.cs new file mode 100644 index 0000000..2832fef --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileProviderService.cs @@ -0,0 +1,520 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OnceMi.AspNetCore.OSS; + +namespace Admin.NET.Core.Service; + +/// +/// 系统文件存储提供者服务 🧩 +/// +[ApiDescriptionSettings(Order = 411, Description = "文件存储提供者")] +public class SysFileProviderService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysFileProviderRep; + private readonly SysCacheService _sysCacheService; + private readonly IOSSServiceFactory _ossServiceFactory; + private readonly IOSSServiceManager _ossServiceManager; + private static readonly string CacheKey = "sys_file_provider"; + + public SysFileProviderService(UserManager userManager, + SqlSugarRepository sysFileProviderRep, + SysCacheService sysCacheService, + IOSSServiceFactory ossServiceFactory, + IOSSServiceManager ossServiceManager) + { + _userManager = userManager; + _sysFileProviderRep = sysFileProviderRep; + _sysCacheService = sysCacheService; + _ossServiceFactory = ossServiceFactory; + _ossServiceManager = ossServiceManager; + } + + /// + /// 获取文件存储提供者分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件存储提供者分页列表")] + [NonAction] + public async Task> GetFileProviderPage([FromQuery] PageFileProviderInput input) + { + return await _sysFileProviderRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Provider), u => u.Provider.Contains(input.Provider!)) + .WhereIF(!string.IsNullOrWhiteSpace(input.BucketName), u => u.BucketName.Contains(input.BucketName!)) + .WhereIF(input.IsEnable.HasValue, u => u.IsEnable == input.IsEnable) + .OrderBy(u => u.OrderNo) + .OrderBy(u => u.Id) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取文件存储提供者列表 🔖 + /// + /// + [DisplayName("获取文件存储提供者列表")] + [NonAction] + public async Task> GetFileProviderList() + { + return await _sysFileProviderRep.AsQueryable() + .Where(u => u.IsEnable == true) + .OrderBy(u => u.OrderNo) + .OrderBy(u => u.Id) + .ToListAsync(); + } + + /// + /// 增加文件存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加文件存储提供者")] + [NonAction] + public async Task AddFileProvider(AddFileProviderInput input) + { + // 验证输入参数 + if (input == null) + throw Oops.Oh("输入参数不能为空").StatusCode(400); + + if (string.IsNullOrWhiteSpace(input.Provider)) + throw Oops.Oh("存储提供者不能为空").StatusCode(400); + + if (string.IsNullOrWhiteSpace(input.BucketName)) + throw Oops.Oh("存储桶名称不能为空").StatusCode(400); + + // 验证提供者类型 + if (!Enum.TryParse(input.Provider, true, out _)) + throw Oops.Oh($"不支持的存储提供者类型: {input.Provider}").StatusCode(400); + + var isExist = await _sysFileProviderRep.AsQueryable() + .AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName); + if (isExist) + throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400); + + var fileProvider = input.Adapt(); + + // 验证配置完整性 + await ValidateProviderConfiguration(fileProvider); + + // 处理默认提供者逻辑 + await HandleDefaultProviderLogic(fileProvider); + + await _sysFileProviderRep.InsertAsync(fileProvider); + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + } + + /// + /// 更新文件存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新文件存储提供者")] + [NonAction] + public async Task UpdateFileProvider(UpdateFileProviderInput input) + { + // 验证输入参数 + if (input == null) + throw Oops.Oh("输入参数不能为空").StatusCode(400); + + var isExist = await _sysFileProviderRep.AsQueryable() + .AnyAsync(u => u.Provider == input.Provider && u.BucketName == input.BucketName && u.Id != input.Id); + if (isExist) + throw Oops.Oh(ErrorCodeEnum.D1006).StatusCode(400); + + var fileProvider = input.Adapt(); + + // 验证配置完整性 + await ValidateProviderConfiguration(fileProvider); + + // 处理默认提供者逻辑 + await HandleDefaultProviderLogic(fileProvider); + + await _sysFileProviderRep.AsUpdateable(fileProvider).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + } + + /// + /// 删除文件存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除文件存储提供者")] + [NonAction] + public async Task DeleteFileProvider(DeleteFileProviderInput input) + { + // 检查是否为默认提供者 + var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400); + var isDefault = provider.IsDefault == true; + + await _sysFileProviderRep.DeleteByIdAsync(input.Id); + + // 如果删除的是默认提供者,自动设置第一个启用的提供者为默认 + if (isDefault) + { + var firstEnabledProvider = await _sysFileProviderRep.AsQueryable() + .Where(p => p.IsEnable == true) + .OrderBy(p => p.OrderNo) + .OrderBy(p => p.Id) + .FirstAsync(); + + if (firstEnabledProvider != null) + { + await _sysFileProviderRep.AsUpdateable() + .SetColumns(p => p.IsDefault == true) + .Where(p => p.Id == firstEnabledProvider.Id) + .ExecuteCommandAsync(); + + Debug.WriteLine($"自动设置新的默认提供者: {firstEnabledProvider.DisplayName}"); + } + } + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + } + + /// + /// 获取文件存储提供者详情 🔖 + /// + /// + /// + [DisplayName("获取文件存储提供者详情")] + [NonAction] + public async Task GetFileProvider([FromQuery] QueryFileProviderInput input) + { + return await _sysFileProviderRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 根据提供者和存储桶获取配置 + /// + /// + /// + /// + [NonAction] + public async Task GetFileProviderByBucket(string provider, string bucketName) + { + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(x => x.Provider == provider && x.BucketName == bucketName && x.IsEnable == true); + } + + /// + /// 根据ID获取配置 + /// + /// + /// + [NonAction] + public async Task GetFileProviderById(long id) + { + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(x => x.Id == id && x.IsEnable == true); + } + + /// + /// 根据存储桶名称获取存储提供者 + /// + /// 存储桶名称 + /// + [NonAction] + public async Task GetProviderByBucketName(string bucketName) + { + if (string.IsNullOrWhiteSpace(bucketName)) + return null; + + var providers = await GetCachedFileProviders(); + return providers.FirstOrDefault(p => p.BucketName == bucketName); + } + + /// + /// 获取默认存储提供者 + /// + /// + [NonAction] + public async Task GetDefaultProvider() + { + var providers = await GetCachedFileProviders(); + + // 优先返回标记为默认的提供者 + var defaultProvider = providers.FirstOrDefault(p => p.IsDefault == true); + if (defaultProvider != null) + return defaultProvider; + + // 如果没有标记为默认的,返回第一个启用的提供者(兼容旧逻辑) + return providers.FirstOrDefault(); + } + + /// + /// 获取默认存储提供者信息 🔖 + /// + /// + [DisplayName("获取默认存储提供者信息")] + [NonAction] + public async Task GetDefaultProviderInfo() + { + return await GetDefaultProvider(); + } + + /// + /// 设置默认存储提供者 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "SetDefault"), HttpPost] + [DisplayName("设置默认存储提供者")] + [NonAction] + public async Task SetDefaultProvider(SetDefaultProviderInput input) + { + // 验证提供者是否存在且启用 + var provider = await _sysFileProviderRep.GetByIdAsync(input.Id) ?? throw Oops.Oh("存储提供者不存在").StatusCode(400); + if (provider.IsEnable != true) + throw Oops.Oh("只能设置启用状态的存储提供者为默认").StatusCode(400); + + // 开启事务,确保数据一致性 + await _sysFileProviderRep.AsTenant().BeginTranAsync(); + try + { + // 先将所有提供者的默认标识设为false + await _sysFileProviderRep.AsUpdateable() + .SetColumns(p => p.IsDefault == false) + .Where(p => p.IsDefault == true) + .ExecuteCommandAsync(); + + // 设置指定提供者为默认 + await _sysFileProviderRep.AsUpdateable() + .SetColumns(p => p.IsDefault == true) + .Where(p => p.Id == input.Id) + .ExecuteCommandAsync(); + + await _sysFileProviderRep.AsTenant().CommitTranAsync(); + + // 清除缓存 + await ClearCache(); + + // 清除OSS服务缓存 + _ossServiceManager?.ClearCache(); + + Debug.WriteLine($"已设置默认存储提供者: {provider.DisplayName}"); + } + catch (Exception) + { + await _sysFileProviderRep.AsTenant().RollbackTranAsync(); + throw; + } + } + + /// + /// 获取缓存的文件提供者列表 + /// + /// + [NonAction] + public async Task> GetCachedFileProviders() + { + return await _sysCacheService.AdGetAsync(CacheKey, async () => + { + return await _sysFileProviderRep.AsQueryable() + .Where(u => u.IsEnable == true) + .OrderBy(u => u.OrderNo) + .OrderBy(u => u.Id) + .ToListAsync(); + }, TimeSpan.FromMinutes(30)); + } + + /// + /// 清除缓存 + /// + /// + [NonAction] + public async Task ClearCache() + { + _sysCacheService.Remove(CacheKey); + await Task.CompletedTask; + } + + /// + /// 获取所有可用的存储桶列表 + /// + /// + [NonAction] + public async Task> GetAvailableBuckets() + { + var providers = await GetCachedFileProviders(); + return providers.Select(p => p.BucketName).Distinct().OrderBy(b => b).ToList(); + } + + /// + /// 获取存储桶和提供者的映射关系 + /// + /// + [NonAction] + public async Task>> GetBucketProviderMapping() + { + var providers = await GetCachedFileProviders(); + var mapping = new Dictionary>(); + + foreach (var provider in providers) + { + if (!mapping.TryGetValue(provider.BucketName, out List value)) + { + value = new List(); + mapping[provider.BucketName] = value; + } + + value.Add(provider); + } + + return mapping; + } + + /// + /// 验证存储提供者配置 + /// + /// 存储提供者配置 + /// + [NonAction] + private async Task ValidateProviderConfiguration(SysFileProvider provider) + { + if (provider == null) + throw Oops.Oh("存储提供者配置不能为空").StatusCode(400); + + // 基础字段验证 + if (string.IsNullOrWhiteSpace(provider.Provider)) + throw Oops.Oh("存储提供者类型不能为空").StatusCode(400); + + if (string.IsNullOrWhiteSpace(provider.BucketName)) + throw Oops.Oh("存储桶名称不能为空").StatusCode(400); + + if (string.IsNullOrWhiteSpace(provider.Endpoint)) + throw Oops.Oh("端点地址不能为空").StatusCode(400); + + // 所有提供者都需要AccessKey和SecretKey + if (string.IsNullOrWhiteSpace(provider.AccessKey)) + throw Oops.Oh($"{provider.Provider} AccessKey不能为空").StatusCode(400); + if (string.IsNullOrWhiteSpace(provider.SecretKey)) + throw Oops.Oh($"{provider.Provider} SecretKey不能为空").StatusCode(400); + + // 根据不同提供者验证特定字段 + switch (provider.Provider.ToUpper()) + { + case "ALIYUN": + if (string.IsNullOrWhiteSpace(provider.Region)) + throw Oops.Oh("阿里云Region不能为空").StatusCode(400); + break; + + case "QCLOUD": + if (string.IsNullOrWhiteSpace(provider.Endpoint)) + throw Oops.Oh("腾讯云Endpoint(AppId)不能为空").StatusCode(400); + if (string.IsNullOrWhiteSpace(provider.Region)) + throw Oops.Oh("腾讯云Region不能为空").StatusCode(400); + break; + + case "MINIO": + // Minio只需要AccessKey和SecretKey,已在上面验证 + break; + + default: + throw Oops.Oh($"不支持的存储提供者类型: {provider.Provider}").StatusCode(400); + } + + // 验证存储桶名称格式 + await ValidateBucketName(provider.Provider, provider.BucketName); + } + + /// + /// 验证存储桶名称格式 + /// + /// 存储提供者类型 + /// 存储桶名称 + /// + [NonAction] + private async Task ValidateBucketName(string provider, string bucketName) + { + if (string.IsNullOrWhiteSpace(bucketName)) + return; + + switch (provider.ToUpper()) + { + case "ALIYUN": + // 阿里云存储桶命名规则 + if (bucketName.Length < 3 || bucketName.Length > 63) + throw Oops.Oh("阿里云存储桶名称长度必须在3-63字符之间").StatusCode(400); + + if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$")) + throw Oops.Oh("阿里云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400); + break; + + case "QCLOUD": + // 腾讯云存储桶命名规则 + if (bucketName.Length < 1 || bucketName.Length > 40) + throw Oops.Oh("腾讯云存储桶名称长度必须在1-40字符之间").StatusCode(400); + + if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-]*[a-z0-9]$")) + throw Oops.Oh("腾讯云存储桶名称只能包含小写字母、数字和短横线,且必须以字母或数字开头和结尾").StatusCode(400); + break; + + case "MINIO": + // Minio存储桶命名规则 + if (bucketName.Length < 3 || bucketName.Length > 63) + throw Oops.Oh("Minio存储桶名称长度必须在3-63字符之间").StatusCode(400); + + if (!Regex.IsMatch(bucketName, @"^[a-z0-9][a-z0-9\-\.]*[a-z0-9]$")) + throw Oops.Oh("Minio存储桶名称只能包含小写字母、数字、短横线和点,且必须以字母或数字开头和结尾").StatusCode(400); + break; + } + + await Task.CompletedTask; + } + + /// + /// 处理默认提供者逻辑 + /// + /// 存储提供者配置 + /// + [NonAction] + private async Task HandleDefaultProviderLogic(SysFileProvider provider) + { + // 如果设置为默认提供者 + if (provider.IsDefault == true) + { + // 确保只有一个默认提供者,将其他提供者的默认标识设为false + await _sysFileProviderRep.AsUpdateable() + .SetColumns(p => p.IsDefault == false) + .Where(p => p.IsDefault == true && p.Id != provider.Id) + .ExecuteCommandAsync(); + } + else + // 如果没有设置IsDefault值,默认为false + { + provider.IsDefault ??= false; + } + + // 检查是否还有其他默认提供者,如果没有且当前提供者启用,则设为默认 + var hasDefaultProvider = await _sysFileProviderRep.AsQueryable() + .Where(p => p.IsDefault == true && p.IsEnable == true && p.Id != provider.Id) + .AnyAsync(); + + if (!hasDefaultProvider && provider.IsEnable == true && provider.IsDefault != true) + { + // 如果没有其他默认提供者且当前提供者启用,则设为默认 + provider.IsDefault = true; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs new file mode 100644 index 0000000..7c68dd6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs @@ -0,0 +1,445 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Aliyun.OSS.Util; +using Furion.AspNetCore; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Admin.NET.Core.Service; + +/// +/// 系统文件服务 🧩 +/// +[ApiDescriptionSettings(Order = 410, Description = "系统文件")] +public class SysFileService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysFileRep; + private readonly OSSProviderOptions _OSSProviderOptions; + private readonly UploadOptions _uploadOptions; + private readonly IConfiguration _configuration; + private readonly string _imageType = ".jpeg.jpg.png.bmp.gif.tif"; + private readonly INamedServiceProvider _namedServiceProvider; + private readonly ICustomFileProvider _customFileProvider; + + public SysFileService(UserManager userManager, + SqlSugarRepository sysFileRep, + IOptions oSSProviderOptions, + IOptions uploadOptions, + INamedServiceProvider namedServiceProvider, + IConfiguration configuration) + { + _namedServiceProvider = namedServiceProvider; + _userManager = userManager; + _sysFileRep = sysFileRep; + _OSSProviderOptions = oSSProviderOptions.Value; + _uploadOptions = uploadOptions.Value; + _configuration = configuration; + + // 简化提供者选择逻辑 + if (_OSSProviderOptions.Enabled || _configuration["MultiOSS:Enabled"].ToBoolean()) + { + // 统一使用MultiOSSFileProvider处理所有OSS情况 + _customFileProvider = _namedServiceProvider.GetService(nameof(MultiOSSFileProvider)); + } + else if (_configuration["SSHProvider:Enabled"].ToBoolean()) + { + _customFileProvider = _namedServiceProvider.GetService(nameof(SSHFileProvider)); + } + else + { + _customFileProvider = _namedServiceProvider.GetService(nameof(DefaultFileProvider)); + } + } + + /// + /// 获取文件分页列表 🔖 + /// + /// + /// + [DisplayName("获取文件分页列表")] + public async Task> Page(PageFileInput input) + { + // 获取所有公开附件 + var publicList = _sysFileRep.AsQueryable().ClearFilter().Where(u => u.IsPublic == true); + // 获取私有附件 + var privateList = _sysFileRep.AsQueryable().Where(u => u.IsPublic == false); + // 合并公开和私有附件并分页 + return await _sysFileRep.Context.UnionAll(publicList, privateList) + .WhereIF(!string.IsNullOrWhiteSpace(input.FileName), u => u.FileName.Contains(input.FileName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.FilePath), u => u.FilePath.Contains(input.FilePath.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()), + u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 上传文件Base64 🔖 + /// + /// + /// + [DisplayName("上传文件Base64")] + public async Task UploadFileFromBase64(UploadFileFromBase64Input input) + { + var pattern = @"data:(?.+?);base64,(?[^""]+)"; + var regex = new Regex(pattern, RegexOptions.Compiled); + var match = regex.Match(input.FileDataBase64); + + byte[] fileData = Convert.FromBase64String(match.Groups["data"].Value); + var contentType = match.Groups["type"].Value; + if (string.IsNullOrEmpty(input.FileName)) + input.FileName = $"{YitIdHelper.NextId()}.{contentType.AsSpan(contentType.LastIndexOf('/') + 1)}"; + + using var ms = new MemoryStream(); + ms.Write(fileData); + ms.Seek(0, SeekOrigin.Begin); + IFormFile formFile = new FormFile(ms, 0, fileData.Length, "file", input.FileName) + { + Headers = new HeaderDictionary(), + ContentType = contentType + }; + var uploadFileInput = input.Adapt(); + uploadFileInput.File = formFile; + return await UploadFile(uploadFileInput); + } + + /// + /// 上传多文件 🔖 + /// + /// + /// + [DisplayName("上传多文件")] + public async Task> UploadFiles([Required] List files) + { + var fileList = new List(); + foreach (var file in files) + { + var uploadedFile = await UploadFile(new UploadFileInput { File = file }); + fileList.Add(uploadedFile); + } + return fileList; + } + + /// + /// 根据文件Id或Url下载 🔖 + /// + /// + /// + [DisplayName("根据文件Id或Url下载")] + public async Task DownloadFile(SysFile input) + { + var file = input.Id > 0 ? await GetFile(input.Id) : await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == input.Url); + var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + return await GetFileStreamResult(file, fileName); + } + + /// + /// 文件预览 🔖 + /// + /// + /// + [DisplayName("文件预览")] + public async Task GetPreview([FromRoute] long id) + { + var file = await GetFile(id); + //var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + return await GetFileStreamResult(file, file.Id + ""); + } + + /// + /// 获取文件流 + /// + /// + /// + /// + private async Task GetFileStreamResult(SysFile file, string fileName) + { + return await _customFileProvider.GetFileStreamResultAsync(file, fileName); + } + + /// + /// 获取文件流 + /// + [NonAction] + public async Task GetFileStream(SysFile file) + { + var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8")); + var result = await _customFileProvider.GetFileStreamResultAsync(file, fileName); + return result.FileStream; + } + + /// + /// 下载指定文件Base64格式 🔖 + /// + /// + /// + [DisplayName("下载指定文件Base64格式")] + public async Task DownloadFileBase64([FromBody] string url) + { + var sysFile = await _sysFileRep.CopyNew().GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在"); + return await _customFileProvider.DownloadFileBase64Async(sysFile); + } + + /// + /// 删除文件 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除文件")] + public async Task DeleteFile(BaseIdInput input) + { + var file = await _sysFileRep.GetByIdAsync(input.Id) ?? throw Oops.Oh($"文件不存在"); + await _sysFileRep.DeleteAsync(file); + await _customFileProvider.DeleteFileAsync(file); + } + + /// + /// 更新文件 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新文件")] + public async Task UpdateFile(SysFile input) + { + var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id); + if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000); + + await _sysFileRep.UpdateAsync(input); + } + + /// + /// 获取文件 🔖 + /// + /// + /// + [DisplayName("获取文件")] + public async Task GetFile([FromQuery] long id) + { + var file = await _sysFileRep.CopyNew().GetByIdAsync(id); + return file ?? throw Oops.Oh(ErrorCodeEnum.D8000); + } + + /// + /// 根据文件Id集合获取文件 🔖 + /// + /// + /// + [DisplayName("根据文件Id集合获取文件")] + public async Task> GetFileByIds([FromQuery][FlexibleArray] List ids) + { + return await _sysFileRep.AsQueryable().Where(u => ids.Contains(u.Id)).ToListAsync(); + } + + /// + /// 获取文件路径 🔖 + /// + /// + [DisplayName("获取文件路径")] + public async Task> GetFolder() + { + // 优化:直接在数据库层面获取不重复的文件路径 + var folders = await _sysFileRep.AsQueryable() + .Select(u => u.FilePath) + .Distinct() + .ToListAsync(); + + var pathTreeBuilder = new PathTreeBuilder(); + var tree = pathTreeBuilder.BuildTree(folders); + return tree.Children; + } + + /// + /// 上传文件 🔖 + /// + /// + /// 存储目标路径 + /// + [DisplayName("上传文件")] + public async Task UploadFile([FromForm] UploadFileInput input, [BindNever] string targetPath = "") + { + if (input.File == null || input.File.Length <= 0) throw Oops.Oh(ErrorCodeEnum.D8000); + + if (input.File.FileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) throw Oops.Oh(ErrorCodeEnum.D8005); + + // 判断是否重复上传的文件 + var sizeKb = input.File.Length / 1024; // 大小KB + var fileMd5 = string.Empty; + if (_uploadOptions.EnableMd5) + { + await using (var fileStream = input.File.OpenReadStream()) + { + fileMd5 = OssUtils.ComputeContentMd5(fileStream, fileStream.Length); + } + // Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString() + // 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句,而是构造了CAST(123 AS CHAR)这样的语句,这样这个返回值是utf8mb4_general_ci,所以容易出错。 + var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb); + if (sysFile != null) return sysFile; + } + + // 验证文件类型 + if (!_uploadOptions.ContentType.Contains(input.File.ContentType)) throw Oops.Oh($"{ErrorCodeEnum.D8001}:{input.File.ContentType}"); + + // 验证文件大小 + if (sizeKb > _uploadOptions.MaxSize) throw Oops.Oh($"{ErrorCodeEnum.D8002},允许最大:{_uploadOptions.MaxSize}KB"); + + // 获取文件后缀 + var suffix = Path.GetExtension(input.File.FileName).ToLower(); // 后缀 + if (string.IsNullOrWhiteSpace(suffix)) + suffix = string.Concat(".", input.File.ContentType.AsSpan(input.File.ContentType.LastIndexOf('/') + 1)); + if (!string.IsNullOrWhiteSpace(suffix)) + { + //var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); + //suffix = contentTypeProvider.Mappings.FirstOrDefault(u => u.Value == file.ContentType).Key; + // 修改 image/jpeg 类型返回的 .jpeg、jpe 后缀 + if (suffix == ".jpeg" || suffix == ".jpe") + suffix = ".jpg"; + } + if (string.IsNullOrWhiteSpace(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); + + // 防止客户端伪造文件类型 + if (!string.IsNullOrWhiteSpace(input.AllowSuffix) && !input.AllowSuffix.Contains(suffix)) throw Oops.Oh(ErrorCodeEnum.D8003); + //if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix)) throw Oops.Oh(ErrorCodeEnum.D8001); + + // 文件存储位置 + var path = string.IsNullOrWhiteSpace(targetPath) ? _uploadOptions.Path : targetPath; + path = path.ParseToDateTimeForRep(); + + var newFile = input.Adapt(); + newFile.Id = YitIdHelper.NextId(); + + // 优先使用用户指定的存储桶名称,如果没有指定则使用默认配置 + if (!string.IsNullOrEmpty(input.BucketName)) + { + newFile.BucketName = input.BucketName; + } + else + { + // MultiOSSFileProvider会自动使用默认配置 + newFile.BucketName = _OSSProviderOptions.Enabled ? _OSSProviderOptions.Bucket : "Local"; + } + + newFile.FileName = Path.GetFileNameWithoutExtension(input.File.FileName); + newFile.Suffix = suffix; + newFile.SizeKb = sizeKb; + newFile.FilePath = path; + newFile.FileMd5 = fileMd5; + newFile.DataId = input.DataId; + + var finalName = newFile.Id + suffix; // 文件最终名称 + + newFile = await _customFileProvider.UploadFileAsync(input.File, newFile, path, finalName); + await _sysFileRep.AsInsertable(newFile).ExecuteCommandAsync(); + return newFile; + } + + /// + /// 上传头像 🔖 + /// + /// + /// + [DisplayName("上传头像")] + public async Task UploadAvatar([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/avatar"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.GetByIdAsync(_userManager.UserId); + await sysUserRep.UpdateAsync(u => new SysUser() { Avatar = sysFile.Url }, u => u.Id == user.Id); + // 删除已有头像文件 + if (!string.IsNullOrWhiteSpace(user.Avatar)) + { + var fileId = Path.GetFileNameWithoutExtension(user.Avatar); + if (long.TryParse(fileId, out var id)) + { + try + { + await DeleteFile(new BaseIdInput { Id = id }); + } + catch + { + // 忽略删除旧头像文件的错误,不影响新头像上传 + } + } + } + + return sysFile; + } + + /// + /// 上传电子签名 🔖 + /// + /// + /// + [DisplayName("上传电子签名")] + public async Task UploadSignature([Required] IFormFile file) + { + var sysFile = await UploadFile(new UploadFileInput { File = file, AllowSuffix = _imageType }, "upload/signature"); + + var sysUserRep = _sysFileRep.ChangeRepository>(); + var user = await sysUserRep.GetByIdAsync(_userManager.UserId); + // 删除已有电子签名文件 + if (!string.IsNullOrWhiteSpace(user.Signature) && user.Signature.EndsWith(".png")) + { + var fileId = Path.GetFileNameWithoutExtension(user.Signature); + if (long.TryParse(fileId, out var id)) + { + try + { + await DeleteFile(new BaseIdInput { Id = id }); + } + catch + { + // 忽略删除旧签名文件的错误,不影响新签名上传 + } + } + } + await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id); + return sysFile; + } + + #region 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 + + /// + /// 更新文件的业务数据Id + /// + /// + /// + /// + [NonAction] + public async Task UpdateFileByDataId(long dataId, List sysFiles) + { + var newFileIds = sysFiles.Select(u => u.Id).ToList(); + + // 求文件Id差集并删除(无效文件) + var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); + var tmpFileIds = tmpFiles.Select(u => u.Id).ToList(); + var deleteFileIds = tmpFileIds.Except(newFileIds); + foreach (var fileId in deleteFileIds) + await DeleteFile(new BaseIdInput() { Id = fileId }); + + await _sysFileRep.UpdateAsync(u => new SysFile() { DataId = dataId }, u => newFileIds.Contains(u.Id)); + } + + /// + /// 删除业务数据对应的文件 + /// + /// + /// + [NonAction] + public async Task DeleteFileByDataId(long dataId) + { + // 删除冗余无效的物理文件 + var tmpFiles = await _sysFileRep.GetListAsync(u => u.DataId == dataId); + foreach (var file in tmpFiles) + await _customFileProvider.DeleteFileAsync(file); + await _sysFileRep.AsDeleteable().Where(u => u.DataId == dataId).ExecuteCommandAsync(); + } + + #endregion 统一实体与文件关联时,业务应用实体只需要定义一个SysFile集合导航属性,业务增加和更新、删除分别调用即可 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/DbJobPersistence.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/DbJobPersistence.cs new file mode 100644 index 0000000..5611835 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/DbJobPersistence.cs @@ -0,0 +1,188 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业持久化(数据库) +/// +public class DbJobPersistence : IJobPersistence +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + + public DbJobPersistence(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + /// + /// 作业调度服务启动时 + /// + /// + /// + /// + public async Task> PreloadAsync(CancellationToken stoppingToken) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + var dynamicJobCompiler = scope.ServiceProvider.GetRequiredService(); + + // 获取所有定义的作业 + var allJobs = App.EffectiveTypes.ScanToBuilders().ToList(); + // 若数据库不存在任何作业,则直接返回 + if (!await db.Queryable().AnyAsync(u => true, stoppingToken)) return allJobs; + + // 遍历所有定义的作业 + foreach (var schedulerBuilder in allJobs) + { + // 获取作业信息构建器 + var jobBuilder = schedulerBuilder.GetJobBuilder(); + + // 加载数据库数据 + var dbDetail = await db.Queryable().FirstAsync(u => u.JobId == jobBuilder.JobId, stoppingToken); + if (dbDetail == null) continue; + + // 同步数据库数据 + jobBuilder.LoadFrom(dbDetail); + + // 获取作业的所有数据库的触发器 + var dbTriggers = await db.Queryable().Where(u => u.JobId == jobBuilder.JobId).ToListAsync(stoppingToken); + // 遍历所有作业触发器 + foreach (var (_, triggerBuilder) in schedulerBuilder.GetEnumerable()) + { + // 加载数据库数据 + var dbTrigger = dbTriggers.FirstOrDefault(u => u.JobId == jobBuilder.JobId && u.TriggerId == triggerBuilder.TriggerId); + if (dbTrigger == null) continue; + + triggerBuilder.LoadFrom(dbTrigger).Updated(); // 标记更新 + } + // 遍历所有非编译时定义的触发器加入到作业中 + foreach (var dbTrigger in dbTriggers) + { + if (schedulerBuilder.GetTriggerBuilder(dbTrigger.TriggerId)?.JobId == jobBuilder.JobId) continue; + var triggerBuilder = TriggerBuilder.Create(dbTrigger.TriggerId).LoadFrom(dbTrigger); + schedulerBuilder.AddTriggerBuilder(triggerBuilder); // 先添加 + triggerBuilder.Updated(); // 再标记更新 + } + + // 标记更新 + schedulerBuilder.Updated(); + } + + // 获取数据库所有通过脚本创建的作业 + var allDbScriptJobs = await db.Queryable().Where(u => u.CreateType != JobCreateTypeEnum.BuiltIn).ToListAsync(stoppingToken); + foreach (var dbDetail in allDbScriptJobs) + { + // 动态创建作业 + Type jobType = dbDetail.CreateType switch + { + JobCreateTypeEnum.Script => dynamicJobCompiler.BuildJob(dbDetail.ScriptCode), + JobCreateTypeEnum.Http => typeof(HttpJob), + _ => throw new NotSupportedException(), + }; + + // 动态构建的 jobType 的程序集名称为随机名称,需重新设置 + dbDetail.AssemblyName = jobType.Assembly.FullName!.Split(',')[0]; + var jobBuilder = JobBuilder.Create(jobType).LoadFrom(dbDetail); + + // 强行设置为不扫描 IJob 实现类 [Trigger] 特性触发器,否则 SchedulerBuilder.Create 会再次扫描,导致重复添加同名触发器 + jobBuilder.SetIncludeAnnotations(false); + + // 获取作业的所有数据库的触发器加入到作业中 + var dbTriggers = await db.Queryable().Where(u => u.JobId == jobBuilder.JobId).ToListAsync(); + var triggerBuilders = dbTriggers.Select(u => TriggerBuilder.Create(u.TriggerId).LoadFrom(u).Updated()); + var schedulerBuilder = SchedulerBuilder.Create(jobBuilder, triggerBuilders.ToArray()); + + // 标记更新 + schedulerBuilder.Updated(); + + allJobs.Add(schedulerBuilder); + } + + return allJobs; + } + + /// + /// 作业计划初始化通知 + /// + /// + /// + /// + public Task OnLoadingAsync(SchedulerBuilder builder, CancellationToken stoppingToken) + { + return Task.FromResult(builder); + } + + /// + /// 作业计划Scheduler的JobDetail变化时 + /// + /// + /// + public async Task OnChangedAsync(PersistenceContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobDetail = context.JobDetail.Adapt(); + switch (context.Behavior) + { + case PersistenceBehavior.Appended: + await db.Insertable(jobDetail).ExecuteCommandAsync(); + break; + + case PersistenceBehavior.Updated: + await db.Updateable(jobDetail).WhereColumns(u => new { u.JobId }).IgnoreColumns(u => new { u.Id, u.CreateType, u.ScriptCode }).ExecuteCommandAsync(); + break; + + case PersistenceBehavior.Removed: + await db.Deleteable().Where(u => u.JobId == jobDetail.JobId).ExecuteCommandAsync(); + break; + } + } + + /// + /// 作业计划Scheduler的触发器Trigger变化时 + /// + /// + /// + public async Task OnTriggerChangedAsync(PersistenceTriggerContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobTrigger = context.Trigger.Adapt(); + switch (context.Behavior) + { + case PersistenceBehavior.Appended: + await db.Insertable(jobTrigger).ExecuteCommandAsync(); + break; + + case PersistenceBehavior.Updated: + await db.Updateable(jobTrigger).WhereColumns(u => new { u.TriggerId, u.JobId }).IgnoreColumns(u => new { u.Id }).ExecuteCommandAsync(); + break; + + case PersistenceBehavior.Removed: + await db.Deleteable().Where(u => u.TriggerId == jobTrigger.TriggerId && u.JobId == jobTrigger.JobId).ExecuteCommandAsync(); + break; + } + } + + /// + /// 作业触发器运行记录 + /// + /// + /// + public async Task OnExecutionRecordAsync(PersistenceExecutionRecordContext context) + { + using var scope = _serviceScopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService().CopyNew(); + + var jobTriggerRecord = context.Timeline.Adapt(); + await db.Insertable(jobTriggerRecord).ExecuteCommandAsync(); + + await scope.ServiceProvider.GetRequiredService().ClearExpireJobTriggerRecord(jobTriggerRecord); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs new file mode 100644 index 0000000..24ff520 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailInput.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobDetailInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } +} + +public class PageJobDetailInput : BasePageInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 组名称 + /// + public string GroupName { get; set; } + + /// + /// 描述信息 + /// + public string Description { get; set; } +} + +public class AddJobDetailInput : SysJobDetail +{ + /// + /// 作业Id + /// + [Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")] + public override string JobId { get; set; } +} + +public class UpdateJobDetailInput : AddJobDetailInput +{ +} + +public class DeleteJobDetailInput : JobDetailInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs new file mode 100644 index 0000000..a6efa85 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobDetailOutput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobDetailOutput +{ + /// + /// 作业信息 + /// + public SysJobDetail JobDetail { get; set; } + + /// + /// 触发器集合 + /// + public List JobTriggers { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs new file mode 100644 index 0000000..334f2ba --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerInput.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class JobTriggerInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 触发器Id + /// + public string TriggerId { get; set; } +} + +public class AddJobTriggerInput : SysJobTrigger +{ + /// + /// 作业Id + /// + [Required(ErrorMessage = "作业Id不能为空"), MinLength(2, ErrorMessage = "作业Id不能少于2个字符")] + public override string JobId { get; set; } + + /// + /// 触发器Id + /// + [Required(ErrorMessage = "触发器Id不能为空"), MinLength(2, ErrorMessage = "触发器Id不能少于2个字符")] + public override string TriggerId { get; set; } +} + +public class UpdateJobTriggerInput : AddJobTriggerInput +{ +} + +public class DeleteJobTriggerInput : JobTriggerInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs new file mode 100644 index 0000000..2b909b2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/Dto/JobTriggerRecordInput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageJobTriggerRecordInput : BasePageInput +{ + /// + /// 作业Id + /// + public string JobId { get; set; } + + /// + /// 触发器Id + /// + public string TriggerId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobClusterServer.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobClusterServer.cs new file mode 100644 index 0000000..a7156b1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobClusterServer.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业集群控制 +/// +public class JobClusterServer : IJobClusterServer +{ + private static readonly SqlSugarRepository _sysJobClusterRep = App.GetRequiredService>(); + private readonly Random _random = new(DateTime.Now.Millisecond); + + /// + /// 当前作业调度器启动通知 + /// + /// 作业集群服务上下文 + public async void Start(JobClusterContext context) + { + // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting + if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId)) + { + await _sysJobClusterRep.AsUpdateable().SetColumns(u => u.Status == ClusterStatus.Waiting).Where(u => u.ClusterId == context.ClusterId).ExecuteCommandAsync(); + } + else + { + await _sysJobClusterRep.AsInsertable(new SysJobCluster { ClusterId = context.ClusterId, Status = ClusterStatus.Waiting }).ExecuteCommandAsync(); + } + } + + /// + /// 等待被唤醒 + /// + /// 作业集群服务上下文 + /// + public async Task WaitingForAsync(JobClusterContext context) + { + var clusterId = context.ClusterId; + + while (true) + { + // 控制集群心跳频率(放在头部为了防止 IsAnyAsync continue 没sleep占用大量IO和CPU) + await Task.Delay(3000 + _random.Next(500, 1000)); // 错开集群同时启动 + + try + { + ICache cache = App.GetRequiredService().Cache; + // 使用分布式锁 + using (cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000)) + { + // 在这里查询数据库,根据以下两种情况处理 + // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环 + // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return; + // 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return; + if (await _sysJobClusterRep.IsAnyAsync(u => u.Status == ClusterStatus.Working)) continue; + + await WorkNowAsync(clusterId); + return; + } + } + catch { } + } + } + + /// + /// 当前作业调度器停止通知 + /// + /// 作业集群服务上下文 + public async void Stop(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId); + } + + /// + /// 当前作业调度器宕机 + /// + /// 作业集群服务上下文 + public async void Crash(JobClusterContext context) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId); + } + + /// + /// 指示集群可以工作 + /// + /// 集群 Id + /// + private static async Task WorkNowAsync(string clusterId) + { + // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working + await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobMonitor.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobMonitor.cs new file mode 100644 index 0000000..3585ba8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/JobMonitor.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 作业执行监视器 +/// +public class JobMonitor : IJobMonitor +{ + private readonly SysConfigService _sysConfigService; + private readonly IEventPublisher _eventPublisher; + private readonly ILogger _logger; + + public JobMonitor(IServiceScopeFactory serviceScopeFactory, IEventPublisher eventPublisher, ILogger logger) + { + var serviceScope = serviceScopeFactory.CreateScope(); + _sysConfigService = serviceScope.ServiceProvider.GetRequiredService(); + _eventPublisher = eventPublisher; + _logger = logger; + } + + public Task OnExecutingAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + return Task.CompletedTask; + } + + public async Task OnExecutedAsync(JobExecutedContext context, CancellationToken stoppingToken) + { + if (context.Exception == null) return; + + var exception = $"定时任务【{context.Trigger.Description}】错误:{context.Exception}"; + // 将作业异常信息记录到本地 + _logger.LogError(exception); + + if (await _sysConfigService.GetConfigValue(ConfigConst.SysErrorMail)) + { + // 将作业异常信息发送到邮件 + await _eventPublisher.PublishAsync(CommonConst.SendErrorMail, exception, stoppingToken); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs new file mode 100644 index 0000000..aad0bb7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs @@ -0,0 +1,398 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统作业任务服务 🧩 +/// +[ApiDescriptionSettings(Order = 320, Description = "作业任务")] +public class SysJobService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysJobDetailRep; + private readonly SqlSugarRepository _sysJobTriggerRep; + private readonly SqlSugarRepository _sysJobTriggerRecordRep; + private readonly SqlSugarRepository _sysJobClusterRep; + private readonly ISchedulerFactory _schedulerFactory; + private readonly DynamicJobCompiler _dynamicJobCompiler; + + public SysJobService(SqlSugarRepository sysJobDetailRep, + SqlSugarRepository sysJobTriggerRep, + SqlSugarRepository sysJobTriggerRecordRep, + SqlSugarRepository sysJobClusterRep, + ISchedulerFactory schedulerFactory, + DynamicJobCompiler dynamicJobCompiler) + { + _sysJobDetailRep = sysJobDetailRep; + _sysJobTriggerRep = sysJobTriggerRep; + _sysJobTriggerRecordRep = sysJobTriggerRecordRep; + _sysJobClusterRep = sysJobClusterRep; + _schedulerFactory = schedulerFactory; + _dynamicJobCompiler = dynamicJobCompiler; + } + + /// + /// 获取作业分页列表 ⏰ + /// + [DisplayName("获取作业分页列表")] + public async Task> PageJobDetail(PageJobDetailInput input) + { + var jobDetails = await _sysJobDetailRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), u => u.GroupName.Contains(input.GroupName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Description), u => u.Description.Contains(input.Description.Trim())) + .Select(d => new JobDetailOutput + { + JobDetail = d, + }).ToPagedListAsync(input.Page, input.PageSize); + await _sysJobDetailRep.AsSugarClient().ThenMapperAsync(jobDetails.Items, async u => + { + u.JobTriggers = await _sysJobTriggerRep.GetListAsync(t => t.JobId == u.JobDetail.JobId); + }); + + // 提取中括号里面的参数值 + var rgx = new Regex(@"(?i)(?<=\[)(.*)(?=\])"); + foreach (var job in jobDetails.Items) + { + foreach (var jobTrigger in job.JobTriggers) + { + jobTrigger.Args = rgx.Match(jobTrigger.Args ?? "").Value; + } + } + return jobDetails; + } + + /// + /// 获取作业组名称集合 ⏰ + /// + [DisplayName("获取作业组名称集合")] + public async Task> ListJobGroup() + { + return await _sysJobDetailRep.AsQueryable().Distinct().Select(e => e.GroupName).ToListAsync(); + } + + /// + /// 添加作业 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "AddJobDetail"), HttpPost] + [DisplayName("添加作业")] + public async Task AddJobDetail(AddJobDetailInput input) + { + var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006); + + // 动态创建作业 + Type jobType; + switch (input.CreateType) + { + case JobCreateTypeEnum.Script when string.IsNullOrEmpty(input.ScriptCode): + throw Oops.Oh(ErrorCodeEnum.D1701); + case JobCreateTypeEnum.Script: + { + jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode); + + if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute) + throw Oops.Oh(ErrorCodeEnum.D1702); + if (jobDetailAttribute.JobId != input.JobId) + throw Oops.Oh(ErrorCodeEnum.D1703); + break; + } + case JobCreateTypeEnum.Http: + jobType = typeof(HttpJob); + break; + + default: + throw new NotSupportedException(); + } + + _schedulerFactory.AddJob(JobBuilder.Create(jobType).LoadFrom(input.Adapt()).SetJobType(jobType)); + + // 延迟一下等待持久化写入,再执行其他字段的更新 + await Task.Delay(500); + await _sysJobDetailRep.AsUpdateable() + .SetColumns(u => new SysJobDetail { CreateType = input.CreateType, ScriptCode = input.ScriptCode }) + .Where(u => u.JobId == input.JobId).ExecuteCommandAsync(); + } + + /// + /// 更新作业 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "UpdateJobDetail"), HttpPost] + [DisplayName("更新作业")] + public async Task UpdateJobDetail(UpdateJobDetailInput input) + { + var isExist = await _sysJobDetailRep.IsAnyAsync(u => u.JobId == input.JobId && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006); + + var sysJobDetail = await _sysJobDetailRep.GetFirstAsync(u => u.Id == input.Id); + if (sysJobDetail.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1704); + + var scheduler = _schedulerFactory.GetJob(sysJobDetail.JobId); + var oldScriptCode = sysJobDetail.ScriptCode; // 旧脚本代码 + input.Adapt(sysJobDetail); + + if (input.CreateType == JobCreateTypeEnum.Script) + { + if (string.IsNullOrEmpty(input.ScriptCode)) throw Oops.Oh(ErrorCodeEnum.D1701); + + if (input.ScriptCode != oldScriptCode) + { + // 动态创建作业 + var jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode); + + if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute) + throw Oops.Oh(ErrorCodeEnum.D1702); + if (jobDetailAttribute.JobId != input.JobId) throw Oops.Oh(ErrorCodeEnum.D1703); + + scheduler?.UpdateDetail(JobBuilder.Create(jobType).LoadFrom(sysJobDetail).SetJobType(jobType)); + } + } + else + { + scheduler?.UpdateDetail(scheduler.GetJobBuilder().LoadFrom(sysJobDetail)); + } + + // Tip: 假如这次更新有变更了 JobId,变更 JobId 后触发的持久化更新执行,会由于找不到 JobId 而更新不到数据 + // 延迟一下等待持久化写入,再执行其他字段的更新 + await Task.Delay(500); + await _sysJobDetailRep.UpdateAsync(sysJobDetail); + } + + /// + /// 删除作业 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "DeleteJobDetail"), HttpPost] + [DisplayName("删除作业")] + public async Task DeleteJobDetail(DeleteJobDetailInput input) + { + _schedulerFactory.RemoveJob(input.JobId); + + // 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下面的代码确保作业和触发器能被删除 + await _sysJobDetailRep.DeleteAsync(u => u.JobId == input.JobId); + await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId); + } + + /// + /// 获取触发器列表 ⏰ + /// + [DisplayName("获取触发器列表")] + public async Task> GetJobTriggerList([FromQuery] JobDetailInput input) + { + return await _sysJobTriggerRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId.Contains(input.JobId)) + .ToListAsync(); + } + + /// + /// 添加触发器 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "AddJobTrigger"), HttpPost] + [DisplayName("添加触发器")] + public async Task AddJobTrigger(AddJobTriggerInput input) + { + var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006); + + var jobTrigger = input.Adapt(); + jobTrigger.Args = "[" + jobTrigger.Args + "]"; + + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.AddTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger)); + } + + /// + /// 更新触发器 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "UpdateJobTrigger"), HttpPost] + [DisplayName("更新触发器")] + public async Task UpdateJobTrigger(UpdateJobTriggerInput input) + { + var isExist = await _sysJobTriggerRep.IsAnyAsync(u => u.TriggerId == input.TriggerId && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1006); + + var jobTrigger = input.Adapt(); + if (jobTrigger.EndTime.HasValue && jobTrigger.EndTime.Value.Year < 1901) + { + jobTrigger.EndTime = null; + } + if (jobTrigger.StartTime.HasValue && jobTrigger.StartTime.Value.Year < 1901) + { + jobTrigger.StartTime = null; + } + jobTrigger.Args = "[" + jobTrigger.Args + "]"; + + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.UpdateTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger)); + } + + /// + /// 删除触发器 ⏰ + /// + /// + [ApiDescriptionSettings(Name = "DeleteJobTrigger"), HttpPost] + [DisplayName("删除触发器")] + public async Task DeleteJobTrigger(DeleteJobTriggerInput input) + { + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.RemoveTrigger(input.TriggerId); + + // 如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下行代码确保触发器能被删除 + await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId); + } + + /// + /// 暂停所有作业 ⏰ + /// + /// + [DisplayName("暂停所有作业")] + public void PauseAllJob() + { + _schedulerFactory.PauseAll(); + } + + /// + /// 启动所有作业 ⏰ + /// + /// + [DisplayName("启动所有作业")] + public void StartAllJob() + { + _schedulerFactory.StartAll(); + } + + /// + /// 暂停作业 ⏰ + /// + [DisplayName("暂停作业")] + public void PauseJob(JobDetailInput input) + { + _schedulerFactory.TryPauseJob(input.JobId, out _); + } + + /// + /// 启动作业 ⏰ + /// + [DisplayName("启动作业")] + public void StartJob(JobDetailInput input) + { + _schedulerFactory.TryStartJob(input.JobId, out _); + } + + /// + /// 取消作业 ⏰ + /// + [DisplayName("取消作业")] + public void CancelJob(JobDetailInput input) + { + _schedulerFactory.TryCancelJob(input.JobId, out _); + } + + /// + /// 执行作业 ⏰ + /// + /// + [DisplayName("执行作业")] + public void RunJob(JobDetailInput input) + { + if (_schedulerFactory.TryRunJob(input.JobId, out _) != ScheduleResult.Succeed) throw Oops.Oh(ErrorCodeEnum.D1705); + } + + /// + /// 暂停触发器 ⏰ + /// + [DisplayName("暂停触发器")] + public void PauseTrigger(JobTriggerInput input) + { + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.PauseTrigger(input.TriggerId); + } + + /// + /// 启动触发器 ⏰ + /// + [DisplayName("启动触发器")] + public void StartTrigger(JobTriggerInput input) + { + var scheduler = _schedulerFactory.GetJob(input.JobId); + scheduler?.StartTrigger(input.TriggerId); + } + + /// + /// 强制唤醒作业调度器 ⏰ + /// + [DisplayName("强制唤醒作业调度器")] + public void CancelSleep() + { + _schedulerFactory.CancelSleep(); + } + + /// + /// 强制触发所有作业持久化 ⏰ + /// + [DisplayName("强制触发所有作业持久化")] + public void PersistAll() + { + _schedulerFactory.PersistAll(); + } + + /// + /// 获取集群列表 ⏰ + /// + [DisplayName("获取集群列表")] + public async Task> GetJobClusterList() + { + return await _sysJobClusterRep.GetListAsync(); + } + + /// + /// 获取作业触发器运行记录分页列表 ⏰ + /// + [DisplayName("获取作业触发器运行记录分页列表")] + public async Task> PageJobTriggerRecord(PageJobTriggerRecordInput input) + { + return await _sysJobTriggerRecordRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.JobId), u => u.JobId == input.JobId) + .WhereIF(!string.IsNullOrWhiteSpace(input.TriggerId), u => u.TriggerId == input.TriggerId) + .OrderByDescending(u => u.Id) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 清空作业触发器运行记录 🔖 + /// + /// + [ApiDescriptionSettings(Name = "ClearJobTriggerRecord"), HttpPost] + [DisplayName("清空作业触发器运行记录")] + public void ClearJobTriggerRecord() + { + _sysJobTriggerRecordRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 清空不保留的作业触发器运行记录 🔖 + /// + /// + [NonAction] + [DisplayName("清空过期的作业触发器运行记录")] + public async Task ClearExpireJobTriggerRecord(SysJobTriggerRecord input) + { + int keepRecords = 30;//保留记录条数 + // 使用CopyNew()创建新的数据库连接实例,避免连接冲突 + var db = _sysJobTriggerRecordRep.AsSugarClient().CopyNew(); + await db.Deleteable().In(it => it.Id, + db.Queryable() + .Skip(keepRecords) + .OrderByDescending(it => it.LastRunTime) + .Where(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId) + .Select(it => it.Id) //注意Select不要ToList(), ToList就2次查询了 + ).ExecuteCommandAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs new file mode 100644 index 0000000..07f20e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangDto.cs @@ -0,0 +1,108 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言输出参数 +/// +public class SysLangDto +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + public WeekEnum WeekStart { get; set; } + + /// + /// 分组符号 + /// + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + public bool Active { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs new file mode 100644 index 0000000..19b1e58 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangInput.cs @@ -0,0 +1,413 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 语言基础输入参数 +/// +public class SysLangBaseInput +{ + /// + /// 主键Id + /// + public virtual long? Id { get; set; } + + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + public virtual string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + public virtual string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + public virtual string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + public virtual string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public virtual DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + public virtual string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + public virtual string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public virtual WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + public virtual string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + public virtual string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public virtual string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public virtual bool? Active { get; set; } +} + +/// +/// 多语言分页查询输入参数 +/// +public class PageSysLangInput : BasePageInput +{ + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 是否启用 + /// + public bool? Active { get; set; } + + /// + /// 选中主键列表 + /// + public List SelectKeyList { get; set; } +} + +/// +/// 多语言增加输入参数 +/// +public class AddSysLangInput +{ + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + [MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + [MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + [MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + [MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + [MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public bool? Active { get; set; } +} + +/// +/// 多语言删除输入参数 +/// +public class DeleteSysLangInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } +} + +/// +/// 多语言更新输入参数 +/// +public class UpdateSysLangInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } + + /// + /// 语言名称 + /// + [Required(ErrorMessage = "语言名称不能为空")] + [MaxLength(255, ErrorMessage = "语言名称字符长度不能超过255")] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [Required(ErrorMessage = "ISO 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "ISO 语言代码字符长度不能超过255")] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [Required(ErrorMessage = "URL 语言代码不能为空")] + [MaxLength(255, ErrorMessage = "URL 语言代码字符长度不能超过255")] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [Required(ErrorMessage = "书写方向不能为空")] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [Required(ErrorMessage = "日期格式不能为空")] + [MaxLength(255, ErrorMessage = "日期格式字符长度不能超过255")] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [Required(ErrorMessage = "时间格式不能为空")] + [MaxLength(255, ErrorMessage = "时间格式字符长度不能超过255")] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [Required(ErrorMessage = "每周起始日不能为空")] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [Required(ErrorMessage = "分组符号不能为空")] + [MaxLength(255, ErrorMessage = "分组符号字符长度不能超过255")] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [Required(ErrorMessage = "小数点符号不能为空")] + [MaxLength(255, ErrorMessage = "小数点符号字符长度不能超过255")] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [MaxLength(255, ErrorMessage = "千分位分隔符字符长度不能超过255")] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [Required(ErrorMessage = "是否启用不能为空")] + public bool? Active { get; set; } +} + +/// +/// 多语言主键查询输入参数 +/// +public class QueryByIdSysLangInput : DeleteSysLangInput +{ +} + +/// +/// 多语言数据导入实体 +/// +[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +public class ImportSysLangInput : BaseImportInput +{ + /// + /// 语言名称 + /// + [ImporterHeader(Name = "*语言名称")] + [ExporterHeader("*语言名称", Format = "", Width = 25, IsBold = true)] + public string Name { get; set; } + + /// + /// 语言代码 + /// + [ImporterHeader(Name = "*语言代码")] + [ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)] + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + [ImporterHeader(Name = "*ISO 语言代码")] + [ExporterHeader("*ISO 语言代码", Format = "", Width = 25, IsBold = true)] + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + [ImporterHeader(Name = "*URL 语言代码")] + [ExporterHeader("*URL 语言代码", Format = "", Width = 25, IsBold = true)] + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + [ImporterHeader(Name = "*书写方向")] + [ExporterHeader("*书写方向", Format = "", Width = 25, IsBold = true)] + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + [ImporterHeader(Name = "*日期格式")] + [ExporterHeader("*日期格式", Format = "", Width = 25, IsBold = true)] + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + [ImporterHeader(Name = "*时间格式")] + [ExporterHeader("*时间格式", Format = "", Width = 25, IsBold = true)] + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + [ImporterHeader(Name = "*每周起始日")] + [ExporterHeader("*每周起始日", Format = "", Width = 25, IsBold = true)] + public WeekEnum? WeekStart { get; set; } + + /// + /// 分组符号 + /// + [ImporterHeader(Name = "*分组符号")] + [ExporterHeader("*分组符号", Format = "", Width = 25, IsBold = true)] + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + [ImporterHeader(Name = "*小数点符号")] + [ExporterHeader("*小数点符号", Format = "", Width = 25, IsBold = true)] + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + [ImporterHeader(Name = "千分位分隔符")] + [ExporterHeader("千分位分隔符", Format = "", Width = 25, IsBold = true)] + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + [ImporterHeader(Name = "*是否启用")] + [ExporterHeader("*是否启用", Format = "", Width = 25, IsBold = true)] + public bool? Active { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs new file mode 100644 index 0000000..a4cf56b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/Dto/SysLangOutput.cs @@ -0,0 +1,117 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +namespace Admin.NET.Core; + +/// +/// 语言输出参数 +/// +public class SysLangOutput +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 语言名称 + /// + public string Name { get; set; } + + /// + /// 语言代码 + /// + public string Code { get; set; } + + /// + /// ISO 语言代码 + /// + public string IsoCode { get; set; } + + /// + /// URL 语言代码 + /// + public string UrlCode { get; set; } + + /// + /// 书写方向 + /// + public DirectionEnum Direction { get; set; } + + /// + /// 日期格式 + /// + public string DateFormat { get; set; } + + /// + /// 时间格式 + /// + public string TimeFormat { get; set; } + + /// + /// 每周起始日 + /// + public WeekEnum WeekStart { get; set; } + + /// + /// 分组符号 + /// + public string Grouping { get; set; } + + /// + /// 小数点符号 + /// + public string DecimalPoint { get; set; } + + /// + /// 千分位分隔符 + /// + public string? ThousandsSep { get; set; } + + /// + /// 是否启用 + /// + public bool Active { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} + +/// +/// 多语言数据导入模板实体 +/// +public class ExportSysLangOutput : ImportSysLangInput +{ + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public override string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/SysLangService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/SysLangService.cs new file mode 100644 index 0000000..c3c5c09 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Lang/SysLangService.cs @@ -0,0 +1,113 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 语言服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "语言服务")] +public partial class SysLangService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLangRep; + + public SysLangService(SqlSugarRepository sysLangRep) + { + _sysLangRep = sysLangRep; + } + + /// + /// 分页查询语言 🔖 + /// + /// + /// + [DisplayName("分页查询语言")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> Page(PageSysLangInput input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _sysLangRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword) || u.Code.Contains(input.Keyword) || u.IsoCode.Contains(input.Keyword) || u.UrlCode.Contains(input.Keyword)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.IsoCode), u => u.IsoCode.Contains(input.IsoCode.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.UrlCode), u => u.UrlCode.Contains(input.UrlCode.Trim())) + .Select(); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取语言详情 ℹ️ + /// + /// + /// + [DisplayName("获取语言详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task Detail([FromQuery] QueryByIdSysLangInput input) + { + return await _sysLangRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 增加语言 ➕ + /// + /// + /// + [DisplayName("增加语言")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddSysLangInput input) + { + var entity = input.Adapt(); + return await _sysLangRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新语言 ✏️ + /// + /// + /// + [DisplayName("更新语言")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateSysLangInput input) + { + var entity = input.Adapt(); + await _sysLangRep.AsUpdateable(entity) + .ExecuteCommandAsync(); + } + + /// + /// 删除语言 ❌ + /// + /// + /// + [DisplayName("删除语言")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(DeleteSysLangInput input) + { + var entity = await _sysLangRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + await _sysLangRep.DeleteAsync(entity); //真删除 + } + + /// + /// 获取下拉列表数据 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取下拉列表数据")] + [ApiDescriptionSettings(Name = "DropdownData"), HttpPost] + public async Task DropdownData() + { + var data = await _sysLangRep.Context.Queryable() + .Where(m => m.Active == true) + .Select(u => new + { + Code = u.Code, + Value = u.UrlCode, + Label = $"{u.Name}" + }).ToListAsync(); + return data; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs new file mode 100644 index 0000000..58c4f6f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 翻译表输出参数 +/// +public class SysLangTextDto +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs new file mode 100644 index 0000000..899ad52 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs @@ -0,0 +1,274 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 翻译表基础输入参数 +/// +public class SysLangTextBaseInput +{ + /// + /// 主键Id + /// + public virtual long? Id { get; set; } + + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + public virtual string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public virtual long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + public virtual string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + public virtual string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public virtual string Content { get; set; } +} + +/// +/// 翻译表分页查询输入参数 +/// +public class PageSysLangTextInput : BasePageInput +{ + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 选中主键列表 + /// + public List SelectKeyList { get; set; } +} + +/// +/// 翻译表增加输入参数 +/// +public class AddSysLangTextInput +{ + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public string Content { get; set; } +} + +/// +/// 翻译表输入参数 +/// +public class ListSysLangTextInput +{ + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } +} + +/// +/// 翻译表删除输入参数 +/// +public class DeleteSysLangTextInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } +} + +/// +/// 翻译表更新输入参数 +/// +public class UpdateSysLangTextInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long? Id { get; set; } + + /// + /// 所属实体名 + /// + [Required(ErrorMessage = "所属实体名不能为空")] + [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [Required(ErrorMessage = "所属实体ID不能为空")] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [Required(ErrorMessage = "字段名不能为空")] + [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [Required(ErrorMessage = "语言代码不能为空")] + [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [Required(ErrorMessage = "翻译内容不能为空")] + public string Content { get; set; } +} + +/// +/// 翻译表主键查询输入参数 +/// +public class QueryByIdSysLangTextInput : DeleteSysLangTextInput +{ +} + +/// +/// 翻译表数据导入实体 +/// +[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +public class ImportSysLangTextInput : BaseImportInput +{ + /// + /// 所属实体名 + /// + [ImporterHeader(Name = "*所属实体名")] + [ExporterHeader("*所属实体名", Format = "", Width = 25, IsBold = true)] + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + [ImporterHeader(Name = "*所属实体ID")] + [ExporterHeader("*所属实体ID", Format = "", Width = 25, IsBold = true)] + public long? EntityId { get; set; } + + /// + /// 字段名 + /// + [ImporterHeader(Name = "*字段名")] + [ExporterHeader("*字段名", Format = "", Width = 25, IsBold = true)] + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + [ImporterHeader(Name = "*语言代码")] + [ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)] + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + [ImporterHeader(Name = "*翻译内容")] + [ExporterHeader("*翻译内容", Format = "", Width = 25, IsBold = true)] + public string Content { get; set; } +} + +/// +/// +/// +public class AiTranslateTextInput +{ + /// + /// 原文 + /// + public string OriginalText { get; set; } + + /// + /// 目标语言 + /// + public string TargetLang { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs new file mode 100644 index 0000000..0a43668 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +namespace Admin.NET.Core; + +/// +/// 翻译表输出参数 +/// +public class SysLangTextOutput +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 所属实体名 + /// + public string EntityName { get; set; } + + /// + /// 所属实体ID + /// + public long EntityId { get; set; } + + /// + /// 字段名 + /// + public string FieldName { get; set; } + + /// + /// 语言代码 + /// + public string LangCode { get; set; } + + /// + /// 翻译内容 + /// + public string Content { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } +} + +/// +/// 翻译表数据导入模板实体 +/// +public class ExportSysLangTextOutput : ImportSysLangTextInput +{ + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public override string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs new file mode 100644 index 0000000..d6c39a6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextCacheService.cs @@ -0,0 +1,343 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class LangFieldMap +{ + /// 实体名,如 Product + public string EntityName { get; set; } + + /// 字段名,如 Name/Description + public string FieldName { get; set; } + + /// 如何取主键ID + public Func IdSelector { get; set; } + + /// 如何写回翻译值 + public Action SetTranslatedValue { get; set; } +} + +/// +/// 翻译缓存服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "翻译缓存服务")] +public class SysLangTextCacheService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysLangTextRep; + private TimeSpan expireSeconds = TimeSpan.FromHours(1); + + public SysLangTextCacheService( + SysCacheService sysCacheService, + SqlSugarRepository sysLangTextRep) + { + _sysCacheService = sysCacheService; + _sysLangTextRep = sysLangTextRep; + } + + private string BuildKey(string entityName, string fieldName, long entityId, string langCode) + { + return $"LangCache_{entityName}_{fieldName}_{entityId}_{langCode}"; + } + + /// + /// 【单条翻译获取】 + /// 根据实体类型、字段、主键ID 和语言编码获取翻译内容。
+ /// 适用于:小表(如菜单、字典),可设置较长缓存时间。
+ ///
+ /// 【示例】
+ /// var content = await _sysLangTextCacheService.GetTranslation("Product", "Name", 123, "en-US"); + ///
+ /// 实体名称,如 "Product" + /// 字段名称,如 "Name" + /// 实体主键ID + /// 语言编码,如 "zh-CN" + /// 翻译后的内容(若无则返回 null 或空) + [NonAction] + public async Task GetTranslation(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode); + var value = _sysCacheService.Get(key); + if (!string.IsNullOrEmpty(value)) return value; + + value = await _sysLangTextRep.AsQueryable() + .Where(u => u.EntityName == entityName && u.FieldName == fieldName && u.EntityId == entityId && u.LangCode == langCode) + .Select(u => u.Content) + .FirstAsync(); + + if (!string.IsNullOrEmpty(value)) + { + _sysCacheService.Set(key, value, expireSeconds); // 设置过期 + } + + return value; + } + + /// + /// 根据实体类型、字段、主键ID 和语言编码获取翻译实体 + /// + /// 实体名称 + /// 字段名称 + /// 实体主键ID + /// 语言编码 + /// + [NonAction] + public async Task GetTranslationEntity(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode) + "_entity"; + var value = _sysCacheService.Get(key); + if (!value.IsNullOrEmpty()) return value; + + value = await _sysLangTextRep.AsQueryable() + .Where(u => u.EntityName == entityName && u.FieldName == fieldName && u.EntityId == entityId && u.LangCode == langCode) + .FirstAsync(); + + if (!value.IsNullOrEmpty()) + { + _sysCacheService.Set(key, value, expireSeconds); // 设置过期 + } + + return value; + } + + /// + /// 【批量翻译获取】
+ /// 根据实体、字段和一批主键ID获取对应翻译内容,自动从缓存或数据库获取。
+ /// 适用于:SKU、多商品、批量字典等需要高效批量获取的场景。
+ /// + /// 【示例】
+ /// var dict = await _sysLangTextCacheService.GetTranslations("SKU", "Name", skuIds, "en_US"); + ///
+ /// 实体名称 + /// 字段名称 + /// 主键ID集合 + /// 语言编码 + /// 主键ID到翻译内容的字典 + [NonAction] + public async Task> GetTranslations(string entityName, string fieldName, List entityIds, string langCode) + { + var result = new Dictionary(); + var missingIds = new HashSet(); // 用 HashSet 提高后面 Contains 的性能 + + foreach (var id in entityIds.Distinct()) // 先去重,防止重复缓存 Key + { + var key = BuildKey(entityName, fieldName, id, langCode); + var value = _sysCacheService.Get(key); + if (!string.IsNullOrWhiteSpace(value)) + { + result[id] = value; + } + else + { + missingIds.Add(id); + } + } + + if (missingIds.Any()) + { + var list = await _sysLangTextRep.AsQueryable() + .Where(u => u.EntityName == entityName && + u.FieldName == fieldName && + missingIds.Contains(u.EntityId) && + u.LangCode == langCode) + .ToListAsync(); + + foreach (var item in list) + { + if (string.IsNullOrWhiteSpace(item.Content)) continue; // 跳过脏数据 + + var key = BuildKey(item.EntityName, item.FieldName, item.EntityId, item.LangCode); + _sysCacheService.Set(key, item.Content, expireSeconds); + + // 用 TryAdd 防止异常 + result[item.EntityId] = item.Content; + } + } + + return result; + } + + /// + /// 【列表翻译】
+ /// 按配置把同一字段的翻译写回到实体列表中。内部会调用批量翻译接口。
+ ///
+ /// 【示例】
+ /// await _sysLangTextCacheService.TranslateList(products, "Product", "Name", p => p.Id, (p, val) => p.Name = val, "zh-CN"); + ///
+ /// 实体类型 + /// 待翻译的实体列表 + /// 实体名称 + /// 字段名称 + /// 用于取出主键ID的表达式 + /// 写回翻译值的委托 + /// 语言编码 + /// 翻译后的实体列表(引用传递) + [NonAction] + public async Task> TranslateList(List list, string entityName, string fieldName, Func idSelector, Action setTranslatedValue, string langCode) + { + var ids = list.Select(idSelector).Distinct().ToList(); + var dict = await GetTranslations(entityName, fieldName, ids, langCode); + + foreach (var item in list) + { + var id = idSelector(item); + if (dict.TryGetValue(id, out var value)) + { + setTranslatedValue(item, value); + } + } + + return list; + } + + /// + /// 【多字段批量翻译】 + /// 对列表中的实体对象,按配置的字段映射进行多字段翻译处理。
+ /// 常用于:菜单多语言、商品多语言、SKU多语言等需要多字段翻译的场景。

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

+ /// 【使用示例】:
+ /// + /// var fields = new List<LangFieldMap<Product>> + /// { + /// new LangFieldMap<Product> { + /// EntityName = "Product", + /// FieldName = "Name", + /// IdSelector = p => p.Id, + /// SetTranslatedValue = (p, val) => p.Name = val + /// }, + /// new LangFieldMap<Product> { + /// EntityName = "Product", + /// FieldName = "Description", + /// IdSelector = p => p.Id, + /// SetTranslatedValue = (p, val) => p.Description = val + /// } + /// }; + /// await _sysLangTextCacheService.TranslateMultiFields(products, fields, "zh-CN"); + /// + ///
+ /// 要翻译的实体类型,如 Product/Menu/SKU 等 + /// 需要翻译的实体对象列表 + /// 需要翻译的字段映射集合,支持多个字段 + /// 语言编码,如 "zh-CN"、"en-US"、"it-IT" 等 + /// 翻译后的实体列表(引用传递,原对象已直接赋值) + [NonAction] + public async Task> TranslateMultiFields( + List list, + List> fields, + string langCode) + { + var keyToField = new Dictionary FieldMap)>(); + var missingKeys = new List(); + + // 先尝试从缓存读取 + foreach (var item in list) + { + foreach (var field in fields) + { + var id = field.IdSelector(item); + var key = BuildKey(field.EntityName, field.FieldName, id, langCode); + var cached = _sysCacheService.Get(key); + if (!string.IsNullOrEmpty(cached)) + { + // 命中缓存,直接赋值 + field.SetTranslatedValue(item, cached); + } + else + { + // 缓存未命中,加入待查表 + keyToField[key] = (item, field); + missingKeys.Add(key); + } + } + } + + if (missingKeys.Any()) + { + // 把缺失的 keys 拆解成组合实体 + var missingTuples = missingKeys + .Select(key => + { + var parts = key.Split('_'); + return new + { + EntityName = parts[1], + FieldName = parts[2], + EntityId = long.Parse(parts[3]) + }; + }) + .ToList(); + + // 按 EntityName + FieldName 分组 + var grouped = missingTuples + .GroupBy(x => new { x.EntityName, x.FieldName }) + .ToList(); + + var result = new List(); + + // 分批查询,每组单独查询 + const int chunkSize = 500; + foreach (var g in grouped) + { + var allIds = g.Select(x => x.EntityId).Distinct().ToList(); + for (int i = 0; i < allIds.Count; i += chunkSize) + { + var chunk = allIds.Skip(i).Take(chunkSize).ToList(); + var temp = await _sysLangTextRep.AsQueryable() + .Where(u => u.LangCode == langCode + && u.EntityName == g.Key.EntityName + && u.FieldName == g.Key.FieldName + && chunk.Contains(u.EntityId)) + .ToListAsync(); + result.AddRange(temp); + } + } + + // 遍历查询结果,写回实体和缓存 + foreach (var item in result) + { + var key = BuildKey(item.EntityName, item.FieldName, item.EntityId, item.LangCode); + if (keyToField.TryGetValue(key, out var tuple)) + { + tuple.FieldMap.SetTranslatedValue(tuple.Entity, item.Content); + _sysCacheService.Set(key, item.Content, expireSeconds); + } + } + } + + return list; + } + + /// + /// 删除缓存 + /// + /// + /// + /// + /// + public void DeleteCache(string entityName, string fieldName, long entityId, string langCode) + { + var key = BuildKey(entityName, fieldName, entityId, langCode); + _sysCacheService.Remove(key); + } + + /// + /// 更新缓存 + /// + /// + /// + /// + /// + /// + public void UpdateCache(string entityName, string fieldName, long entityId, string langCode, string newValue) + { + var key = BuildKey(entityName, fieldName, entityId, langCode); + _sysCacheService.Set(key, newValue, expireSeconds); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextService.cs new file mode 100644 index 0000000..07910a1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextService.cs @@ -0,0 +1,436 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 翻译服务 🧩 +/// +[ApiDescriptionSettings(Order = 100, Description = "翻译服务")] +public partial class SysLangTextService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLangTextRep; + private readonly ISqlSugarClient _sqlSugarClient; + private readonly SysLangTextCacheService _sysLangTextCacheService; + + public SysLangTextService( + SqlSugarRepository sysLangTextRep, + SysLangTextCacheService sysLangTextCacheService, + ISqlSugarClient sqlSugarClient) + { + _sysLangTextRep = sysLangTextRep; + _sqlSugarClient = sqlSugarClient; + _sysLangTextCacheService = sysLangTextCacheService; + } + + /// + /// 分页查询翻译表 🔖 + /// + /// + /// + [DisplayName("分页查询翻译表")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> Page(PageSysLangTextInput input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _sysLangTextRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.EntityName.Contains(input.Keyword) || u.FieldName.Contains(input.Keyword) || u.LangCode.Contains(input.Keyword) || u.Content.Contains(input.Keyword)) + .WhereIF(!string.IsNullOrWhiteSpace(input.EntityName), u => u.EntityName.Contains(input.EntityName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.FieldName), u => u.FieldName.Contains(input.FieldName.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode.Contains(input.LangCode.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Content), u => u.Content.Contains(input.Content.Trim())) + .WhereIF(input.EntityId != null, u => u.EntityId == input.EntityId) + .Select(); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + [DisplayName("获取翻译表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public async Task> List(ListSysLangTextInput input) + { + var query = _sysLangTextRep.AsQueryable() + .Where(u => u.EntityName == input.EntityName.Trim() && u.FieldName == input.FieldName.Trim() && u.EntityId == input.EntityId) + .WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode == input.LangCode.Trim()) + .Select(); + return await query.ToListAsync(); + } + + /// + /// 获取翻译表详情 ℹ️ + /// + /// + /// + [DisplayName("获取翻译表详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task Detail([FromQuery] QueryByIdSysLangTextInput input) + { + return await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id); + } + + /// + /// 增加翻译表 ➕ + /// + /// + /// + [DisplayName("增加翻译表")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddSysLangTextInput input) + { + var entity = input.Adapt(); + return await _sysLangTextRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新翻译表 ✏️ + /// + /// + /// + [DisplayName("更新翻译表")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateSysLangTextInput input) + { + var entity = input.Adapt(); + await _sysLangTextRep.AsUpdateable(entity) + .ExecuteCommandAsync(); + _sysLangTextCacheService.UpdateCache(entity.EntityName, entity.FieldName, entity.EntityId, entity.LangCode, entity.Content); + } + + /// + /// 删除翻译表 ❌ + /// + /// + /// + [DisplayName("删除翻译表")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(DeleteSysLangTextInput input) + { + var entity = await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + await _sysLangTextRep.DeleteAsync(entity); //真删除 + _sysLangTextCacheService.DeleteCache(entity.EntityName, entity.FieldName, entity.EntityId, entity.LangCode); + } + + /// + /// 批量删除翻译表 ❌ + /// + /// + /// + [DisplayName("批量删除翻译表")] + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")] List input) + { + var exp = Expressionable.Create(); + foreach (var row in input) exp = exp.Or(it => it.Id == row.Id); + var list = await _sysLangTextRep.AsQueryable().Where(exp.ToExpression()).ToListAsync(); + + await _sysLangTextRep.DeleteAsync(list); //真删除 + foreach (var item in list) + { + _sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode); + } + } + + private static readonly object _sysLangTextBatchSaveLock = new object(); + + /// + /// 批量保存翻译表 ✏️ + /// + /// + /// + [DisplayName("批量保存翻译表")] + [ApiDescriptionSettings(Name = "BatchSave"), HttpPost] + public void BatchSave([Required(ErrorMessage = "列表不能为空")] List input) + { + lock (_sysLangTextBatchSaveLock) + { + // 校验并过滤必填基本类型为null的字段 + var rows = input.Where(x => + { + if (!string.IsNullOrWhiteSpace(x.Error)) return false; + if (x.EntityId == null) + { + x.Error = "所属实体ID不能为空"; + return false; + } + return true; + }).Adapt>(); + + var storageable = _sysLangTextRep.Context.Storageable(rows) + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空") + .SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空") + .SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空") + .SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空") + .WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode }) + .SplitInsert(it => it.NotAny()) + .SplitUpdate(it => it.Any()) + .ToStorage(); + + storageable.AsInsertable.ExecuteCommand();// 不存在插入 + storageable.AsUpdateable.UpdateColumns(it => new + { + it.EntityName, + it.EntityId, + it.FieldName, + it.LangCode, + it.Content, + }).ExecuteCommand();// 存在更新 + foreach (var item in rows) + { + _sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode); + } + if (storageable.ErrorList.Any()) + { + throw Oops.Oh($"处理过程中出现以下错误:{string.Join(";", storageable.ErrorList.Distinct())}"); + } + } + } + + /// + /// 导出翻译表记录 🔖 + /// + /// + /// + [DisplayName("导出翻译表记录")] + [ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify] + public async Task Export(PageSysLangTextInput input) + { + var list = (await Page(input)).Items?.Adapt>() ?? new(); + if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.Id)).ToList(); + return ExcelHelper.ExportTemplate(list, "翻译表导出记录"); + } + + /// + /// 下载翻译表数据导入模板 ⬇️ + /// + /// + [DisplayName("下载翻译表数据导入模板")] + [ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify] + public IActionResult DownloadTemplate() + { + return ExcelHelper.ExportTemplate(new List(), "翻译表导入模板"); + } + + private static readonly object _sysLangTextImportLock = new object(); + + /// + /// 导入翻译表记录 💾 + /// + /// + [DisplayName("导入翻译表记录")] + [ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork] + public IActionResult ImportData([Required] IFormFile file) + { + lock (_sysLangTextImportLock) + { + var stream = ExcelHelper.ImportData(file, (list, markerErrorAction) => + { + _sqlSugarClient.Utilities.PageEach(list, 2048, pageItems => + { + // 校验并过滤必填基本类型为null的字段 + var rows = pageItems.Where(x => + { + if (!string.IsNullOrWhiteSpace(x.Error)) return false; + if (x.EntityId == null) + { + x.Error = "所属实体ID不能为空"; + return false; + } + return true; + }).Adapt>(); + + var storageable = _sysLangTextRep.Context.Storageable(rows) + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空") + .SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空") + .SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空") + .SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符") + .SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空") + .SplitError(it => it.Item.Content?.Length > 255, "翻译内容长度不能超过255个字符") + .WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode }) + .SplitInsert(it => it.NotAny()) + .SplitUpdate(it => it.Any()) + .ToStorage(); + + storageable.AsInsertable.ExecuteCommand();// 不存在插入 + storageable.AsUpdateable.UpdateColumns(it => new + { + it.EntityName, + it.EntityId, + it.FieldName, + it.LangCode, + it.Content, + }).ExecuteCommand();// 存在更新 + + foreach (var item in rows) + { + _sysLangTextCacheService.DeleteCache(item.EntityName, item.FieldName, item.EntityId, item.LangCode); + } + // 标记错误信息 + markerErrorAction.Invoke(storageable, pageItems, rows); + }); + }); + + return stream; + } + } + + /// + /// DEEPSEEK 翻译接口 + /// + /// + [DisplayName("DEEPSEEK 翻译接口")] + [ApiDescriptionSettings(Name = "AiTranslateText"), HttpPost] + public async Task AiTranslateText(AiTranslateTextInput input) + { + // 需要先把DeepSeek.example复制改名为DeepSeek.json文件,添加你的 API KEY + var deepSeekOptions = App.GetConfig("DeepSeekSettings", true); + if (deepSeekOptions == null) + { + throw new InvalidOperationException("DeepSeek.json文件 未定义"); + } + if (string.IsNullOrEmpty(deepSeekOptions.ApiKey)) + { + throw new InvalidOperationException("环境变量 DEEPSEEK_API_KEY 未定义"); + } + + using (HttpClient client = new HttpClient()) + { + // 构建请求头 + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {deepSeekOptions.ApiKey}"); + + // 构建系统提示词 + string systemPrompt = BuildSystemPrompt(deepSeekOptions.SourceLang, input.TargetLang); + + // 构建请求体 + var requestBody = new + { + model = "deepseek-chat", + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = input.OriginalText } + }, + temperature = 0.3, + max_tokens = 2000 + }; + + // 使用 Newtonsoft.Json 序列化 + var json = JsonConvert.SerializeObject(requestBody); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + // 发送请求 + HttpResponseMessage response = await client.PostAsync(deepSeekOptions.ApiUrl, content); + + // 处理响应 + string responseBody = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + // 使用 Newtonsoft.Json 反序列化错误响应 + var errorResponse = JsonConvert.DeserializeObject(responseBody); + string errorMsg = errorResponse?.error?.message ?? $"HTTP {response.StatusCode}: {response.ReasonPhrase}"; + throw new HttpRequestException($"翻译API返回错误:{errorMsg}"); + } + + // 解析有效响应 + var result = JsonConvert.DeserializeObject(responseBody); + + if (result?.choices == null || result.choices.Length == 0 || + result.choices[0]?.message?.content == null) + { + throw new InvalidOperationException("API返回无效的翻译结果"); + } + + return result.choices[0].message.content.Trim(); + } + } + + // JSON 响应模型 + private class TranslationResponse + { + public Choice[] choices { get; set; } + } + + private class Choice + { + public Message message { get; set; } + } + + private class Message + { + public string content { get; set; } + } + + private class ErrorResponse + { + public ErrorInfo error { get; set; } + } + + private class ErrorInfo + { + public string message { get; set; } + } + + /// + /// 生成提示词 + /// + /// + /// + /// + private static string BuildSystemPrompt(string sourceLang, string targetLang) + { + return $@"作为企业软件系统专业翻译,严格遵守以下铁律: + +■ 核心原则 +1. 严格逐符号翻译({sourceLang}→{targetLang}) +2. 禁止添加/删除/改写任何内容 +3. 保持批量翻译的编号格式 + +■ 符号保留规则 +! 所有符号必须原样保留: +• 编程符号:\${{ }} <% %> @ # & | +• UI占位符:{{0}} %s [ ] +• 货币单位:¥100.00 kg cm² +• 中文符号:【 】 《 》 : + +■ 中文符号位置规范 +# 三级处理机制: +1. 成对符号必须保持完整结构: + ✓ 正确:【Warning】Text + ✗ 禁止:Warning【 】Text + +2. 独立符号位置: + • 优先句尾 → Text】? + • 次选句首 → 】Text? + • 禁止句中 → Text】Text? + +3. 跨字符串符号处理: + • 前段含【时 → 保留在段尾(""Synchronize【"") + • 后段含】时 → 保留在段首(""】authorization data?"") + • 符号后接字母时添加空格:】 Authorization + +■ 语法规范 +• 外文 → 被动语态(""Item was created"") +• 中文 → 主动语态(""已创建项目"") +• 禁止推测上下文(只翻译当前字符串内容) + +■ 错误预防(绝对禁止) +✗ 将中文符号改为西式符号(】→]) +✗ 移动非中文符号位置 +✗ 添加原文不存在的内容 +✗ 合并/拆分原始字符串 + +■ 批量处理 +▸ 严格保持原始JSON结构 +▸ 语言键名精确匹配(zh-cn/en/it等)"; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs new file mode 100644 index 0000000..bd99c03 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/ExportLogDto.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 导出日志数据 +/// +[ExcelExporter(Name = "日志数据", TableStyle = OfficeOpenXml.Table.TableStyles.None, AutoFitAllColumn = true)] +public class ExportLogDto +{ + /// + /// 记录器类别名称 + /// + [ExporterHeader(DisplayName = "记录器类别名称", IsBold = true)] + public string LogName { get; set; } + + /// + /// 日志级别 + /// + [ExporterHeader(DisplayName = "日志级别", IsBold = true)] + public string LogLevel { get; set; } + + /// + /// 事件Id + /// + [ExporterHeader(DisplayName = "事件Id", IsBold = true)] + public string EventId { get; set; } + + /// + /// 日志消息 + /// + [ExporterHeader(DisplayName = "日志消息", IsBold = true)] + public string Message { get; set; } + + /// + /// 异常对象 + /// + [ExporterHeader(DisplayName = "异常对象", IsBold = true)] + public string Exception { get; set; } + + /// + /// 当前状态值 + /// + [ExporterHeader(DisplayName = "当前状态值", IsBold = true)] + public string State { get; set; } + + /// + /// 日志记录时间 + /// + [ExporterHeader(DisplayName = "日志记录时间", IsBold = true)] + public DateTime LogDateTime { get; set; } + + /// + /// 线程Id + /// + [ExporterHeader(DisplayName = "线程Id", IsBold = true)] + public int ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [ExporterHeader(DisplayName = "请求跟踪Id", IsBold = true)] + public string TraceId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogInput.cs new file mode 100644 index 0000000..490c97b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogInput.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageOpLogInput : PageVisLogInput +{ + /// + /// 模块名称 + /// + public string? ControllerName { get; set; } +} + +public class PageExLogInput : PageOpLogInput +{ +} + +public class PageVisLogInput : PageLogInput +{ + /// + /// 方法名称 + /// + public string? ActionName { get; set; } +} + +public class PageLogInput : BasePageTimeInput +{ + /// + /// 账号 + /// + public string? Account { get; set; } + + /// + /// 操作用时 + /// + public long? Elapsed { get; set; } + + /// + /// 状态 + /// + public string Status { get; set; } + + /// + /// IP地址 + /// + public string? RemoteIp { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class LogInput +{ + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs new file mode 100644 index 0000000..8e69aaf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/Dto/LogVisOutput.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class LogVisOutput +{ + /// + /// 登录地点 + /// + public string Location { get; set; } + + /// + /// 经度 + /// + public double? Longitude { get; set; } + + /// + /// 维度 + /// + public double? Latitude { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 日志时间 + /// + public DateTime? LogDateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs new file mode 100644 index 0000000..268480e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogDiffService.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统差异日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 330)] +public class SysLogDiffService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogDiffRep; + private readonly UserManager _userManager; + + public SysLogDiffService(UserManager userManager, SqlSugarRepository sysLogDiffRep) + { + _sysLogDiffRep = sysLogDiffRep; + _userManager = userManager; + } + + /// + /// 获取差异日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志分页列表")] + public async Task> Page(PageLogInput input) + { + return await _sysLogDiffRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取差异日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取差异日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogDiffRep.GetFirstAsync(u => u.Id == id); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogExService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogExService.cs new file mode 100644 index 0000000..4a93b3e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogExService.cs @@ -0,0 +1,89 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统异常日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 350)] +public class SysLogExService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogExRep; + private readonly UserManager _userManager; + + public SysLogExService(UserManager userManager, SqlSugarRepository sysLogExRep) + { + _sysLogExRep = sysLogExRep; + _userManager = userManager; + } + + /// + /// 获取异常日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取异常日志分页列表")] + public async Task> Page(PageExLogInput input) + { + return await _sysLogExRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account) + .WhereIF(!string.IsNullOrWhiteSpace(input.ControllerName), u => u.ControllerName == input.ControllerName) + .WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName) + .WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp) + .WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed) + .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200") + .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200") + //.OrderBy(u => u.CreateTime, OrderByType.Desc) + .IgnoreColumns(u => new { u.RequestParam, u.ReturnResult, u.Message }) + .OrderBuilder(input) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取异常日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取异常日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogExRep.GetFirstAsync(u => u.Id == id); + } + + /// + /// 清空异常日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空异常日志")] + public void Clear() + { + _sysLogExRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 导出异常日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Export"), NonUnify] + [DisplayName("导出异常日志")] + public async Task ExportLogEx(LogInput input) + { + var logExList = await _sysLogExRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()), + u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .Select().ToListAsync(); + + IExcelExporter excelExporter = new ExcelExporter(); + var res = await excelExporter.ExportAsByteArray(logExList); + return new FileStreamResult(new MemoryStream(res), "application/octet-stream") { FileDownloadName = DateTime.Now.ToString("yyyyMMddHHmm") + "异常日志.xlsx" }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogOpService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogOpService.cs new file mode 100644 index 0000000..4324aa5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogOpService.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统操作日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 360)] +public class SysLogOpService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogOpRep; + private readonly UserManager _userManager; + + public SysLogOpService(UserManager userManager, SqlSugarRepository sysLogOpRep) + { + _sysLogOpRep = sysLogOpRep; + _userManager = userManager; + } + + /// + /// 获取操作日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取操作日志分页列表")] + public async Task> Page(PageOpLogInput input) + { + return await _sysLogOpRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account) + .WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp) + .WhereIF(!string.IsNullOrWhiteSpace(input.ControllerName), u => u.ControllerName == input.ControllerName) + .WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName) + .WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed) + //.OrderBy(u => u.CreateTime, OrderByType.Desc) + .IgnoreColumns(u => new { u.RequestParam, u.ReturnResult, u.Message }) + .OrderBuilder(input) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取操作日志详情 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取操作日志详情")] + public async Task GetDetail(long id) + { + return await _sysLogOpRep.GetFirstAsync(u => u.Id == id); + } + + /// + /// 清空操作日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空操作日志")] + public void Clear() + { + _sysLogOpRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 导出操作日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Export"), NonUnify] + [DisplayName("导出操作日志")] + public async Task ExportLogOp(LogInput input) + { + var logOpList = await _sysLogOpRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()) && !string.IsNullOrWhiteSpace(input.EndTime.ToString()), + u => u.CreateTime >= input.StartTime && u.CreateTime <= input.EndTime) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .Select().ToListAsync(); + + IExcelExporter excelExporter = new ExcelExporter(); + var res = await excelExporter.ExportAsByteArray(logOpList); + return new FileStreamResult(new MemoryStream(res), "application/octet-stream") { FileDownloadName = DateTime.Now.ToString("yyyyMMddHHmm") + "操作日志.xlsx" }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogVisService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogVisService.cs new file mode 100644 index 0000000..c25128a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Log/SysLogVisService.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统访问日志服务 🧩 +/// +[ApiDescriptionSettings(Order = 340)] +public class SysLogVisService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysLogVisRep; + private readonly UserManager _userManager; + + public SysLogVisService(UserManager userManager, SqlSugarRepository sysLogVisRep) + { + _sysLogVisRep = sysLogVisRep; + _userManager = userManager; + } + + /// + /// 获取访问日志分页列表 🔖 + /// + /// + [SuppressMonitor] + [DisplayName("获取访问日志分页列表")] + public async Task> Page(PageVisLogInput input) + { + return await _sysLogVisRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.CreateTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.CreateTime <= input.EndTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account == input.Account) + .WhereIF(!string.IsNullOrWhiteSpace(input.ActionName), u => u.ActionName == input.ActionName) + .WhereIF(!string.IsNullOrWhiteSpace(input.RemoteIp), u => u.RemoteIp == input.RemoteIp) + .WhereIF(!string.IsNullOrWhiteSpace(input.Elapsed.ToString()), u => u.Elapsed >= input.Elapsed) + .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status == "200", u => u.Status == "200") + .WhereIF(!string.IsNullOrWhiteSpace(input.Status) && input.Status != "200", u => u.Status != "200") + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 清空访问日志 🔖 + /// + /// + [ApiDescriptionSettings(Name = "Clear"), HttpPost] + [DisplayName("清空访问日志")] + public void Clear() + { + _sysLogVisRep.AsSugarClient().DbMaintenance.TruncateTable(); + } + + /// + /// 获取访问日志列表 🔖 + /// + /// + [DisplayName("获取访问日志列表")] + public async Task> GetList() + { + return await _sysLogVisRep.AsQueryable() + .Where(u => u.Longitude > 0 && u.Longitude > 0) + .Select(u => new LogVisOutput + { + Location = u.Location, + Longitude = (double?)u.Longitude, + Latitude = (double?)u.Latitude, + RealName = u.RealName, + LogDateTime = u.LogDateTime + }).ToListAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs new file mode 100644 index 0000000..c7c266e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs @@ -0,0 +1,51 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class MenuInput +{ + /// + /// 标题 + /// + public string Title { get; set; } + + /// + /// 菜单类型(1目录 2菜单 3按钮) + /// + public MenuTypeEnum? Type { get; set; } + + /// + /// 租户Id + /// + public virtual long TenantId { get; set; } +} + +public class AddMenuInput : SysMenu +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "菜单名称不能为空")] + public override string Title { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class UpdateMenuInput : AddMenuInput +{ +} + +public class DeleteMenuInput : BaseIdInput +{ +} + +public class MenuStatusInput : BaseStatusInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs new file mode 100644 index 0000000..ab7ad45 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuOutput.cs @@ -0,0 +1,157 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统菜单返回结果 +/// +public class MenuOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 父Id + /// + public long Pid { get; set; } + + /// + /// 菜单类型(0目录 1菜单 2按钮) + /// + public MenuTypeEnum Type { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 路由地址 + /// + public string Path { get; set; } + + /// + /// 组件路径 + /// + public string Component { get; set; } + + /// + /// 权限标识 + /// + public string Permission { get; set; } + + /// + /// 重定向 + /// + public string Redirect { get; set; } + + /// + /// 排序 + /// + public int OrderNo { get; set; } + + /// + /// 状态 + /// + public StatusEnum Status { get; set; } + + /// + /// 备注 + /// + public string Remark { get; set; } + + /// + /// 创建时间 + /// + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + public virtual DateTime UpdateTime { get; set; } + + /// + /// 创建者姓名 + /// + public virtual string CreateUserName { get; set; } + + /// + /// 修改者姓名 + /// + public virtual string UpdateUserName { get; set; } + + /// + /// 菜单Meta + /// + public SysMenuMeta Meta { get; set; } + + /// + /// 菜单子项 + /// + public List Children { get; set; } +} + +/// +/// 菜单Meta配置 +/// +public class SysMenuMeta +{ + /// + /// 标题 + /// + public string Title { get; set; } + + /// + /// 图标 + /// + public string Icon { get; set; } + + /// + /// 是否内嵌 + /// + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + public string IsLink { get; set; } + + /// + /// 是否隐藏 + /// + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + public bool IsKeepAlive { get; set; } + + /// + /// 是否固定 + /// + public bool IsAffix { get; set; } +} + +/// +/// 配置菜单对象映射 +/// +public class SysMenuMapper : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.ForType() + .Map(t => t.Meta.Title, o => o.Title) + .Map(t => t.Meta.Icon, o => o.Icon) + .Map(t => t.Meta.IsIframe, o => o.IsIframe) + .Map(t => t.Meta.IsLink, o => o.OutLink) + .Map(t => t.Meta.IsHide, o => o.IsHide) + .Map(t => t.Meta.IsKeepAlive, o => o.IsKeepAlive) + .Map(t => t.Meta.IsAffix, o => o.IsAffix); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs new file mode 100644 index 0000000..e658916 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs @@ -0,0 +1,462 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统菜单服务 🧩 +/// +[ApiDescriptionSettings(Order = 450)] +public class SysMenuService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTenantMenuRep; + private readonly SqlSugarRepository _sysMenuRep; + private readonly SysRoleMenuService _sysRoleMenuService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysUserMenuService _sysUserMenuService; + private readonly SysCacheService _sysCacheService; + private readonly UserManager _userManager; + private readonly SysLangTextCacheService _sysLangTextCacheService; + private readonly SysLangTextService _sysLangTextService; + + public SysMenuService( + SqlSugarRepository sysTenantMenuRep, + SqlSugarRepository sysMenuRep, + SysRoleMenuService sysRoleMenuService, + SysUserRoleService sysUserRoleService, + SysUserMenuService sysUserMenuService, + SysCacheService sysCacheService, + UserManager userManager, + SysLangTextCacheService sysLangTextCacheService, + SysLangTextService sysLangTextService) + { + _userManager = userManager; + _sysMenuRep = sysMenuRep; + _sysRoleMenuService = sysRoleMenuService; + _sysUserRoleService = sysUserRoleService; + _sysUserMenuService = sysUserMenuService; + _sysTenantMenuRep = sysTenantMenuRep; + _sysCacheService = sysCacheService; + _sysLangTextCacheService = sysLangTextCacheService; + _sysLangTextService = sysLangTextService; + } + + /// + /// 获取登录菜单树 🔖 + /// + /// + [DisplayName("获取登录菜单树")] + public async Task> GetLoginMenuTree() + { + var sysDefaultLang = App.GetOptions().DefaultCulture; + var langCode = _userManager.LangCode; + var (query, _) = GetSugarQueryableAndTenantId(_userManager.TenantId); + + // 查询菜单主表(过滤非按钮和禁用) + var menuQuery = query.Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable); + + if (!(_userManager.SuperAdmin || _userManager.SysAdmin)) + { + var menuIdList = await GetMenuIdList(); + menuQuery = menuQuery.Where(u => menuIdList.Contains(u.Id)); + } + + // 查询主表(不再 LEFT JOIN) + var menuList = await menuQuery + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToListAsync(); + + // 仅当用户语言和系统默认语言不同时,才进行翻译,避免不必要的性能开销 + if (langCode != sysDefaultLang) + { + // 调用缓存翻译:翻译 Title 字段 + var fields = new List> + { + new LangFieldMap + { + EntityName = "SysMenu", + FieldName = "Title", + IdSelector = m => m.Id, + SetTranslatedValue = (m, val) => m.Title = val + } + }; + await _sysLangTextCacheService.TranslateMultiFields(menuList, fields, langCode); + } + + // 构造树 + var menuTree = menuList.ToTree( + it => it.Children, it => it.Pid, 0 + ); + + // 转换为输出 DTO + return menuTree.Adapt>(); + } + + /// + /// 获取菜单列表 🔖 + /// + /// + [DisplayName("获取菜单列表")] + public async Task> GetList([FromQuery] MenuInput input) + { + var langCode = _userManager.LangCode; + var menuIdList = _userManager.SuperAdmin || _userManager.SysAdmin ? new List() : await GetMenuIdList(); + var (query, _) = GetSugarQueryableAndTenantId(input.TenantId); + + // 有条件直接查询菜单列表(带 Title、Type 过滤) + if (!string.IsNullOrWhiteSpace(input.Title) || input.Type is > 0) + { + var menuList = await query + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title)) + .WhereIF(input.Type is > 0, u => u.Type == input.Type) + .WhereIF(menuIdList.Count > 0, u => menuIdList.Contains(u.Id)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToListAsync(); + + // 走缓存批量翻译 + var fields = new List> + { + new LangFieldMap + { + EntityName = "SysMenu", + FieldName = "Title", + IdSelector = m => m.Id, + SetTranslatedValue = (m, val) => m.Title = val + } + }; + await _sysLangTextCacheService.TranslateMultiFields(menuList, fields, langCode); + + return menuList.Distinct().ToList(); + } + + // 无筛选条件则走全量树形结构(带权限) + if (!(_userManager.SuperAdmin || _userManager.SysAdmin)) + { + query = query.Where(u => menuIdList.Contains(u.Id)); + } + + var menuFullList = await query + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToListAsync(); + + // 走缓存批量翻译 + var treeFields = new List> + { + new LangFieldMap + { + EntityName = "SysMenu", + FieldName = "Title", + IdSelector = m => m.Id, + SetTranslatedValue = (m, val) => m.Title = val + } + }; + await _sysLangTextCacheService.TranslateMultiFields(menuFullList, treeFields, langCode); + + // 组装树 + var menuTree = menuFullList.ToTree(it => it.Children, it => it.Pid, 0); + return menuTree.ToList(); + } + + /// + /// 增加菜单 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加菜单")] + public async Task AddMenu(AddMenuInput input) + { + var (query, tenantId) = GetSugarQueryableAndTenantId(input.TenantId); + + var isExist = input.Type != MenuTypeEnum.Btn + ? await query.AnyAsync(u => u.Title == input.Title && u.Pid == input.Pid) + : await query.AnyAsync(u => u.Permission == input.Permission); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D4000); + + if (!string.IsNullOrWhiteSpace(input.Name) && await query.AnyAsync(u => u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D4009); + + if (input.Pid != 0 && await query.AnyAsync(u => u.Id == input.Pid && u.Type == MenuTypeEnum.Btn)) throw Oops.Oh(ErrorCodeEnum.D4010); + + // 校验菜单参数 + var sysMenu = input.Adapt(); + CheckMenuParam(sysMenu); + + // 保存租户菜单权限 + await _sysMenuRep.InsertAsync(sysMenu); + await _sysTenantMenuRep.InsertAsync(new SysTenantMenu { TenantId = tenantId, MenuId = sysMenu.Id }); + + // 清除缓存 + DeleteMenuCache(); + + return sysMenu.Id; + } + + /// + /// 更新菜单 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新菜单")] + public async Task UpdateMenu(UpdateMenuInput input) + { + if (!_userManager.SuperAdmin && new SysMenuSeedData().HasData().Any(u => u.Id == input.Id)) throw Oops.Oh(ErrorCodeEnum.D4012); + + if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.D4008); + var (query, _) = GetSugarQueryableAndTenantId(input.TenantId); + + var isExist = input.Type != MenuTypeEnum.Btn + ? await query.AnyAsync(u => u.Title == input.Title && u.Type == input.Type && u.Pid == input.Pid && u.Id != input.Id) + : await query.AnyAsync(u => u.Permission == input.Permission && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D4000); + + if (!string.IsNullOrWhiteSpace(input.Name) && await query.AnyAsync(u => u.Id != input.Id && u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D4009); + + if (input.Pid != 0 && await query.AnyAsync(u => u.Id == input.Pid && u.Type == MenuTypeEnum.Btn)) throw Oops.Oh(ErrorCodeEnum.D4010); + + // 校验菜单参数 + var sysMenu = input.Adapt(); + CheckMenuParam(sysMenu); + + await _sysMenuRep.AsTenant().UseTranAsync(async () => + { + // 更新菜单 + await _sysMenuRep.AsUpdateable(sysMenu).ExecuteCommandAsync(); + + // 同步更新翻译表 + var menuTranslation = await _sysLangTextCacheService.GetTranslationEntity("SysMenu", "Title", sysMenu.Id, _userManager.LangCode); + if (!menuTranslation.IsNullOrEmpty()) + { + await _sysLangTextService.Update(new UpdateSysLangTextInput + { + Id = menuTranslation.Id, + EntityName = "SysMenu", + EntityId = sysMenu.Id, + FieldName = "Title", + LangCode = _userManager.LangCode, + Content = sysMenu.Title + }); + } + }, err => + { + Oops.Oh("更新数据时发生错误", err.Message); + }); + + // 清除缓存 + DeleteMenuCache(); + } + + /// + /// 删除菜单 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除菜单")] + public async Task DeleteMenu(DeleteMenuInput input) + { + if (!_userManager.SuperAdmin && new SysMenuSeedData().HasData().Any(u => u.Id == input.Id)) throw Oops.Oh(ErrorCodeEnum.D4013); + + var menuTreeList = await _sysMenuRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id); + var menuIdList = menuTreeList.Select(u => u.Id).ToList(); + + await _sysMenuRep.DeleteAsync(u => menuIdList.Contains(u.Id)); + + // 级联删除租户菜单数据 + await _sysTenantMenuRep.AsDeleteable().Where(u => menuIdList.Contains(u.MenuId)).ExecuteCommandAsync(); + + // 级联删除角色菜单数据 + await _sysRoleMenuService.DeleteRoleMenuByMenuIdList(menuIdList); + + // 级联删除用户收藏菜单 + await _sysUserMenuService.DeleteMenuList(menuIdList); + + // 清除缓存 + DeleteMenuCache(); + } + + /// + /// 设置菜单状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("设置菜单状态")] + public virtual async Task SetStatus(MenuStatusInput input) + { + if (_userManager.UserId == input.Id) + throw Oops.Oh(ErrorCodeEnum.D1026); + + var menu = await _sysMenuRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + menu.Status = input.Status; + var rows = await _sysMenuRep.AsUpdateable(menu).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + return rows; + } + + /// + /// 增加和编辑时检查菜单数据 + /// + /// + private static void CheckMenuParam(SysMenu menu) + { + var permission = menu.Permission; + if (menu.Type == MenuTypeEnum.Btn) + { + menu.Name = null; + menu.Path = null; + menu.Component = null; + menu.Icon = null; + menu.Redirect = null; + menu.OutLink = null; + menu.IsHide = false; + menu.IsKeepAlive = true; + menu.IsAffix = false; + menu.IsIframe = false; + + if (string.IsNullOrEmpty(permission)) throw Oops.Oh(ErrorCodeEnum.D4003); + if (!permission.Contains(':')) throw Oops.Oh(ErrorCodeEnum.D4004); + } + else + { + menu.Permission = null; + } + } + + /// + /// 获取用户拥有按钮权限集合(缓存) 🔖 + /// + /// + [DisplayName("获取按钮权限集合")] + public async Task> GetOwnBtnPermList() + { + var userId = _userManager.UserId; + var permissions = _sysCacheService.Get>(CacheConst.KeyUserButton + userId); + if (permissions != null) return permissions; + + var menuIdList = _userManager.SuperAdmin ? new() : await GetMenuIdList(); + if (menuIdList.Count <= 0 && !_userManager.SuperAdmin && !_userManager.SysAdmin) + { + //_sysCacheService.Set(CacheConst.KeyUserButton + userId, new List(), TimeSpan.FromDays(7)); + return new List(); + } + + permissions = await _sysMenuRep.AsQueryable() + .InnerJoinIF(!_userManager.SuperAdmin, (u, t) => t.TenantId == _userManager.TenantId && u.Id == t.MenuId) + .Where(u => u.Type == MenuTypeEnum.Btn) + .WhereIF(menuIdList.Count > 0, u => menuIdList.Contains(u.Id)) + .Select(u => u.Permission).ToListAsync(); + + _sysCacheService.Set(CacheConst.KeyUserButton + userId, permissions, TimeSpan.FromDays(7)); + + return permissions; + } + + /// + /// 获取系统所有按钮权限集合(缓存) + /// + /// + [NonAction] + public async Task> GetAllBtnPermList() + { + var permissions = _sysCacheService.Get>(CacheConst.KeyUserButton + 0); + if (permissions != null && permissions.Count != 0) return permissions; + + permissions = await _sysMenuRep.AsQueryable() + .Where(u => u.Type == MenuTypeEnum.Btn) + .Select(u => u.Permission).ToListAsync(); + _sysCacheService.Set(CacheConst.KeyUserButton + 0, permissions); + + return permissions; + } + + /// + /// 根据租户id获取构建菜单联表查询实例 + /// + /// + /// + [NonAction] + public (ISugarQueryable query, long tenantId) GetSugarQueryableAndTenantId(long tenantId) + { + if (!_userManager.SuperAdmin) tenantId = _userManager.TenantId; + + // 超管用户菜单范围:种子菜单 + 租户id菜单 + ISugarQueryable query; + if (_userManager.SuperAdmin) + { + if (tenantId <= 0) + { + query = _sysMenuRep.AsQueryable().InnerJoinIF(false, (u, t) => true); + } + else + { + // 指定租户的菜单 + var menuIds = _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == tenantId).ToList(u => u.MenuId) ?? new(); + + // 种子菜单 + //menuIds.AddRange(new SysMenuSeedData().HasData().Select(u => u.Id).ToList()); + + menuIds = menuIds.Distinct().ToList(); + query = _sysMenuRep.AsQueryable().InnerJoinIF(false, (u, t) => true).Where(u => menuIds.Contains(u.Id)); + } + } + else + { + query = _sysMenuRep.AsQueryable().InnerJoinIF(tenantId > 0, (u, t) => t.TenantId == tenantId && u.Id == t.MenuId); + } + + return (query, tenantId); + } + + /// + /// 清除菜单和按钮缓存 + /// + [NonAction] + public void DeleteMenuCache() + { + // _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserMenu); + _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserButton); + } + + /// + /// 获取当前用户菜单Id集合 + /// + /// + [NonAction] + public async Task> GetMenuIdList() + { + var roleIdList = await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + return await _sysRoleMenuService.GetRoleMenuIdList(roleIdList); + } + + /// + /// 排除前端存在全选的父级菜单 + /// + /// + [NonAction] + public async Task> ExcludeParentMenuOfFullySelected(List menuIds) + { + // 获取当前用户菜单 + var (query, _) = GetSugarQueryableAndTenantId(0); + var menuList = await query.ToListAsync(); + + // 排除列表,防止前端全选问题 + var exceptList = new List(); + foreach (var id in menuIds) + { + // 排除按钮菜单 + if (menuList.Any(u => u.Id == id && u.Type == MenuTypeEnum.Btn)) continue; + + // 如果没有子集或有全部子集权限 + var children = menuList.ToChildList(u => u.Id, u => u.Pid, id, false).ToList(); + if (children.Count == 0 || children.All(u => menuIds.Contains(u.Id))) continue; + + // 排除没有全部子集权限的菜单 + exceptList.Add(id); + } + return menuIds.Except(exceptList).ToList(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/MessageInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/MessageInput.cs new file mode 100644 index 0000000..d8d43d6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/MessageInput.cs @@ -0,0 +1,55 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class MessageInput +{ + /// + /// 接收者用户Id + /// + public long ReceiveUserId { get; set; } + + /// + /// 接收者名称 + /// + public string ReceiveUserName { get; set; } + + /// + /// 用户ID列表 + /// + public List UserIds { get; set; } + + /// + /// 消息标题 + /// + public string Title { get; set; } + + /// + /// 消息类型 + /// + public MessageTypeEnum MessageType { get; set; } + + /// + /// 消息内容 + /// + public string Message { get; set; } + + /// + /// 发送者Id + /// + public string SendUserId { get; set; } + + /// + /// 发送者名称 + /// + public string SendUserName { get; set; } + + /// + /// 发送时间 + /// + public DateTime SendTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/SmsInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/SmsInput.cs new file mode 100644 index 0000000..8a6020f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/Dto/SmsInput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class SmsVerifyCodeInput +{ + /// + /// 手机号码 + /// + /// admin + [Required(ErrorMessage = "手机号码不能为空")] + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")] + public string Phone { get; set; } + + /// + /// 验证码 + /// + /// 123456 + [Required(ErrorMessage = "验证码不能为空"), MinLength(4, ErrorMessage = "验证码不能少于4个字符")] + public string Code { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs new file mode 100644 index 0000000..8065394 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MailKit.Net.Smtp; +using MimeKit; + +namespace Admin.NET.Core.Service; + +/// +/// 系统邮件发送服务 🧩 +/// +[ApiDescriptionSettings(Order = 370)] +public class SysEmailService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTenantRep; + private readonly EmailOptions _emailOptions; + + public SysEmailService(IOptions emailOptions, SqlSugarRepository sysTenantRep) + { + _emailOptions = emailOptions.Value; + _sysTenantRep = sysTenantRep; + } + + /// + /// 发送邮件 📧 + /// + /// + /// + /// + /// + [DisplayName("发送邮件")] + public async Task SendEmail([Required] string content, string title = "", string toEmail = "") + { + long.TryParse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? SqlSugarConst.DefaultTenantId.ToString(), out var tenantId); + var webTitle = (await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId))?.Title; + title = string.IsNullOrWhiteSpace(title) ? $"{webTitle} 系统邮件" : title; + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress(_emailOptions.DefaultFromEmail, _emailOptions.DefaultFromEmail)); + + message.To.Add(string.IsNullOrWhiteSpace(toEmail) + ? new MailboxAddress(_emailOptions.DefaultToEmail, _emailOptions.DefaultToEmail) + : new MailboxAddress(toEmail, toEmail)); + + message.Subject = title; + message.Body = new TextPart("html") + { + Text = content + }; + + using var client = new SmtpClient(); + client.Connect(_emailOptions.Host, _emailOptions.Port, _emailOptions.EnableSsl); + client.Authenticate(_emailOptions.UserName, _emailOptions.Password); + client.Send(message); + client.Disconnect(true); + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysMessageService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysMessageService.cs new file mode 100644 index 0000000..aa0d40d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysMessageService.cs @@ -0,0 +1,76 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core.Service; + +/// +/// 系统消息发送服务 🧩 +/// +[ApiDescriptionSettings(Order = 370)] +public class SysMessageService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly IHubContext _chatHubContext; + + public SysMessageService(SysCacheService sysCacheService, + IHubContext chatHubContext) + { + _sysCacheService = sysCacheService; + _chatHubContext = chatHubContext; + } + + /// + /// 发送消息给所有人 🔖 + /// + /// + /// + [DisplayName("发送消息给所有人")] + public async Task SendAllUser(MessageInput input) + { + await _chatHubContext.Clients.All.ReceiveMessage(input); + } + + /// + /// 发送消息给除了发送人的其他人 🔖 + /// + /// + /// + [DisplayName("发送消息给除了发送人的其他人")] + public async Task SendOtherUser(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var exceptReceiveUsers = hashKey.Where(u => u.Value.UserId == input.ReceiveUserId).Select(u => u.Value).ToList(); + await _chatHubContext.Clients.AllExcept(exceptReceiveUsers.Select(t => t.ConnectionId)).ReceiveMessage(input); + } + + /// + /// 发送消息给某个人 🔖 + /// + /// + /// + [DisplayName("发送消息给某个人")] + public async Task SendUser(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var receiveUsers = hashKey.Where(u => u.Value.UserId == input.ReceiveUserId).Select(u => u.Value).ToList(); + await receiveUsers.ForEachAsync(u => _chatHubContext.Clients.Client(u.ConnectionId ?? "").ReceiveMessage(input)); + } + + /// + /// 发送消息给某些人 🔖 + /// + /// + /// + [DisplayName("发送消息给某些人")] + public async Task SendUsers(MessageInput input) + { + var hashKey = _sysCacheService.HashGetAll(CacheConst.KeyUserOnline); + var receiveUsers = hashKey.Where(u => input.UserIds.Any(a => a == u.Value.UserId)).Select(u => u.Value).ToList(); + await receiveUsers.ForEachAsync(u => _chatHubContext.Clients.Client(u.ConnectionId ?? "").ReceiveMessage(input)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs new file mode 100644 index 0000000..fea589b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs @@ -0,0 +1,308 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using AlibabaCloud.SDK.Dysmsapi20170525.Models; +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using TencentCloud.Sms.V20190711; + +namespace Admin.NET.Core.Service; + +/// +/// 系统短信服务 🧩 +/// +[AllowAnonymous] +[ApiDescriptionSettings(Order = 150)] +public class SysSmsService : IDynamicApiController, ITransient +{ + private readonly SMSOptions _smsOptions; + private readonly SysCacheService _sysCacheService; + + public SysSmsService(IOptions smsOptions, + SysCacheService sysCacheService) + { + _smsOptions = smsOptions.Value; + _sysCacheService = sysCacheService; + } + + /// + /// 发送短信 📨 + /// + /// + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("发送短信")] + public async Task SendSms([Required] string phoneNumber, string templateId = "0") + { + if (_smsOptions.Custom != null && _smsOptions.Custom.Enabled && !string.IsNullOrWhiteSpace(_smsOptions.Custom.ApiUrl)) + { + await CustomSendSms(phoneNumber, templateId); + } + else if (!string.IsNullOrWhiteSpace(_smsOptions.Aliyun.AccessKeyId) && !string.IsNullOrWhiteSpace(_smsOptions.Aliyun.AccessKeySecret)) + { + await AliyunSendSms(phoneNumber, templateId); + } + else + { + await TencentSendSms(phoneNumber, templateId); + } + } + + /// + /// 校验短信验证码 + /// + /// + /// + [AllowAnonymous] + [DisplayName("校验短信验证码")] + public bool VerifyCode(SmsVerifyCodeInput input) + { + var verifyCode = _sysCacheService.Get($"{CacheConst.KeyPhoneVerCode}{input.Phone}"); + + if (string.IsNullOrWhiteSpace(verifyCode)) throw Oops.Oh("验证码不存在或已失效,请重新获取!"); + + if (verifyCode != input.Code) throw Oops.Oh("验证码错误!"); + + return true; + } + + /// + /// 阿里云发送短信 📨 + /// + /// 手机号 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("阿里云发送短信")] + public async Task AliyunSendSms([Required] string phoneNumber, string templateId = "0") + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + // 生成随机验证码 + var random = new Random(); + var verifyCode = random.Next(100000, 999999); + + var templateParam = new + { + code = verifyCode + }; + + var client = CreateAliyunClient(); + var template = _smsOptions.Aliyun.GetTemplate(templateId); + var sendSmsRequest = new SendSmsRequest + { + PhoneNumbers = phoneNumber, // 待发送手机号, 多个以逗号分隔 + SignName = template.SignName, // 短信签名 + TemplateCode = template.TemplateCode, // 短信模板 + TemplateParam = templateParam.ToJson(), // 模板中的变量替换JSON串 + OutId = YitIdHelper.NextId().ToString() + }; + var sendSmsResponse = await client.SendSmsAsync(sendSmsRequest); + if (sendSmsResponse.Body.Code == "OK" && sendSmsResponse.Body.Message == "OK") + { + // var bizId = sendSmsResponse.Body.BizId; + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + else + { + throw Oops.Oh($"短信发送失败:{sendSmsResponse.Body.Code}-{sendSmsResponse.Body.Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 发送短信模板 + /// + /// 手机号 + /// 短信内容 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("发送短信模板")] + public async Task AliyunSendSmsTemplate([Required] string phoneNumber, [Required] dynamic templateParam, string templateId) + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + if (string.IsNullOrWhiteSpace(templateParam.ToString())) throw Oops.Oh("短信内容不能为空"); + + var client = CreateAliyunClient(); + var template = _smsOptions.Aliyun.GetTemplate(templateId); + var sendSmsRequest = new SendSmsRequest + { + PhoneNumbers = phoneNumber, // 待发送手机号, 多个以逗号分隔 + SignName = template.SignName, // 短信签名 + TemplateCode = template.TemplateCode, // 短信模板 + TemplateParam = templateParam.ToString(), // 模板中的变量替换JSON串 + OutId = YitIdHelper.NextId().ToString() + }; + var sendSmsResponse = await client.SendSmsAsync(sendSmsRequest); + if (sendSmsResponse.Body.Code == "OK" && sendSmsResponse.Body.Message == "OK") + { + } + else + { + throw Oops.Oh($"短信发送失败:{sendSmsResponse.Body.Code}-{sendSmsResponse.Body.Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 腾讯云发送短信 📨 + /// + /// + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("腾讯云发送短信")] + public async Task TencentSendSms([Required] string phoneNumber, string templateId = "0") + { + if (!phoneNumber.TryValidate(ValidationTypes.PhoneNumber).IsValid) throw Oops.Oh("请正确填写手机号码"); + + // 生成随机验证码 + var random = new Random(); + var verifyCode = random.Next(100000, 999999); + + // 实例化要请求产品的client对象,clientProfile是可选的 + var client = new SmsClient(CreateTencentClient(), "ap-guangzhou", new ClientProfile() { HttpProfile = new HttpProfile() { Endpoint = ("sms.tencentcloudapi.com") } }); + var template = _smsOptions.Tencentyun.GetTemplate(templateId); + // 实例化一个请求对象,每个接口都会对应一个request对象 + var req = new TencentCloud.Sms.V20190711.Models.SendSmsRequest + { + PhoneNumberSet = new string[] { "+86" + phoneNumber.Trim(',') }, + SmsSdkAppid = _smsOptions.Tencentyun.SdkAppId, + Sign = template.SignName, + TemplateID = template.TemplateCode, + TemplateParamSet = new string[] { verifyCode.ToString() } + }; + + // 返回的resp是一个SendSmsResponse的实例,与请求对象对应 + TencentCloud.Sms.V20190711.Models.SendSmsResponse resp = client.SendSmsSync(req); + + if (resp.SendStatusSet[0].Code == "Ok" && resp.SendStatusSet[0].Message == "send success") + { + // var bizId = sendSmsResponse.Body.BizId; + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + else + { + throw Oops.Oh($"短信发送失败:{resp.SendStatusSet[0].Code}-{resp.SendStatusSet[0].Message}"); + } + + await Task.CompletedTask; + } + + /// + /// 阿里云短信配置 + /// + /// + private AlibabaCloud.SDK.Dysmsapi20170525.Client CreateAliyunClient() + { + var config = new AlibabaCloud.OpenApiClient.Models.Config + { + AccessKeyId = _smsOptions.Aliyun.AccessKeyId, + AccessKeySecret = _smsOptions.Aliyun.AccessKeySecret, + Endpoint = "dysmsapi.aliyuncs.com" + }; + return new AlibabaCloud.SDK.Dysmsapi20170525.Client(config); + } + + /// + /// 腾讯云短信配置 + /// + /// + private Credential CreateTencentClient() + { + var cred = new Credential + { + SecretId = _smsOptions.Tencentyun.AccessKeyId, + SecretKey = _smsOptions.Tencentyun.AccessKeySecret + }; + return cred; + } + + /// + /// 自定义短信接口发送短信 📨 + /// + /// 手机号 + /// 短信模板id + /// + [AllowAnonymous] + [DisplayName("自定义短信接口发送短信")] + public async Task CustomSendSms([DataValidation(ValidationTypes.PhoneNumber)] string phoneNumber, string templateId = "0") + { + if (_smsOptions.Custom == null || !_smsOptions.Custom.Enabled) + throw Oops.Oh("自定义短信接口未启用"); + + if (string.IsNullOrWhiteSpace(_smsOptions.Custom.ApiUrl)) + throw Oops.Oh("自定义短信接口地址未配置"); + + // 生成随机验证码 + var verifyCode = Random.Shared.Next(100000, 999999); + + // 获取模板 + var template = _smsOptions.Custom.GetTemplate(templateId); + if (template == null) + throw Oops.Oh($"短信模板[{templateId}]不存在"); + + // 替换模板内容中的占位符 + var content = template.Content.Replace("{code}", verifyCode.ToString()); + + try + { + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(30); + + HttpResponseMessage response; + + //替换URL占位符 + var url = _smsOptions.Custom.ApiUrl + .Replace("{templateId}", templateId) + .Replace("{mobile}", phoneNumber) + .Replace("{content}", Uri.EscapeDataString(content)) + .Replace("{code}", verifyCode.ToString()); + + if (_smsOptions.Custom.Method.ToUpper() == "POST") + { + // 替换占位符 + var postData = _smsOptions.Custom.PostData? + .Replace("{templateId}", templateId) + .Replace("{mobile}", phoneNumber) + .Replace("{content}", content) + .Replace("{code}", verifyCode.ToString()); + HttpContent httpContent = new StringContent(postData ?? string.Empty, Encoding.UTF8, _smsOptions.Custom.ContentType ?? "application/x-www-form-urlencoded"); + response = await httpClient.PostAsync(url, httpContent); + } + else + { + // GET 请求 + response = await httpClient.GetAsync(url); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + // 判断是否发送成功 + if (response.IsSuccessStatusCode && responseContent.Contains(_smsOptions.Custom.SuccessFlag)) + { + if (_smsOptions.Custom.ApiUrl.Contains("{code}") || template.Content.Contains("{code}") || (_smsOptions.Custom.PostData?.Contains("{code}") == true)) + { + // 如果模板含有验证码,则添加到缓存 + _sysCacheService.Set($"{CacheConst.KeyPhoneVerCode}{phoneNumber}", verifyCode, TimeSpan.FromSeconds(_smsOptions.VerifyCodeExpireSeconds)); + } + } + else + { + throw Oops.Oh($"短信发送失败:{responseContent}"); + } + } + catch (Exception ex) + { + throw Oops.Oh($"短信发送异常:{ex.Message}"); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs new file mode 100644 index 0000000..e433927 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/Dto/NoticeInput.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageNoticeInput : BasePageInput +{ + /// + /// 标题 + /// + public virtual string Title { get; set; } + + /// + /// 类型(1通知 2公告) + /// + public virtual NoticeTypeEnum? Type { get; set; } +} + +public class AddNoticeInput : SysNotice +{ +} + +public class UpdateNoticeInput : AddNoticeInput +{ +} + +public class DeleteNoticeInput : BaseIdInput +{ +} + +public class NoticeInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs new file mode 100644 index 0000000..627b1e7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Notice/SysNoticeService.cs @@ -0,0 +1,190 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统通知公告服务 🧩 +/// +[ApiDescriptionSettings(Order = 380, Description = "通知公告")] +public class SysNoticeService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysUserRep; + private readonly SqlSugarRepository _sysNoticeRep; + private readonly SqlSugarRepository _sysNoticeUserRep; + private readonly SysOnlineUserService _sysOnlineUserService; + + public SysNoticeService( + UserManager userManager, + SqlSugarRepository sysUserRep, + SqlSugarRepository sysNoticeRep, + SqlSugarRepository sysNoticeUserRep, + SysOnlineUserService sysOnlineUserService) + { + _userManager = userManager; + _sysUserRep = sysUserRep; + _sysNoticeRep = sysNoticeRep; + _sysNoticeUserRep = sysNoticeUserRep; + _sysOnlineUserService = sysOnlineUserService; + } + + /// + /// 获取通知公告分页列表 📢 + /// + /// + /// + [DisplayName("获取通知公告分页列表")] + public async Task> Page(PageNoticeInput input) + { + return await _sysNoticeRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title.Trim())) + .WhereIF(input.Type > 0, u => u.Type == input.Type) + .WhereIF(!_userManager.SuperAdmin, u => u.CreateUserId == _userManager.UserId) + .OrderBy(u => u.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加通知公告 📢 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加通知公告")] + public async Task AddNotice(AddNoticeInput input) + { + var notice = input.Adapt(); + InitNoticeInfo(notice); + await _sysNoticeRep.InsertAsync(notice); + } + + /// + /// 更新通知公告 📢 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新通知公告")] + public async Task UpdateNotice(UpdateNoticeInput input) + { + if (input.CreateUserId != _userManager.UserId) + throw Oops.Oh(ErrorCodeEnum.D7003); + + var notice = input.Adapt(); + InitNoticeInfo(notice); + await _sysNoticeRep.UpdateAsync(notice); + } + + /// + /// 删除通知公告 📢 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除通知公告")] + public async Task DeleteNotice(DeleteNoticeInput input) + { + var sysNotice = await _sysNoticeRep.GetByIdAsync(input.Id); + + if (sysNotice.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D7003); + + if (sysNotice.Status == NoticeStatusEnum.PUBLIC) throw Oops.Oh(ErrorCodeEnum.D7001); + + await _sysNoticeRep.DeleteAsync(u => u.Id == input.Id); + + await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == input.Id); + } + + /// + /// 发布通知公告 📢 + /// + /// + /// + [DisplayName("发布通知公告")] + public async Task Public(NoticeInput input) + { + if (!(await _sysNoticeRep.IsAnyAsync(u => u.Id == input.Id && u.CreateUserId == _userManager.UserId))) + throw Oops.Oh(ErrorCodeEnum.D7003); + + // 更新发布状态和时间 + await _sysNoticeRep.UpdateAsync(u => new SysNotice() { Status = NoticeStatusEnum.PUBLIC, PublicTime = DateTime.Now }, u => u.Id == input.Id); + + var notice = await _sysNoticeRep.GetByIdAsync(input.Id); + + // 通知到的人(所有账号) + var userIdList = await _sysUserRep.AsQueryable().Select(u => u.Id).ToListAsync(); + + await _sysNoticeUserRep.DeleteAsync(u => u.NoticeId == notice.Id); + var noticeUserList = userIdList.Select(u => new SysNoticeUser + { + NoticeId = notice.Id, + UserId = u, + }).ToList(); + await _sysNoticeUserRep.InsertRangeAsync(noticeUserList); + + // 广播所有在线账号 + await _sysOnlineUserService.PublicNotice(notice, userIdList); + } + + /// + /// 设置通知公告已读状态 📢 + /// + /// + /// + [DisplayName("设置通知公告已读状态")] + public async Task SetRead(NoticeInput input) + { + await _sysNoticeUserRep.UpdateAsync(u => new SysNoticeUser + { + ReadStatus = NoticeUserStatusEnum.READ, + ReadTime = DateTime.Now + }, u => u.NoticeId == input.Id && u.UserId == _userManager.UserId); + } + + /// + /// 获取接收的通知公告 + /// + /// + /// + [DisplayName("获取接收的通知公告")] + public async Task> PageReceived(PageNoticeInput input) + { + return await _sysNoticeUserRep.AsQueryable().Includes(u => u.SysNotice) + .Where(u => u.UserId == _userManager.UserId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.SysNotice.Title.Contains(input.Title.Trim())) + .WhereIF(input.Type is > 0, u => u.SysNotice.Type == input.Type) + .OrderBy(u => u.SysNotice.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取未读的通知公告 📢 + /// + /// + [DisplayName("获取未读的通知公告")] + public async Task> GetUnReadList() + { + var noticeUserList = await _sysNoticeUserRep.AsQueryable().Includes(u => u.SysNotice) + .Where(u => u.UserId == _userManager.UserId && u.ReadStatus == NoticeUserStatusEnum.UNREAD) + .OrderBy(u => u.SysNotice.CreateTime, OrderByType.Desc).ToListAsync(); + return noticeUserList.Select(t => t.SysNotice).ToList(); + } + + /// + /// 初始化通知公告信息 + /// + /// + [NonAction] + private void InitNoticeInfo(SysNotice notice) + { + notice.PublicUserId = _userManager.UserId; + notice.PublicUserName = _userManager.RealName; + notice.PublicOrgId = _userManager.OrgId; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthClaim.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthClaim.cs new file mode 100644 index 0000000..db89229 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthClaim.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public static class OAuthClaim +{ + public const string GiteeAvatarUrl = "urn:gitee:avatar_url"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs new file mode 100644 index 0000000..b58aea6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; + +namespace Admin.NET.Core; + +public static class OAuthSetup +{ + /// + /// 三方授权登录OAuth注册 + /// + /// + public static void AddOAuth(this IServiceCollection services) + { + var authOpt = App.GetConfig("OAuth", true); + services.AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddCookie(options => + { + options.Cookie.SameSite = SameSiteMode.Lax; + }) + .AddWeixin(options => + { + options.ClientId = authOpt.Weixin?.ClientId!; + options.ClientSecret = authOpt.Weixin?.ClientSecret!; + }) + .AddGitee(options => + { + options.ClientId = authOpt.Gitee?.ClientId!; + options.ClientSecret = authOpt.Gitee?.ClientSecret!; + + options.ClaimActions.MapJsonKey(OAuthClaim.GiteeAvatarUrl, "avatar_url"); + }); + } + + public static void UseOAuth(this IApplicationBuilder app) + { + app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs new file mode 100644 index 0000000..99d9621 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; + +namespace Admin.NET.Core.Service; + +/// +/// 系统OAuth服务 🧩 +/// +[AllowAnonymous] +[ApiDescriptionSettings(Order = 498)] +public class SysOAuthService : IDynamicApiController, ITransient +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SqlSugarRepository _sysWechatUserRep; + + public SysOAuthService(IHttpContextAccessor httpContextAccessor, + SqlSugarRepository sysWechatUserRep) + { + _httpContextAccessor = httpContextAccessor; + _sysWechatUserRep = sysWechatUserRep; + } + + /// + /// 第三方登录 🔖 + /// + /// + /// + /// + [ApiDescriptionSettings(Name = "SignIn"), HttpGet] + [DisplayName("第三方登录")] + public virtual async Task SignIn([FromQuery] string provider, [FromQuery] string redirectUrl) + { + if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider)) + throw Oops.Oh("不支持的OAuth类型"); + + var request = _httpContextAccessor.HttpContext!.Request; + var url = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}Callback?provider={provider}&redirectUrl={redirectUrl}"; + var properties = new AuthenticationProperties + { + RedirectUri = url, + Items = { ["LoginProvider"] = provider } + }; + return await Task.FromResult(new ChallengeResult(provider, properties)); + } + + /// + /// 授权回调 🔖 + /// + /// + /// + /// + [ApiDescriptionSettings(Name = "SignInCallback"), HttpGet] + [DisplayName("授权回调")] + public virtual async Task SignInCallback([FromQuery] string provider = null, [FromQuery] string redirectUrl = "") + { + if (string.IsNullOrWhiteSpace(provider) || !await _httpContextAccessor.HttpContext.IsProviderSupportedAsync(provider)) + throw Oops.Oh("不支持的OAuth类型"); + + var authenticateResult = await _httpContextAccessor.HttpContext!.AuthenticateAsync(provider); + if (!authenticateResult.Succeeded) + throw Oops.Oh("授权失败"); + + var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier); + if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value)) + throw Oops.Oh("授权失败"); + + var name = authenticateResult.Principal.FindFirst(ClaimTypes.Name)?.Value; + var email = authenticateResult.Principal.FindFirst(ClaimTypes.Email)?.Value; + var mobilePhone = authenticateResult.Principal.FindFirst(ClaimTypes.MobilePhone)?.Value; + var dateOfBirth = authenticateResult.Principal.FindFirst(ClaimTypes.DateOfBirth)?.Value; + var gender = authenticateResult.Principal.FindFirst(ClaimTypes.Gender)?.Value; + var avatarUrl = ""; + + var platformType = PlatformTypeEnum.微信公众号; + if (provider == "Gitee") + { + platformType = PlatformTypeEnum.Gitee; + avatarUrl = authenticateResult.Principal.FindFirst(OAuthClaim.GiteeAvatarUrl)?.Value; + } + + // 若账号不存在则新建 + var wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value); + if (wechatUser == null) + { + var userId = await App.GetRequiredService().AddUser(new AddUserInput() + { + Account = name, + RealName = name, + NickName = name, + Email = email, + Avatar = avatarUrl, + Phone = mobilePhone, + OrgId = 1300000000101, // 根组织架构 + RoleIdList = new List { 1300000000104 } // 仅本人数据角色 + }); + + await _sysWechatUserRep.InsertAsync(new SysWechatUser() + { + UserId = userId, + OpenId = openIdClaim.Value, + Avatar = avatarUrl, + NickName = name, + PlatformType = platformType + }); + + wechatUser = await _sysWechatUserRep.AsQueryable().Includes(u => u.SysUser).ClearFilter().FirstAsync(u => u.OpenId == openIdClaim.Value); + } + + // 构建Token令牌 + var token = await App.GetRequiredService().CreateToken(wechatUser.SysUser); + + return new RedirectResult($"{redirectUrl}/#/login?token={token.AccessToken}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs new file mode 100644 index 0000000..46c40d9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/Dto/OnlineUserInput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageOnlineUserInput : BasePageInput +{ + /// + /// 账号名称 + /// + public string UserName { get; set; } + + /// + /// 真实姓名 + /// + public string RealName { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs new file mode 100644 index 0000000..f54c99b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OnlineUser/SysOnlineUserService.cs @@ -0,0 +1,109 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.SignalR; + +namespace Admin.NET.Core.Service; + +/// +/// 系统在线用户服务 🧩 +/// +[ApiDescriptionSettings(Order = 300)] +public class SysOnlineUserService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysConfigService _sysConfigService; + private readonly IHubContext _onlineUserHubContext; + private readonly SqlSugarRepository _sysOnlineUerRep; + + public SysOnlineUserService(SysConfigService sysConfigService, + IHubContext onlineUserHubContext, + SqlSugarRepository sysOnlineUerRep, + UserManager userManager) + { + _userManager = userManager; + _sysConfigService = sysConfigService; + _onlineUserHubContext = onlineUserHubContext; + _sysOnlineUerRep = sysOnlineUerRep; + } + + /// + /// 获取在线用户分页列表 🔖 + /// + /// + [DisplayName("获取在线用户分页列表")] + public async Task> Page(PageOnlineUserInput input) + { + return await _sysOnlineUerRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.UserName), u => u.UserName.Contains(input.UserName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.RealName.Contains(input.RealName)) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 强制下线 🔖 + /// + /// + /// + [NonValidation] + [DisplayName("强制下线")] + public async Task ForceOffline(SysOnlineUser user) + { + await _onlineUserHubContext.Clients.Client(user.ConnectionId ?? "").ForceOffline("强制下线"); + await _sysOnlineUerRep.DeleteAsync(user); + } + + /// + /// 发布站内消息 + /// + /// + /// + /// + [NonAction] + public async Task PublicNotice(SysNotice notice, List userIds) + { + var userList = await _sysOnlineUerRep.GetListAsync(u => userIds.Contains(u.UserId)); + if (userList.Count == 0) return; + + foreach (var item in userList) + { + await _onlineUserHubContext.Clients.Client(item.ConnectionId ?? "").PublicNotice(notice); + } + } + + /// + /// 单用户登录 + /// + /// + [NonAction] + public async Task SingleLogin(long userId) + { + if (await _sysConfigService.GetConfigValue(ConfigConst.SysSingleLogin)) + { + var users = await _sysOnlineUerRep.GetListAsync(u => u.UserId == userId); + foreach (var user in users) + { + await ForceOffline(user); + } + } + } + + /// + /// 通过用户ID踢掉在线用户 + /// + /// + /// + [NonAction] + public async Task ForceOffline(long userId) + { + var users = await _sysOnlineUerRep.GetListAsync(u => u.UserId == userId); + foreach (var user in users) + { + await ForceOffline(user); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs new file mode 100644 index 0000000..a3d5793 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessInput.cs @@ -0,0 +1,85 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 开放接口身份输入参数 +/// +public class OpenAccessInput : BasePageInput +{ + /// + /// 身份标识 + /// + public string AccessKey { get; set; } +} + +public class AddOpenAccessInput : SysOpenAccess +{ + /// + /// 身份标识 + /// + [Required(ErrorMessage = "身份标识不能为空")] + public override string AccessKey { get; set; } + + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public override string AccessSecret { get; set; } + + /// + /// 绑定用户Id + /// + [Required(ErrorMessage = "绑定用户不能为空")] + public override long BindUserId { get; set; } +} + +public class UpdateOpenAccessInput : AddOpenAccessInput +{ +} + +public class DeleteOpenAccessInput : BaseIdInput +{ +} + +public class GenerateSignatureInput +{ + /// + /// 身份标识 + /// + [Required(ErrorMessage = "身份标识不能为空")] + public string AccessKey { get; set; } + + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public string AccessSecret { get; set; } + + /// + /// 请求方法 + /// + public HttpMethodEnum Method { get; set; } + + /// + /// 请求接口地址 + /// + [Required(ErrorMessage = "请求接口地址不能为空")] + public string Url { get; set; } + + /// + /// 时间戳 + /// + [Required(ErrorMessage = "时间戳不能为空")] + public long Timestamp { get; set; } + + /// + /// 随机数 + /// + [Required(ErrorMessage = "随机数不能为空")] + public string Nonce { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs new file mode 100644 index 0000000..47516fd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/Dto/OpenAccessOutput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class OpenAccessOutput : SysOpenAccess +{ + /// + /// 绑定用户账号 + /// + public string BindUserAccount { get; set; } + + /// + /// 绑定租户名称 + /// + public string BindTenantName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs new file mode 100644 index 0000000..b8fdf2c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs @@ -0,0 +1,195 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Claims; +using System.Security.Cryptography; + +namespace Admin.NET.Core.Service; + +/// +/// 开放接口身份服务 🧩 +/// +[ApiDescriptionSettings(Order = 244)] +public class SysOpenAccessService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysOpenAccessRep; + private readonly SysCacheService _sysCacheService; + + /// + /// 开放接口身份服务构造函数 + /// + public SysOpenAccessService(SqlSugarRepository sysOpenAccessRep, + SysCacheService sysCacheService) + { + _sysOpenAccessRep = sysOpenAccessRep; + _sysCacheService = sysCacheService; + } + + /// + /// 生成签名 + /// + /// + /// + [DisplayName("生成签名")] + public string GenerateSignature(GenerateSignatureInput input) + { + // 密钥 + var appSecretByte = Encoding.UTF8.GetBytes(input.AccessSecret); + + // 拼接参数 + var parameter = $"{input.Method.ToString().ToUpper()}&{input.Url}&{input.AccessKey}&{input.Timestamp}&{input.Nonce}"; + // 使用 HMAC-SHA256 协议创建基于哈希的消息身份验证代码 (HMAC),以appSecretByte 作为密钥,对上面拼接的参数进行计算签名,所得签名进行 Base-64 编码 + using HMAC hmac = new HMACSHA256(); + hmac.Key = appSecretByte; + var sign = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(parameter))); + return sign; + } + + /// + /// 获取开放接口身份分页列表 🔖 + /// + /// + /// + [DisplayName("获取开放接口身份分页列表")] + public async Task> Page(OpenAccessInput input) + { + return await _sysOpenAccessRep.AsQueryable() + .LeftJoin((u, a) => u.BindUserId == a.Id) + .LeftJoin((u, a, b) => u.BindTenantId == b.Id) + .LeftJoin((u, a, b, c) => b.OrgId == c.Id) + .WhereIF(!string.IsNullOrWhiteSpace(input.AccessKey?.Trim()), (u, a, b, c) => u.AccessKey.Contains(input.AccessKey)) + .Select((u, a, b, c) => new OpenAccessOutput + { + BindUserAccount = a.Account, + BindTenantName = c.Name, + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加开放接口身份")] + public async Task AddOpenAccess(AddOpenAccessInput input) + { + if (await _sysOpenAccessRep.AsQueryable().AnyAsync(u => u.AccessKey == input.AccessKey && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.O1000); + + var openAccess = input.Adapt(); + await _sysOpenAccessRep.InsertAsync(openAccess); + } + + /// + /// 更新开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新开放接口身份")] + public async Task UpdateOpenAccess(UpdateOpenAccessInput input) + { + if (await _sysOpenAccessRep.AsQueryable().AnyAsync(u => u.AccessKey == input.AccessKey && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.O1000); + + var openAccess = input.Adapt(); + _sysCacheService.Remove(CacheConst.KeyOpenAccess + openAccess.AccessKey); + + await _sysOpenAccessRep.UpdateAsync(openAccess); + } + + /// + /// 删除开放接口身份 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除开放接口身份")] + public async Task DeleteOpenAccess(DeleteOpenAccessInput input) + { + var openAccess = await _sysOpenAccessRep.GetFirstAsync(u => u.Id == input.Id); + if (openAccess != null) + _sysCacheService.Remove(CacheConst.KeyOpenAccess + openAccess.AccessKey); + + await _sysOpenAccessRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 创建密钥 🔖 + /// + /// + [DisplayName("创建密钥")] + public async Task CreateSecret() + { + return await Task.FromResult(Convert.ToBase64String(Guid.NewGuid().ToByteArray())[..^2]); + } + + /// + /// 根据 Key 获取对象 + /// + /// + /// + [NonAction] + public async Task GetByKey(string accessKey) + { + return await Task.FromResult( + _sysCacheService.GetOrAdd(CacheConst.KeyOpenAccess + accessKey, _ => + { + return _sysOpenAccessRep.AsQueryable() + .Includes(u => u.BindUser) + .Includes(u => u.BindUser, p => p.SysOrg) + .First(u => u.AccessKey == accessKey); + }) + ); + } + + /// + /// Signature 身份验证事件默认实现 + /// + [NonAction] + public static SignatureAuthenticationEvent GetSignatureAuthenticationEventImpl() + { + return new SignatureAuthenticationEvent + { + OnGetAccessSecret = context => + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + try + { + var openAccessService = context.HttpContext.RequestServices.GetRequiredService(); + var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult(); + return Task.FromResult(openAccess == null ? "" : openAccess.AccessSecret); + } + catch (Exception ex) + { + logger.LogError(ex, "开放接口身份验证"); + return Task.FromResult(""); + } + }, + OnValidated = context => + { + var openAccessService = context.HttpContext.RequestServices.GetRequiredService(); + var openAccess = openAccessService.GetByKey(context.AccessKey).GetAwaiter().GetResult(); + var identity = ((ClaimsIdentity)context.Principal!.Identity!); + + identity.AddClaims(new[] + { + new Claim(ClaimConst.UserId, openAccess.BindUserId + ""), + new Claim(ClaimConst.TenantId, openAccess.BindTenantId + ""), + new Claim(ClaimConst.Account, openAccess.BindUser.Account + ""), + new Claim(ClaimConst.RealName, openAccess.BindUser.RealName), + new Claim(ClaimConst.AccountType, ((int)openAccess.BindUser.AccountType).ToString()), + new Claim(ClaimConst.OrgId, openAccess.BindUser.OrgId + ""), + new Claim(ClaimConst.OrgName, openAccess.BindUser.SysOrg?.Name + ""), + new Claim(ClaimConst.OrgType, openAccess.BindUser.SysOrg?.Type + ""), + }); + return Task.CompletedTask; + } + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgInput.cs new file mode 100644 index 0000000..8c88745 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgInput.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class OrgInput : BaseIdInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 机构类型 + /// + public string Type { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddOrgInput : SysOrg +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "机构名称不能为空")] + public override string Name { get; set; } + + /// + /// 机构类型 + /// + [Dict("org_type", ErrorMessage = "机构类型不能合法", AllowNullValue = true, AllowEmptyStrings = true)] + public override string? Type { get; set; } +} + +public class UpdateOrgInput : AddOrgInput +{ +} + +public class DeleteOrgInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs new file mode 100644 index 0000000..85ed203 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/Dto/OrgTreeOutput.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 机构树形输出 +/// +public class OrgTreeOutput +{ + /// + /// 主键Id + /// + [SugarColumn(IsTreeKey = true)] + public long Id { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 父Id + /// + public long Pid { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 机构子项 + /// + public List Children { get; set; } + + /// + /// 是否禁止选中 + /// + public bool Disabled { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs new file mode 100644 index 0000000..0c64e60 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs @@ -0,0 +1,490 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统机构服务 🧩 +/// +[ApiDescriptionSettings(Order = 470)] +public class SysOrgService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysCacheService _sysCacheService; + private readonly SysUserExtOrgService _sysUserExtOrgService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysRoleOrgService _sysRoleOrgService; + private readonly SqlSugarRepository _sysOrgRep; + + public SysOrgService(UserManager userManager, + SysCacheService sysCacheService, + SysUserExtOrgService sysUserExtOrgService, + SysUserRoleService sysUserRoleService, + SysRoleOrgService sysRoleOrgService, + SqlSugarRepository sysOrgRep) + { + _userManager = userManager; + _sysCacheService = sysCacheService; + _sysUserExtOrgService = sysUserExtOrgService; + _sysUserRoleService = sysUserRoleService; + _sysRoleOrgService = sysRoleOrgService; + _sysOrgRep = sysOrgRep; + } + + /// + /// 获取机构列表 🔖 + /// + /// + [DisplayName("获取机构列表")] + public async Task> GetList([FromQuery] OrgInput input) + { + // 获取拥有的机构Id集合 + var userOrgIdList = await GetUserOrgIdList(); + + var queryable = _sysOrgRep.AsQueryable().WhereIF(input.TenantId > 0, u => u.TenantId == input.TenantId).OrderBy(u => new { u.OrderNo, u.Id }); + // 带条件筛选时返回列表数据 + if (!string.IsNullOrWhiteSpace(input.Name) || !string.IsNullOrWhiteSpace(input.Code) || !string.IsNullOrWhiteSpace(input.Type)) + { + return await queryable.WhereIF(userOrgIdList.Count > 0, u => userOrgIdList.Contains(u.Id)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code == input.Code) + .WhereIF(!string.IsNullOrWhiteSpace(input.Type), u => u.Type == input.Type) + .ToListAsync(); + } + + List orgTree; + if (_userManager.SuperAdmin) + { + orgTree = await queryable.ToTreeAsync(u => u.Children, u => u.Pid, input.Id); + } + else + { + orgTree = await queryable.ToTreeAsync(u => u.Children, u => u.Pid, input.Id, userOrgIdList.Select(d => (object)d).ToArray()); + // 递归禁用没权限的机构(防止用户修改或创建无权的机构和用户) + HandlerOrgTree(orgTree, userOrgIdList); + } + + var sysOrg = await _sysOrgRep.GetSingleAsync(u => u.Id == input.Id); + if (sysOrg == null) return orgTree; + + sysOrg.Children = orgTree; + orgTree = new List { sysOrg }; + return orgTree; + } + + /// + /// 递归禁用没权限的机构 + /// + /// + /// + private static void HandlerOrgTree(List orgTree, List userOrgIdList) + { + foreach (var org in orgTree) + { + org.Disabled = !userOrgIdList.Contains(org.Id); // 设置禁用/不可选择 + if (org.Children != null) + HandlerOrgTree(org.Children, userOrgIdList); + } + } + + /// + /// 获取机构树 🔖 + /// + /// + /// + [DisplayName("获取机构树")] + public async Task> GetTree([FromQuery] OrgInput input) + { + // 获取拥有的机构Id集合 + var userOrgIdList = await GetUserOrgIdList(); + + var queryable = _sysOrgRep.AsQueryable().WhereIF(input.TenantId > 0, u => u.TenantId == input.TenantId).OrderBy(u => new { u.OrderNo, u.Id }); + List orgTree; + if (_userManager.SuperAdmin) + { + orgTree = await queryable.Select().ToTreeAsync(u => u.Children, u => u.Pid, input.Id); + } + else + { + orgTree = await queryable.Select().ToTreeAsync(u => u.Children, u => u.Pid, input.Id, userOrgIdList.Select(d => (object)d).ToArray()); + // 递归禁用没权限的机构(防止用户修改或创建无权的机构和用户) + HandlerOrgTree(orgTree, userOrgIdList); + } + + var sysOrg = await _sysOrgRep.AsQueryable().Select().FirstAsync(u => u.Id == input.Id); + if (sysOrg == null) return orgTree; + + sysOrg.Children = orgTree; + orgTree = new List { sysOrg }; + return orgTree; + } + + /// + /// 递归禁用没权限的机构 + /// + /// + /// + private static void HandlerOrgTree(List orgTree, List userOrgIdList) + { + foreach (var org in orgTree) + { + org.Disabled = !userOrgIdList.Contains(org.Id); // 设置禁用/不可选择 + if (org.Children != null) + HandlerOrgTree(org.Children, userOrgIdList); + } + } + + /// + /// 增加机构 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加机构")] + public async Task AddOrg(AddOrgInput input) + { + if (!_userManager.SuperAdmin && input.Pid == 0) + throw Oops.Oh(ErrorCodeEnum.D2009); + + if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) + throw Oops.Oh(ErrorCodeEnum.D2002); + + if (!_userManager.SuperAdmin && input.Pid != 0) + { + // 新增机构父Id不是0,则进行权限校验 + var orgIdList = await GetUserOrgIdList(); + // 新增机构的父机构不在自己的数据范围内 + if (orgIdList.Count < 1 || !orgIdList.Contains(input.Pid)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + // 删除与此父机构有关的用户机构缓存 + if (input.Pid == 0) + { + DeleteAllUserOrgCache(0, 0); + } + else + { + var pOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Pid); + if (pOrg != null) + DeleteAllUserOrgCache(pOrg.Id, pOrg.Pid); + } + + var newOrg = await _sysOrgRep.AsInsertable(input.Adapt()).ExecuteReturnEntityAsync(); + return newOrg.Id; + } + + /// + /// 批量增加机构 + /// + /// + /// + [NonAction] + public async Task BatchAddOrgs(List orgs) + { + DeleteAllUserOrgCache(0, 0); + await _sysOrgRep.AsDeleteable().ExecuteCommandAsync(); + await _sysOrgRep.AsInsertable(orgs).ExecuteCommandAsync(); + } + + /// + /// 更新机构 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新机构")] + public async Task UpdateOrg(UpdateOrgInput input) + { + if (!_userManager.SuperAdmin && input.Pid == 0) + throw Oops.Oh(ErrorCodeEnum.D2012); + + if (input.Pid != 0) + { + //var pOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Pid); + //_ = pOrg ?? throw Oops.Oh(ErrorCodeEnum.D2000); + + // 若父机构发生变化则清空用户机构缓存 + var sysOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Id); + if (sysOrg != null && sysOrg.Pid != input.Pid) + { + // 删除与此机构、新父机构有关的用户机构缓存 + DeleteAllUserOrgCache(sysOrg.Id, input.Pid); + } + } + if (input.Id == input.Pid) + throw Oops.Oh(ErrorCodeEnum.D2001); + + if (await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D2002); + + // 父Id不能为自己的子节点 + var childIdList = await GetChildIdListWithSelfById(input.Id); + if (childIdList.Contains(input.Pid)) + throw Oops.Oh(ErrorCodeEnum.D2001); + + // 是否有权限操作此机构 + if (!_userManager.SuperAdmin) + { + var orgIdList = await GetUserOrgIdList(); + if (orgIdList.Count < 1 || !orgIdList.Contains(input.Id)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + await _sysOrgRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除机构 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除机构")] + public async Task DeleteOrg(DeleteOrgInput input) + { + var sysOrg = await _sysOrgRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + // 是否有权限操作此机构 + if (!_userManager.SuperAdmin) + { + var orgIdList = await GetUserOrgIdList(); + if (orgIdList.Count < 1 || !orgIdList.Contains(sysOrg.Id)) + throw Oops.Oh(ErrorCodeEnum.D2003); + } + + // 若机构为租户默认机构禁止删除 + var isTenantOrg = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => u.OrgId == input.Id); + if (isTenantOrg) + throw Oops.Oh(ErrorCodeEnum.D2008); + + // 若机构有用户则禁止删除 + var orgHasEmp = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => u.OrgId == input.Id); + if (orgHasEmp) + throw Oops.Oh(ErrorCodeEnum.D2004); + + // 若扩展机构有用户则禁止删除 + var hasExtOrgEmp = await _sysUserExtOrgService.HasUserOrg(sysOrg.Id); + if (hasExtOrgEmp) + throw Oops.Oh(ErrorCodeEnum.D2005); + + // 若子机构有用户则禁止删除 + var childOrgTreeList = await _sysOrgRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var childOrgIdList = childOrgTreeList.Select(u => u.Id).ToList(); + + // 若子机构有用户则禁止删除 + var cOrgHasEmp = await _sysOrgRep.ChangeRepository>() + .IsAnyAsync(u => childOrgIdList.Contains(u.OrgId)); + if (cOrgHasEmp) throw Oops.Oh(ErrorCodeEnum.D2007); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysOrgRep.Context.Queryable().AnyAsync(u => u.OrgId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D2010); + + // 删除与此机构、父机构有关的用户机构缓存 + DeleteAllUserOrgCache(sysOrg.Id, sysOrg.Pid); + + // 级联删除机构子节点 + await _sysOrgRep.DeleteAsync(u => childOrgIdList.Contains(u.Id)); + + // 级联删除角色机构数据 + await _sysRoleOrgService.DeleteRoleOrgByOrgIdList(childOrgIdList); + + // 级联删除用户机构数据 + await _sysUserExtOrgService.DeleteUserExtOrgByOrgIdList(childOrgIdList); + } + + /// + /// 删除与此机构、父机构有关的用户机构缓存 + /// + /// + /// + private void DeleteAllUserOrgCache(long orgId, long orgPid) + { + var userOrgKeyList = _sysCacheService.GetKeysByPrefixKey(CacheConst.KeyUserOrg); + if (userOrgKeyList is not { Count: > 0 }) return; + + foreach (var userOrgKey in userOrgKeyList) + { + var userOrgList = _sysCacheService.Get>(userOrgKey); + var userId = long.Parse(userOrgKey.Substring(CacheConst.KeyUserOrg)); + if (userOrgList != null && (userOrgList.Contains(orgId) || userOrgList.Contains(orgPid))) + SqlSugarFilter.DeleteUserOrgCache(userId, _sysOrgRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + + if (orgPid != 0) continue; + + var dataScope = _sysCacheService.Get($"{CacheConst.KeyRoleMaxDataScope}{userId}"); + if (dataScope == (int)DataScopeEnum.All) + SqlSugarFilter.DeleteUserOrgCache(userId, _sysOrgRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + } + } + + /// + /// 获取当前用户机构Id集合 + /// + /// + [NonAction] + public async Task> GetUserOrgIdList() + { + if (_userManager.SuperAdmin) return new(); + return await GetUserOrgIdList(_userManager.UserId, _userManager.OrgId); + } + + /// + /// 根据指定用户Id获取机构Id集合 + /// + /// + [NonAction] + public async Task> GetUserOrgIdList(long userId, long userOrgId) + { + var orgIdList = _sysCacheService.Get>($"{CacheConst.KeyUserOrg}{userId}"); // 取缓存 + if (orgIdList is { Count: >= 1 }) return orgIdList; + + // 本人创建机构集合 + var orgList0 = await _sysOrgRep.AsQueryable().Where(u => u.CreateUserId == userId).Select(u => u.Id).ToListAsync(); + + // 扩展机构集合 + var orgList1 = await _sysUserExtOrgService.GetUserExtOrgList(userId); + + // 角色机构集合 + var orgList2 = await GetUserRoleOrgIdList(userId, userOrgId); + + // 机构并集 + orgIdList = orgList1.Select(u => u.OrgId).Union(orgList2).Union(orgList0).ToList(); + + // 当前所属机构 + if (!orgIdList.Contains(userOrgId)) orgIdList.Add(userOrgId); + + _sysCacheService.Set($"{CacheConst.KeyUserOrg}{userId}", orgIdList, TimeSpan.FromDays(7)); // 存缓存 + return orgIdList; + } + + /// + /// 获取用户角色机构Id集合 + /// + /// + /// 用户的机构Id + /// + private async Task> GetUserRoleOrgIdList(long userId, long userOrgId) + { + var roleList = await _sysUserRoleService.GetUserRoleList(userId); + + if (roleList.Count < 1) return new(); // 空机构Id集合 + + return await GetUserOrgIdList(roleList, userId, userOrgId); + } + + /// + /// 判定用户是否有某角色权限 + /// + /// + /// 角色代码 + /// + [NonAction] + public async Task GetUserHasRole(long userId, SysRole role) + { + if (_userManager.SuperAdmin) + return true; + var userOrgId = _userManager.OrgId; + var roleList = await _sysUserRoleService.GetUserRoleList(userId); + if (roleList != null && roleList.Exists(r => r.Code == role.Code) == true) + return true; + roleList = new List { role }; + var orgIds = await GetUserOrgIdList(roleList, userId, userOrgId); + return orgIds.Contains(userOrgId); + } + + /// + /// 根据角色Id集合获取机构Id集合 + /// + /// + /// + /// 用户的机构Id + /// + private async Task> GetUserOrgIdList(List roleList, long userId, long userOrgId) + { + // 按最大范围策略设定(若同时拥有ALL和SELF权限,则结果ALL) + int strongerDataScopeType = (int)DataScopeEnum.Self; + + // 自定义数据范围的角色集合 + var customDataScopeRoleIdList = new List(); + + // 数据范围的机构集合 + var dataScopeOrgIdList = new List(); + + if (roleList is { Count: > 0 }) + { + roleList.ForEach(u => + { + if (u.DataScope == DataScopeEnum.Define) + { + customDataScopeRoleIdList.Add(u.Id); + strongerDataScopeType = (int)u.DataScope; // 自定义数据权限时也要更新最大范围 + } + else if ((int)u.DataScope <= strongerDataScopeType) + { + strongerDataScopeType = (int)u.DataScope; + // 根据数据范围获取机构集合 + var orgIds = GetOrgIdListByDataScope(userOrgId, strongerDataScopeType).GetAwaiter().GetResult(); + dataScopeOrgIdList = dataScopeOrgIdList.Union(orgIds).ToList(); + } + }); + } + + // 缓存当前用户最大角色数据范围 + _sysCacheService.Set(CacheConst.KeyRoleMaxDataScope + userId, strongerDataScopeType, TimeSpan.FromDays(7)); + + // 根据角色集合获取机构集合 + var roleOrgIdList = await _sysRoleOrgService.GetRoleOrgIdList(customDataScopeRoleIdList); + + // 并集机构集合 + return roleOrgIdList.Union(dataScopeOrgIdList).ToList(); + } + + /// + /// 根据数据范围获取机构Id集合 + /// + /// 用户的机构Id + /// + /// + private async Task> GetOrgIdListByDataScope(long userOrgId, int dataScope) + { + var orgId = userOrgId;//var orgId = _userManager.OrgId; + var orgIdList = new List(); + switch (dataScope) + { + // 若数据范围是全部,则获取所有机构Id集合 + case (int)DataScopeEnum.All: + orgIdList = await _sysOrgRep.AsQueryable().Select(u => u.Id).ToListAsync(); + break; + // 若数据范围是本部门及以下,则获取本节点和子节点集合 + case (int)DataScopeEnum.DeptChild: + orgIdList = await GetChildIdListWithSelfById(orgId); + break; + // 若数据范围是本部门不含子节点,则直接返回本部门 + case (int)DataScopeEnum.Dept: + orgIdList.Add(orgId); + break; + } + return orgIdList; + } + + /// + /// 根据节点Id获取子节点Id集合(包含自己) + /// + /// + /// + [NonAction] + public async Task> GetChildIdListWithSelfById(long pid) + { + var orgTreeList = await _sysOrgRep.AsQueryable().ToChildListAsync(u => u.Pid, pid, true); + return orgTreeList.Select(u => u.Id).ToList(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs new file mode 100644 index 0000000..f4815ee --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/Dto/PluginInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PagePluginInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPluginInput : SysPlugin +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "功能名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePluginInput : AddPluginInput +{ +} + +public class DeletePluginInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs new file mode 100644 index 0000000..06694f1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Plugin/SysPluginService.cs @@ -0,0 +1,126 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统动态插件服务 🧩 +/// +[ApiDescriptionSettings(Order = 245)] +public class SysPluginService : IDynamicApiController, ITransient +{ + private readonly IDynamicApiRuntimeChangeProvider _provider; + private readonly SqlSugarRepository _sysPluginRep; + private readonly UserManager _userManager; + + public SysPluginService(IDynamicApiRuntimeChangeProvider provider, + SqlSugarRepository sysPluginRep, + UserManager userManager) + { + _provider = provider; + _userManager = userManager; + _sysPluginRep = sysPluginRep; + } + + /// + /// 获取动态插件列表 🧩 + /// + /// + /// + [DisplayName("获取动态插件列表")] + public async Task> Page(PagePluginInput input) + { + return await _sysPluginRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加动态插件")] + public async Task AddPlugin(AddPluginInput input) + { + var isExist = await _sysPluginRep.IsAnyAsync(u => u.Name == input.Name || u.AssemblyName == input.AssemblyName); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900); + + // 添加动态程序集/接口 + input.AssemblyName = CompileAssembly(input.CsharpCode, input.AssemblyName); + + await _sysPluginRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新动态插件")] + public async Task UpdatePlugin(UpdatePluginInput input) + { + var isExist = await _sysPluginRep.IsAnyAsync(u => (u.Name == input.Name || u.AssemblyName == input.AssemblyName) && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1900); + + // 先移除再添加动态程序集/接口 + RemoveAssembly(input.AssemblyName); + input.AssemblyName = CompileAssembly(input.CsharpCode); + + await _sysPluginRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除动态插件 🧩 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除动态插件")] + public async Task DeletePlugin(DeletePluginInput input) + { + var plugin = await _sysPluginRep.GetByIdAsync(input.Id); + if (plugin == null) return; + + // 移除动态程序集/接口 + RemoveAssembly(plugin.AssemblyName); + + await _sysPluginRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 添加动态程序集/接口 🧩 + /// + /// + /// 程序集名称 + /// + [DisplayName("添加动态程序集/接口")] + public string CompileAssembly([FromBody] string csharpCode, [FromQuery] string assemblyName = default) + { + // 编译 C# 代码并返回动态程序集 + var dynamicAssembly = App.CompileCSharpClassCode(csharpCode, assemblyName); + + // 将程序集添加进动态 WebAPI 应用部件 + _provider.AddAssembliesWithNotifyChanges(dynamicAssembly); + + // 返回动态程序集名称 + return dynamicAssembly.GetName().Name; + } + + /// + /// 移除动态程序集/接口 🧩 + /// + [ApiDescriptionSettings(Name = "RemoveAssembly"), HttpPost] + [DisplayName("移除动态程序集/接口")] + public void RemoveAssembly(string assemblyName) + { + _provider.RemoveAssembliesWithNotifyChanges(assemblyName); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/Dto/PosInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/Dto/PosInput.cs new file mode 100644 index 0000000..615d347 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/Dto/PosInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PosInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPosInput : SysPos +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "职位名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePosInput : AddPosInput +{ +} + +public class DeletePosInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs new file mode 100644 index 0000000..19170f9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs @@ -0,0 +1,110 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统职位服务 🧩 +/// +[ApiDescriptionSettings(Order = 460)] +public class SysPosService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysPosRep; + private readonly SysUserExtOrgService _sysUserExtOrgService; + + public SysPosService(UserManager userManager, + SqlSugarRepository sysPosRep, + SysUserExtOrgService sysUserExtOrgService) + { + _userManager = userManager; + _sysPosRep = sysPosRep; + _sysUserExtOrgService = sysUserExtOrgService; + } + + /// + /// 获取职位列表 🔖 + /// + /// + /// + [DisplayName("获取职位列表")] + public async Task> GetList([FromQuery] PosInput input) + { + return await _sysPosRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Mapper(u => + { + u.UserList = _sysPosRep.Context.Queryable() + .Where(a => a.PosId == u.Id || SqlFunc.Subqueryable() + .Where(t => a.Id == t.UserId && t.PosId == u.Id).Any()) + .ToList(); + }) + .ToListAsync(); + } + + /// + /// 增加职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加职位")] + public async Task AddPos(AddPosInput input) + { + if (await _sysPosRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) throw Oops.Oh(ErrorCodeEnum.D6000); + + await _sysPosRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新职位")] + public async Task UpdatePos(UpdatePosInput input) + { + if (await _sysPosRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D6000); + + var sysPos = await _sysPosRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D6003); + if (!_userManager.SuperAdmin && sysPos.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D6002); + + await _sysPosRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除职位 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除职位")] + public async Task DeletePos(DeletePosInput input) + { + var sysPos = await _sysPosRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D6003); + if (!_userManager.SuperAdmin && sysPos.CreateUserId != _userManager.UserId) throw Oops.Oh(ErrorCodeEnum.D6002); + + // 若职位有用户则禁止删除 + var hasPosEmp = await _sysPosRep.ChangeRepository>() + .IsAnyAsync(u => u.PosId == input.Id); + if (hasPosEmp) throw Oops.Oh(ErrorCodeEnum.D6001); + + // 若附属职位有用户则禁止删除 + var hasExtPosEmp = await _sysUserExtOrgService.HasUserPos(input.Id); + if (hasExtPosEmp) throw Oops.Oh(ErrorCodeEnum.D6001); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysPosRep.Context.Queryable().AnyAsync(u => u.PosId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D6004); + + await _sysPosRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/Dto/PrintInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/Dto/PrintInput.cs new file mode 100644 index 0000000..3cf7b32 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/Dto/PrintInput.cs @@ -0,0 +1,42 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PagePrintInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddPrintInput : SysPrint +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "模板名称不能为空")] + public override string Name { get; set; } +} + +public class UpdatePrintInput : AddPrintInput +{ +} + +public class DeletePrintInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs new file mode 100644 index 0000000..8f70332 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs @@ -0,0 +1,91 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统打印模板服务 🧩 +/// +[ApiDescriptionSettings(Order = 305)] +public class SysPrintService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysPrintRep; + private readonly UserManager _userManager; + + public SysPrintService(SqlSugarRepository sysPrintRep, UserManager userManager) + { + _sysPrintRep = sysPrintRep; + _userManager = userManager; + } + + /// + /// 获取打印模板列表 🖨️ + /// + /// + /// + [DisplayName("获取打印模板列表")] + public async Task> Page(PagePrintInput input) + { + return await _sysPrintRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取打印模板 🖨️ + /// + /// + /// + [DisplayName("获取打印模板")] + public async Task GetPrint(string name) + { + return await _sysPrintRep.GetFirstAsync(u => u.Name == name); + } + + /// + /// 增加打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加打印模板")] + public async Task AddPrint(AddPrintInput input) + { + var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800); + + await _sysPrintRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新打印模板")] + public async Task UpdatePrint(UpdatePrintInput input) + { + var isExist = await _sysPrintRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1800); + + await _sysPrintRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除打印模板 🖨️ + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除打印模板")] + public async Task DeletePrint(DeletePrintInput input) + { + await _sysPrintRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/Dto/RegionInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/Dto/RegionInput.cs new file mode 100644 index 0000000..bb46561 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/Dto/RegionInput.cs @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageRegionInput : BasePageInput +{ + /// + /// 父节点Id + /// + public long Pid { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } +} + +public class RegionInput : BaseIdInput +{ +} + +public class AddRegionInput : SysRegion +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "名称不能为空")] + public override string Name { get; set; } +} + +public class UpdateRegionInput : AddRegionInput +{ +} + +public class DeleteRegionInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs new file mode 100644 index 0000000..fd257b3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Region/SysRegionService.cs @@ -0,0 +1,385 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using NewLife.Http; +using NewLife.Serialization; + +namespace Admin.NET.Core.Service; + +/// +/// 系统行政区域服务 🧩 +/// +[ApiDescriptionSettings(Order = 310)] +public class SysRegionService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysRegionRep; + private readonly SysConfigService _sysConfigService; + + public SysRegionService(SqlSugarRepository sysRegionRep, SysConfigService sysConfigService) + { + _sysRegionRep = sysRegionRep; + _sysConfigService = sysConfigService; + } + + /// + /// 获取行政区域分页列表 🔖 + /// + /// + /// + [DisplayName("获取行政区域分页列表")] + public async Task> Page(PageRegionInput input) + { + return await _sysRegionRep.AsQueryable() + .WhereIF(input.Pid > 0, u => u.Pid == input.Pid || u.Id == input.Pid) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取行政区域列表 🔖 + /// + /// + /// + [DisplayName("获取行政区域列表")] + public async Task> GetList([FromQuery] RegionInput input) + { + return await _sysRegionRep.GetListAsync(u => u.Pid == input.Id); + } + + /// + /// 获取行政区域树 🔖 + /// + /// + [DisplayName("获取行政区域树")] + public async Task> GetTree() + { + return await _sysRegionRep.AsQueryable().ToTreeAsync(u => u.Children, u => u.Pid, null); + } + + /// + /// 增加行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加行政区域")] + public async Task AddRegion(AddRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + if (input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + input.Pid = pRegion.Id; + } + + var isExist = await _sysRegionRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + var sysRegion = input.Adapt(); + var newRegion = await _sysRegionRep.AsInsertable(sysRegion).ExecuteReturnEntityAsync(); + return newRegion.Id; + } + + /// + /// 更新行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新行政区域")] + public async Task UpdateRegion(UpdateRegionInput input) + { + input.Code = input.Code?.Trim() ?? ""; + if (input.Code.Length != 12 && input.Code.Length != 9 && input.Code.Length != 6) throw Oops.Oh(ErrorCodeEnum.R2003); + + var sysRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Id); + if (sysRegion == null) throw Oops.Oh(ErrorCodeEnum.D1002); + + if (sysRegion.Pid != input.Pid && input.Pid != 0) + { + var pRegion = await _sysRegionRep.GetFirstAsync(u => u.Id == input.Pid); + pRegion ??= await _sysRegionRep.GetFirstAsync(u => u.Code == input.Pid.ToString()); + if (pRegion == null) throw Oops.Oh(ErrorCodeEnum.D2000); + + input.Pid = pRegion.Id; + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var childIdList = regionTreeList.Select(u => u.Id).ToList(); + if (childIdList.Contains(input.Pid)) throw Oops.Oh(ErrorCodeEnum.R2004); + } + + if (input.Id == input.Pid) throw Oops.Oh(ErrorCodeEnum.R2001); + + var isExist = await _sysRegionRep.IsAnyAsync(u => (u.Name == input.Name && u.Code == input.Code) && u.Id != sysRegion.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.R2002); + + //// 父Id不能为自己的子节点 + //var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + //var childIdList = regionTreeList.Select(u => u.Id).ToList(); + //if (childIdList.Contains(input.Pid)) + // throw Oops.Oh(ErrorCodeEnum.R2001); + + await _sysRegionRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除行政区域 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除行政区域")] + public async Task DeleteRegion(DeleteRegionInput input) + { + var regionTreeList = await _sysRegionRep.AsQueryable().ToChildListAsync(u => u.Pid, input.Id, true); + var regionIdList = regionTreeList.Select(u => u.Id).ToList(); + await _sysRegionRep.DeleteAsync(u => regionIdList.Contains(u.Id)); + } + + /// + /// 同步行政区域 🔖 + /// + /// + [DisplayName("同步行政区域")] + public async Task Sync() + { + var syncLevel = await _sysConfigService.GetConfigValue(ConfigConst.SysRegionSyncLevel); + if (syncLevel is < 1 or > 5) syncLevel = 3;//默认区县级 + + await _sysRegionRep.AsTenant().UseTranAsync(async () => + { + await _sysRegionRep.DeleteAsync(u => u.Id > 0); + await SyncByMap(syncLevel); + }, err => + { + throw Oops.Oh(ErrorCodeEnum.R2005, err.Message); + }); + + // var context = BrowsingContext.New(AngleSharp.Configuration.Default.WithDefaultLoader()); + // var dom = await context.OpenAsync(_url); + // + // // 省级列表 + // var itemList = dom.QuerySelectorAll("table.provincetable tr.provincetr td a"); + // if (itemList.Length == 0) throw Oops.Oh(ErrorCodeEnum.R2005); + // + // await _sysRegionRep.DeleteAsync(u => u.Id > 0); + // + // foreach (var element in itemList) + // { + // var item = (IHtmlAnchorElement)element; + // var list = new List(); + // + // var region = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = 0, + // Name = item.TextContent, + // Remark = item.Href, + // Level = 1, + // }; + // list.Add(region); + // + // // 市级 + // if (!string.IsNullOrEmpty(item.Href)) + // { + // var dom1 = await context.OpenAsync(item.Href); + // var itemList1 = dom1.QuerySelectorAll("table.citytable tr.citytr td a"); + // for (var i1 = 0; i1 < itemList1.Length; i1 += 2) + // { + // var item1 = (IHtmlAnchorElement)itemList1[i1 + 1]; + // var region1 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region.Id, + // Name = item1.TextContent, + // Code = itemList1[i1].TextContent, + // Remark = item1.Href, + // Level = 2, + // }; + // + // // 若URL中查询的一级行政区域缺少Code则通过二级区域填充 + // if (list.Count == 1 && !string.IsNullOrEmpty(region1.Code)) + // region.Code = region1.Code.Substring(0, 2).PadRight(region1.Code.Length, '0'); + // + // // 同步层级为“1-省级”退出 + // if (syncLevel < 2) break; + // + // list.Add(region1); + // + // // 区县级 + // if (string.IsNullOrEmpty(item1.Href) || syncLevel <= 2) continue; + // + // var dom2 = await context.OpenAsync(item1.Href); + // var itemList2 = dom2.QuerySelectorAll("table.countytable tr.countytr td a"); + // for (var i2 = 0; i2 < itemList2.Length; i2 += 2) + // { + // var item2 = (IHtmlAnchorElement)itemList2[i2 + 1]; + // var region2 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region1.Id, + // Name = item2.TextContent, + // Code = itemList2[i2].TextContent, + // Remark = item2.Href, + // Level = 3, + // }; + // list.Add(region2); + // + // // 街道级 + // if (string.IsNullOrEmpty(item2.Href) || syncLevel <= 3) continue; + // + // var dom3 = await context.OpenAsync(item2.Href); + // var itemList3 = dom3.QuerySelectorAll("table.towntable tr.towntr td a"); + // for (var i3 = 0; i3 < itemList3.Length; i3 += 2) + // { + // var item3 = (IHtmlAnchorElement)itemList3[i3 + 1]; + // var region3 = new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region2.Id, + // Name = item3.TextContent, + // Code = itemList3[i3].TextContent, + // Remark = item3.Href, + // Level = 4, + // }; + // list.Add(region3); + // + // // 村级 + // if (string.IsNullOrEmpty(item3.Href) || syncLevel <= 4) continue; + // + // var dom4 = await context.OpenAsync(item3.Href); + // var itemList4 = dom4.QuerySelectorAll("table.villagetable tr.villagetr td"); + // for (var i4 = 0; i4 < itemList4.Length; i4 += 3) + // { + // list.Add(new SysRegion + // { + // Id = YitIdHelper.NextId(), + // Pid = region3.Id, + // Name = itemList4[i4 + 2].TextContent, + // Code = itemList4[i4].TextContent, + // CityCode = itemList4[i4 + 1].TextContent, + // Level = 5, + // }); + // } + // } + // } + // } + // } + // + // //按省份同步快速写入提升同步效率,全部一次性写入容易出现从统计局获取数据失败 + // await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); + // } + } + + /// + /// 从统计局地图页面同步 + /// + /// + private async Task SyncByMap(int syncLevel) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Referer", "http://xzqh.mca.gov.cn/map"); + var html = await client.GetStringAsync("http://xzqh.mca.gov.cn/map"); + + var municipalityList = new List { "北京", "天津", "上海", "重庆" }; + var provList = Regex.Match(html, @"(?<=var json = )(\[\{.*?\}\])(?=;)").Value.ToJsonEntity>>(); + foreach (var dict1 in provList) + { + var list = new List(); + var provName = dict1.GetValueOrDefault("shengji"); + var province = new SysRegion + { + Id = YitIdHelper.NextId(), + Name = Regex.Replace(provName, "[((].*?[))]", ""), + Code = dict1.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict1.GetValueOrDefault("quhao"), + Level = 1, + Pid = 0, + }; + list.Add(province); + + if (syncLevel <= 1) continue; + + var prefList = await GetSelectList(provName); + foreach (var dict2 in prefList) + { + var prefName = dict2.GetValueOrDefault("diji"); + var city = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = dict2.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict2.GetValueOrDefault("quhao"), + Pid = province.Id, + Name = prefName, + Level = 2 + }; + if (municipalityList.Any(m => city.Name.StartsWith(m))) + { + city.Name = "市辖区"; + if (province.Code == city.Code) city.Code = province.Code.Substring(0, 2) + "0100"; + } + list.Add(city); + + if (syncLevel <= 2) continue; + + var countyList = await GetSelectList(provName, prefName); + foreach (var dict3 in countyList) + { + var countyName = dict3.GetValueOrDefault("xianji"); + var county = new SysRegion + { + Id = YitIdHelper.NextId(), + Code = dict3.GetValueOrDefault("quHuaDaiMa"), + CityCode = dict3.GetValueOrDefault("quhao"), + Name = countyName, + Pid = city.Id, + Level = 3 + }; + if (city.Code.IsNullOrEmpty()) + { + // 省直辖县级行政单位 节点无Code编码处理 + city.Code = county.Code.Substring(0, 3).PadRight(6, '0'); + } + list.Add(county); + } + } + + // 按省份同步快速写入提升同步效率,全部一次性写入容易出现从统计局获取数据失败 + // 仅当数据量大于1000或非Oracle数据库时采用大数据量写入方式(SqlSugar官方已说明,数据量小于1000时,其性能不如普通插入, oracle此方法不支持事务) + if (list.Count > 1000 && _sysRegionRep.Context.CurrentConnectionConfig.DbType != SqlSugar.DbType.Oracle) + { + // 执行大数据量写入 + try + { + await _sysRegionRep.Context.Fastest().BulkCopyAsync(list); + } + catch (SqlSugarException) + { + // 若写入失败则尝试普通插入方式 + await _sysRegionRep.InsertRangeAsync(list); + } + } + else + { + await _sysRegionRep.InsertRangeAsync(list); + } + } + + // 获取选择数据 + async Task>> GetSelectList(string prov, string prefecture = null) + { + var data = ""; + if (!string.IsNullOrWhiteSpace(prov)) data += $"shengji={prov}"; + if (!string.IsNullOrWhiteSpace(prefecture)) data += $"&diji={prefecture}"; + var json = await client.PostFormAsync("http://xzqh.mca.gov.cn/selectJson", data); + return json.ToJsonEntity>>(); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleInput.cs new file mode 100644 index 0000000..8f73a5e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleInput.cs @@ -0,0 +1,55 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class RoleInput : BaseIdInput +{ + /// + /// 状态 + /// + public virtual StatusEnum Status { get; set; } +} + +public class PageRoleInput : BasePageInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 名称 + /// + public virtual string Name { get; set; } + + /// + /// 编码 + /// + public virtual string Code { get; set; } +} + +public class AddRoleInput : SysRole +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "角色名称不能为空")] + public override string Name { get; set; } + + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} + +public class UpdateRoleInput : AddRoleInput +{ +} + +public class DeleteRoleInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs new file mode 100644 index 0000000..7c87f01 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色菜单 +/// +public class RoleMenuInput : BaseIdInput +{ + /// + /// 同步角色Id集合 + /// + public List RoleIdList { get; set; } + + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs new file mode 100644 index 0000000..e2a4511 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleMenuOutput .cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 角色菜单输出参数 +/// +public class RoleMenuOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 名称 + /// + public string Title { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs new file mode 100644 index 0000000..51e8010 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOrgInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 授权角色机构 +/// +public class RoleOrgInput : BaseIdInput +{ + /// + /// 数据范围 + /// + public int DataScope { get; set; } + + /// + /// 机构Id集合 + /// + public List OrgIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs new file mode 100644 index 0000000..9acd598 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/Dto/RoleOutput.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 角色列表输出参数 +/// +public class RoleOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 租户Id + /// + public long tenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleMenuService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleMenuService.cs new file mode 100644 index 0000000..f5d5891 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleMenuService.cs @@ -0,0 +1,99 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色菜单服务 +/// +public class SysRoleMenuService : ITransient +{ + private readonly SqlSugarRepository _sysRoleMenuRep; + private readonly SysCacheService _sysCacheService; + + public SysRoleMenuService(SqlSugarRepository sysRoleMenuRep, + SysCacheService sysCacheService) + { + _sysRoleMenuRep = sysRoleMenuRep; + _sysCacheService = sysCacheService; + } + + /// + /// 根据角色Id集合获取菜单Id集合 + /// + /// + /// + public async Task> GetRoleMenuIdList(List roleIdList) + { + return await _sysRoleMenuRep.AsQueryable() + .Where(u => roleIdList.Contains(u.RoleId)) + .Select(u => u.MenuId).ToListAsync(); + } + + /// + /// 授权角色菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权角色菜单")] + public async Task GrantRoleMenu(RoleMenuInput input) + { + await _sysRoleMenuRep.DeleteAsync(u => u.RoleId == input.Id); + + // 追加父级菜单 + var allIdList = await _sysRoleMenuRep.Context.Queryable().Select(u => new { u.Id, u.Pid }).ToListAsync(); + var pIdList = allIdList.ToChildList(u => u.Pid, u => u.Id, u => input.MenuIdList.Contains(u.Id)).Select(u => u.Pid).Distinct().ToList(); + input.MenuIdList = input.MenuIdList.Concat(pIdList).Distinct().Where(u => u != 0).ToList(); + + // 保存授权数据 + var menus = input.MenuIdList.Select(u => new SysRoleMenu + { + RoleId = input.Id, + MenuId = u + }).ToList(); + + // 同步授权数据 + if (input.RoleIdList?.Count() > 0) + { + await _sysRoleMenuRep.DeleteAsync(u => input.RoleIdList.Contains(u.RoleId)); + input.RoleIdList.ForEach(u => + { + menus.AddRange(input.MenuIdList.Select(v => new SysRoleMenu + { + RoleId = u, + MenuId = v + })); + }); + } + + await _sysRoleMenuRep.InsertRangeAsync(menus); + + // 清除缓存 + // _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserMenu); + _sysCacheService.RemoveByPrefixKey(CacheConst.KeyUserButton); + } + + /// + /// 根据菜单Id集合删除角色菜单 + /// + /// + /// + public async Task DeleteRoleMenuByMenuIdList(List menuIdList) + { + await _sysRoleMenuRep.DeleteAsync(u => menuIdList.Contains(u.MenuId)); + } + + /// + /// 根据角色Id删除角色菜单 + /// + /// + /// + public async Task DeleteRoleMenuByRoleId(long roleId) + { + await _sysRoleMenuRep.DeleteAsync(u => u.RoleId == roleId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleOrgService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleOrgService.cs new file mode 100644 index 0000000..fe07744 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleOrgService.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色机构服务 +/// +public class SysRoleOrgService : ITransient +{ + private readonly SqlSugarRepository _sysRoleOrgRep; + + public SysRoleOrgService(SqlSugarRepository sysRoleOrgRep) + { + _sysRoleOrgRep = sysRoleOrgRep; + } + + /// + /// 授权角色机构 + /// + /// + /// + public async Task GrantRoleOrg(RoleOrgInput input) + { + await _sysRoleOrgRep.DeleteAsync(u => u.RoleId == input.Id); + if (input.DataScope == (int)DataScopeEnum.Define) + { + var roleOrgList = input.OrgIdList.Select(u => new SysRoleOrg + { + RoleId = input.Id, + OrgId = u + }).ToList(); + await _sysRoleOrgRep.InsertRangeAsync(roleOrgList); + } + } + + /// + /// 根据角色Id集合获取角色机构Id集合 + /// + /// + /// + public async Task> GetRoleOrgIdList(List roleIdList) + { + if (roleIdList?.Count > 0) + { + return await _sysRoleOrgRep.AsQueryable() + .Where(u => roleIdList.Contains(u.RoleId)) + .Select(u => u.OrgId).ToListAsync(); + } + else return new List(); + } + + /// + /// 根据机构Id集合删除角色机构 + /// + /// + /// + public async Task DeleteRoleOrgByOrgIdList(List orgIdList) + { + await _sysRoleOrgRep.DeleteAsync(u => orgIdList.Contains(u.OrgId)); + } + + /// + /// 根据角色Id删除角色机构 + /// + /// + /// + public async Task DeleteRoleOrgByRoleId(long roleId) + { + await _sysRoleOrgRep.DeleteAsync(u => u.RoleId == roleId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs new file mode 100644 index 0000000..0dd4d21 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs @@ -0,0 +1,276 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统角色服务 🧩 +/// +[ApiDescriptionSettings(Order = 480)] +public class SysRoleService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysRoleRep; + private readonly SysRoleMenuService _sysRoleMenuService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysRoleOrgService _sysRoleOrgService; + private readonly SysMenuService _sysMenuService; + private readonly SysOrgService _sysOrgService; + private readonly SysCacheService _sysCacheService; + + public SysRoleService(UserManager userManager, + SysOrgService sysOrgService, + SysMenuService sysMenuService, + SysRoleOrgService sysRoleOrgService, + SqlSugarRepository sysRoleRep, + SysRoleMenuService sysRoleMenuService, + SysUserRoleService sysUserRoleService, + SysCacheService sysCacheService) + { + _userManager = userManager; + _sysRoleRep = sysRoleRep; + _sysOrgService = sysOrgService; + _sysMenuService = sysMenuService; + _sysRoleOrgService = sysRoleOrgService; + _sysRoleMenuService = sysRoleMenuService; + _sysUserRoleService = sysUserRoleService; + _sysCacheService = sysCacheService; + } + + /// + /// 获取角色分页列表 🔖 + /// + /// + /// + [DisplayName("获取角色分页列表")] + public async Task> Page(PageRoleInput input) + { + // 当前用户已拥有的角色集合 + var roleIdList = _userManager.SuperAdmin ? new List() : await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + return await _sysRoleRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId) // 若非超管,则只能操作本租户的角色 + .WhereIF(!_userManager.SuperAdmin && !_userManager.SysAdmin, u => u.CreateUserId == _userManager.UserId || roleIdList.Contains(u.Id)) // 若非超管且非系统管理员,则只能操作自己创建的角色|自己拥有的角色 + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取角色列表 🔖 + /// + /// + [DisplayName("获取角色列表")] + public async Task> GetList() + { + // 当前用户已拥有的角色集合 + var roleIdList = _userManager.SuperAdmin ? new List() : await _sysUserRoleService.GetUserRoleIdList(_userManager.UserId); + + return await _sysRoleRep.AsQueryable() + .WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId) // 若非超管,则只能操作本租户的角色 + .WhereIF(!_userManager.SuperAdmin && !_userManager.SysAdmin, u => u.CreateUserId == _userManager.UserId || roleIdList.Contains(u.Id)) // 若非超管且非系统管理员,则只显示自己创建和已拥有的角色 + .Where(u => u.Status != StatusEnum.Disable) // 非禁用的 + .OrderBy(u => new { u.OrderNo, u.Id }).Select().ToListAsync(); + } + + /// + /// 增加角色 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加角色")] + public async Task AddRole(AddRoleInput input) + { + if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code)) + throw Oops.Oh(ErrorCodeEnum.D1006); + + var newRole = await _sysRoleRep.AsInsertable(input.Adapt()).ExecuteReturnEntityAsync(); + input.Id = newRole.Id; + await UpdateRoleMenu(input); + } + + /// + /// 更新角色菜单权限 + /// + /// + /// + private async Task UpdateRoleMenu(AddRoleInput input) + { + if (input.MenuIdList == null || input.MenuIdList.Count < 1) return; + await GrantMenu(new RoleMenuInput() + { + Id = input.Id, + MenuIdList = input.MenuIdList.ToList() + }); + } + + /// + /// 更新角色 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新角色")] + public async Task UpdateRole(UpdateRoleInput input) + { + if (await _sysRoleRep.IsAnyAsync(u => u.Name == input.Name && u.Code == input.Code && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1006); + + await _sysRoleRep.AsUpdateable(input.Adapt()).IgnoreColumns(true) + .IgnoreColumns(u => new { u.DataScope }).ExecuteCommandAsync(); + + await UpdateRoleMenu(input); + } + + /// + /// 删除角色 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除角色")] + public async Task DeleteRole(DeleteRoleInput input) + { + // 若角色有用户则禁止删除 + var userIds = await _sysUserRoleService.GetUserIdList(input.Id); + if (userIds != null && userIds.Count > 0) throw Oops.Oh(ErrorCodeEnum.D1025); + + // 若有绑定注册方案则禁止删除 + var hasUserRegWay = await _sysRoleRep.Context.Queryable().AnyAsync(u => u.RoleId == input.Id); + if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D1033); + + var sysRole = await _sysRoleRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + await _sysRoleRep.DeleteAsync(sysRole); + + // 级联删除角色机构数据 + await _sysRoleOrgService.DeleteRoleOrgByRoleId(sysRole.Id); + + // 级联删除用户角色数据 + await _sysUserRoleService.DeleteUserRoleByRoleId(sysRole.Id); + + // 级联删除角色菜单数据 + await _sysRoleMenuService.DeleteRoleMenuByRoleId(sysRole.Id); + } + + /// + /// 授权角色菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权角色菜单")] + public async Task GrantMenu(RoleMenuInput input) + { + if (input.MenuIdList == null || input.MenuIdList.Count < 1) return; + + await ClearUserApiCache(input.Id); + + await _sysRoleMenuService.GrantRoleMenu(input); + } + + /// + /// 授权角色数据范围 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权角色数据范围")] + public async Task GrantDataScope(RoleOrgInput input) + { + // 删除与该角色相关的用户机构缓存 + var userIdList = await _sysUserRoleService.GetUserIdList(input.Id); + foreach (var userId in userIdList) + { + SqlSugarFilter.DeleteUserOrgCache(userId, _sysRoleRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + } + + var role = await _sysRoleRep.GetFirstAsync(u => u.Id == input.Id); + var dataScope = input.DataScope; + if (!_userManager.SuperAdmin) + { + switch (dataScope) + { + // 非超级管理员没有全部数据范围权限 + case (int)DataScopeEnum.All: throw Oops.Oh(ErrorCodeEnum.D1016); + // 若数据范围自定义,则判断授权数据范围是否有权限 + case (int)DataScopeEnum.Define: + { + var grantOrgIdList = input.OrgIdList; + if (grantOrgIdList.Count > 0) + { + var orgIdList = await _sysOrgService.GetUserOrgIdList(); + if (orgIdList.Count < 1) + throw Oops.Oh(ErrorCodeEnum.D1016); + if (!grantOrgIdList.All(u => orgIdList.Any(c => c == u))) + throw Oops.Oh(ErrorCodeEnum.D1016); + } + + break; + } + } + } + role.DataScope = (DataScopeEnum)dataScope; + await _sysRoleRep.AsUpdateable(role).UpdateColumns(u => new { u.DataScope }).ExecuteCommandAsync(); + await _sysRoleOrgService.GrantRoleOrg(input); + } + + /// + /// 根据角色Id获取菜单Id集合 🔖 + /// + /// + /// + [DisplayName("根据角色Id获取菜单Id集合")] + public async Task> GetOwnMenuList([FromQuery] RoleInput input) + { + var menuIds = await _sysRoleMenuService.GetRoleMenuIdList(new List { input.Id }); + return await _sysMenuService.ExcludeParentMenuOfFullySelected(menuIds); + } + + /// + /// 根据角色Id获取机构Id集合 🔖 + /// + /// + /// + [DisplayName("根据角色Id获取机构Id集合")] + public async Task> GetOwnOrgList([FromQuery] RoleInput input) + { + return await _sysRoleOrgService.GetRoleOrgIdList(new List { input.Id }); + } + + /// + /// 设置角色状态 🔖 + /// + /// + /// + [DisplayName("设置角色状态")] + public async Task SetStatus(RoleInput input) + { + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + return await _sysRoleRep.AsUpdateable() + .SetColumns(u => u.Status == input.Status) + .Where(u => u.Id == input.Id) + .ExecuteCommandAsync(); + } + + /// + /// 删除与该角色相关的用户接口缓存 + /// + /// + /// + [NonAction] + public async Task ClearUserApiCache(long roleId) + { + var userIdList = await _sysUserRoleService.GetUserIdList(roleId); + foreach (var userId in userIdList) + { + _sysCacheService.Remove($"{CacheConst.KeyUserButton}{userId}"); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs new file mode 100644 index 0000000..73887c2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/Dto/ScheduleInput.cs @@ -0,0 +1,44 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class ScheduleInput : BaseIdInput +{ + /// + /// 状态 + /// + public virtual FinishStatusEnum Status { get; set; } +} + +public class ListScheduleInput +{ + public DateTime? StartTime { get; set; } + + public DateTime? EndTime { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +public class AddScheduleInput : SysSchedule +{ + /// + /// 日程内容 + /// + [Required(ErrorMessage = "日程内容不能为空")] + public override string Content { get; set; } +} + +public class UpdateScheduleInput : AddScheduleInput +{ +} + +public class DeleteScheduleInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs new file mode 100644 index 0000000..c7f56e2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统日程服务 +/// +[ApiDescriptionSettings(Order = 295)] +public class SysScheduleService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SqlSugarRepository _sysSchedule; + + public SysScheduleService(UserManager userManager, + SqlSugarRepository sysSchedule) + { + _userManager = userManager; + _sysSchedule = sysSchedule; + } + + /// + /// 获取日程列表 + /// + /// + [DisplayName("获取日程列表")] + public async Task> Page(ListScheduleInput input) + { + return await _sysSchedule.AsQueryable() + .Where(u => u.UserId == _userManager.UserId) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.StartTime.ToString()), u => u.ScheduleTime >= input.StartTime) + .WhereIF(!string.IsNullOrWhiteSpace(input.EndTime.ToString()), u => u.ScheduleTime <= input.EndTime) + .OrderBy(u => u.StartTime, OrderByType.Asc) + .ToListAsync(); + } + + /// + /// 获取日程详情 + /// + /// + /// + [DisplayName("获取日程详情")] + public async Task GetDetail(long id) + { + return await _sysSchedule.GetFirstAsync(u => u.Id == id); + } + + /// + /// 增加日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加日程")] + public async Task AddUserSchedule(AddScheduleInput input) + { + input.UserId = _userManager.UserId; + await _sysSchedule.InsertAsync(input.Adapt()); + } + + /// + /// 更新日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新日程")] + public async Task UpdateUserSchedule(UpdateScheduleInput input) + { + await _sysSchedule.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除日程 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除日程")] + public async Task DeleteUserSchedule(DeleteScheduleInput input) + { + await _sysSchedule.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 设置日程状态 + /// + /// + /// + [DisplayName("设置日程状态")] + public async Task SetStatus(ScheduleInput input) + { + if (!Enum.IsDefined(typeof(FinishStatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + return await _sysSchedule.AsUpdateable() + .SetColumns(u => u.Status == input.Status) + .Where(u => u.Id == input.Id) + .ExecuteCommandAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs new file mode 100644 index 0000000..574cd41 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs @@ -0,0 +1,191 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +#if NET10_0_OR_GREATER + +using XiHan.Framework.Utils.Core; +using XiHan.Framework.Utils.Reflections; +using ReflectionHelper = XiHan.Framework.Utils.Reflections.ReflectionHelper; + +#endif // NET10_0_OR_GREATER + +namespace Admin.NET.Core.Service; + +/// +/// 系统服务器监控服务 🧩 +/// +[ApiDescriptionSettings(Order = 290, Description = "服务器监控")] +public class SysServerService : IDynamicApiController, ITransient +{ + public SysServerService() + { + } + +#if NET10_0_OR_GREATER + + /// + /// 获取服务器硬件信息 + /// + /// + [DisplayName("获取服务器硬件信息")] + public SystemInfo HardwareInfo() + { + var hardwareInfo = SystemInfoManager.GetSystemInfo(); + return hardwareInfo; + } + + /// + /// 获取服务器运行时信息 + /// + /// + [DisplayName("获取服务器运行时信息")] + public XiHan.Framework.Utils.Runtime.RuntimeInfo RuntimeInfo() + { + var systemRuntimeInfo = new XiHan.Framework.Utils.Runtime.RuntimeInfo(); + return systemRuntimeInfo; + } + + /// + /// 获取框架主要程序集 + /// + /// + [DisplayName("获取框架主要程序集")] + public List NuGetPackagesInfo() + { + var nuGetPackages = ReflectionHelper.GetNuGetPackages("Admin.NET"); + return nuGetPackages; + } + +#endif // NET10_0_OR_GREATER + + /// + /// 获取服务器配置信息 🔖 + /// + /// + [DisplayName("获取服务器配置信息")] + public dynamic GetServerBase() + { + return new + { + HostName = Environment.MachineName, // 主机名称 + SystemOs = ComputerUtil.GetOSInfo(),//RuntimeInformation.OSDescription, // 操作系统 + OsArchitecture = Environment.OSVersion.Platform.ToString() + " " + RuntimeInformation.OSArchitecture.ToString(), // 系统架构 + ProcessorCount = Environment.ProcessorCount + " 核", // CPU核心数 + SysRunTime = ComputerUtil.GetRunTime(), // 系统运行时间 + RemoteIp = ComputerUtil.GetIpFromOnline(), // 外网地址 + LocalIp = App.HttpContext?.Connection?.LocalIpAddress!.MapToIPv4().ToString(), // 本地地址 + FrameworkDescription = RuntimeInformation.FrameworkDescription + " / " + App.GetOptions().ConnectionConfigs[0].DbType.ToString(), // NET框架 + 数据库类型 + Environment = App.HostEnvironment.IsDevelopment() ? "Development" : "Production", + Wwwroot = App.WebHostEnvironment.WebRootPath, // 网站根目录 + Stage = App.HostEnvironment.IsStaging() ? "Stage环境" : "非Stage环境", // 是否Stage环境 + }; + } + + /// + /// 获取服务器使用信息 🔖 + /// + /// + [DisplayName("获取服务器使用信息")] + public dynamic GetServerUsed() + { + var programStartTime = Process.GetCurrentProcess().StartTime; + var totalMilliseconds = (DateTime.Now - programStartTime).TotalMilliseconds.ToString(); + var ts = totalMilliseconds.Contains('.') ? totalMilliseconds.Split('.')[0] : totalMilliseconds; + var programRunTime = DateTimeUtil.FormatTime(ts.ParseToLong()); + + var memoryMetrics = ComputerUtil.GetComputerInfo(); + return new + { + memoryMetrics.FreeRam, // 空闲内存 + memoryMetrics.UsedRam, // 已用内存 + memoryMetrics.TotalRam, // 总内存 + memoryMetrics.RamRate, // 内存使用率 + memoryMetrics.CpuRates, // Cpu使用率多CPU未完成 + memoryMetrics.CpuRate, // Cpu 1使用率 + StartTime = programStartTime.ToString("yyyy-MM-dd HH:mm:ss"), // 服务启动时间 + RunTime = programRunTime, // 服务运行时间 + }; + } + + /// + /// 获取服务器磁盘信息 🔖 + /// + /// + [DisplayName("获取服务器磁盘信息")] + public dynamic GetServerDisk() + { + return ComputerUtil.GetDiskInfos(); + } + + /// + /// 获取框架主要程序集 🔖 + /// + /// + [DisplayName("获取框架主要程序集")] + public dynamic GetAssemblyList() + { + var furionAssembly = typeof(App).Assembly.GetName(); + var sqlSugarAssembly = typeof(ISqlSugarClient).Assembly.GetName(); + var yitIdAssembly = typeof(YitIdHelper).Assembly.GetName(); + var redisAssembly = typeof(Redis).Assembly.GetName(); + var jsonAssembly = typeof(NewtonsoftJsonMvcCoreBuilderExtensions).Assembly.GetName(); + var excelAssembly = typeof(IExcelImporter).Assembly.GetName(); + var pdfAssembly = typeof(Magicodes.ExporterAndImporter.Pdf.IPdfExporter).Assembly.GetName(); + var wordAssembly = typeof(Magicodes.ExporterAndImporter.Word.IWordExporter).Assembly.GetName(); + var captchaAssembly = typeof(Lazy.Captcha.Core.ICaptcha).Assembly.GetName(); + var wechatApiAssembly = typeof(WechatApiClient).Assembly.GetName(); + var wechatTenpayAssembly = typeof(WechatTenpayClient).Assembly.GetName(); + var ossAssembly = typeof(OnceMi.AspNetCore.OSS.IOSSServiceFactory).Assembly.GetName(); + var parserAssembly = typeof(Parser).Assembly.GetName(); + var elasticsearchClientAssembly = typeof(Elastic.Clients.Elasticsearch.ElasticsearchClient).Assembly.GetName(); + var limitAssembly = typeof(AspNetCoreRateLimit.IpRateLimitMiddleware).Assembly.GetName(); + var htmlParserAssembly = typeof(AngleSharp.Html.Parser.HtmlParser).Assembly.GetName(); + var fluentEmailAssembly = typeof(MailKit.Net.Smtp.SmtpClient).Assembly.GetName(); + var qRCodeGeneratorAssembly = typeof(QRCoder.QRCodeGenerator).Assembly.GetName(); + var alibabaSendSmsRequestAssembly = typeof(AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest).Assembly.GetName(); + var tencentSendSmsRequestAssembly = typeof(TencentCloud.Sms.V20190711.Models.SendSmsRequest).Assembly.GetName(); + var rabbitMQAssembly = typeof(RabbitMQEventSourceStore).Assembly.GetName(); + var ldapConnectionAssembly = typeof(Novell.Directory.Ldap.LdapConnection).Assembly.GetName(); + var ipToolAssembly = typeof(IPTools.Core.IpTool).Assembly.GetName(); + var weixinAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Weixin.WeixinAuthenticationOptions).Assembly.GetName(); + var giteeAuthenticationOptionsAssembly = typeof(AspNet.Security.OAuth.Gitee.GiteeAuthenticationOptions).Assembly.GetName(); + var hashidsAssembly = typeof(HashidsNet.Hashids).Assembly.GetName(); + var sftpClientAssembly = typeof(Renci.SshNet.SftpClient).Assembly.GetName(); + var hardwareInfoAssembly = typeof(Hardware.Info.HardwareInfo).Assembly.GetName(); + + return new[] + { + new { furionAssembly.Name, furionAssembly.Version }, + new { sqlSugarAssembly.Name, sqlSugarAssembly.Version }, + new { yitIdAssembly.Name, yitIdAssembly.Version }, + new { redisAssembly.Name, redisAssembly.Version }, + new { jsonAssembly.Name, jsonAssembly.Version }, + new { excelAssembly.Name, excelAssembly.Version }, + new { pdfAssembly.Name, pdfAssembly.Version }, + new { wordAssembly.Name, wordAssembly.Version }, + new { captchaAssembly.Name, captchaAssembly.Version }, + new { wechatApiAssembly.Name, wechatApiAssembly.Version }, + new { wechatTenpayAssembly.Name, wechatTenpayAssembly.Version }, + new { ossAssembly.Name, ossAssembly.Version }, + new { parserAssembly.Name, parserAssembly.Version }, + new { elasticsearchClientAssembly.Name, elasticsearchClientAssembly.Version }, + new { limitAssembly.Name, limitAssembly.Version }, + new { htmlParserAssembly.Name, htmlParserAssembly.Version }, + new { fluentEmailAssembly.Name, fluentEmailAssembly.Version }, + new { qRCodeGeneratorAssembly.Name, qRCodeGeneratorAssembly.Version }, + new { alibabaSendSmsRequestAssembly.Name, alibabaSendSmsRequestAssembly.Version }, + new { tencentSendSmsRequestAssembly.Name, tencentSendSmsRequestAssembly.Version }, + new { rabbitMQAssembly.Name, rabbitMQAssembly.Version }, + new { ldapConnectionAssembly.Name, ldapConnectionAssembly.Version }, + new { ipToolAssembly.Name, ipToolAssembly.Version }, + new { weixinAuthenticationOptionsAssembly.Name, weixinAuthenticationOptionsAssembly.Version }, + new { giteeAuthenticationOptionsAssembly.Name, giteeAuthenticationOptionsAssembly.Version }, + new { hashidsAssembly.Name, hashidsAssembly.Version }, + new { sftpClientAssembly.Name, sftpClientAssembly.Version }, + new { hardwareInfoAssembly.Name, hardwareInfoAssembly.Version }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs new file mode 100644 index 0000000..30cdbae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateInput.cs @@ -0,0 +1,114 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class PageTemplateInput : BasePageInput +{ + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 编码 + /// + public string Code { get; set; } + + /// + /// 分组名称 + /// + public string GroupName { get; set; } + + /// + /// 模板类型 + /// + public TemplateTypeEnum? Type { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +/// +/// 新增模板输入参数 +/// +public class AddTemplateInput : SysTemplate +{ + /// + /// 名称 + /// + [Required(ErrorMessage = "名称不能为空")] + public override string Name { get; set; } + + /// + /// 模板类型 + /// + [Enum] + public override TemplateTypeEnum Type { get; set; } + + /// + /// 编码 + /// + [Required(ErrorMessage = "编码不能为空")] + public override string Code { get; set; } + + /// + /// 分组名称 + /// + [Required(ErrorMessage = "分组名称不能为空")] + public override string GroupName { get; set; } + + /// + /// 模板内容 + /// + [Required(ErrorMessage = "内容名称不能为空")] + public override string Content { get; set; } +} + +/// +/// 更新模板输入参数 +/// +public class UpdateTemplateInput : AddTemplateInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "Id不能为空")] + [DataValidation(ValidationTypes.Numeric)] + public override long Id { get; set; } +} + +/// +/// 预览模板输入参数 +/// +public class ProViewTemplateInput : BaseIdInput +{ + /// + /// 渲染参数 + /// + [Required(ErrorMessage = "渲染参数不能为空")] + public object Data { get; set; } +} + +/// +/// 模板渲染输入参数 +/// +public class RenderTemplateInput +{ + /// + /// 模板内容 + /// + [Required(ErrorMessage = "内容名称不能为空")] + public string Content { get; set; } + + /// + /// 渲染参数 + /// + [Required(ErrorMessage = "渲染参数不能为空")] + public object Data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs new file mode 100644 index 0000000..a3a2108 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/Dto/SysTemplateOutput.cs @@ -0,0 +1,11 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class SysTemplateOutput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs new file mode 100644 index 0000000..aec7269 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs @@ -0,0 +1,179 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统消息模板服务 🧩 +/// +[ApiDescriptionSettings(Order = 305)] +public class SysTemplateService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysTemplateRep; + private readonly UserManager _userManager; + private readonly IViewEngine _viewEngine; + + public SysTemplateService( + SqlSugarRepository sysTemplateRep, + IViewEngine viewEngine, + UserManager userManager) + { + _sysTemplateRep = sysTemplateRep; + _userManager = userManager; + _viewEngine = viewEngine; + } + + /// + /// 获取模板列表 📑 + /// + /// + /// + [ApiDescriptionSettings] + [DisplayName("获取模板列表")] + public async Task> Page(PageTemplateInput input) + { + return await _sysTemplateRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.GroupName), u => u.GroupName.Contains(input.GroupName)) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code)) + .WhereIF(input.Type.HasValue, u => u.Type == input.Type) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取模板 📑 + /// + /// + /// + [DisplayName("获取模板")] + [ApiDescriptionSettings] + public async Task GetTemplate(string code) + { + return await _sysTemplateRep.GetFirstAsync(u => u.Name == code); + } + + /// + /// 预览模板内容 📑 + /// + /// + /// + [DisplayName("预览模板内容")] + [ApiDescriptionSettings] + public async Task ProView(ProViewTemplateInput input) + { + var template = await _sysTemplateRep.GetFirstAsync(u => u.Id == input.Id); + return await RenderAsync(template.Content, input.Data); + } + + /// + /// 增加模板 📑 + /// + /// + /// + [DisplayName("增加模板")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task AddTemplate(AddTemplateInput input) + { + var isExist = await _sysTemplateRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1000); + + isExist = await _sysTemplateRep.IsAnyAsync(u => u.Code == input.Code); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1001); + + await _sysTemplateRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新模板 📑 + /// + /// + /// + [DisplayName("更新模板")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task UpdateTemplate(UpdateTemplateInput input) + { + var isExist = await _sysTemplateRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1000); + + isExist = await _sysTemplateRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.T1001); + + await _sysTemplateRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除模板 📑 + /// + /// + /// + [DisplayName("删除模板")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task DeleteTemplate(BaseIdInput input) + { + await _sysTemplateRep.DeleteAsync(u => u.Id == input.Id); + } + + /// + /// 获取分组列表 🔖 + /// + /// + [ApiDescriptionSettings] + [DisplayName("获取分组列表")] + public async Task> GetGroupList() + { + return await _sysTemplateRep.AsQueryable() + .GroupBy(u => u.GroupName) + .Select(u => u.GroupName).ToListAsync(); + } + + /// + /// 渲染模板内容 📑 + /// + /// + /// + [DisplayName("渲染模板内容")] + [ApiDescriptionSettings, HttpPost] + public async Task Render(RenderTemplateInput input) + { + return await RenderAsync(input.Content, input.Data); + } + + /// + /// 渲染模板内容 📑 + /// + /// + /// + /// + [NonAction] + public async Task RenderAsync(string content, object data) + { + return await _viewEngine.RunCompileFromCachedAsync(Regex.Replace(content, "@\\((.*?)\\)", "@(Model.$1)"), data, builderAction: builder => + { + builder.AddAssemblyReferenceByName("System.Text.RegularExpressions"); + builder.AddAssemblyReferenceByName("System.Collections"); + builder.AddAssemblyReferenceByName("System.Linq"); + + builder.AddUsing("System.Text.RegularExpressions"); + builder.AddUsing("System.Collections.Generic"); + builder.AddUsing("System.Linq"); + }); + } + + /// + /// 根据编码渲染模板内容 + /// + /// + /// + /// + [NonAction] + public async Task RenderByCode(string code, Dictionary data) + { + var template = await _sysTemplateRep.GetFirstAsync(u => u.Code == code); + return await RenderAsync(template.Content, data); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs new file mode 100644 index 0000000..f0984a8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs @@ -0,0 +1,129 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class TenantInput : BaseIdInput +{ + /// + /// 状态 + /// + public StatusEnum Status { get; set; } +} + +public class PageTenantInput : BasePageInput +{ + /// + /// 名称 + /// + public virtual string Name { get; set; } + + /// + /// 电话 + /// + public virtual string Phone { get; set; } +} + +public class AddTenantInput : TenantOutput +{ + /// + /// 租户名称 + /// + [Required(ErrorMessage = "租户名称不能为空"), MinLength(2, ErrorMessage = "租户名称不能少于2个字符")] + public override string Name { get; set; } + + /// + /// 租管账号 + /// + [Required(ErrorMessage = "租管账号不能为空"), MinLength(3, ErrorMessage = "租管账号不能少于3个字符")] + public override string AdminAccount { get; set; } + + /// + /// 系统主标题 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Title)", "系统主标题不能为空")] + public override string Title { get; set; } + + /// + /// 系统副标题 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceTitle)", "系统副标题不能为空")] + public override string ViceTitle { get; set; } + + /// + /// 系统描述 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(ViceDesc)", "系统描述不能为空")] + public override string ViceDesc { get; set; } + + /// + /// 版权说明 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Copyright)", "版权说明不能为空")] + public override string Copyright { get; set; } + + /// + /// ICP备案号 + /// + public override string Icp { get; set; } + + /// + /// ICP地址 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && !string.IsNullOrWhiteSpace(Icp) && string.IsNullOrWhiteSpace(IcpUrl)", "ICP地址不能为空")] + public override string IcpUrl { get; set; } + + /// + /// Logo图片Base64码 + /// + [CommonValidation("!string.IsNullOrWhiteSpace(Host) && string.IsNullOrWhiteSpace(Logo) && string.IsNullOrWhiteSpace(LogoBase64)", "图标不能为空")] + public virtual string LogoBase64 { get; set; } + + /// + /// Logo文件名 + /// + public virtual string LogoFileName { get; set; } +} + +public class UpdateTenantInput : AddTenantInput +{ +} + +public class DeleteTenantInput : BaseIdInput +{ +} + +/// +/// 租户菜单 +/// +public class TenantMenuInput : BaseIdInput +{ + /// + /// 同步租户Id集合 + /// + public List TenantIdList { get; set; } + + /// + /// 菜单Id集合 + /// + public List MenuIdList { get; set; } +} + +public class TenantUserInput +{ + /// + /// 用户Id + /// + public long UserId { get; set; } +} + +public class TenantIdInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs new file mode 100644 index 0000000..6837efb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class TenantOutput : SysTenant +{ + /// + /// 租户名称 + /// + public virtual string Name { get; set; } + + /// + /// 管理员账号 + /// + public virtual string AdminAccount { get; set; } + + /// + /// 电子邮箱 + /// + public virtual string Email { get; set; } + + /// + /// 电话 + /// + public virtual string Phone { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs new file mode 100644 index 0000000..0366273 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs @@ -0,0 +1,772 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统租户管理服务 🧩 +/// +[ApiDescriptionSettings(Order = 390)] +public class SysTenantService : IDynamicApiController, ITransient +{ + private static readonly SysMenuService SysMenuService = App.GetService(); + private readonly SqlSugarRepository _sysUserExtOrgRep; + private readonly SqlSugarRepository _sysTenantMenuRep; + private readonly SqlSugarRepository _sysRoleMenuRep; + private readonly SqlSugarRepository _userRoleRep; + private readonly SqlSugarRepository _sysTenantRep; + private readonly SqlSugarRepository _sysRoleRep; + private readonly SqlSugarRepository _sysUserRep; + private readonly SqlSugarRepository _sysOrgRep; + private readonly SqlSugarRepository _sysPosRep; + private readonly SysRoleMenuService _sysRoleMenuService; + private readonly SysConfigService _sysConfigService; + private readonly SysCacheService _sysCacheService; + private readonly UploadOptions _uploadOptions; + + public SysTenantService( + SqlSugarRepository sysUserExtOrgRep, + SqlSugarRepository sysTenantMenuRep, + SqlSugarRepository sysRoleMenuRep, + SqlSugarRepository userRoleRep, + SqlSugarRepository sysTenantRep, + SqlSugarRepository sysUserRep, + SqlSugarRepository sysRoleRep, + SqlSugarRepository sysOrgRep, + SqlSugarRepository sysPosRep, + IOptions uploadOptions, + SysRoleMenuService sysRoleMenuService, + SysConfigService sysConfigService, + SysCacheService sysCacheService) + { + _sysTenantRep = sysTenantRep; + _sysOrgRep = sysOrgRep; + _sysRoleRep = sysRoleRep; + _sysPosRep = sysPosRep; + _sysUserRep = sysUserRep; + _userRoleRep = userRoleRep; + _sysRoleMenuRep = sysRoleMenuRep; + _sysCacheService = sysCacheService; + _uploadOptions = uploadOptions.Value; + _sysConfigService = sysConfigService; + _sysTenantMenuRep = sysTenantMenuRep; + _sysUserExtOrgRep = sysUserExtOrgRep; + _sysRoleMenuService = sysRoleMenuService; + } + + /// + /// 获取租户分页列表 🔖 + /// + /// + /// + [DisplayName("获取租户分页列表")] + public async Task> Page(PageTenantInput input) + { + return await _sysTenantRep.AsQueryable() + .LeftJoin((u, a) => u.UserId == a.Id).ClearFilter() + .LeftJoin((u, a, b) => u.OrgId == b.Id).ClearFilter() + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), (u, a) => a.Phone.Contains(input.Phone.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (u, a, b) => b.Name.Contains(input.Name.Trim())) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Select((u, a, b) => new TenantOutput + { + Id = u.Id, + OrgId = b.Id, + Name = b.Name, + UserId = a.Id, + AdminAccount = a.Account, + Phone = a.Phone, + Host = u.Host, + Email = a.Email, + TenantType = u.TenantType, + DbType = u.DbType, + Connection = u.Connection, + ConfigId = u.ConfigId, + OrderNo = u.OrderNo, + Remark = u.Remark, + Status = u.Status, + CreateTime = u.CreateTime, + CreateUserName = u.CreateUserName, + UpdateTime = u.UpdateTime, + UpdateUserName = u.UpdateUserName, + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取租户列表 + /// + /// + [AllowAnonymous] + [DisplayName("获取租户列表"), HttpGet] + public async Task GetList() + { + return await _sysTenantRep.AsQueryable() + .LeftJoin((u, a) => u.OrgId == a.Id).ClearFilter() + .Where(u => u.Status == StatusEnum.Enable) + .Select((u, a) => new + { + Label = SqlFunc.HasValue(u.Title) ? $"{u.Title}-{a.Name}" : a.Name, + Host = u.Host.ToLower(), + Value = u.Id, + }).ToListAsync(); + } + + /// + /// 获取当前租户系统信息 + /// + /// + [NonAction] + public async Task GetCurrentTenantSysInfo() + { + var tenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0"); + var host = App.HttpContext.Request.Host.Host.ToLower(); + var tenant = await _sysTenantRep.AsQueryable() + .WhereIF(tenantId > 0, u => u.Id == tenantId && SqlFunc.ToLower(u.Host).Contains(host)) + .WhereIF(!(tenantId > 0), u => SqlFunc.ToLower(u.Host).Contains(host)) + .FirstAsync(); + tenant ??= await _sysTenantRep.GetFirstAsync(u => u.Id == SqlSugarConst.DefaultTenantId); + _ = tenant ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return tenant; + } + + /// + /// 获取库隔离的租户列表 + /// + /// + [NonAction] + public async Task> GetTenantDbList() + { + return await _sysTenantRep.GetListAsync(u => u.TenantType == TenantTypeEnum.Db && u.Status == StatusEnum.Enable); + } + + /// + /// 增加租户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加租户")] + public async Task AddTenant(AddTenantInput input) + { + var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300); + + input.Host = input.Host?.ToLower(); + isExist = await _sysTenantRep.IsAnyAsync(u => !string.IsNullOrWhiteSpace(u.Host) && u.Host == input.Host); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303); + + isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1301); + + // 从库配置判断 + if (!string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections)) throw Oops.Oh(ErrorCodeEnum.D1302); + + switch (input.TenantType) + { + // Id隔离时设置与主库一致 + case TenantTypeEnum.Id: + var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig; + input.DbType = config.DbType; + input.Connection = config.ConnectionString; + break; + + case TenantTypeEnum.Db: + if (string.IsNullOrWhiteSpace(input.Connection)) + throw Oops.Oh(ErrorCodeEnum.Z1004); + break; + + default: + throw Oops.Oh(ErrorCodeEnum.D3004); + } + if (input.EnableReg == YesNoEnum.N) input.RegWayId = null; + var tenant = input.Adapt(); + + // 设置logo + SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName); + + tenant.Id = _sysTenantRep.InsertReturnEntity(tenant).Id; + await InitNewTenant(tenant); + + await CacheTenant(); + } + + /// + /// 设置logo + /// + /// + /// + /// + [NonAction] + public void SetLogoUrl(SysTenant tenant, string logoBase64, string logoFileName) + { + if (string.IsNullOrEmpty(tenant?.Logo) && string.IsNullOrEmpty(tenant?.Logo)) return; + + // 旧图标文件相对路径 + var oldSysLogoRelativeFilePath = tenant.Logo ?? ""; + var oldSysLogoAbsoluteFilePath = Path.Combine(App.WebHostEnvironment.WebRootPath, oldSysLogoRelativeFilePath.TrimStart('/')); + + var groups = Regex.Match(logoBase64, @"data:image/(?.+?);base64,(?.+)").Groups; + + //var type = groups["type"].Value; + var base64Data = groups["data"].Value; + var binData = Convert.FromBase64String(base64Data); + + // 根据文件名取扩展名 + var ext = string.IsNullOrWhiteSpace(logoFileName) ? ".png" : Path.GetExtension(logoFileName); + + // 本地图标保存路径 + var fileName = $"{tenant.ViceTitle}-logo{ext}".ToLower(); + var path = _uploadOptions.Path.Replace("/{yyyy}/{MM}/{dd}", ""); + path = path.StartsWith("/") || Regex.IsMatch(path, "^[A-Z|a-z]:") ? path : Path.Combine(App.WebHostEnvironment.WebRootPath, path); + var absoluteFilePath = Path.Combine(path, fileName); + + // 删除已存在文件 + if (File.Exists(oldSysLogoAbsoluteFilePath)) File.Delete(oldSysLogoAbsoluteFilePath); + + // 创建文件夹 + var absoluteFileDir = Path.GetDirectoryName(absoluteFilePath); + if (!Directory.Exists(absoluteFileDir)) Directory.CreateDirectory(absoluteFileDir); + + // 保存图标文件 + File.WriteAllBytesAsync(absoluteFilePath, binData); + + // 保存图标配置 + tenant.Logo = $"/upload/{fileName}"; + } + + /// + /// 设置租户状态 🔖 + /// + /// + /// + [DisplayName("设置租户状态")] + public async Task SetStatus(TenantInput input) + { + var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id); + if (tenant == null || tenant.ConfigId == SqlSugarConst.MainConfigId) throw Oops.Oh(ErrorCodeEnum.Z1001); + + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) throw Oops.Oh(ErrorCodeEnum.D3005); + + tenant.Status = input.Status; + return await _sysTenantRep.AsUpdateable(tenant).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + } + + /// + /// 新增租户初始化 + /// + /// + private async Task InitNewTenant(TenantOutput tenant) + { + var tenantId = tenant.Id; + var tenantName = tenant.Name; + + // 初始化机构 + var newOrg = new SysOrg { TenantId = tenantId, Pid = 0, Name = tenantName, Code = tenantName, Remark = tenantName, }; + await _sysOrgRep.InsertAsync(newOrg); + + // 初始化默认角色 + var newRole = new SysRole { TenantId = tenantId, Name = CommonConst.DefaultBaseRoleName, Code = CommonConst.DefaultBaseRoleCode, DataScope = DataScopeEnum.Self, Remark = "此角色为系统自动创建角色" }; + var baseRole = await _sysRoleRep.InsertReturnEntityAsync(newRole); + var baseRoleMenuIdList = GetBaseRoleMenuIdList().ToList(); + await _sysRoleMenuService.GrantRoleMenu(new RoleMenuInput { Id = baseRole.Id, MenuIdList = baseRoleMenuIdList.Select(u => u.MenuId).ToList() }); + + // 初始化职位 + var newPos = new SysPos { TenantId = tenantId, Name = "管理员-" + tenantName, Code = tenantName, Remark = tenantName }; + await _sysPosRep.InsertAsync(newPos); + + // 初始化租户管理员账号 + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var newUser = new SysUser + { + TenantId = tenantId, + Account = tenant.AdminAccount, + Password = CryptogramUtil.Encrypt(password), + NickName = "系统管理员", + Email = tenant.Email, + Phone = tenant.Phone, + AccountType = AccountTypeEnum.SysAdmin, + OrgId = newOrg.Id, + PosId = newPos.Id, + Birthday = DateTime.Parse("2000-01-01"), + RealName = "系统管理员", + Remark = "系统管理员" + tenantName, + }; + await _sysUserRep.InsertAsync(newUser); + + // 关联租户组织机构和管理员用户 + await _sysTenantRep.UpdateAsync(u => new SysTenant { UserId = newUser.Id, OrgId = newOrg.Id }, u => u.Id == tenantId); + + // 默认租户管理员角色菜单集合 + var menuList = GetTenantDefaultMenuList().ToList(); + await GrantMenu(new TenantMenuInput { Id = tenantId, MenuIdList = menuList.Select(u => u.MenuId).ToList() }); + } + + /// + /// 获取租户默认菜单 + /// + /// 如果某租户需要定制主页,可以忽略 + /// + [NonAction] + public IEnumerable GetTenantDefaultMenuList(bool ignoreHome = false) + { + var menuList = new List(); + + // 默认数据库配置 + var defaultConfig = App.GetOptions().ConnectionConfigs.FirstOrDefault(); + //从程序集中获取种子菜单数据,种子菜单存在于其他类库中,需要动态加载 + var menuSeedDataTypeList = GetSeedDataTypes(defaultConfig, nameof(SysMenuSeedData)); + var allMenuList = new List(); + foreach (var menu in menuSeedDataTypeList) + { + var menuSeedDataList = ((IEnumerable)menu.GetMethod("HasData")?.Invoke(Activator.CreateInstance(menu), null))?.Cast(); + if (menuSeedDataList != null) + { + allMenuList.AddRange(menuSeedDataList); + } + } + + //实现三个层级的菜单 + var topMenuList = allMenuList.Where(u => u.Pid == 0 && u.Type == MenuTypeEnum.Dir).ToList(); + menuList.AddRange(topMenuList); + + var childMenuList = allMenuList.ToChildList(u => u.Id, u => u.Pid, u => topMenuList.Select(p => p.Id).Contains(u.Pid)); + menuList.AddRange(childMenuList); + + var endMenuList = allMenuList.ToChildList(u => u.Id, u => u.Pid, u => childMenuList.Select(p => p.Id).Contains(u.Pid)); + if (endMenuList != null) + { + menuList.AddRange(endMenuList); + } + //是否需要排除首页菜单 + if (ignoreHome) menuList = menuList.Where(u => !(u.Type == MenuTypeEnum.Menu && u.Name == "home")).ToList(); + + menuList = menuList.Distinct().ToList(); + + return menuList.Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.Id, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.Id + }); + } + + /// + /// 获取种子数据类型 + /// + /// 数据库连接配置 + /// + /// 种子数据类型列表 + [NonAction] + private List GetSeedDataTypes(DbConnectionConfig config, string typeName) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.Name == typeName && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)))) + .WhereIF(config.SeedSettings.EnableIncreSeed, u => u.IsDefined(typeof(IncreSeedAttribute), false)) + .OrderBy(u => u.GetCustomAttributes(typeof(SeedDataAttribute), false).Length > 0 ? ((SeedDataAttribute)u.GetCustomAttributes(typeof(SeedDataAttribute), false)[0]).Order : 0) + .ToList(); + } + + /// + /// 获取租户默认菜单 + /// + /// + [NonAction] + public IEnumerable GetBaseRoleMenuIdList() + { + var menuList = new List(); + var allMenuList = new SysMenuSeedData().HasData().ToList(); + + var dashboardMenu = allMenuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "工作台"); + menuList.AddRange(allMenuList.ToChildList(u => u.Id, u => u.Pid, dashboardMenu.Id)); + + var systemMenu = allMenuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "系统管理"); + menuList.Add(systemMenu); + menuList.AddRange(allMenuList.ToChildList(u => u.Id, u => u.Pid, u => u.Pid == systemMenu.Id && new[] { "机构管理", "个人中心" }.Contains(u.Title))); + menuList = menuList.Where(u => !new[] { "增加", "编辑", "删除" }.Contains(u.Title)).ToList(); + + return menuList.Select(u => new SysTenantMenu + { + Id = CommonUtil.GetFixedHashCode("" + SqlSugarConst.DefaultTenantId + u.Id, 1300000000000), + TenantId = SqlSugarConst.DefaultTenantId, + MenuId = u.Id + }); + } + + /// + /// 删除租户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除租户")] + public async Task DeleteTenant(DeleteTenantInput input) + { + // 禁止删除默认租户 + if (input.Id.ToString() == SqlSugarConst.MainConfigId) throw Oops.Oh(ErrorCodeEnum.D1023); + + // 若账号为开放接口绑定租户则禁止删除 + var isOpenAccessTenant = await _sysTenantRep.ChangeRepository>().IsAnyAsync(u => u.BindTenantId == input.Id); + if (isOpenAccessTenant) throw Oops.Oh(ErrorCodeEnum.D1031); + + await _sysTenantRep.DeleteAsync(u => u.Id == input.Id); + + await CacheTenant(input.Id); + + // 删除与租户相关的表数据 + await _sysTenantMenuRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + + var users = await _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.Id).ToListAsync(); + var userIds = users.Select(u => u.Id).ToList(); + await _sysUserRep.AsDeleteable().Where(u => userIds.Contains(u.Id)).ExecuteCommandAsync(); + await _userRoleRep.AsDeleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysUserExtOrgRep.AsDeleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => userIds.Contains(u.UserId)).ExecuteCommandAsync(); + + var roleIds = await _sysRoleRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.Id).Select(u => u.Id).ToListAsync(); + await _sysRoleRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + await _sysRoleMenuRep.AsDeleteable().Where(u => roleIds.Contains(u.RoleId)).ExecuteCommandAsync(); + await _sysTenantRep.Context.Deleteable().Where(u => roleIds.Contains(u.RoleId)).ExecuteCommandAsync(); + + await _sysOrgRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + + await _sysPosRep.AsDeleteable().Where(u => u.TenantId == input.Id).ExecuteCommandAsync(); + } + + /// + /// 更新租户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新租户")] + public async Task UpdateTenant(UpdateTenantInput input) + { + var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.OrgId); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300); + + input.Host = input.Host?.ToLower(); + isExist = await _sysTenantRep.IsAnyAsync(u => !string.IsNullOrWhiteSpace(u.Host) && u.Host == input.Host && u.Id != input.Id); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303); + + isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.AdminAccount && u.Id != input.UserId); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1301); + + // Id隔离时设置与主库一致 + switch (input.TenantType) + { + case TenantTypeEnum.Id: + var config = _sysTenantRep.AsSugarClient().CurrentConnectionConfig; + input.DbType = config.DbType; + input.Connection = config.ConnectionString; + break; + + case TenantTypeEnum.Db: + if (string.IsNullOrWhiteSpace(input.Connection)) + throw Oops.Oh(ErrorCodeEnum.Z1004); + break; + + default: + throw Oops.Oh(ErrorCodeEnum.D3004); + } + // 从库配置判断 + if (!string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections)) + throw Oops.Oh(ErrorCodeEnum.D1302); + + // 设置logo + var tenant = input.Adapt(); + if (!string.IsNullOrWhiteSpace(input.LogoBase64)) SetLogoUrl(tenant, input.LogoBase64, input.LogoFileName); + + // 更新租户信息 + await _sysTenantRep.AsUpdateable(tenant).IgnoreColumns(true).ExecuteCommandAsync(); + + // 更新系统机构 + await _sysOrgRep.UpdateAsync(u => new SysOrg() { Name = input.Name }, u => u.Id == input.OrgId); + + // 更新系统用户 + await _sysUserRep.UpdateAsync(u => new SysUser() { Account = input.AdminAccount, Phone = input.Phone, Email = input.Email }, u => u.Id == input.UserId); + + await CacheTenant(input.Id); + } + + /// + /// 授权租户菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权租户菜单")] + public async Task GrantMenu(TenantMenuInput input) + { + // 获取需要授权的菜单列表 + var menuList = await _sysTenantRep.Context.Queryable() + .Where(u => input.MenuIdList.Contains(u.Id)) + .InnerJoin((u, t) => t.TenantId == input.Id && u.Id == t.MenuId) + .ToListAsync(); + + // 检查是否存在重复菜单 + if (menuList.Where(u => u.Type != MenuTypeEnum.Btn).GroupBy(u => new { u.Pid, u.Title }).Any(u => u.Count() > 1) || + menuList.Where(u => u.Type == MenuTypeEnum.Btn).GroupBy(u => u.Permission).Any(u => u.Count() > 1)) + throw Oops.Oh(ErrorCodeEnum.D1304); + + // 检查路由是否重复 + if (menuList.Where(u => !string.IsNullOrWhiteSpace(u.Name)).GroupBy(u => u.Name).Any(u => u.Count() > 1)) + throw Oops.Oh(ErrorCodeEnum.D4009); + + //获取默认租户授权菜单,种子数据主键ID保持不变,防止重复 + var tenantMenuList = input.Id == SqlSugarConst.DefaultTenantId ? await _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == input.Id).ToListAsync() : null; + + List tenantIdList = [input.Id]; + if (input.TenantIdList?.Count > 0) tenantIdList.AddRange(input.TenantIdList); + // 删除旧记录 + await _sysTenantMenuRep.AsDeleteable().Where(u => tenantIdList.Contains(u.TenantId)).ExecuteCommandAsync(); + + // 追加父级菜单 + var allIdList = await _sysTenantRep.Context.Queryable().Select(u => new { u.Id, u.Pid }).ToListAsync(); + var pIdList = allIdList.ToChildList(u => u.Pid, u => u.Id, u => input.MenuIdList.Contains(u.Id)).Select(u => u.Pid).Distinct().ToList(); + input.MenuIdList = input.MenuIdList.Concat(pIdList).Distinct().Where(u => u != 0).ToList(); + + // 保存租户菜单 + List sysTenantMenuList = new(); + tenantIdList.ForEach(tenantId => + { + sysTenantMenuList.AddRange(input.MenuIdList.Select(menuId => new SysTenantMenu { TenantId = tenantId, MenuId = menuId })); + }); + + //默认租户授权菜单主键ID不变 + foreach (var item in sysTenantMenuList) + { + var tenantMenu = tenantMenuList.FirstOrDefault(u => u.TenantId == item.TenantId && u.MenuId == item.MenuId); + if (tenantMenu != null) item.Id = tenantMenu.Id; + } + await _sysTenantMenuRep.InsertRangeAsync(sysTenantMenuList); + + // 清除菜单权限缓存 + SysMenuService.DeleteMenuCache(); + } + + /// + /// 获取租户菜单Id集合 🔖 + /// + /// + /// + [DisplayName("获取租户菜单Id集合")] + public async Task> GetTenantMenuList([FromQuery] BaseIdInput input) + { + var menuIds = await _sysTenantMenuRep.AsQueryable().Where(u => u.TenantId == input.Id).Select(u => u.MenuId).ToListAsync(); + return await SysMenuService.ExcludeParentMenuOfFullySelected(menuIds); + } + + /// + /// 重置租户管理员密码 🔖 + /// + /// + /// + [DisplayName("重置租户管理员密码")] + public async Task ResetPwd(TenantUserInput input) + { + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var encryptPassword = CryptogramUtil.Encrypt(password); + await _sysUserRep.UpdateAsync(u => new SysUser { Password = encryptPassword }, u => u.Id == input.UserId); + return password; + } + + /// + /// 切换租户 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("切换租户")] + public async Task ChangeTenant(BaseIdInput input) + { + var userId = (App.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value)?.ToLong(); + _ = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var user = await _sysUserRep.GetFirstAsync(u => u.Id == userId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + user.TenantId = input.Id; + + return await GetAccessTokenInNotSingleLogin(user); + } + + /// + /// 进入租管端 🔖 + /// + /// + /// + [DisplayName("进入租管端")] + public async Task GoTenant(BaseIdInput input) + { + var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + var user = await _sysUserRep.GetFirstAsync(u => u.Id == tenant.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + return await GetAccessTokenInNotSingleLogin(user); + } + + /// + /// 同步授权菜单(用于版本更新后,同步授权数据) 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("同步授权菜单")] + public async Task SyncGrantMenu(BaseIdInput input) + { + var menuIdList = input.Id == SqlSugarConst.DefaultTenantId + ? new SysMenuSeedData().HasData().Select(u => u.Id).ToList() + : await _sysRoleRep.AsQueryable().ClearFilter() + .InnerJoin((u, t) => t.Id == input.Id && u.TenantId == t.Id) + .InnerJoin((u, t, rm) => u.Id == rm.RoleId) + .Select((u, t, rm) => rm.MenuId) + .Distinct() + .ToListAsync() ?? throw Oops.Oh(ErrorCodeEnum.D1019); + var adminRole = await _sysRoleRep.AsQueryable().ClearFilter().FirstAsync(u => u.TenantId == input.Id && u.Code == "sys_admin"); + if (adminRole != null) + { + await _sysRoleRep.Context.Deleteable().Where(u => u.RoleId == adminRole.Id).ExecuteCommandAsync(); + await App.GetService().DeleteRole(new DeleteRoleInput { Id = adminRole.Id }); + } + await GrantMenu(new TenantMenuInput { Id = input.Id, MenuIdList = menuIdList }); + } + + /// + /// 在非单用户登录模式下获取登录令牌 + /// + /// + /// + [NonAction] + public async Task GetAccessTokenInNotSingleLogin(SysUser user) + { + // 使用非单用户模式登录 + var singleLogin = _sysCacheService.Get($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}"); + try + { + _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}", false); + return await App.GetService().CreateToken(user); + } + finally + { + // 恢复单用户登录参数 + if (singleLogin) _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysSingleLogin}", true); + } + } + + /// + /// 缓存所有租户 + /// + /// + /// + [NonAction] + public async Task CacheTenant(long tenantId = 0) + { + // 移除 ISqlSugarClient 中的库连接并排除默认主库 + if (tenantId > 0 && tenantId.ToString() != SqlSugarConst.MainConfigId) + _sysTenantRep.AsTenant().RemoveConnection(tenantId); + + var tenantList = await _sysTenantRep.GetListAsync(); + + // 对租户库连接进行SM2加密 + foreach (var tenant in tenantList.Where(tenant => !string.IsNullOrWhiteSpace(tenant.Connection))) + tenant.Connection = CryptogramUtil.SM2Encrypt(tenant.Connection); + + _sysCacheService.Set(CacheConst.KeyTenant, tenantList); + } + + /// + /// 创建租户数据库 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "CreateDb"), HttpPost] + [DisplayName("创建租户数据库")] + public async Task CreateDb(TenantInput input) + { + var tenant = await _sysTenantRep.GetSingleAsync(u => u.Id == input.Id); + if (tenant == null) return; + + if (tenant.DbType == SqlSugar.DbType.Oracle) + throw Oops.Oh(ErrorCodeEnum.Z1002); + + if (string.IsNullOrWhiteSpace(tenant.Connection) || tenant.Connection.Length < 10) + throw Oops.Oh(ErrorCodeEnum.Z1004); + + // 默认数据库配置 + var defaultConfig = App.GetOptions().ConnectionConfigs.FirstOrDefault(); + + var config = new DbConnectionConfig + { + ConfigId = tenant.Id.ToString(), + DbType = tenant.DbType, + ConnectionString = tenant.Connection, + DbSettings = new DbSettings() + { + EnableInitDb = true, + EnableDiffLog = false, + EnableUnderLine = defaultConfig!.DbSettings.EnableUnderLine, + } + }; + SqlSugarSetup.InitTenantDatabase(App.GetRequiredService().AsTenant(), config); + } + + /// + /// 获取租户下的用户列表 🔖 + /// + /// + /// + [DisplayName("获取租户下的用户列表")] + public async Task> UserList(TenantIdInput input) + { + return await _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.TenantId).ToListAsync(); + } + + /// + /// 获取租户数据库连接 + /// + /// + [NonAction] + public SqlSugarScopeProvider GetTenantDbConnectionScope(long tenantId) + { + var iTenant = _sysTenantRep.AsTenant(); + + // 若已存在租户库连接,则直接返回 + if (iTenant.IsAnyConnection(tenantId.ToString())) return iTenant.GetConnectionScope(tenantId.ToString()); + + lock (iTenant) + { + // 从缓存里面获取租户信息 + var tenant = _sysCacheService.Get>(CacheConst.KeyTenant)?.FirstOrDefault(u => u.Id == tenantId); + if (tenant == null || tenant.TenantType == TenantTypeEnum.Id) return null; + + // 获取默认库连接配置 + var dbOptions = App.GetOptions(); + var mainConnConfig = dbOptions.ConnectionConfigs.First(u => u.ConfigId.ToString() == SqlSugarConst.MainConfigId); + + // 设置租户库连接配置 + var tenantConnConfig = new DbConnectionConfig + { + ConfigId = tenant.Id.ToString(), + DbType = tenant.DbType, + TenantType = tenant.TenantType, + IsAutoCloseConnection = true, + ConnectionString = CryptogramUtil.SM2Decrypt(tenant.Connection), // 对租户库连接进行SM2解密 + DbSettings = new DbSettings() + { + EnableUnderLine = mainConnConfig.DbSettings.EnableUnderLine, + }, + SlaveConnectionConfigs = JSON.IsValid(tenant.SlaveConnections) ? JSON.Deserialize>(tenant.SlaveConnections) : null // 从库连接配置 + }; + iTenant.AddConnection(tenantConnConfig); + + var sqlSugarScopeProvider = iTenant.GetConnectionScope(tenantId.ToString()); + SqlSugarSetup.SetDbConfig(tenantConnConfig); + SqlSugarSetup.SetDbAop(sqlSugarScopeProvider, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter); + + return sqlSugarScopeProvider; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs new file mode 100644 index 0000000..5c3e870 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateInput.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 还原输入参数 +/// +public class RestoreInput +{ + /// + /// 文件名 + /// + [Required(ErrorMessage = "文件名不能为空")] + public string FileName { get; set; } +} + +/// +/// WebHook输入参数 +/// +public class WebHookInput +{ + /// + /// 密钥 + /// + [Required(ErrorMessage = "密钥不能为空")] + public string Key { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs new file mode 100644 index 0000000..1a9d236 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/Dto/UpdateOutput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class BackupOutput +{ + /// + /// 文件路径 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public string FilePath { get; set; } + + /// + /// 文件名 + /// + public string FileName { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/SysUpdateService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/SysUpdateService.cs new file mode 100644 index 0000000..584d47f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Update/SysUpdateService.cs @@ -0,0 +1,418 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.IO.Compression; +using System.Net; +using System.Security.Cryptography; + +namespace Admin.NET.Core.Service; + +/// +/// 系统更新管理服务 🧩 +/// +[ApiDescriptionSettings(Order = 390)] +public class SysUpdateService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly CDConfigOptions _cdConfigOptions; + + public SysUpdateService(IOptions giteeOptions, SysCacheService sysCacheService) + { + _cdConfigOptions = giteeOptions.Value; + _sysCacheService = sysCacheService; + } + + /// + /// 备份列表 + /// + /// + [DisplayName("备份列表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public Task> List() + { + const string backendDir = "Admin.NET"; + var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); + return Task.FromResult(Directory.GetFiles(rootPath, backendDir + "*.zip", SearchOption.TopDirectoryOnly) + .Select(filePath => + { + var file = new FileInfo(filePath); + return new BackupOutput + { + CreateTime = file.CreationTime, + FilePath = filePath, + FileName = file.Name + }; + }) + .OrderByDescending(u => u.CreateTime) + .ToList()); + } + + /// + /// 还原 + /// + /// + [DisplayName("还原")] + [ApiDescriptionSettings(Name = "Restore"), HttpPost] + public async Task Restore(RestoreInput input) + { + // 检查参数 + CheckConfig(); + try + { + var file = (await List()).FirstOrDefault(u => u.FileName.EqualIgnoreCase(input.FileName)); + if (file == null) + { + PrintfLog("文件不存在..."); + return; + } + + PrintfLog("正在还原..."); + using ZipArchive archive = new(File.OpenRead(file.FilePath), ZipArchiveMode.Read, leaveOpen: false); + archive.ExtractToDirectory(_cdConfigOptions.BackendOutput, true); + PrintfLog("还原成功..."); + } + catch (Exception ex) + { + PrintfLog("发生异常:" + ex.Message); + throw; + } + } + + /// + /// 从远端更新系统 + /// + /// + [DisplayName("系统更新")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update() + { + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"【{DateTime.Now}】从远端仓库部署项目"); + try + { + PrintfLog("----------------------------从远端仓库部署项目-开始----------------------------"); + + // 检查参数 + CheckConfig(); + + // 检查操作间隔 + if (_cdConfigOptions.UpdateInterval > 0) + { + if (_sysCacheService.Get(CacheConst.KeySysUpdateInterval)) throw Oops.Oh("请勿频繁操作"); + _sysCacheService.Set(CacheConst.KeySysUpdateInterval, true, TimeSpan.FromMinutes(_cdConfigOptions.UpdateInterval)); + } + + PrintfLog($"客户端host:{App.HttpContext.Request.Host}"); + PrintfLog($"客户端IP:{App.HttpContext.GetRemoteIpAddressToIPv4(true)}"); + PrintfLog($"仓库地址:https://gitee.com/{_cdConfigOptions.Owner}/{_cdConfigOptions.Repo}.git"); + PrintfLog($"仓库分支:{_cdConfigOptions.Branch}"); + + // 获取解压后的根目录 + var rootPath = Path.GetFullPath(Path.Combine(_cdConfigOptions.BackendOutput, "..")); + var tempDir = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}-{_cdConfigOptions.Branch}"); + + PrintfLog("清理旧文件..."); + FileHelper.TryDelete(tempDir); + + PrintfLog("拉取远端代码..."); + var stream = await GiteeHelper.DownloadRepoZip(_cdConfigOptions.Owner, _cdConfigOptions.Repo, + _cdConfigOptions.AccessToken, _cdConfigOptions.Branch); + + PrintfLog("文件包解压..."); + using ZipArchive archive = new(stream, ZipArchiveMode.Read, leaveOpen: false); + archive.ExtractToDirectory(rootPath); + + // 项目目录 + var backendDir = "Admin.NET"; // 后端根目录 + var entryProjectName = "Admin.NET.Web.Entry"; // 启动项目目录 + var tempOutput = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_temp"); + + PrintfLog("编译项目..."); + PrintfLog($"发布版本:{_cdConfigOptions.Publish.Configuration}"); + PrintfLog($"目标框架:{_cdConfigOptions.Publish.TargetFramework}"); + PrintfLog($"运行环境:{_cdConfigOptions.Publish.RuntimeIdentifier}"); + var option = _cdConfigOptions.Publish; + var adminNetDir = Path.Combine(tempDir, backendDir); + var args = $"publish \"{entryProjectName}\" -c {option.Configuration} -f {option.TargetFramework} -r {option.RuntimeIdentifier} --output \"{tempOutput}\""; + await RunCommandAsync("dotnet", args, adminNetDir); + + PrintfLog("复制 wwwroot 目录..."); + var wwwrootDir = Path.Combine(adminNetDir, entryProjectName, "wwwroot"); + FileHelper.CopyDirectory(wwwrootDir, Path.Combine(tempOutput, "wwwroot"), true); + + // 删除排除文件 + foreach (var filePath in (_cdConfigOptions.ExcludeFiles ?? new()).SelectMany(file => Directory.GetFiles(tempOutput, file, SearchOption.TopDirectoryOnly))) + { + PrintfLog($"排除文件:{filePath}"); + FileHelper.TryDelete(filePath); + } + + PrintfLog("备份原项目文件..."); + string backupPath = Path.Combine(rootPath, $"{_cdConfigOptions.Repo}_{DateTime.Now:yyyy_MM_dd}.zip"); + if (File.Exists(backupPath)) File.Delete(backupPath); + ZipFile.CreateFromDirectory(_cdConfigOptions.BackendOutput, backupPath); + + // 将临时文件移动到正式目录 + FileHelper.CopyDirectory(tempOutput, _cdConfigOptions.BackendOutput, true); + + PrintfLog("清理文件..."); + FileHelper.TryDelete(tempOutput); + FileHelper.TryDelete(tempDir); + + if (_cdConfigOptions.BackupCount > 0) + { + var fileList = await List(); + if (fileList.Count > _cdConfigOptions.BackupCount) + PrintfLog("清除多余的备份文件..."); + while (fileList.Count > _cdConfigOptions.BackupCount) + { + var last = fileList.Last(); + FileHelper.TryDelete(last.FilePath); + fileList.Remove(last); + } + } + + PrintfLog("重启项目后生效..."); + } + catch (Exception ex) + { + PrintfLog("发生异常:" + ex.Message); + throw; + } + finally + { + PrintfLog("----------------------------从远端仓库部署项目-结束----------------------------"); + Console.ForegroundColor = originColor; + } + } + + /// + /// 仓库WebHook接口 + /// + /// + [AllowAnonymous] + [DisplayName("仓库WebHook接口")] + [ApiDescriptionSettings(Name = "WebHook"), HttpPost] + public async Task WebHook(Dictionary input) + { + if (!_cdConfigOptions.Enabled) throw Oops.Oh("未启用持续部署功能"); + PrintfLog("----------------------------收到WebHook请求-开始----------------------------"); + + try + { + // 获取请求头信息 + var even = App.HttpContext.Request.Headers.FirstOrDefault(u => u.Key == "X-Gitee-Event").Value + .FirstOrDefault(); + var ua = App.HttpContext.Request.Headers.FirstOrDefault(u => u.Key == "User-Agent").Value.FirstOrDefault(); + + var timestamp = input.GetValueOrDefault("timestamp")?.ToString(); + var token = input.GetValueOrDefault("sign")?.ToString(); + PrintfLog("User-Agent:" + ua); + PrintfLog("Gitee-Event:" + even); + PrintfLog("Gitee-Token:" + token); + PrintfLog("Gitee-Timestamp:" + timestamp); + + PrintfLog("开始验签..."); + var secret = GetWebHookKey(); + var stringToSign = $"{timestamp}\n{secret}"; + using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var signData = mac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); + var encodedSignData = Convert.ToBase64String(signData); + var calculatedSignature = WebUtility.UrlEncode(encodedSignData); + + if (calculatedSignature != token) throw Oops.Oh("非法签名"); + PrintfLog("验签成功..."); + + var hookName = input.GetValueOrDefault("hook_name") as string; + PrintfLog("Hook-Name:" + hookName); + + switch (hookName) + { + // 提交修改 + case "push_hooks": + { + var commitList = input.GetValueOrDefault("commits")?.Adapt>>() ?? new(); + foreach (var commit in commitList) + { + var author = commit.GetValueOrDefault("author")?.Adapt>(); + PrintfLog("Commit-Message:" + commit.GetValueOrDefault("message")); + PrintfLog("Commit-Time:" + commit.GetValueOrDefault("timestamp")); + PrintfLog("Commit-Author:" + author?.GetValueOrDefault("username")); + PrintfLog("Modified-List:" + author?.GetValueOrDefault("modified")?.Adapt>().Join()); + PrintfLog("----------------------------------------------------------"); + } + + break; + } + // 合并 Pull Request + case "merge_request_hooks": + { + var pull = input.GetValueOrDefault("pull_request")?.Adapt>(); + var user = pull?.GetValueOrDefault("user")?.Adapt>(); + PrintfLog("Pull-Request-Title:" + pull?.GetValueOrDefault("message")); + PrintfLog("Pull-Request-Time:" + pull?.GetValueOrDefault("created_at")); + PrintfLog("Pull-Request-Author:" + user?.GetValueOrDefault("username")); + PrintfLog("Pull-Request-Body:" + pull?.GetValueOrDefault("body")); + break; + } + // 新的issue + case "issue_hooks": + { + var issue = input.GetValueOrDefault("issue")?.Adapt>(); + var user = issue?.GetValueOrDefault("user")?.Adapt>(); + var labelList = issue?.GetValueOrDefault("labels")?.Adapt>>(); + PrintfLog("Issue-UserName:" + user?.GetValueOrDefault("username")); + PrintfLog("Issue-Labels:" + labelList?.Select(u => u.GetValueOrDefault("name")).Join()); + PrintfLog("Issue-Title:" + issue?.GetValueOrDefault("title")); + PrintfLog("Issue-Time:" + issue?.GetValueOrDefault("created_at")); + PrintfLog("Issue-Body:" + issue?.GetValueOrDefault("body")); + return; + } + // 评论 + case "note_hooks": + { + var comment = input.GetValueOrDefault("comment")?.Adapt>(); + var user = input.GetValueOrDefault("user")?.Adapt>(); + PrintfLog("comment-UserName:" + user?.GetValueOrDefault("username")); + PrintfLog("comment-Time:" + comment?.GetValueOrDefault("created_at")); + PrintfLog("comment-Content:" + comment?.GetValueOrDefault("body")); + return; + } + default: + return; + } + + var updateInterval = _cdConfigOptions.UpdateInterval; + try + { + _cdConfigOptions.UpdateInterval = 0; + await Update(); + } + finally + { + _cdConfigOptions.UpdateInterval = updateInterval; + } + } + finally + { + PrintfLog("----------------------------收到WebHook请求-结束----------------------------"); + } + } + + /// + /// 获取WebHook接口密钥 + /// + /// + [DisplayName("获取WebHook接口密钥")] + [ApiDescriptionSettings(Name = "WebHookKey"), HttpGet] + public string GetWebHookKey() + { + return CryptogramUtil.Encrypt(_cdConfigOptions.AccessToken); + } + + /// + /// 获取日志列表 + /// + /// + [DisplayName("获取日志列表")] + [ApiDescriptionSettings(Name = "Logs"), HttpGet] + public List LogList() + { + return _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); + } + + /// + /// 清空日志 + /// + /// + [DisplayName("清空日志")] + [ApiDescriptionSettings(Name = "Clear"), HttpGet] + public void ClearLog() + { + _sysCacheService.Remove(CacheConst.KeySysUpdateLog); + } + + /// + /// 检查参数 + /// + /// + private void CheckConfig() + { + PrintfLog("检查CD配置参数..."); + + if (_cdConfigOptions == null) throw Oops.Oh("CDConfig配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Owner)) throw Oops.Oh("仓库用户名不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Repo)) throw Oops.Oh("仓库名不能为空"); + + // if (string.IsNullOrWhiteSpace(_cdConfigOptions.Branch)) throw Oops.Oh("分支名不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.AccessToken)) throw Oops.Oh("授权信息不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.BackendOutput)) throw Oops.Oh("部署目录不能为空"); + + if (_cdConfigOptions.Publish == null) throw Oops.Oh("编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.Configuration)) throw Oops.Oh("运行环境编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.TargetFramework)) throw Oops.Oh(".NET版本编译配置不能为空"); + + if (string.IsNullOrWhiteSpace(_cdConfigOptions.Publish.RuntimeIdentifier)) throw Oops.Oh("运行平台配置不能为空"); + } + + /// + /// 打印日志 + /// + /// + private void PrintfLog(string message) + { + var logList = _sysCacheService.Get>(CacheConst.KeySysUpdateLog) ?? new(); + + var content = $"【{DateTime.Now}】 {message}"; + + Console.WriteLine(content); + + logList.Add(content); + + _sysCacheService.Set(CacheConst.KeySysUpdateLog, logList); + } + + /// + /// 执行命令 + /// + /// 命令 + /// 参数 + /// 工作目录 + private async Task RunCommandAsync(string command, string arguments, string workingDirectory) + { + var processStartInfo = new ProcessStartInfo + { + FileName = command, + Arguments = arguments, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = new Process(); + process.StartInfo = processStartInfo; + process.Start(); + + while (!process.StandardOutput.EndOfStream) + { + string line = await process.StandardOutput.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) continue; + PrintfLog(line.Trim()); + } + await process.WaitForExitAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs new file mode 100644 index 0000000..1a5f906 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserExtOrgInput.cs @@ -0,0 +1,35 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class UserExtOrgInput : BaseIdInput +{ + /// + /// 机构Id + /// + public long OrgId { get; set; } + + /// + /// 职位Id + /// + public long PosId { get; set; } + + /// + /// 工号 + /// + public string JobNum { get; set; } + + /// + /// 职级 + /// + public string PosLevel { get; set; } + + /// + /// 入职日期 + /// + public DateTime? JoinDate { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs new file mode 100644 index 0000000..2516f35 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserInput.cs @@ -0,0 +1,133 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 设置用户状态输入参数 +/// +public class UserInput : BaseStatusInput +{ +} + +/// +/// 获取用户分页列表输入参数 +/// +public class PageUserInput : BasePageInput +{ + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 账号 + /// + public string Account { get; set; } + + /// + /// 姓名 + /// + public string RealName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 手机号 + /// + public string Phone { get; set; } + + /// + /// 查询时所选机构Id + /// + public long OrgId { get; set; } +} + +/// +/// 增加用户输入参数 +/// +public class AddUserInput : SysUser +{ + /// + /// 账号 + /// + [Required(ErrorMessage = "账号不能为空")] + public override string Account { get; set; } + + /// + /// 真实姓名 + /// + [Required(ErrorMessage = "真实姓名不能为空")] + public override string RealName { get; set; } + + /// + /// 域用户 + /// + public string DomainAccount { get; set; } + + /// + /// 角色集合 + /// + public List RoleIdList { get; set; } + + /// + /// 扩展机构集合 + /// + public List ExtOrgIdList { get; set; } +} + +/// +/// 更新用户输入参数 +/// +public class UpdateUserInput : AddUserInput +{ +} + +/// +/// 删除用户输入参数 +/// +public class DeleteUserInput : BaseIdInput +{ + /// + /// 机构Id + /// + public long OrgId { get; set; } +} + +/// +/// 重置用户密码输入参数 +/// +public class ResetPwdUserInput : BaseIdInput +{ +} + +/// +/// 修改用户密码输入参数 +/// +public class ChangePwdInput +{ + /// + /// 当前密码 + /// + [Required(ErrorMessage = "当前密码不能为空")] + public string PasswordOld { get; set; } + + /// + /// 新密码 + /// + [Required(ErrorMessage = "新密码不能为空"), MinLength(5, ErrorMessage = "密码需要大于5个字符")] + public string PasswordNew { get; set; } +} + +/// +/// 解除登录锁定输入参数 +/// +public class UnlockLoginInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs new file mode 100644 index 0000000..ef388c4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserMenuInput.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户菜单快捷导航输入 +/// +public class UserMenuInput +{ + /// + /// 收藏菜单Id集合 + /// + public List MenuIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserOutput.cs new file mode 100644 index 0000000..4c508f8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserOutput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class UserOutput : SysUser +{ + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 域用户 + /// + public string DomainAccount { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs new file mode 100644 index 0000000..9f50216 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs @@ -0,0 +1,72 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 注册方案分页查询输入参数 +/// +public class PageUserRegWayInput : BasePageInput +{ + /// + /// 方案名称 + /// + public string? Name { get; set; } + + /// + /// 租户Id + /// + public long TenantId { get; set; } +} + +/// +/// 注册方案增加输入参数 +/// +public class AddUserRegWayInput : SysUserRegWay +{ + /// + /// 方案名称 + /// + [Required(ErrorMessage = "方案名称不能为空")] + [MaxLength(32, ErrorMessage = "方案名称字符长度不能超过32")] + public override string Name { get; set; } + + /// + /// 账号类型 + /// + [Enum(ErrorMessage = "账号类型不正确")] + public override AccountTypeEnum AccountType { get; set; } + + /// + /// 角色 + /// + [Required(ErrorMessage = "角色不能为空")] + public override long RoleId { get; set; } + + /// + /// 机构 + /// + [Required(ErrorMessage = "机构不能为空")] + public override long OrgId { get; set; } + + /// + /// 职位 + /// + [Required(ErrorMessage = "职位不能为空")] + public override long PosId { get; set; } +} + +/// +/// 注册方案更新输入参数 +/// +public class UpdateUserRegWayInput : AddUserRegWayInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public override long Id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs new file mode 100644 index 0000000..9de8132 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 注册方案输出参数 +/// +public class UserRegWayOutput : SysUserRegWay +{ + /// + /// 角色名称 + /// + public string RoleName { get; set; } + + /// + /// 机构名称 + /// + public string OrgName { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs new file mode 100644 index 0000000..5182775 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/Dto/UserRoleInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 授权用户角色 +/// +public class UserRoleInput +{ + /// + /// 用户Id + /// + public long UserId { get; set; } + + /// + /// 角色Id集合 + /// + public List RoleIdList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserExtOrgService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserExtOrgService.cs new file mode 100644 index 0000000..9a96394 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserExtOrgService.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户扩展机构服务 +/// +public class SysUserExtOrgService : ITransient +{ + private readonly SqlSugarRepository _sysUserExtOrgRep; + + public SysUserExtOrgService(SqlSugarRepository sysUserExtOrgRep) + { + _sysUserExtOrgRep = sysUserExtOrgRep; + } + + /// + /// 获取用户扩展机构集合 + /// + /// + /// + public async Task> GetUserExtOrgList(long userId) + { + return await _sysUserExtOrgRep.GetListAsync(u => u.UserId == userId); + } + + /// + /// 更新用户扩展机构 + /// + /// + /// + /// + public async Task UpdateUserExtOrg(long userId, List extOrgList) + { + await _sysUserExtOrgRep.DeleteAsync(u => u.UserId == userId); + + if (extOrgList == null || extOrgList.Count < 1) return; + extOrgList.ForEach(u => + { + u.UserId = userId; + }); + await _sysUserExtOrgRep.InsertRangeAsync(extOrgList); + } + + /// + /// 根据机构Id集合删除扩展机构 + /// + /// + /// + public async Task DeleteUserExtOrgByOrgIdList(List orgIdList) + { + await _sysUserExtOrgRep.DeleteAsync(u => orgIdList.Contains(u.OrgId)); + } + + /// + /// 根据用户Id删除扩展机构 + /// + /// + /// + public async Task DeleteUserExtOrgByUserId(long userId) + { + await _sysUserExtOrgRep.DeleteAsync(u => u.UserId == userId); + } + + /// + /// 根据机构Id判断是否有用户 + /// + /// + /// + public async Task HasUserOrg(long orgId) + { + return await _sysUserExtOrgRep.IsAnyAsync(u => u.OrgId == orgId); + } + + /// + /// 根据职位Id判断是否有用户 + /// + /// + /// + public async Task HasUserPos(long posId) + { + return await _sysUserExtOrgRep.IsAnyAsync(u => u.PosId == posId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserLdapService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserLdapService.cs new file mode 100644 index 0000000..3f1e1e3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserLdapService.cs @@ -0,0 +1,66 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 用户域账号服务 +/// +public class SysUserLdapService : ITransient +{ + private readonly SqlSugarRepository _sysUserLdapRep; + + public SysUserLdapService(SqlSugarRepository sysUserLdapRep) + { + _sysUserLdapRep = sysUserLdapRep; + } + + /// + /// 批量插入域账号 + /// + /// + /// + /// + public async Task InsertUserLdapList(long tenantId, List sysUserLdapList) + { + await _sysUserLdapRep.DeleteAsync(u => u.TenantId == tenantId); + + await _sysUserLdapRep.InsertRangeAsync(sysUserLdapList); + + await _sysUserLdapRep.AsUpdateable() + .InnerJoin((l, u) => l.EmployeeId == u.Account) + .SetColumns((l, u) => new SysUserLdap { UserId = u.Id }) + .Where((l, u) => l.TenantId == tenantId && u.Status == StatusEnum.Enable) + .ExecuteCommandAsync(); + } + + /// + /// 增加域账号 + /// + /// + /// + /// + /// + /// + public async Task AddUserLdap(long tenantId, long userId, string account, string domainAccount) + { + var userLdap = await _sysUserLdapRep.GetFirstAsync(u => u.TenantId == tenantId && (u.Account == account || u.UserId == userId || u.EmployeeId == domainAccount)); + if (userLdap != null) await _sysUserLdapRep.DeleteByIdAsync(userLdap.Id); + + if (!string.IsNullOrWhiteSpace(domainAccount)) + await _sysUserLdapRep.InsertAsync(new SysUserLdap { EmployeeId = account, TenantId = tenantId, UserId = userId, Account = domainAccount }); + } + + /// + /// 删除域账号 + /// + /// + /// + public async Task DeleteUserLdapByUserId(long userId) + { + await _sysUserLdapRep.DeleteAsync(u => u.UserId == userId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserMenuService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserMenuService.cs new file mode 100644 index 0000000..b09b912 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserMenuService.cs @@ -0,0 +1,101 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户菜单快捷导航服务 🧩 +/// +[ApiDescriptionSettings(Order = 445)] +public class SysUserMenuService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysUserMenuRep; + private readonly UserManager _userManager; + + public SysUserMenuService(SqlSugarRepository sysUserMenuRep, UserManager userManager) + { + _sysUserMenuRep = sysUserMenuRep; + _userManager = userManager; + } + + /// + /// 收藏菜单 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("收藏菜单")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task AddUserMenu(UserMenuInput input) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == _userManager.UserId); + + if (input.MenuIdList == null || input.MenuIdList.Count == 0) return; + var menus = input.MenuIdList.Select(u => new SysUserMenu + { + UserId = _userManager.UserId, + MenuId = u + }).ToList(); + await _sysUserMenuRep.InsertRangeAsync(menus); + } + + /// + /// 取消收藏菜单 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "DeleteUserMenu"), HttpPost] + [DisplayName("取消收藏菜单")] + public async Task DeleteUserMenu(UserMenuInput input) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == _userManager.UserId && input.MenuIdList.Contains(u.MenuId)); + } + + /// + /// 获取当前用户收藏的菜单集合 🔖 + /// + /// + [DisplayName("获取当前用户收藏的菜单集合")] + public async Task> GetUserMenuList() + { + var sysUserMenuList = await _sysUserMenuRep.AsQueryable() + .Includes(u => u.SysMenu) + .Where(u => u.UserId == _userManager.UserId).ToListAsync(); + return sysUserMenuList.Where(u => u.SysMenu != null).Select(u => u.SysMenu).ToList().Adapt>(); + } + + /// + /// 获取当前用户收藏的菜单Id集合 🔖 + /// + /// + [DisplayName("获取当前用户收藏的菜单Id集合")] + public async Task> GetUserMenuIdList() + { + return await _sysUserMenuRep.AsQueryable() + .Where(u => u.UserId == _userManager.UserId).Select(u => u.MenuId).ToListAsync(); + } + + /// + /// 删除指定用户的收藏菜单 + /// + /// + [NonAction] + public async Task DeleteUserMenuList(long userId) + { + await _sysUserMenuRep.DeleteAsync(u => u.UserId == userId); + } + + /// + /// 批量删除收藏菜单 + /// + /// + [NonAction] + public async Task DeleteMenuList(List ids) + { + if (ids == null || ids.Count == 0) return; + await _sysUserMenuRep.DeleteAsync(u => ids.Contains(u.MenuId)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRegWayService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRegWayService.cs new file mode 100644 index 0000000..9ad8ecb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRegWayService.cs @@ -0,0 +1,118 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户注册方案服务 🧩 +/// +[ApiDescriptionSettings(Order = 490)] +public class SysUserRegWayService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysUserRegWayRep; + private readonly UserManager _userManager; + + public SysUserRegWayService(SqlSugarRepository sysUserRegWayRep, UserManager userManager) + { + _sysUserRegWayRep = sysUserRegWayRep; + _userManager = userManager; + } + + /// + /// 查询注册方案列表 🔖 + /// + /// + /// + [DisplayName("查询注册方案列表")] + [ApiDescriptionSettings(Name = "List"), HttpPost] + public async Task> List(PageUserRegWayInput input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _sysUserRegWayRep.AsQueryable() + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim())) + .LeftJoin((u, a) => u.RoleId == a.Id) + .LeftJoin((u, a, b) => u.OrgId == b.Id) + .LeftJoin((u, a, b, c) => u.PosId == c.Id) + .Select((u, a, b, c) => new UserRegWayOutput + { + RoleName = a.Name, + OrgName = b.Name, + PosName = c.Name, + }, true); + return await query.OrderBuilder(input).ToListAsync(); + } + + /// + /// 增加注册方案 ➕ + /// + /// + /// + [DisplayName("增加注册方案")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddUserRegWayInput input) + { + var entity = input.Adapt(); + if (await _sysUserRegWayRep.IsAnyAsync(u => u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101); + + await CheckData(input); + return await _sysUserRegWayRep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新注册方案 ✏️ + /// + /// + /// + [DisplayName("更新注册方案")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateUserRegWayInput input) + { + if (await _sysUserRegWayRep.IsAnyAsync(u => u.Id != input.Id && u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101); + + await CheckData(input); + await _sysUserRegWayRep.AsUpdateable(input).ExecuteCommandAsync(); + } + + /// + /// 检查数据 + /// + /// + [NonAction] + public async Task CheckData(AddUserRegWayInput input) + { + // 检查外键数据是否存在 + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.RoleId)) throw Oops.Oh(ErrorCodeEnum.D1036); + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.OrgId)) throw Oops.Oh(ErrorCodeEnum.D2011); + if (!await _sysUserRegWayRep.Context.Queryable().AnyAsync(u => u.Id == input.PosId)) throw Oops.Oh(ErrorCodeEnum.D6003); + + // 禁止注册超级管理员和系统管理员 + if (input.AccountType is AccountTypeEnum.SysAdmin or AccountTypeEnum.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D1037); + } + + /// + /// 删除注册方案 ❌ + /// + /// + /// + [UnitOfWork] + [DisplayName("删除注册方案")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(BaseIdInput input) + { + var entity = await _sysUserRegWayRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + + // 关闭相关租户注册功能 + await _sysUserRegWayRep.Context.Updateable(new SysTenant { EnableReg = YesNoEnum.N, RegWayId = null }) + .UpdateColumns(u => new { u.EnableReg, u.RegWayId }) + .Where(u => u.RegWayId == input.Id) + .ExecuteCommandAsync(); + + // 删除方案 + await _sysUserRegWayRep.DeleteAsync(entity); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRoleService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRoleService.cs new file mode 100644 index 0000000..cc45391 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserRoleService.cs @@ -0,0 +1,106 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户角色服务 +/// +public class SysUserRoleService : ITransient +{ + private readonly SysCacheService _sysCacheService; + private readonly SqlSugarRepository _sysUserRoleRep; + + public SysUserRoleService(SysCacheService sysCacheService, + SqlSugarRepository sysUserRoleRep) + { + _sysCacheService = sysCacheService; + _sysUserRoleRep = sysUserRoleRep; + } + + /// + /// 授权用户角色 + /// + /// + /// + public async Task GrantUserRole(UserRoleInput input) + { + await _sysUserRoleRep.DeleteAsync(u => u.UserId == input.UserId); + + if (input.RoleIdList == null || input.RoleIdList.Count < 1) return; + var roles = input.RoleIdList.Select(u => new SysUserRole + { + UserId = input.UserId, + RoleId = u + }).ToList(); + await _sysUserRoleRep.InsertRangeAsync(roles); + _sysCacheService.Remove(CacheConst.KeyUserButton + input.UserId); + } + + /// + /// 根据角色Id删除用户角色 + /// + /// + /// + public async Task DeleteUserRoleByRoleId(long roleId) + { + await _sysUserRoleRep.AsQueryable() + .Where(u => u.RoleId == roleId) + .Select(u => u.UserId) + .ForEachAsync(userId => + { + _sysCacheService.Remove(CacheConst.KeyUserButton + userId); + }); + + await _sysUserRoleRep.DeleteAsync(u => u.RoleId == roleId); + } + + /// + /// 根据用户Id删除用户角色 + /// + /// + /// + public async Task DeleteUserRoleByUserId(long userId) + { + await _sysUserRoleRep.DeleteAsync(u => u.UserId == userId); + _sysCacheService.Remove(CacheConst.KeyUserButton + userId); + } + + /// + /// 根据用户Id获取角色集合 + /// + /// + /// + public async Task> GetUserRoleList(long userId) + { + var sysUserRoleList = await _sysUserRoleRep.AsQueryable() + .Includes(u => u.SysRole) + .Where(u => u.UserId == userId).ToListAsync(); + return sysUserRoleList.Where(u => u.SysRole != null).Select(u => u.SysRole).ToList(); + } + + /// + /// 根据用户Id获取角色Id集合 + /// + /// + /// + public async Task> GetUserRoleIdList(long userId) + { + return await _sysUserRoleRep.AsQueryable() + .Where(u => u.UserId == userId).Select(u => u.RoleId).ToListAsync(); + } + + /// + /// 根据角色Id获取用户Id集合 + /// + /// + /// + public async Task> GetUserIdList(long roleId) + { + return await _sysUserRoleRep.AsQueryable() + .Where(u => u.RoleId == roleId).Select(u => u.UserId).ToListAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs new file mode 100644 index 0000000..2b164e0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs @@ -0,0 +1,505 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 系统用户服务 🧩 +/// +[ApiDescriptionSettings(Order = 490)] +public class SysUserService : IDynamicApiController, ITransient +{ + private readonly UserManager _userManager; + private readonly SysOrgService _sysOrgService; + private readonly SysUserExtOrgService _sysUserExtOrgService; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysConfigService _sysConfigService; + private readonly SysOnlineUserService _sysOnlineUserService; + private readonly SysUserMenuService _sysUserMenuService; + private readonly SysCacheService _sysCacheService; + private readonly SysUserLdapService _sysUserLdapService; + private readonly SqlSugarRepository _sysUserRep; + private readonly IEventPublisher _eventPublisher; + + public SysUserService(UserManager userManager, + SysOrgService sysOrgService, + SysUserExtOrgService sysUserExtOrgService, + SysUserRoleService sysUserRoleService, + SysConfigService sysConfigService, + SysOnlineUserService sysOnlineUserService, + SysCacheService sysCacheService, + SysUserLdapService sysUserLdapService, + SqlSugarRepository sysUserRep, + SysUserMenuService sysUserMenuService, + IEventPublisher eventPublisher) + { + _userManager = userManager; + _sysOrgService = sysOrgService; + _sysUserExtOrgService = sysUserExtOrgService; + _sysUserRoleService = sysUserRoleService; + _sysConfigService = sysConfigService; + _sysOnlineUserService = sysOnlineUserService; + _sysCacheService = sysCacheService; + _sysUserLdapService = sysUserLdapService; + _sysUserMenuService = sysUserMenuService; + _sysUserRep = sysUserRep; + _eventPublisher = eventPublisher; + } + + /// + /// 获取用户分页列表 🔖 + /// + /// + /// + [DisplayName("获取用户分页列表")] + public virtual async Task> Page(PageUserInput input) + { + //获取子节点Id集合(包含自己) + var orgList = await _sysOrgService.GetChildIdListWithSelfById(input.OrgId); + + return await _sysUserRep.AsQueryable() + .LeftJoin((u, a) => u.OrgId == a.Id) + .LeftJoin((u, a, b) => u.PosId == b.Id) + .Where(u => u.AccountType != AccountTypeEnum.SuperAdmin) + .WhereIF(input.OrgId > 0, u => orgList.Contains(u.OrgId)) + .WhereIF(!_userManager.SuperAdmin, u => u.AccountType != AccountTypeEnum.SysAdmin) + .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Account.Contains(input.Account)) + .WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.RealName.Contains(input.RealName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.PosName), (u, a, b) => b.Name.Contains(input.PosName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), u => u.Phone.Contains(input.Phone)) + .OrderBy(u => new { u.OrderNo, u.Id }) + .Select((u, a, b) => new UserOutput + { + OrgName = a.Name, + PosName = b.Name, + RoleName = SqlFunc.Subqueryable().LeftJoin((m, n) => m.RoleId == n.Id).Where(m => m.UserId == u.Id).SelectStringJoin((m, n) => n.Name, ","), + DomainAccount = SqlFunc.Subqueryable().Where(m => m.UserId == u.Id).Select(m => m.Account) + }, true) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加用户")] + public virtual async Task AddUser(AddUserInput input) + { + var isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权新增超级管理员和系统管理员 + if (_userManager.AccountType != AccountTypeEnum.SuperAdmin && input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + // 若没有设置密码则取默认密码 + var password = !string.IsNullOrWhiteSpace(input.Password) ? input.Password : await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + var user = input.Adapt(); + user.Password = CryptogramUtil.Encrypt(password); + var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync(); + + input.Id = newUser.Id; + await UpdateRoleAndExtOrg(input); + + // 增加域账号 + if (!string.IsNullOrWhiteSpace(input.DomainAccount)) + await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Add, new + { + Entity = newUser, + Input = input + }); + + return newUser.Id; + } + + /// + /// 注册用户 🔖 + /// + /// + /// + [NonAction] + public virtual async Task RegisterUser(AddUserInput input) + { + var isExist = await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account); + if (isExist) throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权注册 + if (input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + if (string.IsNullOrWhiteSpace(input.Password)) + { + var password = await _sysConfigService.GetConfigValue(ConfigConst.SysPassword); + input.Password = CryptogramUtil.Encrypt(password); + } + + var user = input.Adapt(); + var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync(); + + input.Id = newUser.Id; + await UpdateRoleAndExtOrg(input); + + // 增加域账号 + if (!string.IsNullOrWhiteSpace(input.DomainAccount)) + await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Register, new + { + Entity = newUser, + Input = input + }); + + return newUser.Id; + } + + /// + /// 更新用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新用户")] + public virtual async Task UpdateUser(UpdateUserInput input) + { + if (await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Account == input.Account && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1003); + + if (!string.IsNullOrWhiteSpace(input.Phone) && await _sysUserRep.AsQueryable().ClearFilter().AnyAsync(u => u.Phone == input.Phone && u.Id != input.Id)) + throw Oops.Oh(ErrorCodeEnum.D1032); + + // 禁止越权更新超级管理员或系统管理员信息 + if (_userManager.AccountType != AccountTypeEnum.SuperAdmin && input.AccountType is AccountTypeEnum.SuperAdmin or AccountTypeEnum.SysAdmin) throw Oops.Oh(ErrorCodeEnum.D1038); + + await _sysUserRep.AsUpdateable(input.Adapt()).IgnoreColumns(true) + .IgnoreColumns(u => new { u.Password, u.Status, u.TenantId }).ExecuteCommandAsync(); + + await UpdateRoleAndExtOrg(input); + + // 删除用户机构缓存 + SqlSugarFilter.DeleteUserOrgCache(input.Id, _sysUserRep.Context.CurrentConnectionConfig.ConfigId.ToString()); + + // 若账号的角色和组织架构发生变化,则强制下线账号进行权限更新 + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == input.Id); + var roleIds = await GetOwnRoleList(input.Id); + if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u))) + await _sysOnlineUserService.ForceOffline(input.Id); + // 更新域账号 + await _sysUserLdapService.AddUserLdap(user.TenantId!.Value, user.Id, user.Account, input.DomainAccount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Update, new + { + Entity = user, + Input = input + }); + } + + /// + /// 更新当前用户语言 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "SetLangCode"), HttpPost] + [DisplayName("更新当前用户语言")] + public virtual async Task SetLangCode(string langCode) + { + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401); + user.LangCode = langCode; + await _sysUserRep.AsUpdateable(user).UpdateColumns(it => it.LangCode).ExecuteCommandAsync(); + } + + /// + /// 更新角色和扩展机构 + /// + /// + /// + private async Task UpdateRoleAndExtOrg(AddUserInput input) + { + await GrantRole(new UserRoleInput { UserId = input.Id, RoleIdList = input.RoleIdList }); + + await _sysUserExtOrgService.UpdateUserExtOrg(input.Id, input.ExtOrgIdList); + } + + /// + /// 删除用户 🔖 + /// + /// + /// + [UnitOfWork] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除用户")] + public virtual async Task DeleteUser(DeleteUserInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + user.ValidateIsSuperAdminAccountType(); + user.ValidateIsUserId(_userManager.UserId); + + // 若账号为租户默认账号则禁止删除 + var isTenantUser = await _sysUserRep.ChangeRepository>().IsAnyAsync(u => u.UserId == input.Id); + if (isTenantUser) throw Oops.Oh(ErrorCodeEnum.D1029); + + // 若账号为开放接口绑定账号则禁止删除 + var isOpenAccessUser = await _sysUserRep.ChangeRepository>().IsAnyAsync(u => u.BindUserId == input.Id); + if (isOpenAccessUser) throw Oops.Oh(ErrorCodeEnum.D1030); + + // 强制下线 + await _sysOnlineUserService.ForceOffline(user.Id); + + await _sysUserRep.DeleteAsync(user); + + // 删除用户角色 + await _sysUserRoleService.DeleteUserRoleByUserId(input.Id); + + // 删除用户扩展机构 + await _sysUserExtOrgService.DeleteUserExtOrgByUserId(input.Id); + + // 删除域账号 + await _sysUserLdapService.DeleteUserLdapByUserId(input.Id); + + // 删除用户收藏菜单 + await _sysUserMenuService.DeleteUserMenuList(input.Id); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.Delete, new + { + Entity = user, + Input = input + }); + } + + /// + /// 查看用户基本信息 🔖 + /// + /// + [DisplayName("查看用户基本信息")] + public virtual async Task GetBaseInfo() + { + return await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(c => c.Id == _userManager.UserId); + } + + /// + /// 查询用户组织机构信息 🔖 + /// + /// + [DisplayName("查询用户组织机构信息")] + public virtual async Task> GetOrgInfo() + { + return await _sysOrgService.GetTree(new OrgInput { Id = 0 }); + } + + /// + /// 更新用户基本信息 🔖 + /// + /// + [ApiDescriptionSettings(Name = "BaseInfo"), HttpPost] + [DisplayName("更新用户基本信息")] + public virtual async Task UpdateBaseInfo(SysUser user) + { + return await _sysUserRep.AsUpdateable(user) + .IgnoreColumns(u => new { u.CreateTime, u.Account, u.Password, u.AccountType, u.OrgId, u.PosId }).ExecuteCommandAsync(); + } + + /// + /// 设置用户状态 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("设置用户状态")] + public virtual async Task SetStatus(UserInput input) + { + if (_userManager.UserId == input.Id) + throw Oops.Oh(ErrorCodeEnum.D1026); + + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + user.ValidateIsSuperAdminAccountType(ErrorCodeEnum.D1015); + if (!Enum.IsDefined(typeof(StatusEnum), input.Status)) + throw Oops.Oh(ErrorCodeEnum.D3005); + + // 账号禁用则增加黑名单,账号启用则移除黑名单 + var sysCacheService = App.GetRequiredService(); + if (input.Status == StatusEnum.Disable) + { + sysCacheService.Set($"{CacheConst.KeyBlacklist}{user.Id}", $"{user.RealName}-{user.Phone}"); + + // 强制下线 + await _sysOnlineUserService.ForceOffline(user.Id); + } + else + { + sysCacheService.Remove($"{CacheConst.KeyBlacklist}{user.Id}"); + } + + user.Status = input.Status; + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new { u.Status }).ExecuteCommandAsync(); + + // 发布系统用户操作事件 + if (rows > 0) + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.SetStatus, new + { + Entity = user, + Input = input + }); + + return rows; + } + + /// + /// 授权用户角色 🔖 + /// + /// + /// + [UnitOfWork] + [DisplayName("授权用户角色")] + public async Task GrantRole(UserRoleInput input) + { + //var user = await _sysUserRep.GetFirstAsync(u => u.Id == input.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + //if (user.AccountType == AccountTypeEnum.SuperAdmin) + // throw Oops.Oh(ErrorCodeEnum.D1022); + + await _sysUserRoleService.GrantUserRole(input); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.UpdateRole, input); + } + + /// + /// 修改用户密码 🔖 + /// + /// + /// + [DisplayName("修改用户密码")] + public virtual async Task ChangePwd(ChangePwdInput input) + { + // 国密SM2解密(前端密码传输SM2加密后的) + input.PasswordOld = CryptogramUtil.SM2Decrypt(input.PasswordOld); + input.PasswordNew = CryptogramUtil.SM2Decrypt(input.PasswordNew); + + var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(c => c.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString()) + { + if (user.Password != MD5Encryption.Encrypt(input.PasswordOld)) + throw Oops.Oh(ErrorCodeEnum.D1004); + } + else + { + if (CryptogramUtil.Decrypt(user.Password) != input.PasswordOld) + throw Oops.Oh(ErrorCodeEnum.D1004); + } + + if (input.PasswordOld == input.PasswordNew) + throw Oops.Oh(ErrorCodeEnum.D1028); + + // 验证密码强度 + if (CryptogramUtil.StrongPassword) + { + user.Password = input.PasswordNew.TryValidate(CryptogramUtil.PasswordStrengthValidation) + ? CryptogramUtil.Encrypt(input.PasswordNew) + : throw Oops.Oh(CryptogramUtil.PasswordStrengthValidationMsg); + } + else + { + user.Password = CryptogramUtil.Encrypt(input.PasswordNew); + } + + var rows = await _sysUserRep.AsUpdateable(user).UpdateColumns(u => u.Password).ExecuteCommandAsync(); + + // 发布系统用户操作事件 + if (rows > 0) + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.ChangePwd, new + { + Entity = user, + Input = input + }); + + return rows; + } + + /// + /// 重置用户密码 🔖 + /// + /// + /// + [DisplayName("重置用户密码")] + public virtual async Task ResetPwd(ResetPwdUserInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + string randomPassword = new(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 6).Select(s => s[Random.Shared.Next(s.Length)]).ToArray()); + user.Password = CryptogramUtil.Encrypt(randomPassword); + await _sysUserRep.AsUpdateable(user).UpdateColumns(u => u.Password).ExecuteCommandAsync(); + + // 清空密码错误次数 + var keyErrorPasswordCount = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; + _sysCacheService.Remove(keyErrorPasswordCount); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.ResetPwd, new + { + Entity = user, + Input = input + }); + + return randomPassword; + } + + /// + /// 解除登录锁定 🔖 + /// + /// + /// + [DisplayName("解除登录锁定")] + public virtual async Task UnlockLogin(UnlockLoginInput input) + { + var user = await _sysUserRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D0009); + + // 清空密码错误次数 + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}"; + _sysCacheService.Remove(keyPasswordErrorTimes); + + // 发布系统用户操作事件 + await _eventPublisher.PublishAsync(SysUserEventTypeEnum.UnlockLogin, new + { + Entity = user, + Input = input + }); + } + + /// + /// 获取用户拥有角色集合 🔖 + /// + /// + /// + [DisplayName("获取用户拥有角色集合")] + public async Task> GetOwnRoleList(long userId) + { + return await _sysUserRoleService.GetUserRoleIdList(userId); + } + + /// + /// 获取用户扩展机构集合 🔖 + /// + /// + /// + [DisplayName("获取用户扩展机构集合")] + public async Task> GetOwnExtOrgList(long userId) + { + return await _sysUserExtOrgService.GetUserExtOrgList(userId); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/UserManager.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/UserManager.cs new file mode 100644 index 0000000..81d7d8e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/User/UserManager.cs @@ -0,0 +1,67 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 当前登录用户 +/// +public class UserManager : IScoped +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// 用户ID + /// + public long UserId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value).ToLong(); + + /// + /// 租户ID + /// + public long TenantId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong(); + + /// + /// 用户账号 + /// + public string Account => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.Account)?.Value; + + /// + /// 真实姓名 + /// + public string RealName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.RealName)?.Value; + + /// + /// 账号类型 + /// + public AccountTypeEnum? AccountType => int.TryParse(_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value, out var val) ? (AccountTypeEnum?)val : null; + + /// + /// 是否超级管理员 + /// + public bool SuperAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString(); + + /// + /// 是否系统管理员 + /// + public bool SysAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SysAdmin).ToString(); + + /// + /// 组织机构Id + /// + public long OrgId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgId)?.Value).ToLong(); + + /// + /// 微信OpenId + /// + public string OpenId => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OpenId)?.Value; + + public string LangCode => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.LangCode)?.Value ?? "zh_CN"; + + public UserManager(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs new file mode 100644 index 0000000..87fcb47 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatInput.cs @@ -0,0 +1,149 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 生成网页授权Url +/// +public class GenAuthUrlInput +{ + /// + /// RedirectUrl + /// + public string RedirectUrl { get; set; } + + /// + /// Scope + /// + public string Scope { get; set; } + + /// + /// State + /// + public string State { get; set; } +} + +/// +/// 获取微信用户OpenId +/// +public class WechatOAuth2Input +{ + /// + /// Code + /// + [Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")] + public string Code { get; set; } +} + +/// +/// 微信用户登录 +/// +public class WechatUserLogin +{ + /// + /// OpenId + /// + [Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识长错误")] + public string OpenId { get; set; } +} + +/// +/// 获取配置签名 +/// +public class SignatureInput +{ + /// + /// Url + /// + public string Url { get; set; } +} + +/// +/// 获取消息模板列表 +/// +public class MessageTemplateSendInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } + + /// + /// 接收者的OpenId + /// + [Required(ErrorMessage = "接收者的OpenId不能为空")] + public string ToUserOpenId { get; set; } + + /// + /// 模板数据,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + /// + [Required(ErrorMessage = "模板数据不能为空")] + public Dictionary Data { get; set; } + + /// + /// 模板跳转链接 + /// + public string Url { get; set; } + + /// + /// 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar) + /// + public string MiniProgramPagePath { get; set; } +} + +/// +/// 删除消息模板 +/// +public class DeleteMessageTemplateInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } +} + +public class UploadAvatarInput +{ + /// + /// 小程序用户身份标识 + /// + [Required(ErrorMessage = "OpenId不能为空")] + public string OpenId { get; set; } + + /// + /// 文件 + /// + [Required] + public IFormFile File { get; set; } + + /// + /// 文件类型 + /// + public string FileType { get; set; } + + /// + /// 文件路径 + /// + public string Path { get; set; } +} + +public class SetNickNameInput +{ + /// + /// 小程序用户身份标识 + /// + [Required(ErrorMessage = "OpenId不能为空")] + public string OpenId { get; set; } + + /// + /// 昵称 + /// + [Required(ErrorMessage = "昵称不能为空")] + public string NickName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs new file mode 100644 index 0000000..8ba4663 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayInput.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatPayTransactionInput +{ + /// + /// OpenId + /// + public string OpenId { get; set; } + + /// + /// 订单金额 + /// + public int Total { get; set; } + + /// + /// 商品描述 + /// + public string Description { get; set; } + + /// + /// 附加数据 + /// + public string Attachment { get; set; } + + /// + /// 优惠标记 + /// + public string GoodsTag { get; set; } + + /// + /// 业务标签,用来区分做什么业务 + /// + public string Tags { get; set; } + + /// + /// 对应业务的主键 + /// + public long BusinessId { get; set; } +} + +public class WechatPayParaInput +{ + /// + /// 订单Id + /// + public string PrepayId { get; set; } +} + +public class WechatPayRefundDomesticInput +{ + /// + /// 商户端生成的业务流水号 + /// + [Required] + public string TradeId { get; set; } + + /// + /// 退款原因 + /// + public string Reason { get; set; } + + /// + /// 退款金额 + /// + [Required] + public int Refund { get; set; } + + /// + /// 原订单金额 + /// + [Required] + public int Total { get; set; } +} + +public class WechatPayPageInput : BasePageInput +{ + /// + /// 添加时间范围 + /// + public List CreateTimeRange { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs new file mode 100644 index 0000000..d423f00 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatPayOutput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatPayOutput +{ + /// + /// OpenId + /// + public string OpenId { get; set; } + + /// + /// 订单金额 + /// + public int Total { get; set; } + + /// + /// 附加数据 + /// + public string Attachment { get; set; } + + /// + /// 优惠标记 + /// + public string GoodsTag { get; set; } +} + +public class WechatPayTransactionOutput +{ + public string PrepayId { get; set; } + + public string OutTradeNumber { get; set; } + + public WechatPayParaOutput SingInfo { get; set; } +} + +public class WechatPayParaOutput +{ + public string AppId { get; set; } + + public string TimeStamp { get; set; } + + public string NonceStr { get; set; } + + public string Package { get; set; } + + public string SignType { get; set; } + + public string PaySign { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs new file mode 100644 index 0000000..f7aa580 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WechatUserInput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WechatUserInput : BasePageInput +{ + /// + /// 昵称 + /// + public string NickName { get; set; } + + /// + /// 手机号码 + /// + public string Mobile { get; set; } +} + +public class DeleteWechatUserInput : BaseIdInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs new file mode 100644 index 0000000..0f15627 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenInput.cs @@ -0,0 +1,150 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 获取微信用户OpenId +/// +public class JsCode2SessionInput +{ + /// + /// JsCode + /// + [Required(ErrorMessage = "JsCode不能为空"), MinLength(10, ErrorMessage = "JsCode错误")] + public string JsCode { get; set; } +} + +/// +/// 获取微信用户电话号码 +/// +public class WxPhoneInput : WxOpenIdLoginInput +{ + /// + /// Code + /// + [Required(ErrorMessage = "Code不能为空"), MinLength(10, ErrorMessage = "Code错误")] + public string Code { get; set; } +} + +/// +/// 微信小程序登录 +/// +public class WxOpenIdLoginInput +{ + /// + /// OpenId + /// + [Required(ErrorMessage = "微信标识不能为空"), MinLength(10, ErrorMessage = "微信标识错误")] + public string OpenId { get; set; } +} + +/// +/// 微信手机号登录 +/// +public class WxPhoneLoginInput +{ + /// + /// 电话号码 + /// + [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "电话号码错误")] + public string PhoneNumber { get; set; } +} + +/// +/// 发送订阅消息 +/// +public class SendSubscribeMessageInput +{ + /// + /// 订阅模板Id + /// + [Required(ErrorMessage = "订阅模板Id不能为空")] + public string TemplateId { get; set; } + + /// + /// 接收者的OpenId + /// + [Required(ErrorMessage = "接收者的OpenId不能为空")] + public string ToUserOpenId { get; set; } + + /// + /// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + /// + [Required(ErrorMessage = "模板内容不能为空")] + public Dictionary Data { get; set; } + + /// + /// 跳转小程序类型 + /// + public string MiniprogramState { get; set; } + + /// + /// 语言类型 + /// + public string Language { get; set; } + + /// + /// 点击模板卡片后的跳转页面(仅限本小程序内的页面),支持带参数(示例pages/app/index?foo=bar) + /// + public string MiniProgramPagePath { get; set; } +} + +/// +/// 增加订阅消息模板 +/// +public class AddSubscribeMessageTemplateInput +{ + /// + /// 模板标题Id + /// + [Required(ErrorMessage = "模板标题Id不能为空")] + public string TemplateTitleId { get; set; } + + /// + /// 模板关键词列表,例如 [3,5,4] + /// + [Required(ErrorMessage = "模板关键词列表不能为空")] + public List KeyworkIdList { get; set; } + + /// + /// 服务场景描述,15个字以内 + /// + [Required(ErrorMessage = "服务场景描述不能为空")] + public string SceneDescription { get; set; } +} + +/// +/// 生成带参数小程序二维码(总共生成的码数量限制为 100,000) +/// +public class GenerateQRImageInput +{ + /// + /// 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages/index?id=0001 + /// + public string PagePath { get; set; } + + /// + /// 文件保存的名称 + /// + public string ImageName { get; set; } + + /// + /// 图片宽度 默认430 + /// + public int Width { get; set; } = 430; +} + +/// +/// 生成带参数小程序二维码(获取不受限制的小程序码) +/// +public class GenerateQRImageUnLimitInput : GenerateQRImageInput +{ + /// + /// 二维码携带的参数 eg:a=1(最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:) + /// + public string Scene { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs new file mode 100644 index 0000000..6b83bb3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/Dto/WxOpenOutput.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +public class WxOpenIdOutput +{ + public string OpenId { get; set; } +} + +public class WxPhoneOutput +{ + public string PhoneNumber { get; set; } +} + +public class GenerateQRImageOutput +{ + /// + /// 生成状态 + /// + public bool Success { get; set; } = false; + + /// + /// 生成图片的绝对路径 + /// + public string ImgPath { get; set; } = ""; + + /// + /// 生成图片的相对路径 + /// + public string RelativeImgPath { get; set; } = ""; + + /// + /// 生成图片的错误信息 + /// + public string Message { get; set; } = ""; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs new file mode 100644 index 0000000..31aac14 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs @@ -0,0 +1,520 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 微信支付服务 🧩 +/// +[ApiDescriptionSettings(Order = 210)] +public class SysWechatPayService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatPayRep; + private readonly SqlSugarRepository _sysWechatRefundRep; + + private readonly WechatPayOptions _wechatPayOptions; + private readonly PayCallBackOptions _payCallBackOptions; + + private readonly WechatTenpayClient _wechatTenpayClient; + + public SysWechatPayService(SqlSugarRepository sysWechatPayUserRep, + SqlSugarRepository sysWechatRefundRep, + IOptions wechatPayOptions, + IOptions payCallBackOptions) + { + _sysWechatPayRep = sysWechatPayUserRep; + _sysWechatRefundRep = sysWechatRefundRep; + _wechatPayOptions = wechatPayOptions.Value; + _payCallBackOptions = payCallBackOptions.Value; + + _wechatTenpayClient = CreateTenpayClient(); + } + + /// + /// 初始化微信支付客户端 + /// + /// + private WechatTenpayClient CreateTenpayClient() + { + var cerFilePath = Path.Combine(App.WebHostEnvironment.ContentRootPath, _wechatPayOptions.MerchantCertificatePrivateKey.Replace("/", Path.DirectorySeparatorChar.ToString())); + + var tenpayClientOptions = new WechatTenpayClientOptions() + { + MerchantId = _wechatPayOptions.MerchantId, + MerchantV3Secret = _wechatPayOptions.MerchantV3Secret, + MerchantCertificateSerialNumber = _wechatPayOptions.MerchantCertificateSerialNumber, + MerchantCertificatePrivateKey = File.Exists(cerFilePath) ? File.ReadAllText(cerFilePath) : "", + PlatformCertificateManager = new InMemoryCertificateManager() + }; + return new WechatTenpayClient(tenpayClientOptions); + } + + /// + /// 分页查询支付列表 🔖 + /// + /// + /// + [HttpPost] + [ApiDescriptionSettings(Name = "Page")] + public async Task> Page(WechatPayPageInput input) + { + var query = _sysWechatPayRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.OutTradeNumber == input.Keyword || u.TransactionId == input.Keyword) + .WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 0 && input.CreateTimeRange[0].HasValue, x => x.CreateTime >= input.CreateTimeRange[0]) + .WhereIF(input.CreateTimeRange != null && input.CreateTimeRange.Count > 1 && input.CreateTimeRange[1].HasValue, x => x.CreateTime < ((DateTime)input.CreateTimeRange[1]).AddDays(1)); + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 查询退款信息列表 + /// + /// + /// + [HttpPost] + [DisplayName("根据支付id获取退款信息列表")] + public async Task> ListRefund([FromBody] string id) + { + var query = _sysWechatRefundRep.AsQueryable() + .Where(u => u.TransactionId == id); + return await query.ToListAsync(); + } + + /// + /// 生成JSAPI调起支付所需参数 🔖 + /// + /// + /// + [DisplayName("生成JSAPI调起支付所需参数")] + public WechatPayParaOutput GenerateParametersForJsapiPay(WechatPayParaInput input) + { + var data = _wechatTenpayClient.GenerateParametersForJsapiPayRequest(_wechatPayOptions.AppId, input.PrepayId); + return new WechatPayParaOutput() + { + AppId = data["appId"], + TimeStamp = data["timeStamp"], + NonceStr = data["nonceStr"], + Package = data["package"], + SignType = data["signType"], + PaySign = data["paySign"] + }; + } + + /// + /// 微信支付下单(商户直连) 🔖 + /// + [DisplayName("微信支付下单(商户直连)")] + public async Task CreatePayTransaction([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayTransactionJsapiRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayTransactionJsapiRequest.Types.Amount() { Total = input.Total }, + Payer = new CreatePayTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayTransactionJsapiAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage); + + var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId }); + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + OpenId = input.OpenId, + TransactionId = "", + Tags = input.Tags, + BusinessId = input.BusinessId, + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + + return new WechatPayTransactionOutput() + { + PrepayId = response.PrepayId, + OutTradeNumber = request.OutTradeNumber, + SingInfo = singInfo + }; + } + + /// + /// 微信支付下单(商户直连)Native + /// + [DisplayName("微信支付下单(商户直连)Native")] + public async Task CreatePayTransactionNative([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayTransactionNativeRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayTransactionNativeRequest.Types.Amount() { Total = input.Total }, + //Payer = new CreatePayTransactionNativeRequest.Types.Payer() { OpenId = input.OpenId } + Scene = new CreatePayTransactionNativeRequest.Types.Scene() { ClientIp = "127.0.0.1" } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayTransactionNativeAsync(request); + if (!response.IsSuccessful()) + { + JsonConvert.SerializeObject(response).LogInformation(); + throw Oops.Oh(response.ErrorMessage); + } + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + //OpenId = input.OpenId, + TransactionId = "", + QrcodeContent = response.QrcodeUrl, + Tags = input.Tags, + BusinessId = input.BusinessId, + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + return new + { + request.OutTradeNumber, + response.QrcodeUrl + }; + } + + /// + /// 微信支付下单(服务商模式) 🔖 + /// + [DisplayName("微信支付下单(服务商模式)")] + public async Task CreatePayPartnerTransaction([FromBody] WechatPayTransactionInput input) + { + var request = new CreatePayPartnerTransactionJsapiRequest() + { + OutTradeNumber = DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + ExpireTime = DateTimeOffset.Now.AddMinutes(10), + NotifyUrl = _payCallBackOptions.WechatPayUrl, + Amount = new CreatePayPartnerTransactionJsapiRequest.Types.Amount() { Total = input.Total }, + Payer = new CreatePayPartnerTransactionJsapiRequest.Types.Payer() { OpenId = input.OpenId } + }; + var response = await _wechatTenpayClient.ExecuteCreatePayPartnerTransactionJsapiAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage); + var singInfo = this.GenerateParametersForJsapiPay(new WechatPayParaInput() { PrepayId = response.PrepayId }); + // 保存订单信息 + var wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Description = input.Description, + Attachment = input.Attachment, + GoodsTag = input.GoodsTag, + Total = input.Total, + OpenId = input.OpenId, + TransactionId = "" + }; + await _sysWechatPayRep.InsertAsync(wechatPay); + + return new + { + response.PrepayId, + request.OutTradeNumber, + singInfo + }; + } + + /// + /// 获取支付订单详情(本地库) 🔖 + /// + /// + /// + [DisplayName("获取支付订单详情(本地库)")] + public async Task GetPayInfo(string tradeId) + { + return await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == tradeId); + } + + /// + /// 获取支付订单详情(微信接口) 🔖 + /// + /// + /// + [DisplayName("获取支付订单详情(微信接口)")] + public async Task GetPayInfoFromWechat(string tradeId) + { + var request = new GetPayTransactionByOutTradeNumberRequest(); + request.OutTradeNumber = tradeId; + var response = await _wechatTenpayClient.ExecuteGetPayTransactionByOutTradeNumberAsync(request); + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber + && u.MerchantId == response.MerchantId); + // 如果状态不一致就更新数据库中的记录 + if (wechatPay != null && wechatPay.TradeState != response.TradeState) + { + wechatPay.OpenId = response.Payer.OpenId; + wechatPay.TransactionId = response.TransactionId; // 支付订单号 + wechatPay.TradeType = response.TradeType; // 交易类型 + wechatPay.TradeState = response.TradeState; // 交易状态 + wechatPay.TradeStateDescription = response.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = response.BankType; // 付款银行类型 + wechatPay.Total = response.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = response.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间 + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + } + wechatPay = new SysWechatPay() + { + AppId = _wechatPayOptions.AppId, + MerchantId = _wechatPayOptions.MerchantId, + SubAppId = _wechatPayOptions.AppId, + SubMerchantId = _wechatPayOptions.MerchantId, + OutTradeNumber = request.OutTradeNumber, + Attachment = response.Attachment, + Total = response.Amount.Total, // 订单总金额 + TransactionId = response.TransactionId, + TradeType = response.TradeType, // 交易类型 + TradeState = response.TradeState, // 交易状态 + TradeStateDescription = response.TradeStateDescription, // 交易状态描述 + BankType = response.BankType, // 付款银行类型 + PayerTotal = response.Amount.PayerTotal, // 用户支付金额 + SuccessTime = response.SuccessTime.Value.DateTime // 支付完成时间 + }; + return wechatPay; + } + + /// + /// 退款申请 + /// + /// + /// + [DisplayName("退款申请")] + [HttpPost] + public async Task CreateRefundDomestic([FromBody] WechatPayRefundDomesticInput input) + { + // refund/domestic/refunds + var request = new CreateRefundDomesticRefundRequest() + { + Amount = new CreateRefundDomesticRefundRequest.Types.Amount() + { + Refund = input.Refund, + Total = input.Total, + Currency = "CNY" + }, + + OutTradeNumber = input.TradeId, + OutRefundNumber = "R" + DateTimeOffset.Now.ToString("yyyyMMddHHmmssfff") + (new Random()).Next(100, 1000), // 订单号 + NotifyUrl = _payCallBackOptions.WechatRefundUrl, // 应采用WechatRefundUrl参数,如与WechatPayUrl入口相同,也应分开设置参数 + Reason = input.Reason, + }; + var response = await _wechatTenpayClient.ExecuteCreateRefundDomesticRefundAsync(request); + if (string.IsNullOrEmpty(response.ErrorCode)) + { + // 成功了,这里应该保存退款订单信息 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == response.OutTradeNumber); + // 保存订单信息 + if (wechatPay != null) + { + var wechatRefund = new SysWechatRefund() + { + WechatPayId = wechatPay.Id, + TransactionId = response.TransactionId, + Refund = input.Refund, + Reason = input.Reason, + OutRefundNumber = request.OutRefundNumber, + Channel = response.Channel, + UserReceivedAccount = response.UserReceivedAccount, + }; + await _sysWechatRefundRep.InsertAsync(wechatRefund); + } + } + else + { + throw Oops.Bah($"[{response.ErrorCode}]{response.ErrorMessage}"); + } + return response; + } + + /// + /// 获取退款订单详情(微信接口) + /// + /// + /// + [DisplayName("获取退款订单详情(微信接口)")] + public async Task GetRefundInfoFromWechat(string refundId) + { + var request = new GetRefundDomesticRefundByOutRefundNumberRequest(); + request.OutRefundNumber = refundId; + var response = await _wechatTenpayClient.ExecuteGetRefundDomesticRefundByOutRefundNumberAsync(request); + // 修改订单支付状态 + var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == refundId); + // 如果状态不一致就更新数据库中的记录 + if (wechatRefund != null && wechatRefund.TradeState != response.Status) + { + wechatRefund.TransactionId = response.TransactionId; // 支付订单号 + wechatRefund.TradeState = response.Status; // 交易状态 + wechatRefund.SuccessTime = response.SuccessTime.Value.DateTime; // 支付完成时间 + await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync(); + // 有退款,刷新一下订单状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.Id == wechatRefund.WechatPayId); + if (wechatPay != null) + await GetPayInfoFromWechat(wechatPay.OutTradeNumber); + } + wechatRefund = new SysWechatRefund() + { + TransactionId = response.TransactionId, + Refund = response.Amount.Refund, + OutRefundNumber = request.OutRefundNumber, + Channel = response.Channel, + UserReceivedAccount = response.UserReceivedAccount, + TradeState = response.Status, // 交易状态 + SuccessTime = response.SuccessTime.Value.DateTime, // 支付完成时间 + }; + return wechatRefund; + } + + /// + /// 微信支付成功回调(商户直连) + /// + /// + [AllowAnonymous] + [DisplayName("微信支付成功回调(商户直连)")] + public async Task PayCallBack() + { + using var ms = new MemoryStream(); + await App.HttpContext.Request.Body.CopyToAsync(ms); + var b = ms.ToArray(); + var callbackJson = Encoding.UTF8.GetString(b); + + var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson); + if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType)) + { + try + { + var callbackPayResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackPayResource.OutTradeNumber + && u.MerchantId == callbackPayResource.MerchantId); + if (wechatPay == null) return null; + wechatPay.OpenId = callbackPayResource.Payer.OpenId; // 支付者标识 + //wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号 + //wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号 + wechatPay.TransactionId = callbackPayResource.TransactionId; // 支付订单号 + wechatPay.TradeType = callbackPayResource.TradeType; // 交易类型 + wechatPay.TradeState = callbackPayResource.TradeState; // 交易状态 + wechatPay.TradeStateDescription = callbackPayResource.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = callbackPayResource.BankType; // 付款银行类型 + wechatPay.Total = callbackPayResource.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = callbackPayResource.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = callbackPayResource.SuccessTime.DateTime; // 支付完成时间 + + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + + return new WechatPayOutput() + { + Total = wechatPay.Total, + Attachment = wechatPay.Attachment, + GoodsTag = wechatPay.GoodsTag + }; + } + catch (Exception ex) + { + "微信支付回调时出错:".LogError(ex); + } + } + else if ("REFUND.SUCCESS".Equals(callbackModel.EventType)) + { + //参考:https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/refund-result-notice.html + try + { + var callbackRefundResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + // 修改订单支付状态 + var wechatRefund = await _sysWechatRefundRep.GetFirstAsync(u => u.OutRefundNumber == callbackRefundResource.OutRefundNumber); + if (wechatRefund == null) return null; + wechatRefund.TradeState = callbackRefundResource.RefundStatus; // 交易状态 + wechatRefund.SuccessTime = callbackRefundResource.SuccessTime.Value.DateTime; // 支付完成时间 + + await _sysWechatRefundRep.AsUpdateable(wechatRefund).IgnoreColumns(true).ExecuteCommandAsync(); + // 有退款,刷新一下订单状态 + await GetPayInfoFromWechat(callbackRefundResource.OutTradeNumber); + } + catch (Exception ex) + { + "微信退款回调时出错:".LogError(ex); + } + } + else + { + callbackModel.EventType.LogInformation(); + } + + return null; + } + + /// + /// 微信支付成功回调(服务商模式) 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("微信支付成功回调(服务商模式)")] + public async Task PayPartnerCallBack() + { + using var ms = new MemoryStream(); + await App.HttpContext.Request.Body.CopyToAsync(ms); + var b = ms.ToArray(); + var callbackJson = Encoding.UTF8.GetString(b); + + var callbackModel = _wechatTenpayClient.DeserializeEvent(callbackJson); + if ("TRANSACTION.SUCCESS".Equals(callbackModel.EventType)) + { + var callbackResource = _wechatTenpayClient.DecryptEventResource(callbackModel); + + // 修改订单支付状态 + var wechatPay = await _sysWechatPayRep.GetFirstAsync(u => u.OutTradeNumber == callbackResource.OutTradeNumber + && u.MerchantId == callbackResource.MerchantId); + if (wechatPay == null) return; + //wechatPay.OpenId = callbackResource.Payer.OpenId; // 支付者标识 + //wechatPay.MerchantId = callbackResource.MerchantId; // 微信商户号 + //wechatPay.OutTradeNumber = callbackResource.OutTradeNumber; // 商户订单号 + wechatPay.TransactionId = callbackResource.TransactionId; // 支付订单号 + wechatPay.TradeType = callbackResource.TradeType; // 交易类型 + wechatPay.TradeState = callbackResource.TradeState; // 交易状态 + wechatPay.TradeStateDescription = callbackResource.TradeStateDescription; // 交易状态描述 + wechatPay.BankType = callbackResource.BankType; // 付款银行类型 + wechatPay.Total = callbackResource.Amount.Total; // 订单总金额 + wechatPay.PayerTotal = callbackResource.Amount.PayerTotal; // 用户支付金额 + wechatPay.SuccessTime = callbackResource.SuccessTime.DateTime; // 支付完成时间 + + await _sysWechatPayRep.AsUpdateable(wechatPay).IgnoreColumns(true).ExecuteCommandAsync(); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs new file mode 100644 index 0000000..881b9a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs @@ -0,0 +1,202 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信公众号服务 🧩 +/// +[ApiDescriptionSettings(Order = 230)] +public class SysWechatService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + private readonly SysConfigService _sysConfigService; + private readonly WechatApiClientFactory _wechatApiClientFactory; + private readonly WechatApiClient _wechatApiClient; + + public SysWechatService(SqlSugarRepository sysWechatUserRep, + SysConfigService sysConfigService, + WechatApiClientFactory wechatApiClientFactory, + SysCacheService sysCacheService) + { + _sysWechatUserRep = sysWechatUserRep; + _sysConfigService = sysConfigService; + _wechatApiClientFactory = wechatApiClientFactory; + _wechatApiClient = wechatApiClientFactory.CreateWechatClient(); + } + + /// + /// 生成网页授权Url 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("生成网页授权Url")] + public string GenAuthUrl(GenAuthUrlInput input) + { + return _wechatApiClient.GenerateParameterizedUrlForConnectOAuth2Authorize(input.RedirectUrl, input.Scope, input.State); + } + + /// + /// 获取微信用户OpenId 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户OpenId")] + public async Task SnsOAuth2([FromQuery] WechatOAuth2Input input) + { + var reqOAuth2 = new SnsOAuth2AccessTokenRequest() + { + Code = input.Code, + }; + var resOAuth2 = await _wechatApiClient.ExecuteSnsOAuth2AccessTokenAsync(reqOAuth2); + if (resOAuth2.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resOAuth2.ErrorMessage + " " + resOAuth2.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resOAuth2.OpenId); + if (wxUser == null) + { + var reqUserInfo = new SnsUserInfoRequest() + { + OpenId = resOAuth2.OpenId, + AccessToken = resOAuth2.AccessToken, + }; + var resUserInfo = await _wechatApiClient.ExecuteSnsUserInfoAsync(reqUserInfo); + wxUser = resUserInfo.Adapt(); + wxUser.Avatar = resUserInfo.HeadImageUrl; + wxUser.NickName = resUserInfo.Nickname; + wxUser.OpenId = resOAuth2.OpenId; + wxUser.UnionId = resOAuth2.UnionId; + wxUser.AccessToken = resOAuth2.AccessToken; + wxUser.RefreshToken = resOAuth2.RefreshToken; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + wxUser.AccessToken = resOAuth2.AccessToken; + wxUser.RefreshToken = resOAuth2.RefreshToken; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return resOAuth2.OpenId; + } + + /// + /// 微信用户登录OpenId 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("微信用户登录OpenId")] + public async Task OpenIdLogin(WechatUserLogin input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("微信用户登录OpenId错误"); + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + return new + { + wxUser.Avatar, + accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, wxUser.Id }, + { ClaimConst.NickName, wxUser.NickName }, + { ClaimConst.LoginMode, LoginModeEnum.APP }, + }, tokenExpire) + }; + } + + /// + /// 获取配置签名参数(wx.config) 🔖 + /// + /// + [DisplayName("获取配置签名参数(wx.config)")] + public async Task GenConfigPara(SignatureInput input) + { + string ticket = await _wechatApiClientFactory.TryGetWechatJsApiTicketAsync(); + return _wechatApiClient.GenerateParametersForJSSDKConfig(ticket, input.Url); + } + + /// + /// 获取模板列表 🔖 + /// + [DisplayName("获取模板列表")] + public async Task GetMessageTemplateList() + { + var accessToken = await GetCgibinToken(); + var reqTemplate = new CgibinTemplateGetAllPrivateTemplateRequest() + { + AccessToken = accessToken + }; + var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateGetAllPrivateTemplateAsync(reqTemplate); + if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode); + + return resTemplate.TemplateList; + } + + /// + /// 发送模板消息 🔖 + /// + /// + /// + [DisplayName("发送模板消息")] + public async Task SendTemplateMessage(MessageTemplateSendInput input) + { + var dataInfo = input.Data.ToDictionary(k => k.Key, k => k.Value); + var messageData = new Dictionary(); + foreach (var item in dataInfo) + { + messageData.Add(item.Key, new CgibinMessageTemplateSendRequest.Types.DataItem() { Value = "" + item.Value.Value.ToString() + "" }); + } + + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinMessageTemplateSendRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId, + ToUserOpenId = input.ToUserOpenId, + Url = input.Url, + MiniProgram = new CgibinMessageTemplateSendRequest.Types.MiniProgram + { + AppId = _wechatApiClientFactory._wechatOptions.WxOpenAppId, + PagePath = input.MiniProgramPagePath, + }, + Data = messageData + }; + var resMessage = await _wechatApiClient.ExecuteCgibinMessageTemplateSendAsync(reqMessage); + return resMessage; + } + + /// + /// 删除模板 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "DeleteMessageTemplate"), HttpPost] + [DisplayName("删除模板")] + public async Task DeleteMessageTemplate(DeleteMessageTemplateInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinTemplateDeletePrivateTemplateRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId + }; + var resTemplate = await _wechatApiClient.ExecuteCgibinTemplateDeletePrivateTemplateAsync(reqMessage); + return resTemplate; + } + + /// + /// 获取Access_token + /// + [NonAction] + public async Task GetCgibinToken() + { + return await _wechatApiClientFactory.TryGetWechatAccessTokenAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs new file mode 100644 index 0000000..3e3f38a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatUserService.cs @@ -0,0 +1,73 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信账号服务 🧩 +/// +[ApiDescriptionSettings(Order = 220)] +public class SysWechatUserService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + + public SysWechatUserService(SqlSugarRepository sysWechatUserRep) + { + _sysWechatUserRep = sysWechatUserRep; + } + + /// + /// 获取微信用户列表 🔖 + /// + /// + /// + [DisplayName("获取微信用户列表")] + public async Task> Page(WechatUserInput input) + { + return await _sysWechatUserRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.NickName), u => u.NickName.Contains(input.NickName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Mobile), u => u.Mobile.Contains(input.Mobile)) + .OrderBy(u => u.Id, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + [DisplayName("增加微信用户")] + public async Task AddWechatUser(SysWechatUser input) + { + await _sysWechatUserRep.InsertAsync(input.Adapt()); + } + + /// + /// 更新微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + [DisplayName("更新微信用户")] + public async Task UpdateWechatUser(SysWechatUser input) + { + var weChatUser = input.Adapt(); + await _sysWechatUserRep.AsUpdateable(weChatUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除微信用户 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + [DisplayName("删除微信用户")] + public async Task DeleteWechatUser(DeleteWechatUserInput input) + { + await _sysWechatUserRep.DeleteAsync(u => u.Id == input.Id); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs new file mode 100644 index 0000000..24e2694 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/SysWxOpenService.cs @@ -0,0 +1,407 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core.Service; + +/// +/// 微信小程序服务 🧩 +/// +[ApiDescriptionSettings(Order = 240)] +public class SysWxOpenService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _sysWechatUserRep; + private readonly SysConfigService _sysConfigService; + private readonly WechatApiClient _wechatApiClient; + private readonly SysFileService _sysFileService; + private readonly WechatApiClientFactory _wechatApiClientFactory; + + public SysWxOpenService(SqlSugarRepository sysWechatUserRep, + SysConfigService sysConfigService, + WechatApiClientFactory wechatApiClientFactory, + SysFileService sysFileService) + { + _sysWechatUserRep = sysWechatUserRep; + _sysConfigService = sysConfigService; + _wechatApiClient = wechatApiClientFactory.CreateWxOpenClient(); + _sysFileService = sysFileService; + _wechatApiClientFactory = wechatApiClientFactory; + } + + /// + /// 获取微信用户OpenId 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户OpenId")] + public async Task GetWxOpenId([FromQuery] JsCode2SessionInput input) + { + var reqJsCode2Session = new SnsJsCode2SessionRequest() + { + JsCode = input.JsCode, + }; + var resCode2Session = await _wechatApiClient.ExecuteSnsJsCode2SessionAsync(reqJsCode2Session); + if (resCode2Session.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCode2Session.ErrorMessage + " " + resCode2Session.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == resCode2Session.OpenId); + if (wxUser == null) + { + wxUser = new SysWechatUser + { + OpenId = resCode2Session.OpenId, + UnionId = resCode2Session.UnionId, + SessionKey = resCode2Session.SessionKey, + PlatformType = PlatformTypeEnum.微信小程序 + }; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return new WxOpenIdOutput + { + OpenId = resCode2Session.OpenId + }; + } + + /// + /// 获取微信用户电话号码 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("获取微信用户电话号码")] + public async Task GetWxPhone([FromQuery] WxPhoneInput input) + { + var accessToken = await GetCgibinToken(); + var reqUserPhoneNumber = new WxaBusinessGetUserPhoneNumberRequest() + { + Code = input.Code, + AccessToken = accessToken, + }; + var resUserPhoneNumber = await _wechatApiClient.ExecuteWxaBusinessGetUserPhoneNumberAsync(reqUserPhoneNumber); + if (resUserPhoneNumber.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resUserPhoneNumber.ErrorMessage + " " + resUserPhoneNumber.ErrorCode); + + var wxUser = await _sysWechatUserRep.GetFirstAsync(p => p.OpenId == input.OpenId); + if (wxUser == null) + { + wxUser = new SysWechatUser + { + OpenId = input.OpenId, + Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber, + PlatformType = PlatformTypeEnum.微信小程序 + }; + wxUser = await _sysWechatUserRep.AsInsertable(wxUser).ExecuteReturnEntityAsync(); + } + else + { + wxUser.Mobile = resUserPhoneNumber.PhoneInfo?.PhoneNumber; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + } + + return new WxPhoneOutput + { + PhoneNumber = resUserPhoneNumber.PhoneInfo?.PhoneNumber + }; + } + + /// + /// 微信小程序登录OpenId 🔖 + /// + /// + /// + [AllowAnonymous] + [DisplayName("微信小程序登录OpenId")] + public async Task WxOpenIdLogin(WxOpenIdLoginInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("微信小程序登录失败"); + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + return new + { + wxUser.Avatar, + accessToken = JWTEncryption.Encrypt(new Dictionary + { + { ClaimConst.UserId, wxUser.Id }, + { ClaimConst.RealName, wxUser.NickName }, + { ClaimConst.LoginMode, LoginModeEnum.APP }, + }, tokenExpire) + }; + } + + /// + /// 上传小程序头像 + /// + /// + /// + [AllowAnonymous] + [DisplayName("上传小程序头像")] + public async Task UploadAvatar([FromForm] UploadAvatarInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("未找到用户上传失败"); + + var res = await _sysFileService.UploadFile(new UploadFileInput { File = input.File, FileType = input.FileType }, "upload/wechatAvatar"); + wxUser.Avatar = res.Url; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + + return res; + } + + /// + /// 设置小程序用户昵称 + /// + /// + /// + [AllowAnonymous] + [HttpPost] + public async Task SetNickName(SetNickNameInput input) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == input.OpenId); + if (wxUser == null) + throw Oops.Oh("未找到用户信息设置失败"); + wxUser.NickName = input.NickName; + await _sysWechatUserRep.AsUpdateable(wxUser).IgnoreColumns(true).ExecuteCommandAsync(); + return; + } + + /// + /// 获取小程序用户信息 + /// + /// + /// + [AllowAnonymous] + public async Task GetUserInfo(string openid) + { + var wxUser = await _sysWechatUserRep.GetFirstAsync(u => u.OpenId == openid); + if (wxUser == null) + throw Oops.Oh("未找到用户信息获取失败"); + return new { nickName = wxUser.NickName, avator = wxUser.Avatar }; + } + + /// + /// 获取订阅消息模板列表 🔖 + /// + [DisplayName("获取订阅消息模板列表")] + public async Task GetMessageTemplateList() + { + var accessToken = await GetCgibinToken(); + var reqTemplate = new WxaApiNewTemplateGetTemplateRequest() + { + AccessToken = accessToken + }; + var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateGetTemplateAsync(reqTemplate); + if (resTemplate.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resTemplate.ErrorMessage + " " + resTemplate.ErrorCode); + + return resTemplate.TemplateList; + } + + /// + /// 发送订阅消息 🔖 + /// + /// + /// + [DisplayName("发送订阅消息")] + public async Task SendSubscribeMessage(SendSubscribeMessageInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new CgibinMessageSubscribeSendRequest() + { + AccessToken = accessToken, + TemplateId = input.TemplateId, + ToUserOpenId = input.ToUserOpenId, + Data = input.Data, + MiniProgramState = input.MiniprogramState, + Language = input.Language, + MiniProgramPagePath = input.MiniProgramPagePath + }; + var resMessage = await _wechatApiClient.ExecuteCgibinMessageSubscribeSendAsync(reqMessage); + return resMessage; + } + + /// + /// 增加订阅消息模板 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "AddSubscribeMessageTemplate"), HttpPost] + [DisplayName("增加订阅消息模板")] + public async Task AddSubscribeMessageTemplate(AddSubscribeMessageTemplateInput input) + { + var accessToken = await GetCgibinToken(); + var reqMessage = new WxaApiNewTemplateAddTemplateRequest() + { + AccessToken = accessToken, + TemplateTitleId = input.TemplateTitleId, + KeyworkIdList = input.KeyworkIdList, + SceneDescription = input.SceneDescription + }; + var resTemplate = await _wechatApiClient.ExecuteWxaApiNewTemplateAddTemplateAsync(reqMessage); + return resTemplate; + } + + /// + /// 生成带参数小程序二维码(总共生成的码数量限制为 100,000) + /// + /// 扫码进入的小程序页面路径,最大长度 128 个字符,不能为空; eg: pages / index ? id = AY000001 + /// + [DisplayName("生成小程序二维码")] + [ApiDescriptionSettings(Name = "GenerateQRImage")] + public async Task GenerateQRImageAsync(GenerateQRImageInput input) + { + GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput(); + if (input.PagePath.IsNullOrEmpty()) + { + generateQRImageOutInput.Message = $"生成失败 页面路径不能为空"; + return generateQRImageOutInput; + } + + if (input.ImageName.IsNullOrEmpty()) + { + input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss"); + } + + var accessToken = await GetCgibinToken(); + var request = new CgibinWxaappCreateWxaQrcodeRequest + { + AccessToken = accessToken, + Path = input.PagePath, + Width = input.Width + }; + var response = await _wechatApiClient.ExecuteCgibinWxaappCreateWxaQrcodeAsync(request); + + if (response.IsSuccessful()) + { + var QRImagePath = App.GetConfig("Wechat:QRImagePath"); + var relativeImgPath = string.Empty; + + // 判断路径是绝对路径还是相对路径 + var isPathRooted = Path.IsPathRooted(QRImagePath); + if (!isPathRooted) + { + // 相对路径 + relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImage") : QRImagePath; + QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath); + } + + //判断文件存放路径是否存在 + if (!Directory.Exists(QRImagePath)) + { + Directory.CreateDirectory(QRImagePath); + } + // 将二维码图片数据保存为文件 + var fileName = $"{input.ImageName.ToUpper()}.png"; + var filePath = Path.Combine(QRImagePath, fileName); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.WriteAllBytes(filePath, response.GetRawBytes()); + + generateQRImageOutInput.Success = true; + generateQRImageOutInput.ImgPath = filePath; + generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName); + generateQRImageOutInput.Message = "生成成功"; + } + else + { + // 处理错误情况 + generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}"; + } + return generateQRImageOutInput; + } + + /// + /// 生成二维码(获取不受限制的小程序码) + /// + /// 入参 + /// + [DisplayName("生成小程序二维码")] + [ApiDescriptionSettings(Name = "GenerateQRImageUnlimit")] + public async Task GenerateQRImageUnlimitAsync(GenerateQRImageUnLimitInput input) + { + GenerateQRImageOutput generateQRImageOutInput = new GenerateQRImageOutput(); + if (input.PagePath.IsNullOrEmpty()) + { + generateQRImageOutInput.Message = $"生成失败,页面路径不能为空"; + return generateQRImageOutInput; + } + + if (input.Scene.Length > 32) + { + generateQRImageOutInput.Message = $"生成失败,携带的参数长度超过限制"; + return generateQRImageOutInput; + } + + if (input.ImageName.IsNullOrEmpty()) + { + input.ImageName = DateTime.Now.ToString("yyyyMMddHHmmss"); + } + + var accessToken = await GetCgibinToken(); + var request = new WxaGetWxaCodeRequest + { + AccessToken = accessToken, + Width = input.Width, + PagePath = input.PagePath, + }; + var response = await _wechatApiClient.ExecuteWxaGetWxaCodeAsync(request); + + if (response.IsSuccessful()) + { + var QRImagePath = App.GetConfig("Wechat:QRImagePath"); + var relativeImgPath = string.Empty; + + // 判断路径是绝对路径还是相对路径 + var isPathRooted = Path.IsPathRooted(QRImagePath); + if (!isPathRooted) + { + // 相对路径 + relativeImgPath = string.IsNullOrEmpty(QRImagePath) ? Path.Combine("upload", "QRImageUnLimit") : QRImagePath; + QRImagePath = Path.Combine(App.WebHostEnvironment.WebRootPath, relativeImgPath); + } + + //判断文件存放路径是否存在 + if (!Directory.Exists(QRImagePath)) + { + Directory.CreateDirectory(QRImagePath); + } + // 将二维码图片数据保存为文件 + var fileName = $"{input.ImageName.ToUpper()}.png"; + var filePath = Path.Combine(QRImagePath, fileName); + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + File.WriteAllBytes(filePath, response.GetRawBytes()); + + generateQRImageOutInput.Success = true; + generateQRImageOutInput.ImgPath = filePath; + generateQRImageOutInput.RelativeImgPath = Path.Combine(relativeImgPath, fileName); + generateQRImageOutInput.Message = "生成成功"; + } + else + { + // 处理错误情况 + generateQRImageOutInput.Message = $"生成失败 错误代码:{response.ErrorCode} 错误描述:{response.ErrorMessage}"; + } + return generateQRImageOutInput; + } + + /// + /// 获取Access_token + /// + private async Task GetCgibinToken() + { + return await _wechatApiClientFactory.TryGetWxOpenAccessTokenAsync(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs new file mode 100644 index 0000000..ae9a175 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Service/Wechat/WechatApiHttpClient.cs @@ -0,0 +1,208 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core.Service; + +/// +/// 微信API客户端 +/// +public partial class WechatApiClientFactory : ISingleton +{ + private readonly IHttpClientFactory _httpClientFactory; + public readonly WechatOptions _wechatOptions; + private readonly SysCacheService _sysCacheService; + + public WechatApiClientFactory(IHttpClientFactory httpClientFactory, IOptions wechatOptions, SysCacheService sysCacheService) + { + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + _wechatOptions = wechatOptions.Value ?? throw new ArgumentNullException(nameof(wechatOptions)); + _sysCacheService = sysCacheService; + } + + /// + /// 微信公众号 + /// + /// + public WechatApiClient CreateWechatClient() + { + if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) + throw Oops.Oh("微信公众号配置错误"); + + var client = WechatApiClientBuilder.Create(new WechatApiClientOptions() + { + AppId = _wechatOptions.WechatAppId, + AppSecret = _wechatOptions.WechatAppSecret, + PushToken = _wechatOptions.WechatToken, + PushEncodingAESKey = _wechatOptions.WechatEncodingAESKey, + }) + .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁 + .Build(); + + client.Configure(config => + { + JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings(); + jsonSerializerSettings.Formatting = Formatting.Indented; + config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化 + // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化 + }); + + return client; + } + + /// + /// 微信小程序 + /// + /// + public WechatApiClient CreateWxOpenClient() + { + if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) + throw Oops.Oh("微信小程序配置错误"); + + var client = WechatApiClientBuilder.Create(new WechatApiClientOptions() + { + AppId = _wechatOptions.WxOpenAppId, + AppSecret = _wechatOptions.WxOpenAppSecret, + PushToken = _wechatOptions.WxToken, + PushEncodingAESKey = _wechatOptions.WxEncodingAESKey, + }) + .UseHttpClient(_httpClientFactory.CreateClient(), disposeClient: false) // 设置 HttpClient 不随客户端一同销毁 + .Build(); + + client.Configure(config => + { + JsonSerializerSettings jsonSerializerSettings = NewtonsoftJsonSerializer.GetDefaultSerializerSettings(); + jsonSerializerSettings.Formatting = Formatting.Indented; + config.JsonSerializer = new NewtonsoftJsonSerializer(jsonSerializerSettings); // 指定 System.Text.Json JSON序列化 + // config.JsonSerializer = new SystemTextJsonSerializer(jsonSerializerOptions); // 指定 Newtonsoft.Json JSON序列化 + }); + + return client; + } + + /// + /// 获取微信公众号AccessToken + /// + /// + public async Task TryGetWechatAccessTokenAsync() + { + if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxAccessToken_{_wechatOptions.WechatAppId}"))) + { + var client = CreateWechatClient(); + var reqCgibinToken = new CgibinTokenRequest(); + var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken); + if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode); + _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WechatAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60)); + } + + return _sysCacheService.Get($"WxAccessToken_{_wechatOptions.WechatAppId}"); + } + + /// + /// 获取微信小程序AccessToken + /// + /// + public async Task TryGetWxOpenAccessTokenAsync() + { + if (!_sysCacheService.ExistKey($"WxAccessToken_{_wechatOptions.WxOpenAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxAccessToken_{_wechatOptions.WxOpenAppId}"))) + { + var client = CreateWxOpenClient(); + var reqCgibinToken = new CgibinTokenRequest(); + var resCgibinToken = await client.ExecuteCgibinTokenAsync(reqCgibinToken); + if (resCgibinToken.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + throw Oops.Oh(resCgibinToken.ErrorMessage + " " + resCgibinToken.ErrorCode); + _sysCacheService.Set($"WxAccessToken_{_wechatOptions.WxOpenAppId}", resCgibinToken.AccessToken, TimeSpan.FromSeconds(resCgibinToken.ExpiresIn - 60)); + } + + return _sysCacheService.Get($"WxAccessToken_{_wechatOptions.WxOpenAppId}"); + } + + /// + /// 检查微信公众号AccessToken + /// + /// + public async Task CheckWechatAccessTokenAsync() + { + if (string.IsNullOrEmpty(_wechatOptions.WechatAppId) || string.IsNullOrEmpty(_wechatOptions.WechatAppSecret)) return; + + var req = new CgibinOpenApiQuotaGetRequest + { + AccessToken = await TryGetWechatAccessTokenAsync(), + CgiPath = "/cgi-bin/token" + }; + var client = CreateWechatClient(); + var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req); + + var originColor = Console.ForegroundColor; + if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + { + _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WechatAppId}"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 无效"); + } + else + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信公众号令牌 有效"); + } + Console.ForegroundColor = originColor; + } + + /// + /// 检查微信小程序AccessToken + /// + /// + public async Task CheckWxOpenAccessTokenAsync() + { + if (string.IsNullOrEmpty(_wechatOptions.WxOpenAppId) || string.IsNullOrEmpty(_wechatOptions.WxOpenAppSecret)) return; + + var req = new CgibinOpenApiQuotaGetRequest + { + AccessToken = await TryGetWxOpenAccessTokenAsync(), + CgiPath = "/cgi-bin/token" + }; + var client = CreateWxOpenClient(); + var res = await client.ExecuteCgibinOpenApiQuotaGetAsync(req); + + var originColor = Console.ForegroundColor; + if (res.ErrorCode != (int)WechatReturnCodeEnum.请求成功) + { + _sysCacheService.Remove($"WxAccessToken_{_wechatOptions.WxOpenAppId}"); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 无效"); + } + else + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine("【" + DateTime.Now + "】" + _wechatOptions.WxOpenAppId + " 微信小程序令牌 有效"); + } + Console.ForegroundColor = originColor; + } + + /// + /// 获取微信JS接口临时票据jsapi_ticket + /// + /// + public async Task TryGetWechatJsApiTicketAsync() + { + if (!_sysCacheService.ExistKey($"WxJsApiTicket_{_wechatOptions.WechatAppId}") || string.IsNullOrEmpty(_sysCacheService.Get($"WxJsApiTicket_{_wechatOptions.WechatAppId}"))) + { + var accessToken = await TryGetWechatAccessTokenAsync(); + var client = CreateWechatClient(); + var request = new CgibinTicketGetTicketRequest() + { + AccessToken = accessToken + }; + var response = await client.ExecuteCgibinTicketGetTicketAsync(request); + if (!response.IsSuccessful()) + throw Oops.Oh(response.ErrorMessage + " " + response.ErrorCode); + _sysCacheService.Set($"WxJsApiTicket_{_wechatOptions.WechatAppId}", response.Ticket, TimeSpan.FromSeconds(response.ExpiresIn - 60)); + } + return _sysCacheService.Get($"WxJsApiTicket_{_wechatOptions.WechatAppId}"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignalR/SignalRSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignalR/SignalRSetup.cs new file mode 100644 index 0000000..7029ac3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignalR/SignalRSetup.cs @@ -0,0 +1,104 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.Logging.Extensions; +using Microsoft.AspNetCore.DataProtection; +using Newtonsoft.Json; +using StackExchange.Redis; + +namespace Admin.NET.Core; + +public static class SignalRSetup +{ + /// + /// 即时消息SignalR注册 + /// + /// + /// + public static void AddSignalR(this IServiceCollection services, Action SetNewtonsoftJsonSetting) + { + var signalRBuilder = services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.ClientTimeoutInterval = TimeSpan.FromMinutes(2); + options.KeepAliveInterval = TimeSpan.FromMinutes(1); + options.MaximumReceiveMessageSize = 1024 * 1024 * 10; // 数据包大小10M,默认最大为32K + }).AddNewtonsoftJsonProtocol(options => SetNewtonsoftJsonSetting(options.PayloadSerializerSettings)); + + // 若未启用Redis缓存,直接返回 + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType != CacheTypeEnum.Redis.ToString()) + return; + + // 若已开启集群配置,则把SignalR配置为支持集群模式 + var clusterOpt = App.GetConfig("Cluster", true); + if (!clusterOpt.Enabled) + return; + + var redisOptions = clusterOpt.SentinelConfig; + ConnectionMultiplexer connection1; + if (clusterOpt.IsSentinel) // 哨兵模式 + { + var redisConfig = new ConfigurationOptions + { + AbortOnConnectFail = false, + ServiceName = redisOptions.ServiceName, + AllowAdmin = true, + DefaultDatabase = redisOptions.DefaultDb, + Password = redisOptions.Password + }; + redisOptions.EndPoints.ForEach(u => redisConfig.EndPoints.Add(u)); + connection1 = ConnectionMultiplexer.Connect(redisConfig); + } + else + { + connection1 = ConnectionMultiplexer.Connect(clusterOpt.SignalR.RedisConfiguration); + } + // 密钥存储(数据保护) + services.AddDataProtection().PersistKeysToStackExchangeRedis(connection1, clusterOpt.DataProtecteKey); + + signalRBuilder.AddStackExchangeRedis(options => + { + // 此处设置的ChannelPrefix并不会生效,如果两个不同的项目,且[程序集名+类名]一样,使用同一个redis服务,请注意修改 Hub/OnlineUserHub 的类名。 + // 原因请参考下边链接: + // https://github.com/dotnet/aspnetcore/blob/f9121bc3e976ec40a959818451d126d5126ce868/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs#L74 + // https://github.com/dotnet/aspnetcore/blob/f9121bc3e976ec40a959818451d126d5126ce868/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs#L33 + options.Configuration.ChannelPrefix = new RedisChannel(clusterOpt.SignalR.ChannelPrefix, RedisChannel.PatternMode.Auto); + options.ConnectionFactory = async writer => + { + ConnectionMultiplexer connection; + if (clusterOpt.IsSentinel) + { + var config = new ConfigurationOptions + { + AbortOnConnectFail = false, + ServiceName = redisOptions.ServiceName, + AllowAdmin = true, + DefaultDatabase = redisOptions.DefaultDb, + Password = redisOptions.Password + }; + redisOptions.EndPoints.ForEach(u => config.EndPoints.Add(u)); + connection = await ConnectionMultiplexer.ConnectAsync(config, writer); + } + else + { + connection = await ConnectionMultiplexer.ConnectAsync(clusterOpt.SignalR.RedisConfiguration); + } + + connection.ConnectionFailed += (_, e) => + { + "连接 Redis 失败".LogError(); + }; + + if (!connection.IsConnected) + { + "无法连接 Redis".LogError(); + } + return connection; + }; + }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs new file mode 100644 index 0000000..1708cf3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/GetAccessSecretContext.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// 获取 AccessKey 关联 AccessSecret 方法的上下文 +/// +public class GetAccessSecretContext : BaseContext +{ + public GetAccessSecretContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// 身份标识 + /// + public string AccessKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs new file mode 100644 index 0000000..ce42ff7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationDefaults.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证处理程序相关的默认值 +/// +public static class SignatureAuthenticationDefaults +{ + /// + /// SignatureAuthenticationOptions.AuthenticationScheme 使用的默认值 + /// + public const string AuthenticationScheme = "Signature"; + + /// + /// 附加在 HttpContext Item 中验证失败消息的 Key + /// + public const string AuthenticateFailMsgKey = "SignatureAuthenticateFailMsg"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs new file mode 100644 index 0000000..281ced8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证事件 +/// +public class SignatureAuthenticationEvent +{ + public SignatureAuthenticationEvent() + { + } + + /// + /// 获取或设置获取 AccessKey 的 AccessSecret 的逻辑处理 + /// + public Func> OnGetAccessSecret { get; set; } + + /// + /// 获取或设置质询的逻辑处理 + /// + public Func OnChallenge { get; set; } = _ => Task.CompletedTask; + + /// + /// 获取或设置已验证的逻辑处理 + /// + public Func OnValidated { get; set; } = _ => Task.CompletedTask; + + /// + /// 获取 AccessKey 的 AccessSecret + /// + /// + /// + public virtual Task GetAccessSecret(GetAccessSecretContext context) => OnGetAccessSecret?.Invoke(context) ?? throw new NotImplementedException($"需要提供 {nameof(OnGetAccessSecret)} 实现"); + + /// + /// 质询 + /// + /// + /// + public virtual Task Challenge(SignatureChallengeContext context) => OnChallenge?.Invoke(context) ?? Task.CompletedTask; + + /// + /// 已验证成功 + /// + /// + /// + public virtual Task Validated(SignatureValidatedContext context) => OnValidated?.Invoke(context) ?? Task.CompletedTask; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs new file mode 100644 index 0000000..3910548 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationExtensions.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证扩展 +/// +public static class SignatureAuthenticationExtensions +{ + /// + /// 注册 Signature 身份验证处理模块 + /// + /// + /// + public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder) + { + return builder.AddSignatureAuthentication(options => { }); + } + + /// + /// 注册 Signature 身份验证处理模块 + /// + /// + /// + /// + public static AuthenticationBuilder AddSignatureAuthentication(this AuthenticationBuilder builder, Action options) + { + return builder.AddScheme(SignatureAuthenticationDefaults.AuthenticationScheme, options); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs new file mode 100644 index 0000000..9db338c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs @@ -0,0 +1,177 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text.Encodings.Web; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证处理 +/// +public sealed class SignatureAuthenticationHandler : AuthenticationHandler +{ +#if NET6_0 + + public SignatureAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + +#else + + public SignatureAuthenticationHandler(IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + +#endif + + private readonly SysCacheService _sysCacheService = App.GetRequiredService(); + + private new SignatureAuthenticationEvent Events + { + get => (SignatureAuthenticationEvent)base.Events; + set => base.Events = value; + } + + /// + /// 确保创建的 Event 类型是 DigestEvents + /// + /// + protected override Task CreateEventsAsync() => throw new NotImplementedException($"{nameof(SignatureAuthenticationOptions)}.{nameof(SignatureAuthenticationOptions.Events)} 需要提供一个实例"); + + protected override async Task HandleAuthenticateAsync() + { + var accessKey = Request.Headers["accessKey"].FirstOrDefault(); + var timestampStr = Request.Headers["timestamp"].FirstOrDefault(); // 精确到秒 + var nonce = Request.Headers["nonce"].FirstOrDefault(); + var sign = Request.Headers["sign"].FirstOrDefault(); + + if (string.IsNullOrEmpty(accessKey)) + return await AuthenticateResultFailAsync("accessKey 不能为空"); + if (string.IsNullOrEmpty(timestampStr)) + return await AuthenticateResultFailAsync("timestamp 不能为空"); + if (string.IsNullOrEmpty(nonce)) + return await AuthenticateResultFailAsync("nonce 不能为空"); + if (string.IsNullOrEmpty(sign)) + return await AuthenticateResultFailAsync("sign 不能为空"); + + // 验证请求数据是否在可接受的时间内 + if (!long.TryParse(timestampStr, out var timestamp)) + return await AuthenticateResultFailAsync("timestamp 值不合法"); + + var requestDate = DateTimeUtil.ConvertUnixTime(timestamp); + +#if NET6_0 + var utcNow = Clock.UtcNow; +#else + var utcNow = TimeProvider.GetUtcNow(); +#endif + if (requestDate > utcNow.Add(Options.AllowedDateDrift).LocalDateTime || requestDate < utcNow.Subtract(Options.AllowedDateDrift).LocalDateTime) + return await AuthenticateResultFailAsync("timestamp 值已超过允许的偏差范围"); + + // 获取 accessSecret + var getAccessSecretContext = new GetAccessSecretContext(Context, Scheme, Options) { AccessKey = accessKey }; + var accessSecret = await Events.GetAccessSecret(getAccessSecretContext); + if (string.IsNullOrEmpty(accessSecret)) + return await AuthenticateResultFailAsync("accessKey 无效"); + + // 校验签名 + var appSecretByte = Encoding.UTF8.GetBytes(accessSecret); + string serverSign = SignData(appSecretByte, GetMessageForSign(Context)); + + if (serverSign != sign) + return await AuthenticateResultFailAsync("sign 无效的签名"); + + // 重放检测 + var cacheKey = $"{CacheConst.KeyOpenAccessNonce}{accessKey}|{nonce}"; + if (_sysCacheService.ExistKey(cacheKey)) return await AuthenticateResultFailAsync("重复的请求"); + _sysCacheService.Set(cacheKey, null, Options.AllowedDateDrift * 2); // 缓存过期时间为偏差范围时间的2倍 + + // 已验证成功 + var signatureValidatedContext = new SignatureValidatedContext(Context, Scheme, Options) + { + Principal = new ClaimsPrincipal(new ClaimsIdentity(SignatureAuthenticationDefaults.AuthenticationScheme)), + AccessKey = accessKey + }; + await Events.Validated(signatureValidatedContext); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (signatureValidatedContext.Result != null) + return signatureValidatedContext.Result; + + // ReSharper disable once HeuristicUnreachableCode + signatureValidatedContext.Success(); + return signatureValidatedContext.Result; + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + var authResult = await HandleAuthenticateOnceSafeAsync(); + var challengeContext = new SignatureChallengeContext(Context, Scheme, Options, properties) + { + AuthenticateFailure = authResult.Failure, + }; + await Events.Challenge(challengeContext); + // 质询已处理 + if (challengeContext.Handled) return; + + await base.HandleChallengeAsync(properties); + } + + /// + /// 获取用于签名的消息 + /// + /// + private static string GetMessageForSign(HttpContext context) + { + var method = context.Request.Method; // 请求方法(大写) + var url = context.Request.Path; // 请求 url,去除协议、域名、参数,以 / 开头 + var accessKey = context.Request.Headers["accessKey"].FirstOrDefault(); // 身份标识 + var timestamp = context.Request.Headers["timestamp"].FirstOrDefault(); // 时间戳,精确到秒 + var nonce = context.Request.Headers["nonce"].FirstOrDefault(); // 唯一随机数 + + return $"{method}&{url}&{accessKey}&{timestamp}&{nonce}"; + } + + /// + /// 对数据进行签名 + /// + /// + /// + /// + private static string SignData(byte[] secret, string data) + { + if (secret == null) + throw new ArgumentNullException(nameof(secret)); + + if (data == null) + throw new ArgumentNullException(nameof(data)); + + using HMAC hmac = new HMACSHA256(); + hmac.Key = secret; + return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))); + } + + /// + /// 返回验证失败结果,并在 Items 中增加 ,记录身份验证失败消息 + /// + /// + /// + private Task AuthenticateResultFailAsync(string message) + { + // 写入身份验证失败消息 + Context.Items[SignatureAuthenticationDefaults.AuthenticateFailMsgKey] = message; + return Task.FromResult(AuthenticateResult.Fail(message)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs new file mode 100644 index 0000000..9b91ba2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证选项 +/// +public class SignatureAuthenticationOptions : AuthenticationSchemeOptions +{ + /// + /// 请求时间允许的偏差范围 + /// + public TimeSpan AllowedDateDrift { get; set; } = TimeSpan.FromMinutes(5); + + /// + /// Signature 身份验证事件 + /// + public new SignatureAuthenticationEvent Events + { + get => (SignatureAuthenticationEvent)base.Events; + set => base.Events = value; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs new file mode 100644 index 0000000..a975bd4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureChallengeContext.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证质询上下文 +/// +public class SignatureChallengeContext : PropertiesContext +{ + public SignatureChallengeContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options, + AuthenticationProperties properties) + : base(context, scheme, options, properties) + { + } + + /// + /// 在认证期间出现的异常 + /// + public Exception AuthenticateFailure { get; set; } + + /// + /// 指定是否已被处理,如果已处理,则跳过默认认证逻辑 + /// + public bool Handled { get; private set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs new file mode 100644 index 0000000..7bad7e3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SignatureAuth/SignatureValidatedContext.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authentication; + +namespace Admin.NET.Core; + +/// +/// Signature 身份验证已验证上下文 +/// +public class SignatureValidatedContext : ResultContext +{ + public SignatureValidatedContext(HttpContext context, + AuthenticationScheme scheme, + SignatureAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// + /// 身份标识 + /// + public string AccessKey { get; set; } + + /// + /// 密钥 + /// + public string AccessSecret { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs new file mode 100644 index 0000000..bad2faa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarEntitySeedData.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 实体种子数据接口 +/// +/// +public interface ISqlSugarEntitySeedData + where TEntity : class, new() +{ + /// + /// 种子数据 + /// + /// + IEnumerable HasData(); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs new file mode 100644 index 0000000..63fae4f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarRepository.cs @@ -0,0 +1,91 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 分表操作仓储接口 +/// +/// +public interface ISqlSugarRepository : ISugarRepository, ISimpleClient where T : class, new() +{ + /// + /// 创建数据 + /// + /// + /// + Task SplitTableInsertAsync(T input); + + /// + /// 批量创建数据 + /// + /// + /// + Task SplitTableInsertAsync(List input); + + /// + /// 更新数据 + /// + /// + /// + Task SplitTableUpdateAsync(T input); + + /// + /// 批量更新数据 + /// + /// + /// + Task SplitTableUpdateAsync(List input); + + /// + /// 删除数据 + /// + /// + /// + Task SplitTableDeleteableAsync(T input); + + /// + /// 批量删除数据 + /// + /// + /// + Task SplitTableDeleteableAsync(List input); + + /// + /// 获取第一条 + /// + /// + /// + Task SplitTableGetFirstAsync(Expression> whereExpression); + + /// + /// 判断是否存在 + /// + /// + /// + Task SplitTableIsAnyAsync(Expression> whereExpression); + + /// + /// 获取列表 + /// + /// + Task> SplitTableGetListAsync(); + + /// + /// 获取列表 + /// + /// + /// + Task> SplitTableGetListAsync(Expression> whereExpression); + + /// + /// 获取列表 + /// + /// + /// 表名 + /// + Task> SplitTableGetListAsync(Expression> whereExpression, string[] tableNames); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarView.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarView.cs new file mode 100644 index 0000000..299c1a8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/ISqlSugarView.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 视图实体接口 +/// +public interface ISqlSugarView +{ + /// + /// 获取视图查询sql语句 + /// + /// + /// + public string GetQueryableSqlString(SqlSugarScopeProvider db); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs new file mode 100644 index 0000000..78c41ba --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs @@ -0,0 +1,205 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class SqlSugarFilter +{ + /// + /// 缓存全局查询过滤器(内存缓存) + /// + private static readonly ICache Cache = NewLife.Caching.Cache.Default; + + private static readonly SysOrgService SysOrgService = App.GetRequiredService(); + private static readonly SysCacheService SysCacheService = App.GetRequiredService(); + + /// + /// 删除用户机构缓存 + /// + /// + /// + public static void DeleteUserOrgCache(long userId, string dbConfigId) + { + // 删除用户机构集合缓存 + SysCacheService.Remove($"{CacheConst.KeyUserOrg}{userId}"); + // 删除最大数据权限缓存 + SysCacheService.Remove($"{CacheConst.KeyRoleMaxDataScope}{userId}"); + // 用户权限缓存(按钮集合) + SysCacheService.Remove($"{CacheConst.KeyUserButton}{userId}"); + } + + /// + /// 删除自定义过滤器缓存 + /// + /// + /// + public static void DeleteCustomCache(long userId, string dbConfigId) + { + // 删除自定义缓存——过滤器 + Cache.Remove($"db:{dbConfigId}:custom:{userId}"); + } + + /// + /// 配置用户机构集合过滤器 + /// + public static void SetOrgEntityFilter(SqlSugarScopeProvider db) + { + // 若仅本人数据,则直接返回 + var maxDataScope = SetDataScopeFilter(db); + // 获取用户最大数据范围,如果是全部数据、仅本人,则跳过 + if (maxDataScope is 0 or (int)DataScopeEnum.Self or (int)DataScopeEnum.All) return; + + // 获取用户所属机构,保证同一作用域 + var orgIds = new List(); + Scoped.Create((factory, scope) => + { + var services = scope.ServiceProvider; + orgIds = services.GetRequiredService().GetUserOrgIdList().GetAwaiter().GetResult(); + }); + if (orgIds == null || orgIds.Count == 0) return; + + //配置机构Id过滤器 + db.QueryFilter.AddTableFilter(o => SqlFunc.ContainsArray(orgIds, o.OrgId)); + } + + /// + /// 配置用户仅本人数据过滤器 + /// + private static int SetDataScopeFilter(SqlSugarScopeProvider db) + { + var maxDataScope = (int)DataScopeEnum.All; + + long.TryParse(App.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value, out var userId); + if (userId <= 0) return maxDataScope; + + // 获取用户最大数据范围---仅本人数据 + maxDataScope = App.GetRequiredService().Get(CacheConst.KeyRoleMaxDataScope + userId); + // 若为0则获取用户机构组织集合建立缓存 + if (maxDataScope == 0) + { + // 获取用户所属机构,保证同一作用域 + Scoped.Create((factory, scope) => + { + SysOrgService.GetUserOrgIdList().GetAwaiter().GetResult(); + maxDataScope = SysCacheService.Get(CacheConst.KeyRoleMaxDataScope + userId); + }); + } + if (maxDataScope != (int)DataScopeEnum.Self) return maxDataScope; + + // 配置用户数据范围缓存 + var cacheKey = $"db:{db.CurrentConnectionConfig.ConfigId}:dataScope:{userId}"; + var dataScopeFilter = Cache.Get>(cacheKey); + if (dataScopeFilter == null) + { + // 获取业务实体数据表 + var entityTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass + && (u.IsSubclassOf(typeof(EntityBaseOrg)) || u.IsSubclassOf(typeof(EntityBaseOrgDel)))); + if (!entityTypes.Any()) return maxDataScope; + + dataScopeFilter = new ConcurrentDictionary(); + foreach (var entityType in entityTypes) + { + // 排除非当前数据库实体 + var tAtt = entityType.GetCustomAttribute(); + if ((tAtt != null && db.CurrentConnectionConfig.ConfigId.ToString() != tAtt.configId.ToString())) + continue; + + //var lambda = DynamicExpressionParser.ParseLambda(new[] { + // Expression.Parameter(entityType, "u") }, typeof(bool), $"u.{nameof(EntityBaseData.CreateUserId)}=@0", userId); + var lambda = entityType.GetConditionExpression(new List { userId }); + + db.QueryFilter.AddTableFilter(entityType, lambda); + dataScopeFilter.TryAdd(entityType, lambda); + } + Cache.Add(cacheKey, dataScopeFilter); + } + else + { + foreach (var filter in dataScopeFilter) + db.QueryFilter.AddTableFilter(filter.Key, filter.Value); + } + return maxDataScope; + } + + /// + /// 配置自定义过滤器 + /// + public static void SetCustomEntityFilter(SqlSugarScopeProvider db) + { + // 配置自定义缓存 + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + var cacheKey = $"db:{db.CurrentConnectionConfig.ConfigId}:custom:{userId}"; + var tableFilterItemList = Cache.Get>>(cacheKey); + if (tableFilterItemList == null) + { + // 获取自定义实体过滤器 + var entityFilterTypes = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass + && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(IEntityFilter)))); + if (!entityFilterTypes.Any()) return; + + var tableFilterItems = new List>(); + foreach (var entityFilter in entityFilterTypes) + { + var instance = Activator.CreateInstance(entityFilter); + var entityFilterMethod = entityFilter.GetMethod("AddEntityFilter"); + var entityFilters = ((IList)entityFilterMethod?.Invoke(instance, null))?.Cast(); + if (entityFilters == null) continue; + + foreach (var u in entityFilters) + { + var tableFilterItem = (TableFilterItem)u; + var entityType = tableFilterItem.GetType().GetProperty("type", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(tableFilterItem, null) as Type; + // 排除非当前数据库实体 + var tAtt = entityType.GetCustomAttribute(); + if ((tAtt != null && db.CurrentConnectionConfig.ConfigId.ToString() != tAtt.configId.ToString()) || + (tAtt == null && db.CurrentConnectionConfig.ConfigId.ToString() != SqlSugarConst.MainConfigId)) + continue; + + tableFilterItems.Add(tableFilterItem); + db.QueryFilter.Add(tableFilterItem); + } + } + Cache.Add(cacheKey, tableFilterItems); + } + else + { + tableFilterItemList.ForEach(u => + { + db.QueryFilter.Add(u); + }); + } + } +} + +/// +/// 自定义实体过滤器接口 +/// +public interface IEntityFilter +{ + /// + /// 实体过滤器 + /// + /// + IEnumerable> AddEntityFilter(); +} + +///// +///// 自定义业务实体过滤器示例 +///// +//public class TestEntityFilter : IEntityFilter +//{ +// public IEnumerable> AddEntityFilter() +// { +// // 构造自定义条件的过滤器 +// Expression> dynamicExpression = u => u.Remark.Contains("xxx"); +// var tableFilterItem = new TableFilterItem(typeof(SysUser), dynamicExpression); + +// return new[] +// { +// tableFilterItem +// }; +// } +//} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs new file mode 100644 index 0000000..009b581 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs @@ -0,0 +1,183 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 分页泛型集合 +/// +/// +public class SqlSugarPagedList +{ + /// + /// 页码 + /// + public int Page { get; set; } + + /// + /// 页容量 + /// + public int PageSize { get; set; } + + /// + /// 总条数 + /// + public int Total { get; set; } + + /// + /// 总页数 + /// + public int TotalPages { get; set; } + + /// + /// 当前页集合 + /// + public IEnumerable Items { get; set; } + + /// + /// 是否有上一页 + /// + public bool HasPrevPage { get; set; } + + /// + /// 是否有下一页 + /// + public bool HasNextPage { get; set; } +} + +/// +/// 分页拓展类 +/// +public static class SqlSugarPagedExtensions +{ + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 脱敏分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static async Task> ToPagedListDataMaskAsync(this ISugarQueryable query, int pageIndex, int pageSize) where TEntity : class + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total); + items.ForEach(x => x.MaskSensitiveData()); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 脱敏分页拓展 + /// + /// 集合对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedListDataMask(this IEnumerable list, int pageIndex, int pageSize) where TEntity : class + { + var total = list.Count(); + var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + items.ForEach(x => x.MaskSensitiveData()); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 集合对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this IEnumerable list, int pageIndex, int pageSize) + { + var total = list.Count(); + var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 创建 对象 + /// + /// + /// 分页内容的对象集合 + /// 总条数 + /// 当前页码,从1开始 + /// 页码容量 + /// + private static SqlSugarPagedList CreateSqlSugarPagedList(IEnumerable items, int total, int pageIndex, int pageSize) + { + var totalPages = pageSize > 0 ? (int)Math.Ceiling(total / (double)pageSize) : 0; + return new SqlSugarPagedList + { + Page = pageIndex, + PageSize = pageSize, + Items = items, + Total = total, + TotalPages = totalPages, + HasNextPage = pageIndex < totalPages, + HasPrevPage = pageIndex - 1 > 0 + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs new file mode 100644 index 0000000..c29d2c9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs @@ -0,0 +1,171 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar 实体仓储 +/// +/// 实体类型,使用泛型让一个类可以操作所有表 +public class SqlSugarRepository : SimpleClient, ISqlSugarRepository where T : class, new() +{ + /// + /// 构造函数 - 仓储初始化时自动执行租户库切换逻辑 + /// 【对比 Ruoyi】Ruoyi 使用 MyBatis-Plus 的 @Mapper 注解 + @Autowired 注入, + /// 而 Admin.NET 使用泛型仓储 + 构造函数自动切库,开发者无需关心连接切换 + /// + public SqlSugarRepository() + { + // 获取 SqlSugar 的多租户管理器(类似 Spring 的 DataSource 路由抽象) + // SqlSugarSetup.ITenant 是静态属性,存储了所有数据库连接的引用 + var iTenant = SqlSugarSetup.ITenant; // App.GetRequiredService().AsTenant(); + + // 默认连接主库(类似 Ruoyi 的 master 数据源) + base.Context = iTenant.GetConnectionScope(SqlSugarConst.MainConfigId); + + // 【特性机制】C# 的 Attribute(特性)类似于 Java 的注解,用于标记类的元信息 + // 若实体贴有多库特性,则返回指定库连接 + // TenantAttribute 是 Admin.NET 自定义特性,标记该实体属于哪个租户库 + if (typeof(T).IsDefined(typeof(TenantAttribute), false)) + { + base.Context = iTenant.GetConnectionScopeWithAttr(); + return; + } + + // 若实体贴有日志表特性,则返回日志库连接 + // LogTableAttribute 标记日志表,查询时自动切换到日志库 + if (typeof(T).IsDefined(typeof(LogTableAttribute), false)) + { + if (iTenant.IsAnyConnection(SqlSugarConst.LogConfigId)) + base.Context = iTenant.GetConnectionScope(SqlSugarConst.LogConfigId); + return; + } + + // 若实体贴有系统表特性,则返回默认库连接 + // SysTableAttribute 标记系统表(如用户、角色等),始终在主库查询 + if (typeof(T).IsDefined(typeof(SysTableAttribute), false)) + return; + + // 【多租户实现】从 HTTP 请求头获取租户 ID(类似 Ruoyi 的 tenant_id 参数) + // Ruoyi 多租户:mybatis-plus: tenant: { mode: 1 } 自动注入 tenant_id + // Admin.NET 多租户:手动从 Header 获取,优先级:请求头 > 用户登录信息 + var tenantId = App.HttpContext?.Request.Headers[ClaimConst.TenantId].FirstOrDefault(); + if (tenantId == SqlSugarConst.MainConfigId) return; + else if (string.IsNullOrWhiteSpace(tenantId)) + { + // 若未贴任何表特性或当前未登录或是默认租户Id,则返回默认库连接 + // App.User 是当前登录用户的信息,类似于 Spring Security 的 SecurityContextHolder + tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; + if (string.IsNullOrWhiteSpace(tenantId) || tenantId == SqlSugarConst.MainConfigId) return; + } + + // 根据租户Id切换库连接 为空则返回默认库连接 + // SysTenantService 管理所有租户的数据库连接映射 + var sqlSugarScopeProviderTenant = App.GetRequiredService().GetTenantDbConnectionScope(long.Parse(tenantId)); + if (sqlSugarScopeProviderTenant == null) return; + base.Context = sqlSugarScopeProviderTenant; + } + + #region 分表操作 + // 【分表机制】SqlSugar 内置分表支持,类似 ShardingSphere 但更轻量 + // 适用场景:数据量过亿级别,按时间/地区分表存储 + + /// + /// 分表插入(单条) + /// 【对比 Ruoyi】Ruoyi 需手动配置 ShardingSphere 分片规则,SqlSugar 只需调用 SplitTable() 方法 + /// + public async Task SplitTableInsertAsync(T input) + { + return await base.AsInsertable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + /// + /// 分表插入(批量) + /// 【语法说明】async/await 是 C# 的异步编程模式,等同于 Java 的 CompletableFuture + /// Task 表示返回一个异步结果,类似 Future 但更强大 + /// + public async Task SplitTableInsertAsync(List input) + { + return await base.AsInsertable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + /// + /// 分表更新 + /// 【ORM 差异】SqlSugar 的 AsUpdateable 类似 MyBatis-Plus 的 UpdateWrapper + /// 但语法更链式流畅:AsUpdateable(entity).SplitTable().ExecuteCommandAsync() + /// + public async Task SplitTableUpdateAsync(T input) + { + return await base.AsUpdateable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableUpdateAsync(List input) + { + return await base.AsInsertable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + /// + /// 分表删除 + /// 【语法说明】base.Context.Deleteable() 链式调用,类似于 MyBatis-Plus 的 lambdaQuery + /// 这种 Builder 模式让 SQL 构建更直观,避免 XML 配置文件 + /// + public async Task SplitTableDeleteableAsync(T input) + { + return await base.Context.Deleteable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + public async Task SplitTableDeleteableAsync(List input) + { + return await base.Context.Deleteable(input).SplitTable().ExecuteCommandAsync() > 0; + } + + /// + /// 分表查询(单条) + /// 【语法说明】Expression> 是 C# 的 Lambda 表达式类型 + /// 等同于 Java 的 Function,用于构建查询条件 + /// 例如:p => p.Name == "张三" 会被翻译成 WHERE name = '张三' + /// + public Task SplitTableGetFirstAsync(Expression> whereExpression) + { + return base.AsQueryable().SplitTable().FirstAsync(whereExpression); + } + + /// + /// 分表判断是否存在 + /// 【对比 Ruoyi】MyBatis-Plus 用 count(*) > 0,SqlSugar 用 AnyAsync() 更语义化 + /// + public Task SplitTableIsAnyAsync(Expression> whereExpression) + { + return base.Context.Queryable().Where(whereExpression).SplitTable().AnyAsync(); + } + + /// + /// 分表查询(全部) + /// 【语法说明】Context.Queryable() 是 SqlSugar 的查询入口 + /// 链式调用:Queryable -> Where -> SplitTable -> ToListAsync + /// 类似 MyBatis-Plus 的 query().lambdaQuery().list() + /// + public Task> SplitTableGetListAsync() + { + return Context.Queryable().SplitTable().ToListAsync(); + } + + public Task> SplitTableGetListAsync(Expression> whereExpression) + { + return Context.Queryable().Where(whereExpression).SplitTable().ToListAsync(); + } + + /// + /// 分表查询(指定表名) + /// 【高级用法】可以显式指定查询哪些分表,类似 ShardingSphere 的只读表 + /// + public Task> SplitTableGetListAsync(Expression> whereExpression, string[] tableNames) + { + return Context.Queryable().Where(whereExpression).SplitTable(t => t.InTableNames(tableNames)).ToListAsync(); + } + + #endregion 分表操作 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs new file mode 100644 index 0000000..1bb58db --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs @@ -0,0 +1,730 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.Data.Sqlite; +using DbType = SqlSugar.DbType; + +namespace Admin.NET.Core; + +public static class SqlSugarSetup +{ + // 多租户实例 + public static ITenant ITenant { get; set; } + + // 是否正在处理种子数据 + private static bool _isHandlingSeedData = false; + + /// + /// SqlSugar 上下文初始化 + /// + /// + public static void AddSqlSugar(this IServiceCollection services) + { + // 注册雪花Id + var snowIdOpt = App.GetConfig("SnowId", true); + YitIdHelper.SetIdGenerator(snowIdOpt); + + // 自定义 SqlSugar 雪花ID算法 + SnowFlakeSingle.WorkId = snowIdOpt.WorkerId; + StaticConfig.CustomSnowFlakeFunc = YitIdHelper.NextId; + // 注册 MongoDb + InstanceFactory.CustomAssemblies = [typeof(SqlSugar.MongoDb.MongoDbProvider).Assembly]; + // 动态表达式 SqlFunc 支持,https://www.donet5.com/Home/Doc?typeId=2569 + StaticConfig.DynamicExpressionParserType = typeof(DynamicExpressionParser); + StaticConfig.DynamicExpressionParsingConfig = new ParsingConfig + { + CustomTypeProvider = new SqlSugarTypeProvider() + }; + + var dbOptions = App.GetConfig("DbConnection", true); + dbOptions.ConnectionConfigs.ForEach(SetDbConfig); + + SqlSugarScope sqlSugar = new(dbOptions.ConnectionConfigs.Adapt>(), db => + { + dbOptions.ConnectionConfigs.ForEach(config => + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + SetDbAop(dbProvider, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter); + SetDbDiffLog(dbProvider, config); + }); + }); + ITenant = sqlSugar; + + services.AddSingleton(sqlSugar); // 单例注册 + services.AddScoped(typeof(SqlSugarRepository<>)); // 仓储注册 + services.AddUnitOfWork(); // 事务与工作单元注册 + + // 初始化数据库表结构及种子数据 + dbOptions.ConnectionConfigs.ForEach(config => + { + InitDatabase(sqlSugar, config); + }); + } + + /// + /// 配置连接属性 + /// + /// + public static void SetDbConfig(DbConnectionConfig config) + { + if (config.DbSettings.EnableConnStringEncrypt) + config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString); + + var configureExternalServices = new ConfigureExternalServices + { + EntityNameService = (type, entity) => // 处理表 + { + entity.IsDisabledDelete = true; // 禁止删除非 sqlsugar 创建的列 + // 只处理贴了特性[SugarTable]表 + if (!type.GetCustomAttributes().Any()) + return; + if (config.DbSettings.EnableUnderLine && !entity.DbTableName.Contains('_')) + entity.DbTableName = entity.DbTableName.ToUnderLine(); // 驼峰转下划线 + }, + EntityService = (type, column) => // 处理列 + { + // 只处理贴了特性[SugarColumn]列 + if (!type.GetCustomAttributes().Any()) + return; + if (new NullabilityInfoContext().Create(type).WriteState is NullabilityState.Nullable) + column.IsNullable = true; + if (config.DbSettings.EnableUnderLine && !column.IsIgnore && !column.DbColumnName.Contains('_')) + column.DbColumnName = column.DbColumnName.ToUnderLine(); // 驼峰转下划线 + }, + DataInfoCacheService = new SqlSugarCache(), + }; + config.ConfigureExternalServices = configureExternalServices; + config.InitKeyType = InitKeyType.Attribute; + config.IsAutoCloseConnection = true; + config.MoreSettings = new ConnMoreSettings + { + IsAutoRemoveDataCache = true, // 启用自动删除缓存,所有增删改会自动调用.RemoveDataCache() + IsAutoDeleteQueryFilter = true, // 启用删除查询过滤器 + IsAutoUpdateQueryFilter = true, // 启用更新查询过滤器 + SqlServerCodeFirstNvarchar = true // 采用Nvarchar + }; + + // 若库类型是人大金仓则默认设置PG模式 + if (config.DbType == DbType.Kdbndp) + config.MoreSettings.DatabaseModel = DbType.PostgreSQL; // 配置PG模式主要是兼容系统表差异 + + // 若库类型是Oracle则默认主键名字和参数名字最大长度 + if (config.DbType == DbType.Oracle) + config.MoreSettings.MaxParameterNameLength = 30; + } + + /// + /// 配置Aop + /// + /// + /// + /// + public static void SetDbAop(SqlSugarScopeProvider db, bool enableConsoleSql, bool superAdminIgnoreIDeletedFilter) + { + // 设置超时时间 + db.Ado.CommandTimeOut = 30; + + // 打印SQL语句 + if (enableConsoleSql) + { + db.Aop.OnLogExecuting = (sql, pars) => + { + //// 若参数值超过100个字符则进行截取 + //foreach (var par in pars) + //{ + // if (par.DbType != System.Data.DbType.String || par.Value == null) continue; + // if (par.Value.ToString().Length > 100) + // par.Value = string.Concat(par.Value.ToString()[..100], "......"); + //} + + var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetNativeSql(sql, pars)}\r\n"; + var originColor = Console.ForegroundColor; + if (sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Green; + if (sql.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) || sql.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Yellow; + if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(log); + Console.ForegroundColor = originColor; + }; + } + db.Aop.OnError = ex => + { + if (ex.Parametres == null) return; + var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetNativeSql(ex.Sql, (SugarParameter[])ex.Parametres)}\r\n"; + Log.Error(log, ex); + }; + db.Aop.OnLogExecuted = (sql, pars) => + { + //// 若参数值超过100个字符则进行截取 + //foreach (var par in pars) + //{ + // if (par.DbType != System.Data.DbType.String || par.Value == null) continue; + // if (par.Value.ToString().Length > 100) + // par.Value = string.Concat(par.Value.ToString()[..100], "......"); + //} + + // 执行时间超过5秒时 + if (!(db.Ado.SqlExecutionTime.TotalSeconds > 5)) return; + + var fileName = db.Ado.SqlStackTrace.FirstFileName; // 文件名 + var fileLine = db.Ado.SqlStackTrace.FirstLine; // 行号 + var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名 + var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}"; + Log.Warning(log); + }; + + // 数据审计 + db.Aop.DataExecuting = (_, entityInfo) => + { + // 若正在处理种子数据则直接返回 + if (_isHandlingSeedData) return; + + // 新增/插入 + if (entityInfo.OperationType == DataFilterType.InsertByObject) + { + // 若主键是长整型且空则赋值雪花Id + if (entityInfo.EntityColumnInfo.IsPrimarykey && !entityInfo.EntityColumnInfo.IsIdentity && entityInfo.EntityColumnInfo.PropertyInfo.PropertyType == typeof(long)) + { + var id = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue); + if (id == null || (long)id == 0) + entityInfo.SetValue(YitIdHelper.NextId()); + } + // 若创建时间为空则赋值当前时间 + else if (entityInfo.PropertyName == nameof(EntityBase.CreateTime)) + { + var createTime = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue)!; + if (createTime == null || createTime.Equals(DateTime.MinValue)) + entityInfo.SetValue(DateTime.Now); + } + // 若当前用户为空(非web线程时) + if (App.User == null) return; + + dynamic entityValue = entityInfo.EntityValue; + if (entityInfo.PropertyName == nameof(EntityBaseTenantId.TenantId)) + { + var tenantId = entityValue.TenantId; + if (tenantId == null || tenantId == 0) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.TenantId)?.Value); + } + else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserId)) + { + var createUserId = entityValue.CreateUserId; + if (createUserId == 0 || createUserId == null) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.UserId)?.Value); + } + else if (entityInfo.PropertyName == nameof(EntityBase.CreateUserName)) + { + var createUserName = entityValue.CreateUserName; + if (string.IsNullOrEmpty(createUserName)) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.RealName)?.Value); + } + else if (entityInfo.PropertyName == "CreateOrgId") + { + var createOrgId = entityValue.CreateOrgId; + if (createOrgId == 0 || createOrgId == null) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgId)?.Value); + } + else if (entityInfo.PropertyName == "CreateOrgName") + { + var createOrgName = entityValue.CreateOrgName; + if (string.IsNullOrEmpty(createOrgName)) + entityInfo.SetValue(App.User.FindFirst(ClaimConst.OrgName)?.Value); + } + } + // 编辑/更新 + else if (entityInfo.OperationType == DataFilterType.UpdateByObject) + { + if (entityInfo.PropertyName == nameof(EntityBase.UpdateTime)) + entityInfo.SetValue(DateTime.Now); + else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserId)) + entityInfo.SetValue(App.User?.FindFirst(ClaimConst.UserId)?.Value); + else if (entityInfo.PropertyName == nameof(EntityBase.UpdateUserName)) + entityInfo.SetValue(App.User?.FindFirst(ClaimConst.RealName)?.Value); + else if (entityInfo.PropertyName == nameof(EntityBaseDel.DeleteTime)) + { + dynamic entityValue = entityInfo.EntityValue; + var isDelete = entityValue.IsDelete; + if (isDelete == true) + { + entityInfo.SetValue(DateTime.Now); + } + } + } + }; + + // 是否为超级管理员 + var isSuperAdmin = App.User?.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString(); + + // 配置假删除过滤器,如果当前用户是超级管理员并且允许忽略软删除过滤器则不会应用 + if (!isSuperAdmin || !superAdminIgnoreIDeletedFilter) + db.QueryFilter.AddTableFilter(u => u.IsDelete == false); + + // 超管排除其他过滤器 + if (isSuperAdmin) return; + + // 配置租户过滤器 + var tenantId = App.User?.FindFirst(ClaimConst.TenantId)?.Value; + if (!string.IsNullOrWhiteSpace(tenantId)) + db.QueryFilter.AddTableFilter(u => u.TenantId == long.Parse(tenantId)); + + // 配置用户机构(数据范围)过滤器 + SqlSugarFilter.SetOrgEntityFilter(db); + + // 配置自定义过滤器 + SqlSugarFilter.SetCustomEntityFilter(db); + } + + /// + /// 开启库表差异化日志 + /// + /// + /// + private static void SetDbDiffLog(SqlSugarScopeProvider db, DbConnectionConfig config) + { + if (!config.DbSettings.EnableDiffLog) return; + + async void AopOnDiffLogEvent(DiffLogModel u) + { + // 记录差异数据 + var diffData = new List(); + for (int i = 0; i < u.AfterData.Count; i++) + { + var diffColumns = new List(); + var afterColumns = u.AfterData[i].Columns; + var beforeColumns = u.BeforeData[i].Columns; + for (int j = 0; j < afterColumns.Count; j++) + { + if (afterColumns[j].Value.Equals(beforeColumns[j].Value)) continue; + diffColumns.Add(new + { + afterColumns[j].IsPrimaryKey, + afterColumns[j].ColumnName, + afterColumns[j].ColumnDescription, + BeforeValue = beforeColumns[j].Value, + AfterValue = afterColumns[j].Value, + }); + } + + diffData.Add(new { u.AfterData[i].TableName, u.AfterData[i].TableDescription, Columns = diffColumns }); + } + + var logDiff = new SysLogDiff + { + // 差异数据(字段描述、列名、值、表名、表描述) + DiffData = JSON.Serialize(diffData), + // 传进来的对象(如果对象为空,则使用首个数据的表名作为业务对象) + BusinessData = u.BusinessData == null ? u.AfterData.FirstOrDefault()?.TableName : JSON.Serialize(u.BusinessData), + // 枚举(insert、update、delete) + DiffType = u.DiffType.ToString(), + Sql = u.Sql, + Parameters = JSON.Serialize(u.Parameters.Select(e => new { e.ParameterName, e.Value, TypeName = e.DbType.ToString() })), + Elapsed = u.Time == null ? 0 : (long)u.Time.Value.TotalMilliseconds + }; + var logDb = ITenant.IsAnyConnection(SqlSugarConst.LogConfigId) ? ITenant.GetConnectionScope(SqlSugarConst.LogConfigId) : ITenant.GetConnectionScope(SqlSugarConst.MainConfigId); + await logDb.CopyNew().Insertable(logDiff).ExecuteCommandAsync(); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(DateTime.Now + $"\r\n*****开始差异日志*****\r\n{Environment.NewLine}{JSON.Serialize(logDiff)}{Environment.NewLine}*****结束差异日志*****\r\n"); + } + + db.Aop.OnDiffLogEvent = AopOnDiffLogEvent; + } + + /// + /// 初始化视图 + /// + /// + private static void InitView(SqlSugarScopeProvider dbProvider) + { + var totalWatch = Stopwatch.StartNew(); // 开始总计时 + Log.Information($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId}"); + var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList(); + + int taskIndex = 0, size = viewTypeList.Count; + var taskList = viewTypeList.Select(viewType => Task.Run(() => + { + // 开始计时 + var stopWatch = Stopwatch.StartNew(); + + // 获取视图实体和配置信息 + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误"); + + // 如果视图存在,则删除视图 + if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName))) + dbProvider.DbMaintenance.DropView(entityInfo.DbTableName); + + // 获取初始化视图查询SQL + var sql = viewType.GetMethod(nameof(ISqlSugarView.GetQueryableSqlString))?.Invoke(Activator.CreateInstance(viewType), [dbProvider]) as string; + if (string.IsNullOrWhiteSpace(sql)) throw new Exception("视图初始化Sql语句不能为空"); + + // 创建视图 + dbProvider.Ado.ExecuteCommand($"CREATE VIEW {entityInfo.DbTableName} AS " + Environment.NewLine + " " + sql); + + // 停止计时 + stopWatch.Stop(); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"初始化视图 {viewType.FullName,-58} ({dbProvider.CurrentConnectionConfig.ConfigId} - {Interlocked.Increment(ref taskIndex):D003}/{size:D003},耗时:{stopWatch.ElapsedMilliseconds:N0} ms)"); + })); + Task.WaitAll(taskList.ToArray()); + + totalWatch.Stop(); // 停止总计时 + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"初始化视图 {dbProvider.CurrentConnectionConfig.DbType} - {dbProvider.CurrentConnectionConfig.ConfigId} 总耗时:{totalWatch.ElapsedMilliseconds:N0} ms"); + } + + /// + /// 等待数据库就绪 + /// + /// + private static void WaitForDatabaseReady(SqlSugarScopeProvider dbProvider) + { + do + { + try + { + if (dbProvider.Ado.Connection.State != ConnectionState.Open) + dbProvider.Ado.Connection.Open(); + + // 如果连接成功,直接返回 + Log.Information("数据库连接成功。"); + return; + } + catch (Exception ex) + { + Log.Warning($"数据库尚未就绪,等待中... 错误:{ex.Message}"); + Thread.Sleep(1000); + } + } while (true); + } + + /// + /// 初始化数据库 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitDatabase(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + + // 初始化数据库 如果是没有数据库的话,是先初始化数据库再做连接 + if (config.DbSettings.EnableInitDb) + { + Log.Information($"初始化数据库 {config.DbType} - {config.ConfigId} - {config.ConnectionString}"); + if (config.DbType != DbType.Oracle) dbProvider.DbMaintenance.CreateDatabase(); + } + + // 等待数据库连接就绪 + WaitForDatabaseReady(dbProvider); + + // 初始化表结构 + if (config.TableSettings.EnableInitTable) + { + Log.Information($"初始化表结构 {config.DbType} - {config.ConfigId}"); + var entityTypes = GetEntityTypesForInit(config); + InitializeTables(dbProvider, entityTypes, config); + } + + // 初始化视图 + if (config.DbSettings.EnableInitView) InitView(dbProvider); + + // 初始化种子数据 + if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); + } + + /// + /// 获取需要初始化的实体类型 + /// + /// 数据库连接配置 + /// 实体类型列表 + private static List GetEntityTypesForInit(DbConnectionConfig config) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false)) + .Where(u => !u.GetCustomAttributes().Any()) + .WhereIF(config.TableSettings.EnableIncreTable, u => u.IsDefined(typeof(IncreTableAttribute), false)) + .Where(u => IsEntityForConfig(u, config)) + .ToList(); + } + + /// + /// 判断实体是否属于当前配置 + /// + /// 实体类型 + /// 数据库连接配置 + /// 是否属于当前配置 + private static bool IsEntityForConfig(Type entityType, DbConnectionConfig config) + { + switch (config.ConfigId.ToString()) + { + case SqlSugarConst.MainConfigId: + return entityType.GetCustomAttributes().Any() || + (!entityType.GetCustomAttributes().Any() && + !entityType.GetCustomAttributes().Any(o => o.configId.ToString() != config.ConfigId.ToString())); + + case SqlSugarConst.LogConfigId: + return entityType.GetCustomAttributes().Any(); + + default: + { + var tenantAttribute = entityType.GetCustomAttribute(); + return tenantAttribute != null && tenantAttribute.configId.ToString() == config.ConfigId.ToString(); + } + } + } + + /// + /// 初始化表结构 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型列表 + /// 数据库连接配置 + private static void InitializeTables(SqlSugarScopeProvider dbProvider, List entityTypes, DbConnectionConfig config) + { + // 删除视图再初始化表结构,防止因为视图导致无法同步表结构 + var viewTypeList = App.EffectiveTypes.Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarView)))).ToList(); + foreach (var viewType in viewTypeList) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(viewType) ?? throw new Exception("获取视图实体配置有误"); + if (dbProvider.DbMaintenance.GetViewInfoList(false).Any(it => it.Name.EqualIgnoreCase(entityInfo.DbTableName))) + dbProvider.DbMaintenance.DropView(entityInfo.DbTableName); + } + + int count = 0, sum = entityTypes.Count; + var tasks = entityTypes.Select(entityType => Task.Run(() => + { + Console.WriteLine($"初始化表结构 {entityType.FullName,-64} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003})"); + UpdateNullableColumns(dbProvider, entityType); + InitializeTable(dbProvider, entityType); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + } + + /// + /// 更新表中不存在于实体的字段为可空 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void UpdateNullableColumns(SqlSugarScopeProvider dbProvider, Type entityType) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dbColumns = dbProvider.DbMaintenance.GetColumnInfosByTableName(entityInfo.DbTableName) ?? new List(); + + foreach (var dbColumn in dbColumns.Where(c => !c.IsPrimarykey && entityInfo.Columns.All(u => u.DbColumnName != c.DbColumnName))) + { + dbColumn.IsNullable = true; + Retry(() => + { + dbProvider.DbMaintenance.UpdateColumn(entityInfo.DbTableName, dbColumn); + }, maxRetry: 3, retryIntervalMs: 1000); + } + } + + /// + /// 初始化表 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void InitializeTable(SqlSugarScopeProvider dbProvider, Type entityType) + { + Retry(() => + { + if (entityType.GetCustomAttribute() == null) + { + dbProvider.CodeFirst.InitTables(entityType); + } + else + { + dbProvider.CodeFirst.SplitTables().InitTables(entityType); + } + }, maxRetry: 3, retryIntervalMs: 1000); + } + + /// + /// 初始化种子数据 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitSeedData(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + _isHandlingSeedData = true; + + Log.Information($"初始化种子数据 {config.DbType} - {config.ConfigId}"); + var seedDataTypes = GetSeedDataTypes(config); + + int count = 0, sum = seedDataTypes.Count; + var tasks = seedDataTypes.Select(seedType => Task.Run(() => + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + if (!IsEntityForConfig(entityType, config)) return; + + var seedData = GetSeedData(seedType)?.ToList(); + if (seedData == null) return; + + AdjustSeedDataIds(seedData, config); + InsertOrUpdateSeedData(dbProvider, seedType, entityType, seedData, config, ref count, sum); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + _isHandlingSeedData = false; + } + + /// + /// 获取种子数据类型 + /// + /// 数据库连接配置 + /// 种子数据类型列表 + private static List GetSeedDataTypes(DbConnectionConfig config) + { + return App.EffectiveTypes + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.GetInterfaces().Any(i => i.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>)))) + .WhereIF(config.SeedSettings.EnableIncreSeed, u => u.IsDefined(typeof(IncreSeedAttribute), false)) + .OrderBy(u => u.GetCustomAttributes(typeof(SeedDataAttribute), false).Length > 0 ? ((SeedDataAttribute)u.GetCustomAttributes(typeof(SeedDataAttribute), false)[0]).Order : 0) + .ToList(); + } + + /// + /// 获取种子数据 + /// + /// 种子数据类型 + /// 种子数据列表 + private static IEnumerable GetSeedData(Type seedType) + { + var instance = Activator.CreateInstance(seedType); + var hasDataMethod = seedType.GetMethod("HasData"); + return ((IEnumerable)hasDataMethod?.Invoke(instance, null))?.Cast(); + } + + /// + /// 调整种子数据的 ID + /// + /// 种子数据列表 + /// 数据库连接配置 + private static void AdjustSeedDataIds(IEnumerable seedData, DbConnectionConfig config) + { + var seedId = config.ConfigId.ToLong(); + foreach (var data in seedData) + { + var idProperty = data.GetType().GetProperty(nameof(EntityBaseId.Id)); + if (idProperty == null || idProperty.PropertyType != typeof(Int64)) continue; + + var idValue = idProperty.GetValue(data); + if (idValue == null || idValue.ToString() == "0" || string.IsNullOrWhiteSpace(idValue.ToString())) + { + idProperty.SetValue(data, ++seedId); + } + } + } + + /// + /// 插入或更新种子数据 + /// + /// SqlSugarScopeProvider 实例 + /// 种子数据类型 + /// 实体类型 + /// 种子数据列表 + /// 数据库连接配置 + /// 当前处理的数量 + /// 总数量 + private static void InsertOrUpdateSeedData(SqlSugarScopeProvider dbProvider, Type seedType, Type entityType, IEnumerable seedData, DbConnectionConfig config, ref int count, int sum) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dataList = seedData.ToList(); + + if (entityType.GetCustomAttribute(true) != null) + { + var initMethod = seedType.GetMethod("Init"); + initMethod?.Invoke(Activator.CreateInstance(seedType), new object[] { dbProvider }); + } + else + { + int updateCount = 0, insertCount = 0; + if (entityInfo.Columns.Any(u => u.IsPrimarykey)) + { + var storage = dbProvider.StorageableByObject(dataList).ToStorage(); + if (seedType.GetCustomAttribute() == null) + { + updateCount = storage.AsUpdateable + .IgnoreColumns(entityInfo.Columns + .Where(u => u.PropertyInfo.GetCustomAttribute() != null) + .Select(u => u.PropertyName).ToArray()) + .ExecuteCommand(); + } + insertCount = storage.AsInsertable.ExecuteCommand(); + } + else + { + if (!dbProvider.Queryable(entityInfo.DbTableName, entityInfo.DbTableName).Any()) + { + insertCount = dataList.Count; + dbProvider.InsertableByObject(dataList).ExecuteCommand(); + } + } + Console.WriteLine($"添加数据 {entityInfo.DbTableName,-32} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003},数据量:{dataList.Count:D003},插入 {insertCount:D003} 条记录,修改 {updateCount:D003} 条记录)"); + } + } + + /// + /// 初始化租户业务数据库 + /// + /// + /// + public static void InitTenantDatabase(ITenant iTenant, DbConnectionConfig config) + { + SetDbConfig(config); + + if (!iTenant.IsAnyConnection(config.ConfigId.ToString())) + iTenant.AddConnection(config); + var db = iTenant.GetConnectionScope(config.ConfigId.ToString()); + db.DbMaintenance.CreateDatabase(); + + // 获取所有业务表-初始化租户库表结构(排除系统表、日志表、特定库表) + var entityTypes = App.EffectiveTypes + .Where(u => !u.GetCustomAttributes().Any()) + .Where(u => !u.IsInterface && !u.IsAbstract && u.IsClass && u.IsDefined(typeof(SugarTable), false) && + !u.IsDefined(typeof(SysTableAttribute), false) && !u.IsDefined(typeof(LogTableAttribute), false) && !u.IsDefined(typeof(TenantAttribute), false)).ToList(); + if (entityTypes.Count == 0) return; + + foreach (var entityType in entityTypes) + { + var splitTable = entityType.GetCustomAttribute(); + if (splitTable == null) + db.CodeFirst.InitTables(entityType); + else + db.CodeFirst.SplitTables().InitTables(entityType); + } + } + + /// + /// 简单的重试机制 + /// + /// + /// + /// + private static void Retry(Action action, int maxRetry, int retryIntervalMs) + { + int attempt = 0; + while (true) + { + try + { + action(); + return; + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 5) // SQLITE_BUSY + { + if (++attempt >= maxRetry) + { + Log.Error($"简单的重试机制:{ex.Message}"); throw; + } + Log.Information($"数据库忙,正在重试... (尝试 {attempt}/{maxRetry})"); + Thread.Sleep(retryIntervalMs); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs new file mode 100644 index 0000000..2165426 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarTypeProvider.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Linq.Dynamic.Core.CustomTypeProviders; + +namespace Admin.NET.Core; + +/// +/// 扩展支持 SqlFunc,不支持 Subqueryable +/// +public class SqlSugarTypeProvider : DefaultDynamicLinqCustomTypeProvider +{ + public SqlSugarTypeProvider(bool cacheCustomTypes = true) : base(ParsingConfig.Default, cacheCustomTypes) + { + } + + public override HashSet GetCustomTypes() + { + var customTypes = base.GetCustomTypes(); + customTypes.Add(typeof(SqlFunc)); + return customTypes; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs new file mode 100644 index 0000000..0975eef --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs @@ -0,0 +1,71 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// SqlSugar 事务和工作单元 +/// +public sealed class SqlSugarUnitOfWork : IUnitOfWork +{ + /// + /// SqlSugar 对象 + /// + private readonly ISqlSugarClient _sqlSugarClient; + + /// + /// 构造函数 + /// + /// + public SqlSugarUnitOfWork(ISqlSugarClient sqlSugarClient) + { + _sqlSugarClient = sqlSugarClient; + } + + /// + /// 开启工作单元处理 + /// + /// + /// + /// + public void BeginTransaction(FilterContext context, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().BeginTran(); + } + + /// + /// 提交工作单元处理 + /// + /// + /// + /// + public void CommitTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().CommitTran(); + } + + /// + /// 回滚工作单元处理 + /// + /// + /// + /// + public void RollbackTransaction(FilterContext resultContext, UnitOfWorkAttribute unitOfWork) + { + _sqlSugarClient.AsTenant().RollbackTran(); + } + + /// + /// 执行完毕(无论成功失败) + /// + /// + /// + /// + public void OnCompleted(FilterContext context, FilterContext resultContext) + { + _sqlSugarClient.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Update/AutoVersionUpdate.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Update/AutoVersionUpdate.cs new file mode 100644 index 0000000..645acb4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Update/AutoVersionUpdate.cs @@ -0,0 +1,240 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +#if NET10_0_OR_GREATER + +using Microsoft.AspNetCore.Builder; +using XiHan.Framework.Utils.Logging; +using XiHan.Framework.Utils.Reflections; + +namespace Admin.NET.Core.Update; + +/// +/// 自动版本更新中间件拓展 +/// +/// +/// 使用方法 +/// 1.在 Admin.NET.Web.Core 的 Startup.cs 中的 Configure 方法中调用 app.UseAutoVersionUpdate()。 +/// 2.在入口项目 Admin.NET.Web.Entry 的根目录下创建一个名为 UpdateScripts 的文件夹,并在其中放置 .sql 后缀的脚本文件 +/// 3.脚本文件命名格式为版本号,例如 1.0.0.sql、1.0.1.sql 等,版本号应符合语义化版本规范。 +/// 4.脚本的属性:复制到输出目录,设置为:始终复制。 +/// 5.设置主节点的 Admin.NET.Application 的 Configuration/App.json 的 WorkerId 为 1。 +/// 6.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version。 +/// ================================================== +/// 更新新版本时 +/// 1.需在 UpdateScripts 文件夹中添加新的脚本文件,脚本文件名应为新版本号。 +/// 2.设置入口项目 Admin.NET.Web.Entry.csproj 的 Version +/// +[SuppressSniffer] +public static class AutoVersionUpdate +{ + /// + /// 使用自动版本更新中间件 + /// + /// + /// + public static IApplicationBuilder UseAutoVersionUpdate(this IApplicationBuilder app) + { + LogHelper.Info("AutoVersionUpdate 中间件运行"); + + var snowIdOpt = App.GetConfig("SnowId", true); + if (snowIdOpt.WorkerId != 1) + { + LogHelper.Handle("非主节点,不执行脚本"); + return app; + } + + var currentVersion = GetEntryAssemblyCurrentVersion(); + LogHelper.Handle($"当前版本:{currentVersion}"); + + var historyVersionInfo = GetEntryAssemblyHistoryVersionInfo(); + var historyVersion = historyVersionInfo.Version; + var historyDate = historyVersionInfo.Date; + var historyIsRunScript = historyVersionInfo.IsRunScript; + + LogHelper.Handle($"历史版本:{historyVersion},更新时间:{historyDate},是否已执行{historyIsRunScript}"); + + // 历史版本为空、版本号相同,不执行脚本 + if (historyVersion == string.Empty) + { + LogHelper.Handle("历史版本为空,默认为最新版本,不执行脚本"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, true); + + return app; + } + else if (currentVersion.CompareTo(historyVersion) <= 0 && historyIsRunScript) + { + LogHelper.Handle("当前版本号与历史版本号相同,且已执行过脚本,不再执行"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, false); + + return app; + } + else + { + LogHelper.Handle("当前版本号与历史版本号不同,或版本号相同但未执行过脚本,开始执行脚本"); + + var scriptSqlVersions = GetScriptSqlVersions(); + + // 若不存在当前版本的脚本,则只保存当前版本信息,不执行脚本 + if (scriptSqlVersions.All(s => s.Version.CompareTo(currentVersion) < 0)) + { + LogHelper.Handle("不存在当前版本的脚本,只保存当前版本信息,不执行脚本"); + + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(currentVersion, false); + + return app; + } + + // 执行脚本 + foreach (var sqlFileInfo in scriptSqlVersions) + { + var sqlVersion = sqlFileInfo.Version; + + // 只执行大于历史版本的脚本,或者当前版本但未执行过 + if (sqlVersion.CompareTo(historyVersion) < 0) + { + LogHelper.Handle($"版本{sqlVersion}低于历史版本,跳过"); + continue; + } + if (sqlVersion == historyVersion && historyIsRunScript) + { + LogHelper.Handle($"版本{sqlVersion}等于历史版本,且已执行过脚本,跳过"); + continue; + } + + // 执行脚本 + var sql = File.ReadAllText(sqlFileInfo.FilePath); + if (sql != null) + { + LogHelper.Handle($"执行版本{sqlVersion}脚本"); + + HandleSqlScript(app, sql, sqlVersion); + } + } + } + + LogHelper.Success("AutoVersionUpdate 中间件结束"); + + return app; + } + + #region 辅助方法 + + /// + /// 获取入口程序集当前版本信息 + /// + /// + private static string GetEntryAssemblyCurrentVersion() + { + var entryAssemblyVersion = ReflectionHelper.GetEntryAssemblyVersion(); + return entryAssemblyVersion.ToString(3); + } + + /// + /// 设置入口程序集当前版本信息 + /// + /// + /// + private static void SetEntryAssemblyCurrentVersion(string version, bool isRunScript) + { + var path = Path.Combine(AppContext.BaseDirectory, "version.txt"); + var now = DateTime.Now; + File.WriteAllText(path, $"{version}^{now:yyyy-MM-dd HH:mm:ss}^{isRunScript}"); + } + + /// + /// 获取入口程序集上一次运行版本信息 + /// + /// + private static HistoryVersionInfo GetEntryAssemblyHistoryVersionInfo() + { + var path = Path.Combine(AppContext.BaseDirectory, "version.txt"); + + // 检查文件是否存在 + if (File.Exists(path)) + { + // 文件存在时读取内容 + var info = File.ReadAllText(path); + + if (info.Contains('^')) + { + var parts = info.Split('^'); + var version = parts.Length > 0 ? parts[0].ToString() : string.Empty; + var date = parts.Length > 1 ? parts[1] : string.Empty; + var isRunScript = parts.Length > 2 ? parts[2].ToBoolean() : false; + + return new HistoryVersionInfo(version, date, isRunScript); + } + } + + // 文件不存在或内容格式不正确时返回默认值 + return new HistoryVersionInfo(string.Empty, string.Empty, false); + } + + /// + /// 获取程序目录下的脚本 SQL 文件版本 + /// + /// + private static List GetScriptSqlVersions() + { + // 获取所有脚本文件 + var path = Path.Combine(AppContext.BaseDirectory, "UpdateScripts"); + var scriptFiles = Directory.GetFiles(path, "*.sql").ToList(); + + var sqlVersions = scriptFiles + .Select(s => new SqlFileInfo(Path.GetFileNameWithoutExtension(s), s)) + .OrderBy(s => s.Version).ToList(); + return sqlVersions; + } + + /// + /// 保存当前版本信息 + /// + /// + /// + /// + private static void HandleSqlScript(IApplicationBuilder app, string sql, string sqlVersion) + { + using var scope = App.GetRequiredService().CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var isSuccess = false; + + try + { + // 开启事务 + dbContext.Ado.BeginTran(); + dbContext.Ado.ExecuteCommand(sql); + dbContext.Ado.CommitTran(); + isSuccess = true; + } + catch (Exception ex) + { + dbContext.Ado.RollbackTran(); + LogHelper.Error($"AutoVersionUpdate 执行 SQL 脚本出错,版本:{sqlVersion},错误:{ex.Message}"); + } + finally + { + if (isSuccess) + { + // 保存当前版本信息 + SetEntryAssemblyCurrentVersion(sqlVersion, true); + } + } + } + + #endregion 辅助方法 +} + +public record SqlFileInfo(string Version, string FilePath); +public record HistoryVersionInfo(string Version, string Date, bool IsRunScript); + +#endif // NET10_0_OR_GREATER \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs new file mode 100644 index 0000000..7b3c0f4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs @@ -0,0 +1,193 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 全局规范化结果 +/// +[UnifyModel(typeof(AdminResult<>))] +public class AdminResultProvider : IUnifyResultProvider +{ + /// + /// JWT 授权异常返回值 + /// + /// + /// + /// + public IActionResult OnAuthorizeException(DefaultHttpContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, msg: metadata.Errors), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, msg: metadata.Errors), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 验证失败返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode ?? StatusCodes.Status400BadRequest, data: metadata.Data, msg: metadata.ValidationResult), UnifyContext.GetSerializerSettings(context)); + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + UnifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + var msg = "401 登录已过期,请重新登录"; + // 若存在身份验证失败消息,则返回消息内容 + if (context.Items.TryGetValue(SignatureAuthenticationDefaults.AuthenticateFailMsgKey, out var authFailMsg)) + msg = authFailMsg + ""; + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: msg), + App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: "403 禁止访问,没有权限"), + App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 302 状态码 + case StatusCodes.Status302Found: + if (context.Response.Headers.TryGetValue("Location", out var redirectUrl)) + { + context.Response.Redirect(redirectUrl); + } + else + { + var errorMessage = "302 跳转失败,没有提供 Location 头信息"; + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, msg: errorMessage), + App.GetOptions()?.JsonSerializerOptions); + } + break; + } + } + + /// + /// 返回成功结果集 + /// + /// + /// + /// + public static AdminResult Ok(string message, object data = default) + { + return RESTfulResult(StatusCodes.Status200OK, true, data, message); + } + + /// + /// 返回失败结果集 + /// + /// + /// + /// + /// + public static AdminResult Error(string message, int code = StatusCodes.Status400BadRequest, object data = default) + { + return RESTfulResult(code, false, data, message); + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + private static AdminResult RESTfulResult(int statusCode, bool succeeded = default, object data = default, object msg = default) + { + //// 统一返回值脱敏处理 + //if (data?.GetType() == typeof(String)) + //{ + // data = App.GetRequiredService().ReplaceAsync(data.ToString(), '*').GetAwaiter().GetResult(); + //} + //else if (data?.GetType() == typeof(JsonResult)) + //{ + // data = App.GetRequiredService().ReplaceAsync(JSON.Serialize(data), '*').GetAwaiter().GetResult(); + //} + + return new AdminResult + { + Code = statusCode, + Message = msg is null or string ? (msg + "") : JSON.Serialize(msg), + Result = data, + Type = succeeded ? "success" : "error", + Extras = UnifyContext.Take(), + Time = DateTime.Now + }; + } +} + +/// +/// 全局返回结果 +/// +/// +public class AdminResult +{ + /// + /// 状态码 + /// + public int Code { get; set; } + + /// + /// 类型success、warning、error + /// + public string Type { get; set; } + + /// + /// 错误信息 + /// + public string Message { get; set; } + + /// + /// 数据 + /// + public T Result { get; set; } + + /// + /// 附加数据 + /// + public object Extras { get; set; } + + /// + /// 时间 + /// + public DateTime Time { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseFilter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseFilter.cs new file mode 100644 index 0000000..5ab7d4a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseFilter.cs @@ -0,0 +1,75 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 模糊查询条件 +/// +public class Search +{ + /// + /// 字段名称集合 + /// + public List Fields { get; set; } + + /// + /// 关键字 + /// + public string? Keyword { get; set; } +} + +/// +/// 筛选过滤条件 +/// +public class Filter +{ + /// + /// 过滤条件 + /// + public FilterLogicEnum? Logic { get; set; } + + /// + /// 筛选过滤条件子项 + /// + public IEnumerable? Filters { get; set; } + + /// + /// 字段名称 + /// + public string? Field { get; set; } + + /// + /// 逻辑运算符 + /// + public FilterOperatorEnum? Operator { get; set; } + + /// + /// 字段值 + /// + public object? Value { get; set; } +} + +/// +/// 过滤条件基类 +/// +public abstract class BaseFilter +{ + /// + /// 模糊查询条件 + /// + public Search? Search { get; set; } + + /// + /// 模糊查询关键字 + /// + public string? Keyword { get; set; } + + /// + /// 筛选过滤条件 + /// + public Filter? Filter { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseIdInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseIdInput.cs new file mode 100644 index 0000000..bdc86b2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseIdInput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 主键Id输入参数 +/// +public class BaseIdInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "Id不能为空")] + [DataValidation(ValidationTypes.Numeric)] + public virtual long Id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs new file mode 100644 index 0000000..dbd6ad0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 数据导入输入参数 +/// +public class BaseImportInput +{ + /// + /// 记录Id + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader(IsIgnore = true)] + public virtual long Id { get; set; } + + /// + /// 错误信息 + /// + [ImporterHeader(IsIgnore = true)] + [ExporterHeader("错误信息", ColumnIndex = 9999, IsBold = true, IsAutoFit = true)] + public virtual string Error { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BasePageInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BasePageInput.cs new file mode 100644 index 0000000..87a029f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BasePageInput.cs @@ -0,0 +1,57 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 全局分页查询输入参数 +/// +public class BasePageInput : BaseFilter +{ + /// + /// 当前页码 + /// + [DataValidation(ValidationTypes.Numeric)] + public virtual int Page { get; set; } = 1; + + /// + /// 页码容量 + /// + //[Range(0, 100, ErrorMessage = "页码容量超过最大限制")] + [DataValidation(ValidationTypes.Numeric)] + public virtual int PageSize { get; set; } = 20; + + /// + /// 排序字段 + /// + public virtual string Field { get; set; } + + /// + /// 排序方向 + /// + public virtual string Order { get; set; } + + /// + /// 降序排序 + /// + public virtual string DescStr { get; set; } = "descending"; +} + +/// +/// 全局分页查询输入参数(带时间) +/// +public class BasePageTimeInput : BasePageInput +{ + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs new file mode 100644 index 0000000..317a907 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs @@ -0,0 +1,19 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 设置状态输入参数 +/// +public class BaseStatusInput : BaseIdInput +{ + /// + /// 状态 + /// + [Enum] + public StatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs new file mode 100644 index 0000000..8e9c05e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ChinaDateTimeConverter.cs @@ -0,0 +1,64 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Admin.NET.Core; + +/// +/// JSON时间序列化yyyy-MM-dd HH:mm:ss +/// +public class ChinaDateTimeConverter : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} + +/// +/// JSON时间序列化yyyy-MM-dd HH:mm +/// +public class ChinaDateTimeConverterHH : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd HH:mm" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} + +/// +/// JSON时间序列化yyyy-MM-dd +/// +public class ChinaDateTimeConverterDate : DateTimeConverterBase +{ + private static readonly IsoDateTimeConverter DtConverter = new() { DateTimeFormat = "yyyy-MM-dd" }; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return DtConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + DtConverter.WriteJson(writer, value, serializer); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs new file mode 100644 index 0000000..2ce24cf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs @@ -0,0 +1,290 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using DbType = SqlSugar.DbType; + +namespace Admin.NET.Core; + +/// +/// 代码生成帮助类 +/// +public static class CodeGenUtil +{ + /// + /// 转换大驼峰法命名 + /// + /// 字段名 + /// EntityBase 实体属性名称 + /// + public static string CamelColumnName(string columnName, string[] dbColumnNames) + { + if (columnName.Contains('_')) + { + var arrColName = columnName.Split('_'); + var sb = new StringBuilder(); + foreach (var col in arrColName) + { + if (col.Length > 0) + sb.Append(col[..1].ToUpper() + col[1..].ToLower()); + } + columnName = sb.ToString(); + } + else + { + var propertyName = dbColumnNames.FirstOrDefault(c => c.ToLower() == columnName.ToLower()); + if (!string.IsNullOrEmpty(propertyName)) + { + columnName = propertyName; + } + else + { + columnName = columnName[..1].ToUpper() + columnName[1..].ToLower(); + } + } + return columnName; + } + + // 根据数据库类型来处理对应的数据字段类型 + public static string ConvertDataType(DbColumnInfo dbColumnInfo, DbType dbType = DbType.Custom) + { + if (dbType == DbType.Custom) + dbType = App.GetOptions().ConnectionConfigs[0].DbType; + + var dataType = dbType switch + { + DbType.Oracle => ConvertDataTypeOracleSql(string.IsNullOrEmpty(dbColumnInfo.OracleDataType) ? dbColumnInfo.DataType : dbColumnInfo.OracleDataType, dbColumnInfo.Length, dbColumnInfo.Scale), + DbType.PostgreSQL => ConvertDataTypePostgresSql(dbColumnInfo.DataType), + _ => ConvertDataTypeDefault(dbColumnInfo.DataType), + }; + return dataType + (dbColumnInfo.IsNullable ? "?" : ""); + } + + public static string ConvertDataTypeOracleSql(string dataType, int? length, int? scale) + { + switch (dataType.ToLower()) + { + case "interval year to month": return "int"; + + case "interval day to second": return "TimeSpan"; + + case "smallint": return "Int16"; + + case "int": + case "integer": return "int"; + + case "long": return "long"; + + case "float": return "float"; + + case "decimal": return "decimal"; + + case "number": + if (length == null) return "decimal"; + return scale switch + { + > 0 => "decimal", + 0 or null when length is > 1 and < 12 => "int", + 0 or null when length > 11 => "long", + _ => length == 1 ? "bool" : "decimal" + }; + + case "char": + case "clob": + case "nclob": + case "nchar": + case "nvarchar": + case "varchar": + case "nvarchar2": + case "varchar2": + case "rowid": + return "string"; + + case "timestamp": + case "timestamp with time zone": + case "timestamptz": + case "timestamp without time zone": + case "date": + case "time": + case "time with time zone": + case "timetz": + case "time without time zone": + return "DateTime"; + + case "bfile": + case "blob": + case "raw": + return "byte[]"; + + default: + return "object"; + } + } + + //PostgresSQL数据类型对应的字段类型 + public static string ConvertDataTypePostgresSql(string dataType) + { + switch (dataType) + { + case "int2": + case "smallint": + return "Int16"; + + case "int4": + case "integer": + return "int"; + + case "int8": + case "bigint": + return "long"; + + case "float4": + case "real": + return "float"; + + case "float8": + case "double precision": + return "double"; + + case "numeric": + case "decimal": + case "path": + case "point": + case "polygon": + case "interval": + case "lseg": + case "macaddr": + case "money": + return "decimal"; + + case "boolean": + case "bool": + case "box": + case "bytea": + return "bool"; + + case "varchar": + case "character varying": + case "geometry": + case "name": + case "text": + case "char": + case "character": + case "cidr": + case "circle": + case "tsquery": + case "tsvector": + case "txid_snapshot": + case "xml": + case "json": + return "string"; + + case "uuid": + return "Guid"; + + case "timestamp": + case "timestamp with time zone": + case "timestamptz": + case "timestamp without time zone": + case "date": + case "time": + case "time with time zone": + case "timetz": + case "time without time zone": + return "DateTime"; + + case "bit": + case "bit varying": + return "byte[]"; + + case "varbit": + return "byte"; + + default: + return "object"; + } + } + + public static string ConvertDataTypeDefault(string dataType) + { + return dataType.ToLower() switch + { + "tinytext" or "mediumtext" or "longtext" or "mid" or "text" or "varchar" or "char" or "nvarchar" or "nchar" or "string" or "timestamp" => "string", + "int" or "integer" or "int32" => "int", + "smallint" => "Int16", + //"tinyint" => "byte", + "tinyint" => "bool", // MYSQL + "bigint" or "int64" => "long", + "bit" or "boolean" => "bool", + "money" or "smallmoney" or "numeric" or "decimal" => "decimal", + "real" => "Single", + "datetime" or "datetime2" or "smalldatetime" => "DateTime", + "date" => "DateOnly",// MYSQL + "time" => "TimeOnly",// MYSQL + "float" or "double" => "double", + "image" or "binary" or "varbinary" => "byte[]", + "uniqueidentifier" => "Guid", + _ => "object", + }; + } + + /// + /// 数据类型转显示类型 + /// + /// + /// + public static string DataTypeToEff(string dataType) + { + if (string.IsNullOrEmpty(dataType)) return ""; + return dataType.TrimEnd('?') switch + { + "string" => "Input", + "int" => "InputNumber", + "long" => "Input", + "float" => "InputNumber", + "double" => "InputNumber", + "decimal" => "InputNumber", + "bool" => "Switch", + "Guid" => "Input", + "DateTime" => "DatePicker", + _ => "Input", + }; + } + + // 是否通用字段 + public static bool IsCommonColumn(string columnName) + { + var columnList = new List() + { + nameof(EntityBaseOrg.OrgId), + nameof(EntityBaseTenant.TenantId), + nameof(EntityBase.CreateTime), + nameof(EntityBase.UpdateTime), + nameof(EntityBase.CreateUserId), + nameof(EntityBase.UpdateUserId), + nameof(EntityBase.CreateUserName), + nameof(EntityBase.UpdateUserName), + nameof(EntityBaseDel.IsDelete) + }; + return columnList.Contains(columnName); + } + + /// + /// 获取类型的PropertyInfo列表 + /// + /// + /// + public static PropertyInfo[] GetPropertyInfoArray(Type type) + { + PropertyInfo[] props = null; + try + { + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + } + catch + { } + return props; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs new file mode 100644 index 0000000..b2f824b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs @@ -0,0 +1,503 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using IPTools.Core; +using Magicodes.ExporterAndImporter.Core.Models; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; + +namespace Admin.NET.Core; + +/// +/// 通用工具类 +/// +public static class CommonUtil +{ + private static readonly SysCacheService SysCacheService = App.GetRequiredService(); + private static readonly SysFileService SysFileService = App.GetRequiredService(); + private static readonly SqlSugarRepository SysDictDataRep = App.GetRequiredService>(); + + /// + /// 根据字符串获取固定整型哈希值 + /// + /// + /// + /// + public static long GetFixedHashCode(string str, long startNumber = 0) + { + if (string.IsNullOrWhiteSpace(str)) return 0; + unchecked + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + for (int i = 0; i < str.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ str[i]; + if (i == str.Length - 1) break; + hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; + } + return startNumber + Math.Abs(hash1 + (hash2 * 1566083941)); + } + } + + /// + /// 生成百分数 + /// + /// + /// + /// + public static string ExecPercent(decimal passCount, decimal allCount) + { + string res = ""; + if (allCount > 0) + { + var value = (double)Math.Round(passCount / allCount * 100, 1); + if (value < 0) + res = Math.Round(value + 5 / Math.Pow(10, 0 + 1), 0, MidpointRounding.AwayFromZero).ToString(); + else + res = Math.Round(value, 0, MidpointRounding.AwayFromZero).ToString(); + } + if (res == "") res = "0"; + return res + "%"; + } + + /// + /// 获取服务地址 + /// + /// + public static string GetLocalhost() + { + string result = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}"; + + // 代理模式:获取真正的本机地址 + // X-Original-Host=原始请求 + // X-Forwarded-Server=从哪里转发过来 + if (App.HttpContext.Request.Headers.ContainsKey("Origin")) // 配置成完整的路径如(结尾不要带"/"),比如 https://www.abc.com + result = $"{App.HttpContext.Request.Headers["Origin"]}"; + else if (App.HttpContext.Request.Headers.ContainsKey("X-Original")) // 配置成完整的路径如(结尾不要带"/"),比如 https://www.abc.com + result = $"{App.HttpContext.Request.Headers["X-Original"]}"; + else if (App.HttpContext.Request.Headers.ContainsKey("X-Original-Host")) + result = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Headers["X-Original-Host"]}"; + return result + (string.IsNullOrWhiteSpace(App.Settings.VirtualPath) ? "" : App.Settings.VirtualPath); + } + + /// + /// 对象序列化XML + /// + /// + /// + /// + public static string SerializeObjectToXml(T obj) + { + if (obj == null) return string.Empty; + + var xs = new XmlSerializer(obj.GetType()); + var stream = new MemoryStream(); + var setting = new XmlWriterSettings + { + Encoding = new UTF8Encoding(false), // 不包含BOM + Indent = true // 设置格式化缩进 + }; + using (var writer = XmlWriter.Create(stream, setting)) + { + var ns = new XmlSerializerNamespaces(); + ns.Add("", ""); // 去除默认命名空间 + xs.Serialize(writer, obj, ns); + } + return Encoding.UTF8.GetString(stream.ToArray()); + } + + /// + /// 字符串转XML格式 + /// + /// + /// + public static XElement SerializeStringToXml(string xmlStr) + { + try + { + return XElement.Parse(xmlStr); + } + catch + { + return null; + } + } + + /// + /// 导出模板Excel + /// + /// + public static async Task ExportExcelTemplate(string fileName = null) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var res = await importer.GenerateTemplateBytes(); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" }; + } + + /// + /// 导出数据excel + /// + /// + public static async Task ExportExcelData(ICollection data, string fileName = null) where T : class, new() + { + var export = new ExcelExporter(); + var res = await export.ExportAsByteArray(data); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" }; + } + + /// + /// 导出数据excel,包括字典转换 + /// + /// + public static async Task ExportExcelData(ISugarQueryable query, Func action = null) + where TSource : class, new() where TTarget : class, new() + { + var propMappings = GetExportPropertMap(); + var data = query.ToList(); + //相同属性复制值,字典值转换 + var result = new List(); + foreach (var item in data) + { + var newData = new TTarget(); + foreach (var dict in propMappings) + { + var targetProp = dict.Value.Item3; + if (targetProp != null) + { + var propertyInfo = dict.Value.Item2; + var sourceVal = propertyInfo.GetValue(item, null); + if (sourceVal == null) + { + continue; + } + + var map = dict.Value.Item1; + if (map != null && map.TryGetValue(sourceVal, out string newVal1)) + { + targetProp.SetValue(newData, newVal1); + } + else + { + if (targetProp.PropertyType.FullName == propertyInfo.PropertyType.FullName) + { + targetProp.SetValue(newData, sourceVal); + } + else + { + var newVal = sourceVal.ToString().ParseTo(targetProp.PropertyType); + targetProp.SetValue(newData, newVal); + } + } + } + if (action != null) + { + newData = action(item, newData); + } + } + result.Add(newData); + } + var export = new ExcelExporter(); + var res = await export.ExportAsByteArray(result); + + return new FileContentResult(res, "application/octet-stream") { FileDownloadName = typeof(TTarget).Name + ".xlsx" }; + } + + /// + /// 导入数据Excel + /// + /// + /// + public static async Task> ImportExcelData([Required] IFormFile file) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var res = await importer.Import(file.OpenReadStream()); + var message = string.Empty; + + if (!res.HasError) return res.Data; + + if (res.Exception != null) + message += $"\r\n{res.Exception.Message}"; + foreach (DataRowErrorInfo drErrorInfo in res.RowErrors) + { + int rowNum = drErrorInfo.RowIndex; + foreach (var item in drErrorInfo.FieldErrors) + message += $"\r\n{item.Key}:{item.Value}(文件第{drErrorInfo.RowIndex}行)"; + } + message += "\r\n字段缺失:" + string.Join(",", res.TemplateErrors.Select(m => m.RequireColumnName).ToList()); + throw Oops.Oh("导入异常:" + message); + } + + /// + /// 导入Excel数据并错误标记 + /// + /// + /// + /// + /// + public static async Task> ImportExcelData([Required] IFormFile file, Func, ImportResult> importResultCallback = null) where T : class, new() + { + IImporter importer = new ExcelImporter(); + var resultStream = new MemoryStream(); + var res = await importer.Import(file.OpenReadStream(), resultStream, importResultCallback); + resultStream.Seek(0, SeekOrigin.Begin); + var userId = App.User?.FindFirst(ClaimConst.UserId)?.Value; + + SysCacheService.Remove(CacheConst.KeyExcelTemp + userId); + SysCacheService.Set(CacheConst.KeyExcelTemp + userId, resultStream, TimeSpan.FromMinutes(5)); + + var message = string.Empty; + if (!res.HasError) return res.Data; + + if (res.Exception != null) + message += $"\r\n{res.Exception.Message}"; + foreach (DataRowErrorInfo drErrorInfo in res.RowErrors) + { + message = drErrorInfo.FieldErrors.Aggregate(message, (current, item) => current + $"\r\n{item.Key}:{item.Value}(文件第{drErrorInfo.RowIndex}行)"); + } + if (res.TemplateErrors.Count > 0) + message += "\r\n字段缺失:" + string.Join(",", res.TemplateErrors.Select(m => m.RequireColumnName).ToList()); + + if (message.Length > 200) + message = message.Substring(0, 200) + "...\r\n异常过多,建议下载错误标记文件查看详细错误信息并重新导入。"; + throw Oops.Oh("导入异常:" + message); + } + + /// + /// 导入数据Excel + /// + /// + /// + /// + public static async Task> ImportExcelDataAsync([Required] IFormFile file) where T : class, new() + { + var newFile = await SysFileService.UploadFile(new UploadFileInput { File = file }); + + await using var fileStream = await SysFileService.GetFileStream(newFile); + + IImporter importer = new ExcelImporter(); + var res = await importer.Import(fileStream); + + // 删除文件 + _ = SysFileService.DeleteFile(new BaseIdInput { Id = newFile.Id }); + + if (res == null) + throw Oops.Oh("导入数据为空"); + if (res.Exception != null) + throw Oops.Oh("导入异常:" + res.Exception); + if (res.TemplateErrors?.Count > 0) + throw Oops.Oh("模板异常:" + res.TemplateErrors.Select(x => $"[{x.RequireColumnName}]{x.Message}").Join("\n")); + + return res.Data.ToList(); + } + + // 例:List ls = CommonUtil.ParseList(importResult.Data); + /// + /// 对象转换 含字典转换 + /// + /// + /// + /// + /// + /// + public static List ParseList(IEnumerable data, Func action = null) where TTarget : new() + { + var propMappings = GetImportPropertMap(); + // 相同属性复制值,字典值转换 + var result = new List(); + foreach (var item in data) + { + var newData = new TTarget(); + foreach (var dict in propMappings) + { + var targeProp = dict.Value.Item3; + if (targeProp != null) + { + var propertyInfo = dict.Value.Item2; + var sourceVal = propertyInfo.GetValue(item, null); + if (sourceVal == null) + continue; + + var map = dict.Value.Item1; + if (map != null && map.ContainsKey(sourceVal.ToString())) + { + var newVal = map[sourceVal.ToString()]; + targeProp.SetValue(newData, newVal); + } + else + { + if (targeProp.PropertyType.FullName == propertyInfo.PropertyType.FullName) + { + targeProp.SetValue(newData, sourceVal); + } + else + { + var newVal = sourceVal.ToString().ParseTo(targeProp.PropertyType); + targeProp.SetValue(newData, newVal); + } + } + } + } + if (action != null) + newData = action(item, newData); + + if (newData != null) + result.Add(newData); + } + return result; + } + + /// + /// 获取导入属性映射 + /// + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary, PropertyInfo, PropertyInfo>> GetImportPropertMap() where TTarget : new() + { + // 整理导入对象的属性名称,<字典数据,原属性信息,目标属性信息> + var propMappings = new Dictionary, PropertyInfo, PropertyInfo>>(); + + var dictService = App.GetRequiredService>(); + var tSourceProps = typeof(TSource).GetProperties().ToList(); + var tTargetProps = typeof(TTarget).GetProperties().ToDictionary(u => u.Name); + foreach (var propertyInfo in tSourceProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + var targetProp = tTargetProps[attrs.TargetPropName]; + var mappingValues = dictService.Context.Queryable((u, a) => + new JoinQueryInfos(JoinType.Inner, u.Id == a.DictTypeId)) + .Where(u => u.Code == attrs.TypeCode) + .Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable) + .Select((u, a) => new + { + Label = a.Label, + Value = a.Value + }).ToList() + .ToDictionary(u => u.Label, u => u.Value.ParseTo(targetProp.PropertyType)); + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>(mappingValues, propertyInfo, targetProp)); + } + else + { + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>( + null, propertyInfo, tTargetProps.ContainsKey(propertyInfo.Name) ? tTargetProps[propertyInfo.Name] : null)); + } + } + + return propMappings; + } + + /// + /// 获取导出属性映射 + /// + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary, PropertyInfo, PropertyInfo>> GetExportPropertMap() where TTarget : new() + { + // 整理导入对象的属性名称,<字典数据,原属性信息,目标属性信息> + var propMappings = new Dictionary, PropertyInfo, PropertyInfo>>(); + + var targetProps = typeof(TTarget).GetProperties().ToList(); + var sourceProps = typeof(TSource).GetProperties().ToDictionary(u => u.Name); + foreach (var propertyInfo in targetProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + var targetProp = sourceProps[attrs.TargetPropName]; + var mappingValues = SysDictDataRep.Context.Queryable((u, a) => + new JoinQueryInfos(JoinType.Inner, u.Id == a.DictTypeId)) + .Where(u => u.Code == attrs.TypeCode) + .Where((u, a) => u.Status == StatusEnum.Enable && a.Status == StatusEnum.Enable) + .Select((u, a) => new + { + a.Label, + a.Value + }).ToList() + .ToDictionary(u => u.Value.ParseTo(targetProp.PropertyType), u => u.Label); + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>(mappingValues, targetProp, propertyInfo)); + } + else + { + propMappings.Add(propertyInfo.Name, new Tuple, PropertyInfo, PropertyInfo>( + null, sourceProps.TryGetValue(propertyInfo.Name, out PropertyInfo prop) ? prop : null, propertyInfo)); + } + } + + return propMappings; + } + + /// + /// 获取属性映射 + /// + /// + /// 整理导入对象的 属性名称, 字典数据,原属性信息,目标属性信息 + private static Dictionary> GetExportDictMap() where TTarget : new() + { + // 整理导入对象的属性名称,目标属性名,字典Code + var propMappings = new Dictionary>(); + var tTargetProps = typeof(TTarget).GetProperties(); + foreach (var propertyInfo in tTargetProps) + { + var attrs = propertyInfo.GetCustomAttribute(); + if (attrs != null && !string.IsNullOrWhiteSpace(attrs.TypeCode)) + { + propMappings.Add(propertyInfo.Name, new Tuple(attrs.TargetPropName, attrs.TypeCode)); + } + } + + return propMappings; + } + + /// + /// 解析IP地址 + /// + /// + /// + public static (string ipLocation, double? longitude, double? latitude) GetIpAddress(string ip) + { + try + { + var ipInfo = IpTool.SearchWithI18N(ip); // 国际化查询,默认中文 中文zh-CN、英文en + var addressList = new List() { ipInfo.Country, ipInfo.Province, ipInfo.City, ipInfo.NetworkOperator }; + return (string.Join(" ", addressList.Where(u => u != "0" && !string.IsNullOrWhiteSpace(u)).ToList()), ipInfo.Longitude, ipInfo.Latitude); // 去掉0及空并用空格连接 + } + catch + { + // 不做处理 + } + return ("未知", 0, 0); + } + + /// + /// 获取客户端设备信息(操作系统+浏览器) + /// + /// + /// + public static string GetClientDeviceInfo(string userAgent) + { + try + { + if (userAgent != null) + { + var client = Parser.GetDefault().Parse(userAgent); + if (client.Device.IsSpider) + return "爬虫"; + return $"{client.OS.Family} {client.OS.Major} {client.OS.Minor}" + + $"|{client.UA.Family} {client.UA.Major}.{client.UA.Minor} / {client.Device.Family}"; + } + } + catch + { } + return "未知"; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ComputerUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ComputerUtil.cs new file mode 100644 index 0000000..78d05ec --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ComputerUtil.cs @@ -0,0 +1,550 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public static class ComputerUtil +{ + /// + /// 内存信息 + /// + /// + public static MemoryMetrics GetComputerInfo() + { + MemoryMetrics memoryMetrics; + if (IsMacOS()) + { + memoryMetrics = MemoryMetricsClient.GetMacOSMetrics(); + } + else if (IsUnix()) + { + memoryMetrics = MemoryMetricsClient.GetUnixMetrics(); + } + else + { + memoryMetrics = MemoryMetricsClient.GetWindowsMetrics(); + } + memoryMetrics.FreeRam = Math.Round(memoryMetrics.Free / 1024, 2) + "GB"; + memoryMetrics.UsedRam = Math.Round(memoryMetrics.Used / 1024, 2) + "GB"; + memoryMetrics.TotalRam = Math.Round(memoryMetrics.Total / 1024, 2) + "GB"; + memoryMetrics.RamRate = Math.Ceiling(100 * memoryMetrics.Used / memoryMetrics.Total) + "%"; + var cpuRates = GetCPURates(); + if (cpuRates != null) + { + memoryMetrics.CpuRates = cpuRates.Select(u => Math.Ceiling(u.ParseToDouble()) + "%").ToList(); + } + memoryMetrics.CpuRate = memoryMetrics.CpuRates[0]; + return memoryMetrics; + } + + /// + /// 获取正确的操作系统版本(Linux获取发行版本) + /// + /// + public static String GetOSInfo() + { + string operation = string.Empty; + if (IsMacOS()) + { + var output = ShellUtil.Bash("sw_vers | awk 'NR<=2{printf \"%s \", $NF}'"); + if (output != null) + { + operation = output.Replace("%", string.Empty); + } + } + else if (IsUnix()) + { + var output = ShellUtil.Bash("awk -F= '/^VERSION_ID/ {print $2}' /etc/os-release | tr -d '\"'"); + operation = output ?? string.Empty; + } + else + { + operation = RuntimeInformation.OSDescription; + } + return operation; + } + + /// + /// 磁盘信息 + /// + /// + public static List GetDiskInfos() + { + var diskInfos = new List(); + if (IsMacOS()) + { + var output = ShellUtil.Bash(@"df -m | awk '/^\/dev\/disk/ {print $1,$2,$3,$4,$5}'"); + var disks = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (disks.Length < 1) return diskInfos; + foreach (var item in disks) + { + var disk = item.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (disk.Length < 5) continue; + + var diskInfo = new DiskInfo() + { + DiskName = disk[0], + TypeName = ShellUtil.Bash("diskutil info " + disk[0] + " | awk '/File System Personality/ {print $4}'").Replace("\n", string.Empty), + TotalSize = Math.Round(long.Parse(disk[1]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + Used = Math.Round(long.Parse(disk[2]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(long.Parse(disk[3]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailablePercent = decimal.Parse(disk[4].Replace("%", "")) + }; + diskInfos.Add(diskInfo); + } + } + else if (IsUnix()) + { + var output = ShellUtil.Bash(@"df -mT | awk '/^\/dev\/(sd|vd|xvd|nvme|sda|vda|mapper)/ {print $1,$2,$3,$4,$5,$6}'"); + var disks = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + if (disks.Length < 1) return diskInfos; + + //var rootDisk = disks[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + //if (rootDisk == null || rootDisk.Length < 1) + // return diskInfos; + + foreach (var item in disks) + { + var disk = item.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (disk.Length < 6) continue; + + var diskInfo = new DiskInfo() + { + DiskName = disk[0], + TypeName = disk[1], + TotalSize = Math.Round(long.Parse(disk[2]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + Used = Math.Round(long.Parse(disk[3]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(long.Parse(disk[4]) / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailablePercent = decimal.Parse(disk[5].Replace("%", "")) + }; + diskInfos.Add(diskInfo); + } + } + else + { + var driveList = DriveInfo.GetDrives().Where(u => u.IsReady); + foreach (var item in driveList) + { + if (item.DriveType == DriveType.CDRom) continue; + var diskInfo = new DiskInfo() + { + DiskName = item.Name, + TypeName = item.DriveType.ToString(), + TotalSize = Math.Round(item.TotalSize / 1024 / 1024 / 1024.0m, 2, MidpointRounding.AwayFromZero), + AvailableFreeSpace = Math.Round(item.AvailableFreeSpace / 1024 / 1024 / 1024.0m, 2, MidpointRounding.AwayFromZero), + }; + diskInfo.Used = diskInfo.TotalSize - diskInfo.AvailableFreeSpace; + diskInfo.AvailablePercent = decimal.Ceiling(diskInfo.Used / (decimal)diskInfo.TotalSize * 100); + diskInfos.Add(diskInfo); + } + } + return diskInfos; + } + + /// + /// 获取外网IP地址 + /// + /// + public static string GetIpFromOnline() + { + try + { + var url = "https://4.ipw.cn"; + var httpRemoteService = App.GetRequiredService(); + var ip = httpRemoteService.GetAsString(url); + var (ipLocation, _, _) = CommonUtil.GetIpAddress(ip); + return ip + " " + ipLocation; + } + catch + { + return "unknow"; + } + } + + public static bool IsUnix() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + + public static bool IsMacOS() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + public static List GetCPURates() + { + var cpuRates = new List(); + string output = ""; + if (IsMacOS()) + { + output = ShellUtil.Bash("top -l 1 | grep \"CPU usage\" | awk '{print $3 + $5}'"); + cpuRates.Add(output.Trim()); + } + else if (IsUnix()) + { + output = ShellUtil.Bash("awk '{u=$2+$4; t=$2+$4+$5; if (NR==1){u1=u; t1=t;} else print ($2+$4-u1) * 100 / (t-t1); }' <(grep 'cpu ' /proc/stat) <(sleep 1;grep 'cpu ' /proc/stat)"); + cpuRates.Add(output.Trim()); + } + else + { + try + { + output = ShellUtil.Cmd("wmic", "cpu get LoadPercentage"); + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_Processor | Select-Object LoadPercentage"); + output = output.Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Replace("=", string.Empty).Trim(); + } + cpuRates.AddRange(output.Replace("LoadPercentage", string.Empty).Trim().Split("\r\r\n")); + } + return cpuRates; + } + + /// + /// 获取系统运行时间 + /// + /// + public static string GetRunTime() + { + string runTime = string.Empty; + string output = ""; + if (IsMacOS()) + { + // macOS 获取系统启动时间: + // sysctl -n kern.boottime | awk '{print $4}' | tr -d ',' + // 返回:1705379131 + // 使用date格式化即可 + output = ShellUtil.Bash("date -r $(sysctl -n kern.boottime | awk '{print $4}' | tr -d ',') +\"%Y-%m-%d %H:%M:%S\"").Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + else if (IsUnix()) + { + output = ShellUtil.Bash("date -d \"$(awk -F. '{print $1}' /proc/uptime) second ago\" +\"%Y-%m-%d %H:%M:%S\"").Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + else + { + try + { + output = ShellUtil.Cmd("wmic", "OS get LastBootUpTime/Value"); + string[] outputArr = output.Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + if (outputArr.Length == 2) + runTime = DateTimeUtil.FormatTime((DateTime.Now - outputArr[1].Split('.')[0].ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object LastBootUpTime"); + output = output.Replace("LastBootUpTime", string.Empty).Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Replace("=", string.Empty).Trim(); + runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong()); + } + } + return runTime; + } +} + +/// +/// 内存信息 +/// +public class MemoryMetrics +{ + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Total { get; set; } + + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Used { get; set; } + + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public double Free { get; set; } + + /// + /// 已用内存 + /// + public string UsedRam { get; set; } + + /// + /// CPU使用率% + /// + public List CpuRates { get; set; } + + public string CpuRate { get; set; } + + /// + /// 总内存 GB + /// + public string TotalRam { get; set; } + + /// + /// 内存使用率 % + /// + public string RamRate { get; set; } + + /// + /// 空闲内存 + /// + public string FreeRam { get; set; } +} + +/// +/// 磁盘信息 +/// +public class DiskInfo +{ + /// + /// 磁盘名 + /// + public string DiskName { get; set; } + + /// + /// 类型名 + /// + public string TypeName { get; set; } + + /// + /// 总剩余 + /// + public decimal TotalFree { get; set; } + + /// + /// 总量 + /// + public decimal TotalSize { get; set; } + + /// + /// 已使用 + /// + public decimal Used { get; set; } + + /// + /// 可使用 + /// + public decimal AvailableFreeSpace { get; set; } + + /// + /// 使用百分比 + /// + public decimal AvailablePercent { get; set; } +} + +public class MemoryMetricsClient +{ + /// + /// windows系统获取内存信息 + /// + /// + public static MemoryMetrics GetWindowsMetrics() + { + string output = ""; + var metrics = new MemoryMetrics(); + try + { + output = ShellUtil.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value"); + var lines = output.Trim().Split('\n', (char)StringSplitOptions.RemoveEmptyEntries); + if (lines.Length <= 1) return metrics; + + var freeMemoryParts = lines[0].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + var totalMemoryParts = lines[1].Split('=', (char)StringSplitOptions.RemoveEmptyEntries); + metrics.Total = Math.Round(double.Parse(totalMemoryParts[1]) / 1024, 0); + metrics.Free = Math.Round(double.Parse(freeMemoryParts[1]) / 1024, 0);//m + } + catch (Exception) + { + output = ShellUtil.PowerShell("Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object FreePhysicalMemory, TotalVisibleMemorySize"); + output = output.Replace("@", string.Empty).Replace("{", string.Empty).Replace("}", string.Empty).Trim(); + var lines = output.Trim().Split(';', (char)StringSplitOptions.RemoveEmptyEntries); + + // 跳过表头与分隔线(通常为前两行) + if (lines.Length >= 2) + { + // 解析并转换为MB(原单位为KB) + metrics.Free = Math.Round(double.Parse(lines[0].Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 0); + metrics.Total = Math.Round(double.Parse(lines[1].Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 0); + } + } + metrics.Used = metrics.Total - metrics.Free; + + return metrics; + } + + /// + /// Unix系统获取 + /// + /// + public static MemoryMetrics GetUnixMetrics() + { + string output = ShellUtil.Bash("awk '/MemTotal/ {total=$2} /MemAvailable/ {available=$2} END {print total,available}' /proc/meminfo"); + var metrics = new MemoryMetrics(); + var memory = output.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries); + if (memory.Length != 2) return metrics; + + metrics.Total = double.Parse(memory[0]) / 1024; + metrics.Free = double.Parse(memory[1]) / 1024; + metrics.Used = metrics.Total - metrics.Free; + return metrics; + } + + /// + /// macOS系统获取 + /// + /// + public static MemoryMetrics GetMacOSMetrics() + { + var metrics = new MemoryMetrics(); + //物理内存大小 + var total = ShellUtil.Bash("sysctl -n hw.memsize | awk '{printf \"%.2f\", $1/1024/1024}'"); + metrics.Total = float.Parse(total.Replace("%", string.Empty)); + //TODO:占用内存,检查效率 + var free = ShellUtil.Bash("top -l 1 -s 0 | awk '/PhysMem/ {print $6+$8}'"); + metrics.Free = float.Parse(free); + metrics.Used = metrics.Total - metrics.Free; + return metrics; + } +} + +public class ShellUtil +{ + /// + /// linux 系统命令 + /// + /// + /// + public static string Bash(string command) + { + var escapedArgs = command.Replace("\"", "\\\""); + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + process.Dispose(); + return result; + } + + /// + /// windows CMD 系统命令 + /// + /// + /// + /// + public static string Cmd(string fileName, string args) + { + var info = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true + }; + + var output = string.Empty; + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + return output; + } + + /// + /// Windows POWERSHELL 系统命令 + /// + /// + /// + public static string PowerShell(string script) + { + using var PowerShellInstance = System.Management.Automation.PowerShell.Create(); + PowerShellInstance.AddScript(script); + var PSOutput = PowerShellInstance.Invoke(); + + var output = new StringBuilder(); + foreach (var outputItem in PSOutput) + { + output.AppendLine(outputItem.ToString()); + } + return output.ToString(); + } +} + +public class ShellHelper +{ + /// + /// Linux 系统命令 + /// + /// + /// + public static string Bash(string command) + { + var escapedArgs = command.Replace("\"", "\\\""); + var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escapedArgs}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + process.Dispose(); + return result; + } + + /// + /// Windows CMD 系统命令 + /// + /// + /// + /// + public static string Cmd(string fileName, string args) + { + var info = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true + }; + + var output = string.Empty; + using (var process = Process.Start(info)) + { + output = process.StandardOutput.ReadToEnd(); + } + return output; + } + + /// + /// Windows POWERSHELL 系统命令 + /// + /// + /// + public static string PowerShell(string script) + { + using var PowerShellInstance = System.Management.Automation.PowerShell.Create(); + PowerShellInstance.AddScript(script); + var PSOutput = PowerShellInstance.Invoke(); + + var output = new StringBuilder(); + foreach (var outputItem in PSOutput) + { + output.AppendLine(outputItem.BaseObject.ToString()); + } + return output.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs new file mode 100644 index 0000000..ebbeded --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs @@ -0,0 +1,120 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +public class CryptogramUtil +{ + public static readonly bool StrongPassword = App.GetConfig("Cryptogram:StrongPassword"); // 是否开启密码强度验证 + public static readonly string PasswordStrengthValidation = App.GetConfig("Cryptogram:PasswordStrengthValidation"); // 密码强度验证正则表达式 + public static readonly string PasswordStrengthValidationMsg = App.GetConfig("Cryptogram:PasswordStrengthValidationMsg"); // 密码强度验证提示 + public static readonly string CryptoType = App.GetConfig("Cryptogram:CryptoType"); // 加密类型 + public static readonly string PublicKey = App.GetConfig("Cryptogram:PublicKey"); // 公钥 + public static readonly string PrivateKey = App.GetConfig("Cryptogram:PrivateKey"); // 私钥 + + public static readonly string SM4_key = "0123456789abcdeffedcba9876543210"; + public static readonly string SM4_iv = "595298c7c6fd271f0402f804c33d3f66"; + + /// + /// 加密 + /// + /// + /// + public static string Encrypt(string plainText) + { + if (CryptoType == CryptogramEnum.MD5.ToString()) + { + return MD5Encryption.Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4EncryptECB(plainText); + } + return plainText; + } + + /// + /// 解密 + /// + /// + /// + public static string Decrypt(string cipherText) + { + if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Decrypt(cipherText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4DecryptECB(cipherText); + } + return cipherText; + } + + /// + /// SM2加密 + /// + /// + /// + public static string SM2Encrypt(string plainText) + { + return GMUtil.SM2Encrypt(PublicKey, plainText); + } + + /// + /// SM2解密 + /// + /// + /// + public static string SM2Decrypt(string cipherText) + { + return GMUtil.SM2Decrypt(PrivateKey, cipherText); + } + + /// + /// SM4加密(ECB) + /// + /// + /// + public static string SM4EncryptECB(string plainText) + { + return GMUtil.SM4EncryptECB(SM4_key, plainText); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + public static string SM4DecryptECB(string cipherText) + { + return GMUtil.SM4DecryptECB(SM4_key, cipherText); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + public static string SM4EncryptCBC(string plainText) + { + return GMUtil.SM4EncryptCBC(SM4_key, SM4_iv, plainText); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + public static string SM4DecryptCBC(string cipherText) + { + return GMUtil.SM4DecryptCBC(SM4_key, SM4_iv, cipherText); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs new file mode 100644 index 0000000..464090f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/CustomJsonPropertyConverter.cs @@ -0,0 +1,188 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Admin.NET.Core; + +/// +/// 自定义属性名称转换器 +/// +public class CustomJsonPropertyConverter : JsonConverter +{ + public static readonly JsonSerializerOptions Options = new() + { + Converters = { new CustomJsonPropertyConverter() }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + // 缓存类型信息避免重复反射 + private static readonly ConcurrentDictionary> PropertyCache = new(); + + // 日期时间格式化配置 + private readonly string _dateTimeFormat; + + public CustomJsonPropertyConverter(string dateTimeFormat = "yyyy-MM-dd HH:mm:ss") + { + _dateTimeFormat = dateTimeFormat; + } + + public override bool CanConvert(Type typeToConvert) + { + return PropertyCache.GetOrAdd(typeToConvert, type => + type.GetProperties() + .Where(p => p.GetCustomAttribute() != null) + .Select(p => new PropertyMeta(p)) + .ToList().AsReadOnly() + ).Count > 0; + } + + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var jsonDoc = JsonDocument.ParseValue(ref reader); + var instance = Activator.CreateInstance(typeToConvert); + var properties = PropertyCache.GetOrAdd(typeToConvert, BuildPropertyMeta); + + foreach (var prop in properties) + { + if (jsonDoc.RootElement.TryGetProperty(prop.JsonName, out var value)) + { + object propertyValue; + + // 特殊处理日期时间类型 + if (IsDateTimeType(prop.PropertyType)) + { + propertyValue = HandleDateTimeValue(value, prop.PropertyType); + } + else + { + propertyValue = JsonSerializer.Deserialize( + value.GetRawText(), + prop.PropertyType, + options + ); + } + + prop.SetValue(instance, propertyValue); + } + } + + return instance; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + var properties = PropertyCache.GetOrAdd(value.GetType(), BuildPropertyMeta); + + foreach (var prop in properties) + { + var propertyValue = prop.GetValue(value); + + writer.WritePropertyName(prop.JsonName); + + // 特殊处理日期时间类型 + if (propertyValue != null && IsDateTimeType(prop.PropertyType)) + { + writer.WriteStringValue(FormatDateTime(propertyValue)); + } + else + { + JsonSerializer.Serialize(writer, propertyValue, options); + } + } + + writer.WriteEndObject(); + } + + private static IReadOnlyList BuildPropertyMeta(Type type) + { + return type.GetProperties() + .Select(p => new PropertyMeta(p)) + .ToList().AsReadOnly(); + } + + private object HandleDateTimeValue(JsonElement value, Type targetType) + { + var dateStr = value.GetString(); + if (string.IsNullOrEmpty(dateStr)) return null; + + var date = DateTime.Parse(dateStr); + return targetType == typeof(DateTimeOffset) + ? new DateTimeOffset(date) + : (object)date; + } + + private string FormatDateTime(object dateTime) + { + return dateTime switch + { + DateTime dt => dt.ToString(_dateTimeFormat), + DateTimeOffset dto => dto.ToString(_dateTimeFormat), + _ => dateTime?.ToString() + }; + } + + private static bool IsDateTimeType(Type type) + { + var actualType = Nullable.GetUnderlyingType(type) ?? type; + return actualType == typeof(DateTime) || actualType == typeof(DateTimeOffset); + } + + private class PropertyMeta + { + private readonly PropertyInfo _property; + private readonly Func _getter; + private readonly Action _setter; + + public string JsonName { get; } + public Type PropertyType => _property.PropertyType; + + public PropertyMeta(PropertyInfo property) + { + _property = property; + JsonName = property.GetCustomAttribute()?.Name ?? property.Name; + + // 编译表达式树优化属性访问 + var instanceParam = Expression.Parameter(typeof(object), "instance"); + var valueParam = Expression.Parameter(typeof(object), "value"); + + // Getter + var getterExpr = Expression.Lambda>( + Expression.Convert( + Expression.Property( + Expression.Convert(instanceParam, property.DeclaringType), + property), + typeof(object)), + instanceParam); + _getter = getterExpr.Compile(); + + // Setter + if (property.CanWrite) + { + var setterExpr = Expression.Lambda>( + Expression.Assign( + Expression.Property( + Expression.Convert(instanceParam, property.DeclaringType), + property), + Expression.Convert(valueParam, property.PropertyType)), + instanceParam, valueParam); + _setter = setterExpr.Compile(); + } + } + + public object GetValue(object instance) => _getter(instance); + + public void SetValue(object instance, object value) + { + if (_setter != null) + { + _setter(instance, value); + } + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/DateTimeUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/DateTimeUtil.cs new file mode 100644 index 0000000..e0175ae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/DateTimeUtil.cs @@ -0,0 +1,416 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 时间帮助类 +/// +public class DateTimeUtil +{ + public readonly DateTime Date; + + private DateTimeUtil(TimeSpan timeSpan = default) + { + Date = DateTime.Now.AddTicks(timeSpan.Ticks); + } + + private DateTimeUtil(DateTime time) + { + Date = time; + } + + /// + /// 实例化类 + /// + /// + /// + public static DateTimeUtil Init(TimeSpan timeSpan = default) + { + return new DateTimeUtil(timeSpan); + } + + /// + /// 实例化类 + /// + /// + /// + public static DateTimeUtil Init(DateTime time) + { + return new DateTimeUtil(time); + } + + /// + /// 根据unix时间戳的长度自动判断是秒还是以毫秒为单位 + /// + /// + /// + public static DateTime ConvertUnixTime(long unixTime) + { + // 判断时间戳长度 + bool isMilliseconds = unixTime > 9999999999; + + if (isMilliseconds) + { + return DateTimeOffset.FromUnixTimeMilliseconds(unixTime).ToLocalTime().DateTime; + } + else + { + return DateTimeOffset.FromUnixTimeSeconds(unixTime).ToLocalTime().DateTime; + } + } + + /// + /// 获取开始时间 + /// + /// + /// + /// + public static DateTime GetBeginTime(DateTime? dateTime, int days = 0) + { + return dateTime == DateTime.MinValue || dateTime == null ? DateTime.Now.AddDays(days) : (DateTime)dateTime; + } + + /// + /// 时间戳转本地时间-时间戳精确到秒 + /// + public static DateTime ToLocalTimeDateBySeconds(long unix) + { + return DateTimeOffset.FromUnixTimeSeconds(unix).ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到秒 + /// + public static long ToUnixTimestampBySeconds(DateTime dt) + { + return new DateTimeOffset(dt).ToUnixTimeSeconds(); + } + + /// + /// 时间戳转本地时间-时间戳精确到毫秒 + /// + public static DateTime ToLocalTimeDateByMilliseconds(long unix) + { + return DateTimeOffset.FromUnixTimeMilliseconds(unix).ToLocalTime().DateTime; + } + + /// + /// 时间转时间戳Unix-时间戳精确到毫秒 + /// + public static long ToUnixTimestampByMilliseconds(DateTime dt) + { + return new DateTimeOffset(dt).ToUnixTimeMilliseconds(); + } + + /// + /// 毫秒转天时分秒 + /// + /// TotalMilliseconds + /// 是否简化显示 + /// + public static string FormatTime(long ms, bool isSimplify = false) + { + int ss = 1000; + int mi = ss * 60; + int hh = mi * 60; + int dd = hh * 24; + + long day = ms / dd; + long hour = (ms - day * dd) / hh; + long minute = (ms - day * dd - hour * hh) / mi; + long second = (ms - day * dd - hour * hh - minute * mi) / ss; + //long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss; + + string sDay = day < 10 ? "0" + day : "" + day; //天 + string sHour = hour < 10 ? "0" + hour : "" + hour;//小时 + string sMinute = minute < 10 ? "0" + minute : "" + minute;//分钟 + string sSecond = second < 10 ? "0" + second : "" + second;//秒 + //string sMilliSecond = milliSecond < 10 ? "0" + milliSecond : "" + milliSecond;//毫秒 + //sMilliSecond = milliSecond < 100 ? "0" + sMilliSecond : "" + sMilliSecond; + + if (!isSimplify) + return $"{sDay} 天 {sHour} 小时 {sMinute} 分 {sSecond} 秒"; + else + { + string result = string.Empty; + if (day > 0) + result = $"{sDay}天"; + if (hour > 0) + result = $"{result}{sHour}小时"; + if (minute > 0) + result = $"{result}{sMinute}分"; + if (!result.IsNullOrEmpty()) + result = $"{result}{sSecond}秒"; + else + result = $"{sSecond}秒"; + return result; + } + } + + /// + /// 获取unix时间戳 + /// + /// + /// + public static long GetUnixTimeStamp(DateTime dt) + { + return ((DateTimeOffset)dt).ToUnixTimeMilliseconds(); + } + + /// + /// 获取日期天的最小时间 + /// + /// + /// + public static DateTime GetDayMinDate(DateTime dt) + { + return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0); + } + + /// + /// 获取日期天的最大时间 + /// + /// + /// + + public static DateTime GetDayMaxDate(DateTime dt) + { + return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59); + } + + /// + /// 根据日期是否在当前年份来格式化日期 + /// + /// + /// + public static string FormatDateTime(DateTime? dt) + { + return dt == null ? string.Empty : dt.Value.ToString(dt.Value.Year == DateTime.Now.Year ? "MM-dd HH:mm" : "yyyy-MM-dd HH:mm"); + } + + /// + /// 获取日期范围00:00:00 - 23:59:59 + /// + /// + public static List GetTodayTimeList(DateTime time) + { + return new List + { + Convert.ToDateTime(time.ToString("D")), + Convert.ToDateTime(time.AddDays(1).ToString("D")).AddSeconds(-1) + }; + } + + /// + /// 获取星期几 + /// + /// + /// + public static string GetWeekByDate(DateTime dt) + { + var day = new[] { "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" }; + return day[Convert.ToInt32(dt.DayOfWeek.ToString("d"))]; + } + + /// + /// 获取这个月的第几周 + /// + /// + /// + public static int GetWeekNumInMonth(DateTime daytime) + { + int dayInMonth = daytime.Day; + // 本月第一天 + DateTime firstDay = daytime.AddDays(1 - daytime.Day); + // 本月第一天是周几 + int weekday = (int)firstDay.DayOfWeek == 0 ? 7 : (int)firstDay.DayOfWeek; + // 本月第一周有几天 + int firstWeekEndDay = 7 - (weekday - 1); + // 当前日期和第一周之差 + int diffday = dayInMonth - firstWeekEndDay; + diffday = diffday > 0 ? diffday : 1; + // 当前是第几周,若整除7就减一天 + return ((diffday % 7) == 0 ? (diffday / 7 - 1) : (diffday / 7)) + 1 + (dayInMonth > firstWeekEndDay ? 1 : 0); + } + + /// + /// 获取今天的时间范围 + /// + /// 返回包含开始时间和结束时间的元组 + public (DateTime Start, DateTime End) GetTodayRange() + { + var start = Date.Date; // 当天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 当天结束时间 + return (start, end); + } + + /// + /// 获取本月的时间范围 + /// + /// 返回包含开始时间和结束时间的元组 + public (DateTime Start, DateTime End) GetMonthRange() + { + return (GetFirstDayOfMonth(), GetLastDayOfMonth()); + } + + /// + /// 获取本月的第一天开始时间 + /// + /// 返回当月的第一天 + public DateTime GetFirstDayOfMonth() + { + return new DateTime(Date.Year, Date.Month, 1); + } + + /// + /// 获取本月的最后一天截至时间 + /// + /// 返回当月的最后一天 + public DateTime GetLastDayOfMonth() + { + var firstDayOfNextMonth = new DateTime(Date.Year, Date.Month, 1).AddMonths(1); + return firstDayOfNextMonth.AddSeconds(-1); + } + + /// + /// 获取今年的时间范围 + /// + public (DateTime Start, DateTime End) GetYearRange() + { + return (GetFirstDayOfYear(), GetLastDayOfYear()); + } + + /// + /// 获取今年的第一天时间范围 + /// + public DateTime GetFirstDayOfYear() + { + return new DateTime(Date.Year, 1, 1); + } + + /// + /// 获取今年的最后一天时间范围 + /// + public DateTime GetLastDayOfYear() + { + return new DateTime(Date.Year, 12, 31, 23, 59, 59); + } + + /// + /// 获取前天时间范围 + /// + public (DateTime Start, DateTime End) GetDayBeforeYesterdayRange() + { + var start = Date.Date.AddDays(-2); // 前天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 前天结束时间 + return (start, end); + } + + /// + /// 获取昨天时间范围 + /// + public (DateTime Start, DateTime End) GetYesterdayRange() + { + var start = Date.Date.AddDays(-1); // 昨天开始时间 + var end = start.AddDays(1).AddSeconds(-1); // 昨天结束时间 + return (start, end); + } + + /// + /// 获取上一周时间范围 + /// + public (DateTime Start, DateTime End) GetLastWeekRange() + { + // 计算上周的天数差 + var daysToSubtract = (int)Date.DayOfWeek + 7; // 确保周日也能正确计算 + var start = Date.Date.AddDays(-daysToSubtract); // 上周第一天 + var end = start.AddDays(7).AddSeconds(-1); // 上周最后一天 + return (start, end); + } + + /// + /// 获取本周时间范围 + /// + public (DateTime Start, DateTime End) GetThisWeekRange() + { + // 计算本周的天数差 + var daysToSubtract = (int)Date.DayOfWeek; + var start = Date.Date.AddDays(-daysToSubtract); // 本周第一天 + var end = start.AddDays(7).AddSeconds(-1); // 本周最后一天 + return (start, end); + } + + /// + /// 获取上月时间范围 + /// + public (DateTime Start, DateTime End) GetLastMonthRange() + { + var firstDayOfLastMonth = new DateTime(Date.Year, Date.Month, 1).AddMonths(-1); // 上月第一天 + var lastDayOfLastMonth = firstDayOfLastMonth.AddMonths(1).AddSeconds(-1); // 上月最后一天 + return (firstDayOfLastMonth, lastDayOfLastMonth); + } + + /// + /// 获取近3天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast3DaysRange() + { + var start = Date.Date.AddDays(-2); // 3天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近7天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast7DaysRange() + { + var start = Date.Date.AddDays(-6); // 7天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近15天的时间范围 + /// + public (DateTime Start, DateTime End) GetLast15DaysRange() + { + var start = Date.Date.AddDays(-14); // 15天前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取近3个月的时间范围 + /// + public (DateTime Start, DateTime End) GetLast3MonthsRange() + { + var start = Date.Date.AddMonths(-3); // 3个月前的开始时间 + var end = Date.Date.AddDays(1).AddSeconds(-1); // 当前日期的结束时间 + return (start, end); + } + + /// + /// 获取上半年的时间范围 + /// + public (DateTime Start, DateTime End) GetFirstHalfYearRange() + { + var start = new DateTime(Date.Year, 1, 1); // 上半年开始时间 + var end = new DateTime(Date.Year, 6, 30, 23, 59, 59); // 上半年结束时间 + return (start, end); + } + + /// + /// 获取下半年的时间范围 + /// + public (DateTime Start, DateTime End) GetSecondHalfYearRange() + { + var start = new DateTime(Date.Year, 7, 1); // 下半年开始时间 + var end = new DateTime(Date.Year, 12, 31, 23, 59, 59); // 下半年结束时间 + return (start, end); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs new file mode 100644 index 0000000..d9fa2c8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs @@ -0,0 +1,147 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OfficeOpenXml; + +namespace Admin.NET.Core; + +public class ExcelHelper +{ + /// + /// 数据导入 + /// + /// + /// + /// + public static IActionResult ImportData(IFormFile file, Action, Action, List, List>> action) where IN : BaseImportInput, new() where T : EntityBaseId, new() + { + try + { + var result = CommonUtil.ImportExcelDataAsync(file).Result ?? throw Oops.Oh("有效数据为空"); + result.ForEach(u => u.Id = YitIdHelper.NextId()); + + var tasks = new List(); + action.Invoke(result, (storageable, pageItems, rows) => + { + // 标记校验信息 + tasks.Add(Task.Run(() => + { + if (!storageable.TotalList.Any()) return; + + // 通过Id标记校验信息 + var itemMap = pageItems.ToDictionary(u => u.Id, u => u); + foreach (var item in storageable.TotalList) + { + var temp = itemMap.GetValueOrDefault(item.Item.Id); + if (temp != null) temp.Error ??= item.StorageMessage; + } + })); + }); + + // 等待所有标记验证信息任务完成 + Task.WhenAll(tasks).GetAwaiter().GetResult(); + + // 仅导出错误记录 + var errorList = result.Where(u => !string.IsNullOrWhiteSpace(u.Error)).ToList(); + if (!errorList.Any()) + return new JsonResult(AdminResultProvider.Ok("导入成功")); + return ExportData(errorList); + } + catch (Exception ex) + { + return new JsonResult(AdminResultProvider.Error(ex.Message)); + } + } + + /// + /// 导出Xlsx数据 + /// + /// + /// + /// + public static IActionResult ExportData(dynamic list, string fileName = "导入记录") + { + var exporter = new ExcelExporter(); + var fs = new MemoryStream(exporter.ExportAsByteArray(list).GetAwaiter().GetResult()); + return new XlsxFileResult(stream: fs, fileDownloadName: $"{fileName}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + } + + /// + /// 根据类型导出Xlsx模板 + /// + /// + /// + /// + /// + public static IActionResult ExportTemplate(IEnumerable list, string filename = "导入模板", Func> addListValidationFun = null) + { + using var package = new ExcelPackage((ExportData(list, filename) as XlsxFileResult)!.Stream); + var worksheet = package.Workbook.Worksheets[0]; + + // 创建一个隐藏的sheet,用于添加下拉列表 + var dropdownSheet = package.Workbook.Worksheets.Add("下拉数据"); + dropdownSheet.Hidden = eWorkSheetHidden.Hidden; + + var sysDictTypeService = App.GetService(); + foreach (var prop in typeof(T).GetProperties()) + { + var propType = prop.PropertyType; + + var headerAttr = prop.GetCustomAttribute(); + var isNullableEnum = propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>) && Nullable.GetUnderlyingType(propType).IsEnum(); + if (isNullableEnum) propType = Nullable.GetUnderlyingType(propType); + if (headerAttr == null) continue; + + // 获取列序号 + var columnIndex = 0; + foreach (var item in worksheet.Cells[1, 1, 1, worksheet.Dimension.End.Column]) + if (++columnIndex > 0 && item.Text.Equals(headerAttr.DisplayName)) break; + if (columnIndex <= 0) continue; + + // 优先从代理函数中获取下列列表,若为空且字段为枚举型,则填充枚举项为下列列表,若为字典字段,则填充字典值value列表为下列列表 + var dataList = addListValidationFun?.Invoke(worksheet, prop)?.ToList(); + if (dataList == null) + { + // 填充枚举项为下列列表 + if (propType.IsEnum()) + { + dataList = propType.EnumToList()?.Select(it => it.Describe).ToList(); + } + else + { + // 获取字段上的字典特性 + var dict = prop.GetCustomAttribute(); + if (dict != null) + { + // 填充字典值value为下列列表 + dataList = sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = dict.DictTypeCode }) + .Result?.Select(x => x.Label).ToList(); + } + } + } + + if (dataList != null) + { + // 添加下拉列表 + AddListValidation(dropdownSheet, columnIndex, dataList); + dropdownSheet.Cells[1, columnIndex, dataList.Count, columnIndex].LoadFromCollection(dataList); + } + } + + package.Save(); + package.Stream.Position = 0; + return new XlsxFileResult(stream: package.Stream, fileDownloadName: $"{filename}-{DateTime.Now:yyyy-MM-dd_HHmmss}"); + + void AddListValidation(ExcelWorksheet dropdownSheet, int columnIndex, List dataList) + { + var validation = worksheet.DataValidations.AddListValidation(worksheet.Cells[2, columnIndex, ExcelPackage.MaxRows, columnIndex].Address); + validation!.Formula.ExcelFormula = "=" + dropdownSheet.Cells[1, columnIndex, dataList.Count, columnIndex].FullAddressAbsolute; + validation.ShowErrorMessage = true; + validation.ErrorTitle = "无效输入"; + validation.Error = "请从列表中选择一个有效的选项"; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/FileHelper.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/FileHelper.cs new file mode 100644 index 0000000..2f7e921 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/FileHelper.cs @@ -0,0 +1,125 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 文件帮助类 +/// +public static class FileHelper +{ + /// + /// 尝试删除文件/目录 + /// + /// + /// + public static bool TryDelete(string path) + { + try + { + if (string.IsNullOrEmpty(path)) return false; + if (Directory.Exists(path)) Directory.Delete(path, recursive: true); + else File.Delete(path); + return true; + } + catch (Exception) + { + // ignored + return false; + } + } + + /// + /// 复制目录 + /// + /// + /// + /// + public static void CopyDirectory(string sourceDir, string destinationDir, bool overwrite = false) + { + // 检查源目录是否存在 + if (!Directory.Exists(sourceDir)) throw new DirectoryNotFoundException("Source directory not found: " + sourceDir); + + // 如果目标目录不存在,则创建它 + if (!Directory.Exists(destinationDir)) Directory.CreateDirectory(destinationDir!); + + // 获取源目录下的所有文件并复制它们 + foreach (string file in Directory.GetFiles(sourceDir)) + { + string name = Path.GetFileName(file); + string dest = Path.Combine(destinationDir, name); + File.Copy(file, dest, overwrite); + } + + // 递归复制所有子目录 + foreach (string directory in Directory.GetDirectories(sourceDir)) + { + string name = Path.GetFileName(directory); + string dest = Path.Combine(destinationDir, name); + CopyDirectory(directory, dest, overwrite); + } + } + + /// + /// 在文件倒数第lastIndex个identifier前插入内容(备份原文件) + /// + /// 文件路径 + /// 要插入的内容 + /// 标识符号 + /// 倒数第几个标识符 + /// 是否创建备份文件 + public static async Task InsertsStringAtSpecifiedLocationInFile(string filePath, string insertContent, char identifier, int lastIndex, bool createBackup = false) + { + // 参数校验 + if (lastIndex < 1) throw new ArgumentOutOfRangeException(nameof(lastIndex)); + if (identifier == 0) throw new ArgumentException("标识符不能为空字符"); + + if (!File.Exists(filePath)) + throw new FileNotFoundException("目标文件不存在", filePath); + + // 创建备份文件 + if (createBackup) + { + string backupPath = $"{filePath}.bak_{DateTime.Now:yyyyMMddHHmmss}"; + File.Copy(filePath, backupPath, true); + } + + using var reader = new StreamReader(filePath, Encoding.UTF8); + var content = await reader.ReadToEndAsync(); + reader.Close(); + // 逆向查找算法 + int index = content.LastIndexOf(identifier); + if (index == -1) + { + throw new ArgumentException($"文件中未包含{identifier}"); + } + + int resIndex = content.LastIndexOf(identifier, index - lastIndex); + if (resIndex == -1) + { + throw new ArgumentException($"文件中{identifier}不足{lastIndex}个"); + } + + StringBuilder sb = new StringBuilder(content); + sb = sb.Insert(resIndex, insertContent); + await WriteToFileAsync(filePath, sb); + } + + /// + /// 写入文件内容 + /// + /// + /// + public static async Task WriteToFileAsync(string filePath, StringBuilder sb) + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + await using var writer = new StreamWriter(filePath, false, new UTF8Encoding(false)); // 无BOM + await writer.WriteAsync(sb.ToString()); + writer.Close(); + Console.WriteLine($"文件【{filePath}】写入完成"); + Console.ResetColor(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GM.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GM.cs new file mode 100644 index 0000000..69ea734 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GM.cs @@ -0,0 +1,471 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.GM; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.X509; + +namespace Admin.NET.Core; + +/** + * + * 用BC的注意点: + * 这个版本的BC对SM3withSM2的结果为asn1格式的r和s,如果需要直接拼接的r||s需要自己转换。下面rsAsn1ToPlainByteArray、rsPlainByteArrayToAsn1就在干这事。 + * 这个版本的BC对SM2的结果为C1||C2||C3,据说为旧标准,新标准为C1||C3||C2,用新标准的需要自己转换。下面(被注释掉的)changeC1C2C3ToC1C3C2、changeC1C3C2ToC1C2C3就在干这事。java版的高版本有加上C1C3C2,csharp版没准以后也会加,但目前还没有,java版的目前可以初始化时“ SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);”。 + * + */ + +public class GM +{ + private static X9ECParameters x9ECParameters = GMNamedCurves.GetByName("sm2p256v1"); + private static ECDomainParameters ecDomainParameters = new(x9ECParameters.Curve, x9ECParameters.G, x9ECParameters.N); + + /** + * + * @param msg + * @param userId + * @param privateKey + * @return r||s,直接拼接byte数组的rs + */ + + public static byte[] SignSm3WithSm2(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + return RsAsn1ToPlainByteArray(SignSm3WithSm2Asn1Rs(msg, userId, privateKey)); + } + + /** + * @param msg + * @param userId + * @param privateKey + * @return rs in asn1 format + */ + + public static byte[] SignSm3WithSm2Asn1Rs(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(true, new ParametersWithID(privateKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + byte[] sig = signer.GenerateSignature(); + return sig; + } + + /** + * + * @param msg + * @param userId + * @param rs r||s,直接拼接byte数组的rs + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2(byte[] msg, byte[] userId, byte[] rs, AsymmetricKeyParameter publicKey) + { + if (rs == null || msg == null || userId == null) return false; + if (rs.Length != RS_LEN * 2) return false; + return VerifySm3WithSm2Asn1Rs(msg, userId, RsPlainByteArrayToAsn1(rs), publicKey); + } + + /** + * + * @param msg + * @param userId + * @param rs in asn1 format + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2Asn1Rs(byte[] msg, byte[] userId, byte[] sign, AsymmetricKeyParameter publicKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(false, new ParametersWithID(publicKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + return signer.VerifySignature(sign); + } + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * @param c1c2c3 + * @return + */ + + private static byte[] ChangeC1C2C3ToC1C3C2(byte[] c1c2c3) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c2c3.Length]; + Buffer.BlockCopy(c1c2c3, 0, result, 0, c1Len); //c1 + Buffer.BlockCopy(c1c2c3, c1c2c3.Length - c3Len, result, c1Len, c3Len); //c3 + Buffer.BlockCopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.Length - c1Len - c3Len); //c2 + return result; + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * @param c1c3c2 + * @return + */ + + private static byte[] ChangeC1C3C2ToC1C2C3(byte[] c1c3c2) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().GetDigestSize(); + byte[] result = new byte[c1c3c2.Length]; + Buffer.BlockCopy(c1c3c2, 0, result, 0, c1Len); //c1: 0->65 + Buffer.BlockCopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.Length - c1Len - c3Len); //c2 + Buffer.BlockCopy(c1c3c2, c1Len, result, c1c3c2.Length - c3Len, c3Len); //c3 + return result; + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Decrypt(byte[] data, AsymmetricKeyParameter key) + { + return Sm2DecryptOld(ChangeC1C3C2ToC1C2C3(data), key); + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Encrypt(byte[] data, AsymmetricKeyParameter key) + { + return ChangeC1C2C3ToC1C3C2(Sm2EncryptOld(data, key)); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2EncryptOld(byte[] data, AsymmetricKeyParameter pubkey) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(true, new ParametersWithRandom(pubkey, new SecureRandom())); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2DecryptOld(byte[] data, AsymmetricKeyParameter key) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(false, key); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * @param bytes + * @return + */ + + public static byte[] Sm3(byte[] bytes) + { + SM3Digest digest = new(); + digest.BlockUpdate(bytes, 0, bytes.Length); + byte[] result = DigestUtilities.DoFinal(digest); + return result; + } + + private const int RS_LEN = 32; + + private static byte[] BigIntToFixexLengthBytes(BigInteger rOrS) + { + // for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // r and s are the result of mod n, so they should be less than n and have length<=32 + byte[] rs = rOrS.ToByteArray(); + if (rs.Length == RS_LEN) return rs; + else if (rs.Length == RS_LEN + 1 && rs[0] == 0) return Arrays.CopyOfRange(rs, 1, RS_LEN + 1); + else if (rs.Length < RS_LEN) + { + byte[] result = new byte[RS_LEN]; + Arrays.Fill(result, (byte)0); + Buffer.BlockCopy(rs, 0, result, RS_LEN - rs.Length, rs.Length); + return result; + } + else + { + throw new ArgumentException("err rs: " + Hex.ToHexString(rs)); + } + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + */ + + private static byte[] RsAsn1ToPlainByteArray(byte[] rsDer) + { + Asn1Sequence seq = Asn1Sequence.GetInstance(rsDer); + byte[] r = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[0]).Value); + byte[] s = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[1]).Value); + byte[] result = new byte[RS_LEN * 2]; + Buffer.BlockCopy(r, 0, result, 0, r.Length); + Buffer.BlockCopy(s, 0, result, RS_LEN, s.Length); + return result; + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式 + * @param sign in plain byte array + * @return rs result in asn1 format + */ + + private static byte[] RsPlainByteArrayToAsn1(byte[] sign) + { + if (sign.Length != RS_LEN * 2) throw new ArgumentException("err rs. "); + BigInteger r = new BigInteger(1, Arrays.CopyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.CopyOfRange(sign, RS_LEN, RS_LEN * 2)); + Asn1EncodableVector v = new Asn1EncodableVector + { + new DerInteger(r), + new DerInteger(s) + }; + + return new DerSequence(v).GetEncoded("DER"); + } + + // 生成公私匙对 + public static AsymmetricCipherKeyPair GenerateKeyPair() + { + ECKeyPairGenerator kpGen = new(); + kpGen.Init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom())); + return kpGen.GenerateKeyPair(); + } + + public static ECPrivateKeyParameters GetPrivatekeyFromD(BigInteger d) + { + return new ECPrivateKeyParameters(d, ecDomainParameters); + } + + public static ECPublicKeyParameters GetPublickeyFromXY(BigInteger x, BigInteger y) + { + return new ECPublicKeyParameters(x9ECParameters.Curve.CreatePoint(x, y), ecDomainParameters); + } + + public static AsymmetricKeyParameter GetPublickeyFromX509File(FileInfo file) + { + FileStream fileStream = null; + try + { + //file.DirectoryName + "\\" + file.Name + fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read); + X509Certificate certificate = new X509CertificateParser().ReadCertificate(fileStream); + return certificate.GetPublicKey(); + } + catch (Exception) + { + //log.Error(file.Name + "读取失败,异常:" + e); + } + finally + { + if (fileStream != null) + fileStream.Close(); + } + return null; + } + + public class Sm2Cert + { + public AsymmetricKeyParameter privateKey; + public AsymmetricKeyParameter publicKey; + public string certId; + } + + private static byte[] ToByteArray(int i) + { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte)(i >> 24); + byteArray[1] = (byte)((i & 0xFFFFFF) >> 16); + byteArray[2] = (byte)((i & 0xFFFF) >> 8); + byteArray[3] = (byte)(i & 0xFF); + return byteArray; + } + + /** + * 字节数组拼接 + * + * @param params + * @return + */ + + private static byte[] Join(params byte[][] byteArrays) + { + List byteSource = new(); + for (int i = 0; i < byteArrays.Length; i++) + { + byteSource.AddRange(byteArrays[i]); + } + byte[] data = byteSource.ToArray(); + return data; + } + + /** + * 密钥派生函数 + * + * @param Z + * @param klen + * 生成klen字节数长度的密钥 + * @return + */ + + private static byte[] KDF(byte[] Z, int klen) + { + int ct = 1; + int end = (int)Math.Ceiling(klen * 1.0 / 32); + List byteSource = new(); + + for (int i = 1; i < end; i++) + { + byteSource.AddRange(Sm3(Join(Z, ToByteArray(ct)))); + ct++; + } + byte[] last = Sm3(Join(Z, ToByteArray(ct))); + if (klen % 32 == 0) + { + byteSource.AddRange(last); + } + else + byteSource.AddRange(Arrays.CopyOfRange(last, 0, klen % 32)); + return byteSource.ToArray(); + } + + public static byte[] Sm4DecryptCBC(byte[] keyBytes, byte[] cipher, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(false, new ParametersWithIV(key, iv)); + return c.DoFinal(cipher); + } + + public static byte[] Sm4EncryptCBC(byte[] keyBytes, byte[] plain, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(true, new ParametersWithIV(key, iv)); + return c.DoFinal(plain); + } + + public static byte[] Sm4EncryptECB(byte[] keyBytes, byte[] plain, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + //NoPadding 的情况下需要校验数据长度是16的倍数. + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(true, key); + return c.DoFinal(plain); + } + + public static byte[] Sm4DecryptECB(byte[] keyBytes, byte[] cipher, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(false, key); + return c.DoFinal(cipher); + } + + public const string SM4_ECB_NOPADDING = "SM4/ECB/NoPadding"; + public const string SM4_CBC_NOPADDING = "SM4/CBC/NoPadding"; + public const string SM4_ECB_PKCS7PADDING = "SM4/ECB/PKCS7Padding"; + public const string SM4_CBC_PKCS7PADDING = "SM4/CBC/PKCS7Padding"; + + /** + * cfca官网CSP沙箱导出的sm2文件 + * @param pem 二进制原文 + * @param pwd 密码 + * @return + */ + + public static Sm2Cert ReadSm2File(byte[] pem, string pwd) + { + Sm2Cert sm2Cert = new(); + + Asn1Sequence asn1Sequence = (Asn1Sequence)Asn1Object.FromByteArray(pem); + // ASN1Integer asn1Integer = (ASN1Integer) asn1Sequence.getObjectAt(0); //version=1 + Asn1Sequence priSeq = (Asn1Sequence)asn1Sequence[1];//private key + Asn1Sequence pubSeq = (Asn1Sequence)asn1Sequence[2];//public key and x509 cert + + // ASN1ObjectIdentifier sm2DataOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(0); + // ASN1ObjectIdentifier sm4AlgOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(1); + Asn1OctetString priKeyAsn1 = (Asn1OctetString)priSeq[2]; + byte[] key = KDF(System.Text.Encoding.UTF8.GetBytes(pwd), 32); + byte[] priKeyD = Sm4DecryptCBC(Arrays.CopyOfRange(key, 16, 32), + priKeyAsn1.GetOctets(), + Arrays.CopyOfRange(key, 0, 16), SM4_CBC_PKCS7PADDING); + sm2Cert.privateKey = GetPrivatekeyFromD(new BigInteger(1, priKeyD)); + // log.Info(Hex.toHexString(priKeyD)); + + // ASN1ObjectIdentifier sm2DataOidPub = (ASN1ObjectIdentifier) pubSeq.getObjectAt(0); + Asn1OctetString pubKeyX509 = (Asn1OctetString)pubSeq[1]; + X509Certificate x509 = new X509CertificateParser().ReadCertificate(pubKeyX509.GetOctets()); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + /** + * + * @param cert + * @return + */ + + public static Sm2Cert ReadSm2X509Cert(byte[] cert) + { + Sm2Cert sm2Cert = new(); + + X509Certificate x509 = new X509CertificateParser().ReadCertificate(cert); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + public static byte[] ZeroIv(string algo) + { + IBufferedCipher cipher = CipherUtilities.GetCipher(algo); + int blockSize = cipher.GetBlockSize(); + byte[] iv = new byte[blockSize]; + Arrays.Fill(iv, (byte)0); + return iv; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GMUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GMUtil.cs new file mode 100644 index 0000000..d92853a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GM/GMUtil.cs @@ -0,0 +1,151 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; + +namespace Admin.NET.Core; + +/// +/// GM工具类 +/// +public class GMUtil +{ + /// + /// SM2加密 + /// + /// + /// + /// + public static string SM2Encrypt(string publicKeyHex, string data_string) + { + // 如果是130位公钥,.NET使用的话,把开头的04截取掉 + if (publicKeyHex.Length == 130) + { + publicKeyHex = publicKeyHex.Substring(2, 128); + } + // 公钥X,前64位 + string x = publicKeyHex.Substring(0, 64); + // 公钥Y,后64位 + string y = publicKeyHex.Substring(64); + // 获取公钥对象 + AsymmetricKeyParameter publicKey1 = GM.GetPublickeyFromXY(new BigInteger(x, 16), new BigInteger(y, 16)); + // Sm2Encrypt: C1C3C2 + // Sm2EncryptOld: C1C2C3 + byte[] digestByte = GM.Sm2Encrypt(Encoding.UTF8.GetBytes(data_string), publicKey1); + string strSM2 = Hex.ToHexString(digestByte); + return strSM2; + } + + /// + /// SM2解密 + /// + /// + /// + /// + public static string SM2Decrypt(string privateKey_string, string encryptedData_string) + { + if (!encryptedData_string.StartsWith("04")) + encryptedData_string = "04" + encryptedData_string; + BigInteger d = new(privateKey_string, 16); + // 先拿到私钥对象,用ECPrivateKeyParameters 或 AsymmetricKeyParameter 都可以 + // ECPrivateKeyParameters bcecPrivateKey = GmUtil.GetPrivatekeyFromD(d); + AsymmetricKeyParameter bcecPrivateKey = GM.GetPrivatekeyFromD(d); + byte[] byToDecrypt = Hex.Decode(encryptedData_string); + byte[] byDecrypted = GM.Sm2Decrypt(byToDecrypt, bcecPrivateKey); + string strDecrypted = Encoding.UTF8.GetString(byDecrypted); + return strDecrypted; + } + + /// + /// SM4加密(ECB) + /// + /// + /// + /// + public static string SM4EncryptECB(string key_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4EncryptECB(key, Encoding.UTF8.GetBytes(plainText), GM.SM4_ECB_PKCS7PADDING);//NoPadding 的情况下需要校验数据长度是16的倍数. 使用 HandleSm4Padding 处理 + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + /// + public static string SM4DecryptECB(string key_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4DecryptECB(key, Hex.Decode(cipherText), GM.SM4_ECB_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + /// + /// + public static string SM4EncryptCBC(string key_string, string iv_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4EncryptCBC(key, Encoding.UTF8.GetBytes(plainText), iv, GM.SM4_CBC_PKCS7PADDING); + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + /// + /// + public static string SM4DecryptCBC(string key_string, string iv_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4DecryptCBC(key, Hex.Decode(cipherText), iv, GM.SM4_CBC_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// 补足 16 进制字符串的 0 字符,返回不带 0x 的16进制字符串 + /// + /// + /// 1表示加密,0表示解密 + /// + private static byte[] HandleSm4Padding(byte[] input, int mode) + { + if (input == null) + { + return null; + } + byte[] ret = (byte[])null; + if (mode == 1) + { + int p = 16 - input.Length % 16; + ret = new byte[input.Length + p]; + Array.Copy(input, 0, ret, 0, input.Length); + for (int i = 0; i < p; i++) + { + ret[input.Length + i] = (byte)p; + } + } + else + { + int p = input[input.Length - 1]; + ret = new byte[input.Length - p]; + Array.Copy(input, 0, ret, 0, input.Length - p); + } + return ret; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GiteeHelper.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GiteeHelper.cs new file mode 100644 index 0000000..a4b81a1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/GiteeHelper.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Gitee接口帮助类 +/// +public class GiteeHelper +{ + private const string BaseUrl = "https://gitee.com/api/v5/repos/"; + private static readonly HttpClient Client = new(); + + /// + /// 下载仓库 zip + /// + /// https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoZipball + /// + public static async Task DownloadRepoZip(string owner, string repo, string accessToken = null, string @ref = null) + { + if (string.IsNullOrWhiteSpace(owner)) throw Oops.Bah($"参数 {nameof(owner)} 不能为空"); + if (string.IsNullOrWhiteSpace(repo)) throw Oops.Bah($"参数 {nameof(repo)} 不能为空"); + var query = BuilderQueryString(new + { + access_token = accessToken, + @ref + }); + return await Client.GetStreamAsync($"{BaseUrl}{owner}/{repo}/zipball?{query}"); + } + + /// + /// 构建Query参数 + /// + /// + private static string BuilderQueryString([System.Diagnostics.CodeAnalysis.NotNull] object obj) + { + if (obj == null) return string.Empty; + var query = HttpUtility.ParseQueryString(string.Empty); + foreach (var prop in obj.GetType().GetProperties()) + { + var val = prop.GetValue(obj); + if (val == null) continue; + + // 以元组形式校验参数集 + var name = prop.Name.Trim('@'); + if (val is Tuple { Item1: not null } tuple) + { + if (!tuple.Item2.Split(",").Any(x => x.Trim().Equals(tuple.Item1))) throw Oops.Oh($"参数 {name} 的值只能为:{tuple.Item2}"); + query[name] = tuple.Item1.ToString(); + continue; + } + query[name] = val.ToString(); + } + return query.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/MiniExcelUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/MiniExcelUtil.cs new file mode 100644 index 0000000..d6a9cd6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/MiniExcelUtil.cs @@ -0,0 +1,70 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using MiniExcelLibs; + +namespace Admin.NET.Core; + +public static class MiniExcelUtil +{ + private const string SheetName = "ImportTemplate"; + private const string DirectoryName = "export"; + + /// + /// 导出模板Excel + /// + /// + public static async Task ExportExcelTemplate(string fileName = null) where T : class, new() + { + var values = Array.Empty(); + // 在内存中当开辟空间 + var memoryStream = new MemoryStream(); + // 将数据写到内存当中 + await memoryStream.SaveAsAsync(values, sheetName: SheetName); + // 从0的位置开始写入 + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = $"{(string.IsNullOrEmpty(fileName) ? typeof(T).Name : fileName)}.xlsx" + }; + } + + /// + /// 获取导入数据Excel + /// + /// + /// + public static async Task> GetImportExcelData([Required] IFormFile file) where T : class, new() + { + using MemoryStream stream = new MemoryStream(); + await file.CopyToAsync(stream); + var res = await stream.QueryAsync(sheetName: SheetName); + return res.ToArray(); + } + + /// + /// 获取导出数据excel地址 + /// + /// + public static async Task GetExportDataExcelUrl(IEnumerable exportData) where T : class, new() + { + var fileName = string.Format("{0}.xlsx", YitIdHelper.NextId()); + try + { + var path = Path.Combine(App.WebHostEnvironment.WebRootPath, DirectoryName); + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + var filePath = Path.Combine(path, fileName); + await MiniExcel.SaveAsAsync(filePath, exportData, overwriteFile: true); + } + catch (Exception error) + { + throw Oops.Oh("出现错误:" + error); + } + var host = CommonUtil.GetLocalhost(); + return $"{host}/{DirectoryName}/{fileName}"; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs new file mode 100644 index 0000000..568ffbd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/NewtonsoftJsonSerializerProvider.cs @@ -0,0 +1,59 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Newtonsoft.Json; + +namespace Admin.NET.Core; + +/// +/// 自定义序列化提供器 Newtonsoft.Json 实现 +/// +public class NewtonsoftJsonSerializerProvider : IJsonSerializerProvider, ISingleton +{ + /// + /// 序列化对象 + /// + /// + /// + /// + public string Serialize(object value, object jsonSerializerOptions = null) + { + return JsonConvert.SerializeObject(value, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public T Deserialize(string json, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 反序列化字符串 + /// + /// + /// + /// + /// + public object Deserialize(string json, Type returnType, object jsonSerializerOptions = null) + { + return JsonConvert.DeserializeObject(json, returnType, (jsonSerializerOptions ?? GetSerializerOptions()) as JsonSerializerSettings); + } + + /// + /// 返回读取全局配置的 JSON 选项 + /// + /// + public object GetSerializerOptions() + { + return App.GetOptions()?.SerializerSettings; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/PathTreeBuilder.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/PathTreeBuilder.cs new file mode 100644 index 0000000..d93415a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/PathTreeBuilder.cs @@ -0,0 +1,57 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 树形节点 +/// +public class TreeNode +{ + public int Id { get; set; } + public int Pid { get; set; } + public string Name { get; set; } + public List Children { get; set; } = new(); +} + +/// +/// 根据路径数组生成树结构 +/// +public class PathTreeBuilder +{ + private int _nextId = 1; + + public TreeNode BuildTree(List paths) + { + var root = new TreeNode { Id = 1, Pid = 0, Name = "文件目录" }; // 根节点 + var dict = new Dictionary(); + + foreach (var path in paths) + { + var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + TreeNode currentNode = root; + + foreach (var part in parts) + { + var key = currentNode.Id + "_" + part; // 生成唯一键 + if (!dict.ContainsKey(key)) + { + var newNode = new TreeNode + { + Id = _nextId++, + Pid = currentNode.Id, + Name = part + }; + currentNode.Children.Add(newNode); + dict[key] = newNode; + } + currentNode = dict[key]; // 更新当前节点 + } + } + + return root; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ReflectionUtil.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ReflectionUtil.cs new file mode 100644 index 0000000..0797a1d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/ReflectionUtil.cs @@ -0,0 +1,28 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 反射工具类 +/// +public static class ReflectionUtil +{ + /// + /// 获取字段特性 + /// + /// + /// + /// + public static T GetDescriptionValue(this FieldInfo field) where T : Attribute + { + // 获取字段的指定特性,不包含继承中的特性 + object[] customAttributes = field.GetCustomAttributes(typeof(T), false); + + // 如果没有数据返回null + return customAttributes.Length > 0 ? (T)customAttributes[0] : null; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/RegularValidate.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/RegularValidate.cs new file mode 100644 index 0000000..b7c8bfe --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/RegularValidate.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 正则校验 +/// +public static class RegularValidate +{ + /// + /// 验证密码规则 + /// + /// + /// + public static bool ValidatePassword(string password) + { + var regex = new Regex(@" +(?=.*[0-9]) #必须包含数字 +(?=.*[a-z]) #必须包含小写 +(?=.*[A-Z]) #必须包含大写 +(?=([\x21-\x7e]+)[^a-zA-Z0-9]) #必须包含特殊符号 +.{8,30} #至少8个字符,最多30个字符 +", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); + + //如果要求必须包含小写、大写字母,则上面的(?=.*[a-zA-Z]) 要改为: + /* + * (?=.*[a-z]) + * (?=.*[A-Z]) + */ + return regex.IsMatch(password); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SSHHelper.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SSHHelper.cs new file mode 100644 index 0000000..8205043 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SSHHelper.cs @@ -0,0 +1,212 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Renci.SshNet; + +namespace Admin.NET.Core +{ + /// + /// SSH/Sftp 工具类 + /// + public class SSHHelper : IDisposable + { + private readonly SftpClient _sftp; + + public SSHHelper(string host, int port, string user, string password) + { + _sftp = new SftpClient(host, port, user, password); + } + + /// + /// 连接 + /// + private void Connect() + { + if (!_sftp.IsConnected) + _sftp.Connect(); + } + + /// + /// 是否存在同名文件 + /// + /// + /// + public bool Exists(string ftpFileName) + { + Connect(); + + return _sftp.Exists(ftpFileName); + } + + /// + /// 删除文件 + /// + /// + public void DeleteFile(string ftpFileName) + { + Connect(); + + _sftp.DeleteFile(ftpFileName); + } + + /// + /// 下载到指定目录 + /// + /// + /// + public void DownloadFile(string ftpFileName, string localFileName) + { + Connect(); + + using (Stream fileStream = File.OpenWrite(localFileName)) + { + _sftp.DownloadFile(ftpFileName, fileStream); + } + } + + /// + /// 读取字节 + /// + /// + /// + public byte[] ReadAllBytes(string ftpFileName) + { + Connect(); + + return _sftp.ReadAllBytes(ftpFileName); + } + + /// + /// 读取流 + /// + /// + /// + public Stream OpenRead(string path) + { + return _sftp.Open(path, FileMode.Open, FileAccess.Read); + } + + /// + /// 继续下载 + /// + /// + /// + public void DownloadFileWithResume(string ftpFileName, string localFileName) + { + DownloadFile(ftpFileName, localFileName); + } + + /// + /// 重命名 + /// + /// + /// + public void RenameFile(string oldPath, string newPath) + { + _sftp.RenameFile(oldPath, newPath); + } + + /// + /// 指定目录下文件 + /// + /// + /// + /// + public List GetFileList(string folder, IEnumerable filters) + { + Connect(); + + var files = new List(); + var sftpFiles = _sftp.ListDirectory(folder); + foreach (var file in sftpFiles) + { + if (file.IsRegularFile && filters.Any(f => file.Name.EndsWith(f))) + files.Add(file.Name); + } + return files; + } + + /// + /// 上传指定目录文件 + /// + /// + /// + public void UploadFile(string localFileName, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + using (var fileStream = new FileStream(localFileName, FileMode.Open)) + { + _sftp.UploadFile(fileStream, ftpFileName); + } + } + + /// + /// 上传字节 + /// + /// + /// + public void UploadFile(byte[] bs, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + _sftp.WriteAllBytes(ftpFileName, bs); + } + + /// + /// 上传流 + /// + /// + /// + public void UploadFile(Stream fileStream, string ftpFileName) + { + Connect(); + + var dir = Path.GetDirectoryName(ftpFileName); + CreateDir(_sftp, dir); + _sftp.UploadFile(fileStream, ftpFileName); + fileStream.Dispose(); + } + + /// + /// 创建目录 + /// + /// + /// + /// + private void CreateDir(SftpClient sftp, string dir) + { + ArgumentNullException.ThrowIfNull(dir); + + if (sftp.Exists(dir)) return; + + var index = dir.LastIndexOfAny(new char[] { '/', '\\' }); + if (index > 0) + { + var p = dir[..index]; + if (!sftp.Exists(p)) + CreateDir(sftp, p); + sftp.CreateDirectory(dir); + } + } + + /// + /// 释放对象 + /// + public void Dispose() + { + if (_sftp == null) return; + + if (_sftp.IsConnected) + _sftp.Disconnect(); + _sftp.Dispose(); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SafeMath.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SafeMath.cs new file mode 100644 index 0000000..a0eb5f4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/SafeMath.cs @@ -0,0 +1,133 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Globalization; + +namespace Admin.NET.Core; + +using System; + +/// +/// 安全的基本数学运算方法类 +/// +public static class SafeMath +{ + /// + /// 安全加法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + /// + public static T Add(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a + b, precision, defaultValue, throwOnError); + } + + /// + /// 安全减法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + public static T Sub(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a - b, precision, defaultValue, throwOnError); + } + + /// + /// 安全乘法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出异常 + public static T Mult(object left, object right, int precision = 2, T defaultValue = default, bool throwOnError = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => a * b, precision, defaultValue, throwOnError); + } + + /// + /// 安全除法 + /// + /// 左操作数 + /// 右操作数 + /// 保留小数位数 + /// 默认值 + /// 是否抛出除以零异常 + public static T Div(object left, object right, int precision = 2, T defaultValue = default, bool throwOnDivideByZero = true) where T : struct, IComparable, IConvertible, IFormattable + { + return PerformOperation(left, right, (a, b) => + { + if (b != 0) return a / b; + if (throwOnDivideByZero) throw new DivideByZeroException("除数不能为0"); + return SafeConvert(defaultValue); + }, precision, defaultValue, throwOnDivideByZero); + } + + /// + /// 安全类型转换 + /// + /// 数据源 + /// 默认值 + public static T SafeConvert(object value, T defaultValue = default) where T : struct, IComparable, IConvertible, IFormattable + { + if (value == null) return defaultValue; + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch + { + return defaultValue; + } + } + + /// + /// 执行数学运算 + /// + private static T PerformOperation(object left, object right, Func operation, int precision, T defaultValue, bool throwOnError) where T : struct, IComparable, IConvertible, IFormattable + { + try + { + decimal leftValue = ConvertToDecimal(left); + decimal rightValue = ConvertToDecimal(right); + + decimal result = operation(leftValue, rightValue); + return SafeConvert(Math.Round(result, precision, MidpointRounding.AwayFromZero), defaultValue); + } + catch + { + if (throwOnError) throw; + return defaultValue; + } + } + + /// + /// 将输入值转换为 decimal + /// + public static decimal ConvertToDecimal(object value) + { + return value switch + { + null => 0m, + int intValue => intValue, + float floatValue => (decimal)floatValue, + double doubleValue => (decimal)doubleValue, + decimal decimalValue => decimalValue, + long longValue => longValue, + short shortValue => shortValue, + byte byteValue => byteValue, + string stringValue when decimal.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal parsedValue) => parsedValue, // 尝试解析字符串 + _ => throw new InvalidCastException($"不支持的类型: {value.GetType().Name}") + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs new file mode 100644 index 0000000..53a5bab --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TenantHeaderOperationFilter.cs @@ -0,0 +1,28 @@ +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Admin.NET.Core; + +/// +/// 租户头部参数过滤器 +/// +public class TenantHeaderOperationFilter : IOperationFilter +{ + /// + /// 应用租户头部参数过滤器 + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + operation.Parameters ??= []; + + operation.Parameters.Add(new OpenApiParameter + { + Name = ClaimConst.TenantId, + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = JsonSchemaType.String }, + Required = false, + AllowEmptyValue = true, + Description = "租户ID(留空表示默认租户)" + }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TripleDES.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TripleDES.cs new file mode 100644 index 0000000..fedec3c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/TripleDES.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Security.Cryptography; + +namespace Admin.NET.Core; + +/// +/// 3DES文件加解密 +/// +public static class TripleDES +{ + /// + /// 加密文件 + /// + /// 待加密文件路径 + /// 加密后的文件路径 + /// 密码 (24位长度) + [Obsolete] + public static void EncryptFile(string inputFile, string outputFile, string password) + { + using var ties = new TripleDESCryptoServiceProvider(); + ties.Mode = CipherMode.ECB; + ties.Padding = PaddingMode.PKCS7; + ties.Key = Encoding.UTF8.GetBytes(password); + using var inputFileStream = new FileStream(inputFile, FileMode.Open); + using var encryptedFileStream = new FileStream(outputFile, FileMode.Create); + using var cryptoStream = new CryptoStream(encryptedFileStream, ties.CreateEncryptor(), CryptoStreamMode.Write); + inputFileStream.CopyTo(cryptoStream); + } + + /// + /// 加密文件 + /// + /// 加密的文件路径 + /// 解密后的文件路径 + /// 密码 (24位长度) + [Obsolete] + public static void DecryptFile(string inputFile, string outputFile, string password) + { + using var ties = new TripleDESCryptoServiceProvider(); + ties.Mode = CipherMode.ECB; + ties.Padding = PaddingMode.PKCS7; + ties.Key = Encoding.UTF8.GetBytes(password); + using var encryptedFileStream = new FileStream(inputFile, FileMode.Open); + using var decryptedFileStream = new FileStream(outputFile, FileMode.Create); + using var cryptoStream = new CryptoStream(encryptedFileStream, ties.CreateDecryptor(), CryptoStreamMode.Read); + cryptoStream.CopyTo(decryptedFileStream); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/VerifyFileExtensionName.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/VerifyFileExtensionName.cs new file mode 100644 index 0000000..2c8e8be --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/VerifyFileExtensionName.cs @@ -0,0 +1,163 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// 验证文件类型 +/// +public static class VerifyFileExtensionName +{ + private static readonly IDictionary DicsExt = new Dictionary(); + private static readonly IDictionary> ExtDics = new Dictionary>(); + + static VerifyFileExtensionName() + { + DicsExt.Add("FFD8FFE0", ".jpg"); + DicsExt.Add("FFD8FFE1", ".jpg"); + DicsExt.Add("89504E47", ".png"); + DicsExt.Add("47494638", ".gif"); + DicsExt.Add("49492A00", ".tif"); + DicsExt.Add("424D", ".bmp"); + + // PS和CAD + DicsExt.Add("38425053", ".psd"); + DicsExt.Add("41433130", ".dwg"); // CAD + DicsExt.Add("252150532D41646F6265", ".ps"); + + // 办公文档类 + DicsExt.Add("D0CF11E0", ".ppt,.doc,.xls"); // ppt、doc、xls + DicsExt.Add("504B0304", ".pptx,.docx,.xlsx"); // pptx、docx、xlsx + + /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-START */ + DicsExt.Add("0D0A0D0A", ".txt"); // txt + DicsExt.Add("0D0A2D2D", ".txt"); // txt + DicsExt.Add("0D0AB4B4", ".txt"); // txt + DicsExt.Add("B4B4BDA8", ".txt"); // 文件头部为汉字 + DicsExt.Add("73646673", ".txt"); // txt,文件头部为英文字母 + DicsExt.Add("32323232", ".txt"); // txt,文件头部内容为数字 + DicsExt.Add("0D0A09B4", ".txt"); // txt,文件头部内容为数字 + DicsExt.Add("3132330D", ".txt"); // txt,文件头部内容为数字 + /* 注意由于文本文档录入内容过多,则读取文件头时较为多变-END */ + + DicsExt.Add("7B5C727466", ".rtf"); // 日记本 + + DicsExt.Add("255044462D312E", ".pdf"); + + // 视频或音频类 + DicsExt.Add("3026B275", ".wma"); + DicsExt.Add("57415645", ".wav"); + DicsExt.Add("41564920", ".avi"); + DicsExt.Add("4D546864", ".mid"); + DicsExt.Add("2E524D46", ".rm"); + DicsExt.Add("000001BA", ".mpg"); + DicsExt.Add("000001B3", ".mpg"); + DicsExt.Add("6D6F6F76", ".mov"); + DicsExt.Add("3026B2758E66CF11", ".asf"); + + // 压缩包 + DicsExt.Add("52617221", ".rar"); + DicsExt.Add("504B03040A000000", ".zip"); + DicsExt.Add("504B030414000000", ".zip"); + DicsExt.Add("1F8B08", ".gz"); + + // 程序文件 + DicsExt.Add("3C3F786D6C", ".xml"); + DicsExt.Add("68746D6C3E", ".html"); + DicsExt.Add("04034b50", ".apk"); + //dics_ext.Add("7061636B", ".java"); + //dics_ext.Add("3C254020", ".jsp"); + //dics_ext.Add("4D5A9000", ".exe"); + + DicsExt.Add("44656C69766572792D646174653A", ".eml"); // 邮件 + DicsExt.Add("5374616E64617264204A", ".mdb"); // Access数据库文件 + + DicsExt.Add("46726F6D", ".mht"); + DicsExt.Add("4D494D45", ".mhtml"); + + foreach (var dics in DicsExt) + { + foreach (var ext in dics.Value.Split(",")) + { + if (!ExtDics.ContainsKey(ext)) + ExtDics.Add(ext, new HashSet { dics.Key.Length / 2 }); + else + ExtDics[ext].Add(dics.Key.Length / 2); + } + } + } + + /// + /// 文件格式和文件内容格式是否一致 + /// + /// + /// + /// + public static bool IsSameType(Stream stream, string suffix = ".jpg") + { + if (stream == null) + return false; + + suffix = suffix.ToLower(); + if (!ExtDics.TryGetValue(suffix, out HashSet dic)) return false; + + try + { + foreach (var len in dic) + { + byte[] b = new byte[len]; + stream.ReadExactly(b); + // string fileType = System.Text.Encoding.UTF8.GetString(b); + string fileKey = GetFileHeader(b); + if (DicsExt.ContainsKey(fileKey)) + return true; + } + } + catch (IOException) + { + } + return false; + } + + /** + * 根据文件转换成的字节数组获取文件头信息 + * @param 文件路径 + * @return 文件头信息 + */ + + private static string GetFileHeader(byte[] b) + { + string value = BytesToHexString(b); + return value; + } + + /** + * 将要读取文件头信息的文件的byte数组转换成string类型表示 + * 下面这段代码就是用来对文件类型作验证的方法, + * 将字节数组的前四位转换成16进制字符串,并且转换的时候,要先和0xFF做一次与运算。 + * 这是因为,整个文件流的字节数组中,有很多是负数,进行了与运算后,可以将前面的符号位都去掉, + * 这样转换成的16进制字符串最多保留两位,如果是正数又小于10,那么转换后只有一位, + * 需要在前面补0,这样做的目的是方便比较,取完前四位这个循环就可以终止了 + * @param src要读取文件头信息的文件的byte数组 + * @return 文件头信息 + */ + + private static string BytesToHexString(byte[] src) + { + var builder = new StringBuilder(); + if (src == null || src.Length <= 0) + return null; + + for (int i = 0; i < src.Length; i++) + { + // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写 + string hVal = Convert.ToString(src[i] & 0xFF, 16).ToUpper(); + if (hVal.Length < 2) builder.Append(0); + builder.Append(hVal); + } + return builder.ToString(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs new file mode 100644 index 0000000..75b5ef1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Core/Utils/XlsxFileResult.cs @@ -0,0 +1,99 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Core; + +/// +/// Excel文件ActionResult +/// +/// +public class XlsxFileResult : XlsxFileResultBase where T : class, new() +{ + public string FileDownloadName { get; } + public ICollection Data { get; } + + /// + /// + /// + /// + /// + public XlsxFileResult(ICollection data, string fileDownloadName = null) + { + FileDownloadName = fileDownloadName; + Data = data; + } + + public override async Task ExecuteResultAsync(ActionContext context) + { + var exporter = new ExcelExporter(); + var bytes = await exporter.ExportAsByteArray(Data); + var fs = new MemoryStream(bytes); + await DownloadExcelFileAsync(context, fs, FileDownloadName); + } +} + +/// +/// +/// +public class XlsxFileResult : XlsxFileResultBase +{ + /// + /// + /// + /// + /// + public XlsxFileResult(Stream stream, string fileDownloadName = null) + { + Stream = stream; + FileDownloadName = fileDownloadName; + } + + /// + /// + /// + /// + /// + + public XlsxFileResult(byte[] bytes, string fileDownloadName = null) + { + Stream = new MemoryStream(bytes); + FileDownloadName = fileDownloadName; + } + + public Stream Stream { get; protected set; } + public string FileDownloadName { get; protected set; } + + public override async Task ExecuteResultAsync(ActionContext context) + { + await DownloadExcelFileAsync(context, Stream, FileDownloadName); + } +} + +/// +/// 基类 +/// +public class XlsxFileResultBase : ActionResult +{ + /// + /// 下载Excel文件 + /// + /// + /// + /// + /// + protected virtual async Task DownloadExcelFileAsync(ActionContext context, Stream stream, string downloadFileName) + { + var response = context.HttpContext.Response; + response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + downloadFileName ??= Guid.NewGuid().ToString("N") + ".xlsx"; + + if (string.IsNullOrEmpty(Path.GetExtension(downloadFileName))) downloadFileName += ".xlsx"; + + context.HttpContext.Response.Headers.Append("Content-Disposition", new[] { "attachment; filename=" + HttpUtility.UrlEncode(downloadFileName) }); + await stream.CopyToAsync(context.HttpContext.Response.Body); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj new file mode 100644 index 0000000..c038bff --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj @@ -0,0 +1,26 @@ + + + net8.0;net10.0 + 1701;1702;1591;8632 + + enable + true + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Test/BaseTest.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Test/BaseTest.cs new file mode 100644 index 0000000..8314822 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Test/BaseTest.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OpenQA.Selenium; +using OpenQA.Selenium.Edge; +using OpenQA.Selenium.Support.UI; + +namespace Admin.NET.Test; + +/// +/// 测试基类 +/// +public class BaseTest : IDisposable +{ + private readonly string _baseUrl = "http://localhost:8888"; + protected readonly EdgeDriver Driver = new(); + + protected BaseTest(string token = null) + { + var url = _baseUrl; + if (!string.IsNullOrWhiteSpace(token)) url += $"/#/login?token={token}"; + Driver.Manage().Window.Maximize(); + Driver.Navigate().GoToUrl(url); + + // 隐式等待3秒(隐式等待是元素未被呈现出来,才会等待) + Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); + } + + /// + /// 等待网页加载完成 + /// + protected async Task WaitExecutorCompleteAsync() + { + var wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(30)); + wait.Until(driver => ((IJavaScriptExecutor)driver).ExecuteScript("return document.readyState").Equals("complete")); + await Task.Delay(1000); + } + + /// + /// 用户登录 + /// + /// + /// + protected async Task Login(string account = "superadmin", string password = "123456") + { + await GoToUrlAsync("/#/login"); + var inputList = Driver.FindElements(By.CssSelector("#pane-account input")); + + // 输入用户名 + var accountInput = inputList.First(); + accountInput.Clear(); + accountInput.SendKeys(account); + + // 输入密码 + var passwordInput = inputList.Skip(1).First(); + passwordInput.Clear(); + passwordInput.SendKeys(password); + + // 输入验证码 + var captchaInput = inputList.Skip(2).First(); + captchaInput.Clear(); + captchaInput.SendKeys("0"); + + // 提交 + var button = Driver.FindElement(By.CssSelector("#pane-account button")); + button.Click(); + } + + /// + /// 打开指定页面 + /// + /// + protected async Task GoToUrlAsync(string url) + { + if (url.StartsWith("http")) await Driver.Navigate().GoToUrlAsync(url); + else await Driver.Navigate().GoToUrlAsync(_baseUrl + "/" + url.TrimStart('/')); + await WaitExecutorCompleteAsync(); + } + + /// + /// 等待用户按回车键继续 + /// + /// 提示词 + protected void WaitEnter(string text = "等待用户按回车键继续...") + { + Console.WriteLine(text); + Console.ReadLine(); + } + + public void Dispose() + { + Driver.Quit(); + Driver.Dispose(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Test/User/UserTest.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Test/User/UserTest.cs new file mode 100644 index 0000000..a1132fd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Test/User/UserTest.cs @@ -0,0 +1,65 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using OpenQA.Selenium; +using Xunit; + +namespace Admin.NET.Test.User; + +public class UserTest : BaseTest +{ + // 用户登录 token + private static readonly string Token = "xxxxxxxxx"; + + public UserTest() : base(Token) + { + } + + [Fact] + public async Task Login() + { + await base.Login(); + WaitEnter(); + } + + [Fact] + public async Task AddUser() + { + await Task.Delay(1000); + + await GoToUrlAsync("/#/system/user"); + var addBut = Driver.FindElement(By.XPath("//*[@id=\"app\"]/section/section/div/div[1]/div/main/div/div[1]/div/div[1]/div[1]/div[1]/div[3]/div[1]/div/form/div[6]/div/button")); + addBut.Click(); + + //点击基础信息选项卡 + await Task.Delay(1000); + Driver.FindElement(By.Id("tab-0")).Click(); + + var tab = Driver.FindElement(By.Id("pane-0")); + var formItemList = tab.FindElements(By.CssSelector("input")); + + // 输入 账号名称 + var first = formItemList.First(); + first.Clear(); + first.SendKeys("test1"); + await Task.Delay(1000); + + // 输入 手机号码 + var second = formItemList.Skip(1).First(); + second.Clear(); + second.SendKeys("17396157893"); + await Task.Delay(1000); + + // 输入 姓名 + var third = formItemList.Skip(2).First(); + third.Clear(); + third.SendKeys("测试1"); + await Task.Delay(1000); + + // 阻塞 + WaitEnter(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/DateTimeUtilTests.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/DateTimeUtilTests.cs new file mode 100644 index 0000000..ceb88f2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/DateTimeUtilTests.cs @@ -0,0 +1,287 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Xunit; + +namespace Admin.NET.Test.Utils; + +public class DateTimeUtilTests +{ + [Fact] + public void Init_WithTimeSpan_ReturnsCorrectDateTime() + { + // Arrange + var timeSpan = new TimeSpan(1, 0, 0, 0); // 1天 + + // Act + var dateTimeUtil = DateTimeUtil.Init(timeSpan); + + // Assert + Assert.Equal(DateTime.Now.AddDays(1).Date, dateTimeUtil.Date.Date); + } + + [Fact] + public void Init_WithDateTime_ReturnsCorrectDateTime() + { + // Arrange + var dateTime = new DateTime(2023, 10, 1); + + // Act + var dateTimeUtil = DateTimeUtil.Init(dateTime); + + // Assert + Assert.Equal(dateTime, dateTimeUtil.Date); + } + + [Fact] + public void GetTodayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15, 12, 30, 0)); + + // Act + var (start, end) = dateTimeUtil.GetTodayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 15), start); // 当天开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当天结束时间 + } + + [Fact] + public void GetMonthRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetMonthRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), start); // 本月第一天 + Assert.Equal(new DateTime(2023, 10, 31, 23, 59, 59), end); // 本月最后一天 + } + + [Fact] + public void GetFirstDayOfMonth_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var firstDay = dateTimeUtil.GetFirstDayOfMonth(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), firstDay); // 本月第一天 + } + + [Fact] + public void GetLastDayOfMonth_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var lastDay = dateTimeUtil.GetLastDayOfMonth(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 31, 23, 59, 59), lastDay); // 本月最后一天 + } + + [Fact] + public void GetYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), start); // 今年第一天 + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), end); // 今年最后一天 + } + + [Fact] + public void GetFirstDayOfYear_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var firstDay = dateTimeUtil.GetFirstDayOfYear(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), firstDay); // 今年第一天 + } + + [Fact] + public void GetLastDayOfYear_ReturnsCorrectDate() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var lastDay = dateTimeUtil.GetLastDayOfYear(); + + // Assert + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), lastDay); // 今年最后一天 + } + + [Fact] + public void GetDayBeforeYesterdayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetDayBeforeYesterdayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 13), start); // 前天开始时间 + Assert.Equal(new DateTime(2023, 10, 13, 23, 59, 59), end); // 前天结束时间 + } + + [Fact] + public void GetYesterdayRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetYesterdayRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 14), start); // 昨天开始时间 + Assert.Equal(new DateTime(2023, 10, 14, 23, 59, 59), end); // 昨天结束时间 + } + + [Fact] + public void GetLastWeekRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); // 2023-10-15 是周日 + + // Act + var (start, end) = dateTimeUtil.GetLastWeekRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 8), start); // 上周第一天(周一) + Assert.Equal(new DateTime(2023, 10, 14, 23, 59, 59), end); // 上周最后一天(周日) + } + + [Fact] + public void GetThisWeekRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); // 2023-10-15 是周日 + + // Act + var (start, end) = dateTimeUtil.GetThisWeekRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 15), start); // 本周第一天(周一) + Assert.Equal(new DateTime(2023, 10, 21, 23, 59, 59), end); // 本周最后一天(周日) + } + + [Fact] + public void GetLastMonthRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLastMonthRange(); + + // Assert + Assert.Equal(new DateTime(2023, 9, 1), start); // 上月第一天 + Assert.Equal(new DateTime(2023, 9, 30, 23, 59, 59), end); // 上月最后一天 + } + + [Fact] + public void GetLast3DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast3DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 13), start); // 3天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast7DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast7DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 9), start); // 7天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast15DaysRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast15DaysRange(); + + // Assert + Assert.Equal(new DateTime(2023, 10, 1), start); // 15天前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetLast3MonthsRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetLast3MonthsRange(); + + // Assert + Assert.Equal(new DateTime(2023, 7, 15), start); // 3个月前的开始时间 + Assert.Equal(new DateTime(2023, 10, 15, 23, 59, 59), end); // 当前日期的结束时间 + } + + [Fact] + public void GetFirstHalfYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetFirstHalfYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 1, 1), start); // 上半年开始时间 + Assert.Equal(new DateTime(2023, 6, 30, 23, 59, 59), end); // 上半年结束时间 + } + + [Fact] + public void GetSecondHalfYearRange_ReturnsCorrectRange() + { + // Arrange + var dateTimeUtil = DateTimeUtil.Init(new DateTime(2023, 10, 15)); + + // Act + var (start, end) = dateTimeUtil.GetSecondHalfYearRange(); + + // Assert + Assert.Equal(new DateTime(2023, 7, 1), start); // 下半年开始时间 + Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), end); // 下半年结束时间 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/SafeMathTests.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/SafeMathTests.cs new file mode 100644 index 0000000..b014855 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Test/Utils/SafeMathTests.cs @@ -0,0 +1,313 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Xunit; + +namespace Admin.NET.Test.Utils; + +public class SafeMathTests +{ + [Fact] + public void Add_IntAndDouble_ReturnsCorrectResult() + { + // Arrange + int left = 10; + double right = 20.5; + + // Act + var result = SafeMath.Add(left, right, precision: 2); + + // Assert + Assert.Equal(30, result); // 10 + 20.5 = 30.5,四舍五入后为 30 + } + + [Fact] + public void Add_StringAndDecimal_ReturnsCorrectResult() + { + // Arrange + string left = "15.75"; + decimal right = 4.25m; + + // Act + var result = SafeMath.Add(left, right, precision: 2); + + // Assert + Assert.Equal(20.00m, result); // 15.75 + 4.25 = 20.00 + } + + [Fact] + public void Sub_DoubleAndInt_ReturnsCorrectResult() + { + // Arrange + double left = 50.75; + int right = 25; + + // Act + var result = SafeMath.Sub(left, right, precision: 2); + + // Assert + Assert.Equal(25.75, result); // 50.75 - 25 = 25.75 + } + + [Fact] + public void Mult_DecimalAndFloat_ReturnsCorrectResult() + { + // Arrange + decimal left = 10.5m; + float right = 2.0f; + + // Act + var result = SafeMath.Mult(left, right, precision: 2); + + // Assert + Assert.Equal(21.00m, result); // 10.5 * 2.0 = 21.00 + } + + [Fact] + public void Div_IntAndInt_ReturnsCorrectResult() + { + // Arrange + int left = 10; + int right = 3; + + // Act + var result = SafeMath.Div(left, right, precision: 4); + + // Assert + Assert.Equal(3.3333, result); // 10 / 3 = 3.3333 + } + + [Fact] + public void Div_ByZero_ReturnsDefaultValue() + { + // Arrange + int left = 10; + int right = 0; + + // Act + int result = SafeMath.Div(left, right, defaultValue: -1, throwOnDivideByZero: false); + + // Assert + Assert.Equal(-1, result); // 除数为 0,返回默认值 -1 + } + + [Fact] + public void Div_ByZero_ThrowsException() + { + // Arrange + int left = 10; + int right = 0; + + // Act & Assert + Assert.Throws(() => + { + SafeMath.Div(left, right, throwOnDivideByZero: true); + }); + } + + [Fact] + public void SafeConvert_StringToInt_ReturnsCorrectResult() + { + // Arrange + string value = "42"; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(42, result); // 字符串 "42" 转换为 int 42 + } + + [Fact] + public void SafeConvert_InvalidString_ReturnsDefaultValue() + { + // Arrange + string value = "invalid"; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(-1, result); // 转换失败,返回默认值 -1 + } + + [Fact] + public void ConvertToDecimal_Int_ReturnsCorrectResult() + { + // Arrange + int value = 42; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(42m, result); // int 42 转换为 decimal 42m + } + + [Fact] + public void ConvertToDecimal_String_ReturnsCorrectResult() + { + // Arrange + string value = "42.75"; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(42.75m, result); // 字符串 "42.75" 转换为 decimal 42.75m + } + + [Fact] + public void ConvertToDecimal_InvalidString_ReturnsZero() + { + // Arrange + string value = "invalid"; + + // Act & Assert + Assert.Throws(() => SafeMath.ConvertToDecimal(value)); + } + + [Fact] + public void Add_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Add(left, right); + + // Assert + Assert.Equal(20, result); // 左操作数为 null + } + + [Fact] + public void Add_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + var result = SafeMath.Add(left, right); + + // Assert + Assert.Equal(10, result); // 右操作数为 null + } + + [Fact] + public void Sub_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Sub(left, right); + + // Assert + Assert.Equal(-20, result); // 左操作数为 null + } + + [Fact] + public void Sub_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + var result = SafeMath.Sub(left, right); + + // Assert + Assert.Equal(10, result); // 右操作数为 null + } + + [Fact] + public void Mult_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Mult(left, right); + + // Assert + Assert.Equal(0, result); // 左操作数为 null + } + + [Fact] + public void Mult_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + int result = SafeMath.Mult(left, right); + + // Assert + Assert.Equal(0, result); // 右操作数为 null + } + + [Fact] + public void Div_LeftNull_ReturnsDefaultValue() + { + // Arrange + object left = null; + int right = 20; + + // Act + int result = SafeMath.Div(left, right); + + // Assert + Assert.Equal(0, result); // 左操作数为 null + } + + [Fact] + public void Div_RightNull_ReturnsDefaultValue() + { + // Arrange + int left = 10; + object right = null; + + // Act + Assert.Throws(() => + { + int result = SafeMath.Div(left, right); + // Assert + Assert.Equal(-1, result); // 右操作数为 null,返回默认值 -1 + }); + } + + [Fact] + public void SafeConvert_NullInput_ReturnsDefaultValue() + { + // Arrange + object value = null; + + // Act + int result = SafeMath.SafeConvert(value, defaultValue: -1); + + // Assert + Assert.Equal(-1, result); // 输入为 null,返回默认值 -1 + } + + [Fact] + public void ConvertToDecimal_NullInput_ReturnsZero() + { + // Arrange + object value = null; + + // Act + decimal result = SafeMath.ConvertToDecimal(value); + + // Assert + Assert.Equal(0m, result); // 输入为 null,返回默认值 0m + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj new file mode 100644 index 0000000..768cc03 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Admin.NET.Web.Core.csproj @@ -0,0 +1,21 @@ + + + + net8.0;net10.0 + 1701;1702;1591 + + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs new file mode 100644 index 0000000..7899823 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Admin.NET.Core.Service; +using Furion; +using Furion.Authorization; +using Furion.DataEncryption; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System; +using System.Threading.Tasks; + +namespace Admin.NET.Web.Core; + +public class JwtHandler : AppAuthorizeHandler +{ + private readonly SysCacheService _sysCacheService = App.GetRequiredService(); + private readonly SysConfigService _sysConfigService = App.GetRequiredService(); + private static readonly SysMenuService SysMenuService = App.GetRequiredService(); + + /// + /// 自动刷新Token + /// + /// + /// + /// + public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + var userId = context.User.FindFirst(ClaimConst.UserId)?.Value; + var token = httpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); + + // 🛡️ 黑名单校验(包括用户和token) + if (_sysCacheService.ExistKey($"{CacheConst.KeyBlacklist}{userId}") || + _sysCacheService.ExistKey($"blacklist:token:{token}")) + { + context.Fail(); + context.GetCurrentHttpContext().SignoutToSwagger(); + return; + } + + var tokenExpire = await _sysConfigService.GetTokenExpire(); + var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire(); + if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(), tokenExpire, refreshTokenExpire)) + { + await AuthorizeHandleAsync(context); + } + else + { + context.Fail(); // 授权失败 + var currentHttpContext = context.GetCurrentHttpContext(); + if (currentHttpContext == null) return; + + // 跳过由于 SignatureAuthentication 引发的失败 + if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey)) return; + currentHttpContext.SignoutToSwagger(); + } + } + + public override async Task PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext) + { + // 已自动验证 Jwt Token 有效性 + return await CheckAuthorizeAsync(httpContext); + } + + /// + /// 权限校验核心逻辑 + /// + /// + /// + private static async Task CheckAuthorizeAsync(DefaultHttpContext httpContext) + { + // 登录模式判断PC、APP + if (App.User.FindFirst(ClaimConst.LoginMode)?.Value == ((int)LoginModeEnum.APP).ToString()) + return true; + + // 排除超管 + if (App.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString()) + return true; + + // 路由名称 + var routeName = httpContext.Request.Path.StartsWithSegments("/api") + ? httpContext.Request.Path.Value![5..].Replace("/", ":") + : httpContext.Request.Path.Value![1..].Replace("/", ":"); + + // 获取用户拥有按钮权限集合 + var ownBtnPermList = await SysMenuService.GetOwnBtnPermList(); + if (ownBtnPermList.Exists(u => routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase))) + return true; + + // 获取系统所有按钮权限集合 + var allBtnPermList = await SysMenuService.GetAllBtnPermList(); + return allBtnPermList.TrueForAll(u => !routeName.Equals(u, StringComparison.CurrentCultureIgnoreCase)); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs new file mode 100644 index 0000000..7564181 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs @@ -0,0 +1,51 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using AspNetCoreRateLimit; +using Furion; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Web.Core; + +public static class ProjectOptions +{ + /// + /// 注册项目配置选项 + /// + /// + /// + public static IServiceCollection AddProjectOptions(this IServiceCollection services) + { + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.AddConfigurableOptions(); + services.Configure(App.Configuration.GetSection("IpRateLimiting")); + services.Configure(App.Configuration.GetSection("IpRateLimitPolicies")); + services.Configure(App.Configuration.GetSection("ClientRateLimiting")); + services.Configure(App.Configuration.GetSection("ClientRateLimitPolicies")); + + return services; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Startup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Startup.cs new file mode 100644 index 0000000..83caf9e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Core/Startup.cs @@ -0,0 +1,428 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Admin.NET.Core.ElasticSearch; +using Admin.NET.Core.Service; +using AspNetCoreRateLimit; +using Furion; +using Furion.Logging; +using Furion.SpecificationDocument; +using Furion.VirtualFileServer; +using IPTools.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; +using OnceMi.AspNetCore.OSS; +using Scalar.AspNetCore; +using SixLabors.ImageSharp.Web.DependencyInjection; +using System; +using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; +using System.Threading.Tasks; + +#if NET10_0_OR_GREATER +using Admin.NET.Core.Update; +#endif + +namespace Admin.NET.Web.Core; + +[AppStartup(int.MaxValue)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + // ConfigureServices 是 ASP.NET Core / Furion 的服务注册阶段。 + // 这一阶段只做“注册”,不处理真实请求。 + // 你可以把这里理解为:把项目运行需要的所有零件先装进 IoC 容器。 + + // 配置选项: + // 把 Configuration 目录里的 json 配置文件绑定到强类型配置对象。 + services.AddProjectOptions(); + + // 缓存注册 + services.AddCache(); + // 注册 SqlSugar 数据访问能力。 + // 当前项目绝大多数数据库访问最终都会走这里的统一封装。 + services.AddSqlSugar(); + // 注册 JWT 鉴权。 + // enableGlobalAuthorize: true 表示默认接口都要鉴权,除非显式标记匿名访问。 + services.AddJwt(enableGlobalAuthorize: true, jwtBearerConfigure: options => + { + // JWT Bearer 中间件本身支持事件钩子。 + // 这里通过事件扩展“从 QueryString 中读取 token”的能力。 + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var httpContext = context.HttpContext; + // 若请求 Url 包含 token 参数,则设置 Token 值 + if (httpContext.Request.Query.ContainsKey("token")) + context.Token = httpContext.Request.Query["token"]; + return Task.CompletedTask; + } + }; + }).AddSignatureAuthentication(options => // 添加 Signature 身份验证 + { + options.Events = SysOpenAccessService.GetSignatureAuthenticationEventImpl(); + }); + + // 允许跨域: + // 前后端分离项目本地联调时,浏览器会先触发跨域检查。 + services.AddCorsAccessor(); + // 远程请求:项目里对外部系统的 HTTP 调用通常走 Furion 的统一远程请求能力。 + services.AddHttpRemote(); + // 注册内存任务队列。 + services.AddTaskQueue(); + // 注册任务调度。 + // AddPersistence / AddMonitor 是在调度器上继续挂接“持久化”和“执行监控”。 + services.AddSchedule(options => + { + options.AddPersistence(); // 添加作业持久化器 + options.AddMonitor(); // 添加作业执行监视器 + }); + // 脱敏检测:用于统一识别和处理敏感内容。 + services.AddSensitiveDetection(); + + // 本地静态函数: + // 定义在方法内部,只给当前方法使用,适合封装局部配置逻辑。 + // 这里专门统一 Newtonsoft.Json 的序列化规则。 + static void SetNewtonsoftJsonSetting(JsonSerializerSettings setting) + { + setting.DateFormatHandling = DateFormatHandling.IsoDateFormat; + setting.DateTimeZoneHandling = DateTimeZoneHandling.Local; + //setting.Converters.AddDateTimeTypeConverters(localized: false); // 时间本地化 + setting.DateFormatString = "yyyy-MM-dd HH:mm:ss"; // 时间格式化 + setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // 忽略循环引用 + // setting.ContractResolver = new CamelCasePropertyNamesContractResolver(); // 解决动态对象属性名大写 + // setting.NullValueHandling = NullValueHandling.Ignore; // 忽略空值 + setting.Converters.AddLongTypeConverters(); // long转string(防止js精度溢出) 超过17位开启 + // setting.MetadataPropertyHandling = MetadataPropertyHandling.Ignore; // 解决DateTimeOffset异常 + // setting.DateParseHandling = DateParseHandling.None; // 解决DateTimeOffset异常 + // setting.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }); // 解决DateTimeOffset异常 + } + ; + + // AddControllersWithViews 负责注册 MVC 控制器体系。 + // 后面的链式调用是典型的 Fluent API 风格。 + services.AddControllersWithViews() + .AddAppLocalization() + .AddNewtonsoftJson(options => SetNewtonsoftJsonSetting(options.SerializerSettings)) + //.AddXmlSerializerFormatters() + //.AddXmlDataContractSerializerFormatters() + // 统一返回包装: + // 所有控制器返回值和异常,最终都会按 AdminResultProvider 的格式输出给前端。 + .AddInjectWithUnifyResult() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); // 禁止Unicode转码 + options.JsonSerializerOptions.Converters.AddDateTimeTypeConverters("yyyy-MM-dd HH:mm:ss"); // 时间格式化 + }); + + // 三方授权登录OAuth + services.AddOAuth(); + + // ElasticSearch 客户端注册。 + services.AddElasticSearchClients(); + + // 配置反向代理转发头: + // 当项目部署在 Nginx / 网关 / 负载均衡后面时,客户端真实 IP 需要从转发头里取。 + // 注1:如果负载均衡不是在本机通过 Loopback 地址转发请求的,一定要加上options.KnownNetworks.Clear()和options.KnownProxies.Clear() + // 注2:如果设置环境变量 ASPNETCORE_FORWARDEDHEADERS_ENABLED 为 True,则不需要下面的配置代码 + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.All; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + }); + + // 限流服务: + // 对接口做访问频率控制,避免恶意刷接口。 + services.AddInMemoryRateLimiting(); + services.AddSingleton(); + + // 事件总线: + // 适合把“主流程”和“扩展动作”解耦,例如保存成功后发通知、写日志、同步外部系统等。 + services.AddEventBus(options => + { + options.UseUtcTimestamp = false; + // 不启用事件日志 + options.LogEnabled = false; + // 事件执行器(失败重试) + options.AddExecutor(); + // 事件执行器(重试后依然处理未处理异常的处理器) + options.UnobservedTaskExceptionHandler = (obj, args) => + { + if (args.Exception?.Message != null) + Log.Error($"EeventBus 有未处理异常 :{args.Exception?.Message} ", args.Exception); + }; + // 事件执行器-监视器(每一次处理都会进入) + options.AddMonitor(); + + #region Redis消息队列 + + // 替换事件源存储器为Redis + var cacheOptions = App.GetConfig("Cache", true); + if (cacheOptions.CacheType == CacheTypeEnum.Redis.ToString()) + { + options.ReplaceStorer(serviceProvider => + { + var cacheProvider = serviceProvider.GetRequiredService(); + // 创建默认内存通道事件源对象,可自定义队列路由key,如:adminnet_eventsource_queue + return new RedisEventSourceStorer(cacheProvider, "adminnet_eventsource_queue", 3000); + }); + } + + #endregion Redis消息队列 + + #region RabbitMQ消息队列 + + //// 创建默认内存通道事件源对象,可自定义队列路由key,如:adminnet + //var eventBusOpt = App.GetConfig("EventBus", true); + //var rbmqEventSourceStorer = new RabbitMQEventSourceStore(new ConnectionFactory + //{ + // UserName = eventBusOpt.RabbitMQ.UserName, + // Password = eventBusOpt.RabbitMQ.Password, + // HostName = eventBusOpt.RabbitMQ.HostName, + // Port = eventBusOpt.RabbitMQ.Port + //}, "adminnet", 3000); + + //// 替换默认事件总线存储器 + //options.ReplaceStorer(serviceProvider => + //{ + // return rbmqEventSourceStorer; + //}); + + #endregion RabbitMQ消息队列 + }); + + // 图像处理 + services.AddImageSharp(); + + // 读取对象存储配置,然后按配置注册具体的 OSS Provider。 + var ossOpt = App.GetConfig("OSSProvider", true); + services.AddOSSService(Enum.GetName(ossOpt.Provider), "OSSProvider"); + + // 注册文件存储相关服务。 + // AddTransient 表示每次解析依赖时都创建新实例。 + // AddSingleton 表示整个应用生命周期只保留一个实例。 + services.AddTransient(); + services.AddSingleton(); // 改为单例以保持缓存 + services.AddTransient(); + + // 模板引擎:用于导出、报告、模板渲染等场景。 + services.AddViewEngine(); + + // SignalR 即时通讯: + // 常用于在线用户、实时通知、聊天等功能。 + services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.KeepAliveInterval = TimeSpan.FromSeconds(15); // 服务器端向客户端ping的间隔 + options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); // 客户端向服务器端ping的间隔 + options.MaximumReceiveMessageSize = 1024 * 1014 * 10; // 数据包大小10M,默认最大为32K + }).AddNewtonsoftJsonProtocol(options => SetNewtonsoftJsonSetting(options.PayloadSerializerSettings)); + + // 系统日志 + services.AddLoggingSetup(); + + // 验证码 + services.AddCaptcha(); + + // 控制台logo + services.AddConsoleLogo(); + + //// Swagger 时间格式化 + //services.AddSwaggerGen(c => + //{ + // c.MapType(() => new Microsoft.OpenApi.Models.OpenApiSchema + // { + // Type = "string", + // Format = "date-time", + // Example = new Microsoft.OpenApi.Any.OpenApiString(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")) // 示例值 + // }); + + // // 确保生成的文档包含 OpenAPI 版本字段 + // c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + // { + // Version = "v1", + // Title = "Admin.NET API", + // Description = "Admin.NET 通用权限开发平台" + // }); + // c.OperationFilter(); + //}); + + // 将IP地址数据库文件完全加载到内存,提升查询速度(以空间换时间,内存将会增加60-70M) + IpToolSettings.LoadInternationalDbToMemory = true; + // 设置默认查询器China和International + //IpToolSettings.DefalutSearcherType = IpSearcherType.China; + IpToolSettings.DefalutSearcherType = IpSearcherType.International; + + // 配置gzip与br的压缩等级为最优 + //services.Configure(options => + //{ + // options.Level = CompressionLevel.Optimal; + //}); + //services.Configure(options => + //{ + // options.Level = CompressionLevel.Optimal; + //}); + // 注册压缩响应 + services.AddResponseCompression((options) => + { + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( + [ + "text/html; charset=utf-8", + "application/xhtml+xml", + "application/atom+xml", + "image/svg+xml" + ]); + }); + + // 注册虚拟文件系统服务 + services.AddVirtualFileServer(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 响应压缩 + app.UseResponseCompression(); + + app.UseForwardedHeaders(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); + } + + app.Use(async (context, next) => + { + context.Response.Headers.Append("Admin.NET", "Admin.NET"); + await next(); + }); + + // 图像处理 + app.UseImageSharp(); + + // 特定文件类型(文件后缀)处理 + var contentTypeProvider = FS.GetFileExtensionContentTypeProvider(); + // contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型"; + app.UseStaticFiles(new StaticFileOptions + { + ContentTypeProvider = contentTypeProvider + }); + // 二级目录文件路径解析 + if (!string.IsNullOrEmpty(App.Settings.VirtualPath)) + app.UseStaticFiles(new StaticFileOptions + { + RequestPath = App.Settings.VirtualPath, + FileProvider = App.WebHostEnvironment.WebRootFileProvider + }); + //// 启用HTTPS + //app.UseHttpsRedirection(); + + // 启用OAuth + app.UseOAuth(); + + // 添加状态码拦截中间件 + app.UseUnifyResultStatusCodes(); + + // 启用多语言,必须在 UseRouting 之前 + app.UseAppLocalization(); + + // 路由注册 + app.UseRouting(); + + // 启用跨域,必须在 UseRouting 和 UseAuthentication 之间注册 + app.UseCorsAccessor(); + + // 启用鉴权授权 + app.UseAuthentication(); + app.UseAuthorization(); + + // 限流组件(在跨域之后) + app.UseIpRateLimiting(); + app.UseClientRateLimiting(); + app.UsePolicyRateLimit(); + + // 任务调度看板 + app.UseScheduleUI(options => + { + options.RequestPath = "/schedule"; // 必须以 / 开头且不以 / 结尾 + options.DisableOnProduction = false; // 是否在生产环境中关闭 + options.DisplayEmptyTriggerJobs = true; // 是否显示空作业触发器的作业 + options.DisplayHead = false; // 是否显示页头 + options.DefaultExpandAllJobs = false; // 是否默认展开所有作业 + options.EnableDirectoryBrowsing = false; // 是否启用目录浏览 + options.Title = "定时任务看板"; // 自定义看板标题 + + options.LoginConfig.OnLoging = async (username, password, httpContext) => + { + var res = await httpContext.RequestServices.GetRequiredService().SwaggerSubmitUrl(new SpecificationAuth { UserName = username, Password = password }); + return res == 200; + }; + options.LoginConfig.DefaultUsername = ""; + options.LoginConfig.DefaultPassword = ""; + options.LoginConfig.SessionKey = "schedule_session_key"; // 登录客户端存储的 Session 键 + }); + + app.UseInject(string.Empty, options => + { + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + groupInfo.Description += "
👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!"; + } + options.ConfigureSwagger(m => + { + m.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; + }); + }); + +#if NET10_0_OR_GREATER + app.UseAutoVersionUpdate(); +#endif + + app.UseEndpoints(endpoints => + { + // 配置 Scalar 第三方 UI 集成(路由前缀一致代表独立,不同则代表共存) + if (App.GetConfig("AppSettings:InjectSpecificationDocument", true)) + { + endpoints.MapScalarApiReference("sapi", options => + { + options.WithTitle("Admin.NET"); + + // 配置 OpenAPI 文档 + foreach (var groupInfo in SpecificationDocumentBuilder.GetOpenApiGroups()) + { + options.AddDocument(groupInfo.Group, groupInfo.Title, groupInfo.RouteTemplate); + } + }); + } + // 注册集线器 + endpoints.MapHubs(); + + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj new file mode 100644 index 0000000..01c62bf --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj @@ -0,0 +1,67 @@ + + + + net8.0;net10.0 + enable + zh-Hans + true + false + ad9369d1-f29b-4f8f-a7df-8b4d7aa0726b + Linux + true + Admin.NET + Admin.NET 通用权限开发平台 + 1.0.0 + 1.0.0 + 1.0.0 + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + PreserveNewest + + + Always + + + + + + Never + + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/alipayPublicCert.crt b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/alipayPublicCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/alipayRootCert.crt b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/alipayRootCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/appPublicCert.crt b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/AlipayCrt/appPublicCert.crt new file mode 100644 index 0000000..e69de29 diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Controllers/HomeController.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Controllers/HomeController.cs new file mode 100644 index 0000000..97ab952 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Controllers/HomeController.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Admin.NET.Web.Entry.Controllers +{ + [AllowAnonymous] + public class HomeController : Controller + { + //private readonly ISystemService _systemService; + + //public HomeController(ISystemService systemService) + //{ + // _systemService = systemService; + //} + + public IActionResult Index() + { + //ViewBag.Description = _systemService.GetDescription(); + + return View(); + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Dockerfile b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Dockerfile new file mode 100644 index 0000000..a549a55 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 5005 + +COPY . . + +# 设置语言/区域设置环境变量 +ENV LANG zh-Hans + +# 使用阿里云的镜像源进行更新 +# .NET6使用 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/' /etc/apt/sources.list +# .NET8使用 +# RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/' /etc/apt/sources.list.d/debian.sources +# 更新包管理器并安装free命令 +RUN apt-get update && apt-get install -y procps + +ENTRYPOINT ["dotnet", "Admin.NET.Web.Entry.dll"] \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/FakeStartup.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/FakeStartup.cs new file mode 100644 index 0000000..29914ca --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/FakeStartup.cs @@ -0,0 +1,14 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Web.Entry; + +/// +/// 供集成测试使用 +/// +public class FakeStartup +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/GeoLite2-City.mmdb b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/GeoLite2-City.mmdb new file mode 100644 index 0000000..f5e23dc Binary files /dev/null and b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/GeoLite2-City.mmdb differ diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Program.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Program.cs new file mode 100644 index 0000000..d2ecdc7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Program.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +// 顶层语句(Top-level statements): +// .NET 6+ 开始允许不写传统的 Program.Main 方法,文件中的语句会直接作为程序入口执行。 +// 这里通过 Furion 的 Serve.Run 启动整个 Web 应用。 +Serve.Run(RunOptions.Default.AddWebComponent()); + +public class WebComponent : IWebComponent +{ + public void Load(WebApplicationBuilder builder, ComponentContext componentContext) + { + // WebApplicationBuilder 是 ASP.NET Core 的宿主构建器。 + // 可以把它理解成“程序正式启动前的总配置对象”。 + + // 设置日志过滤: + // 这里把微软基础设施层的高频日志过滤掉,避免控制台被大量框架日志刷屏。 + builder.Logging.AddFilter((provider, category, logLevel) => + { + // Lambda 表达式 => 是 C# 里常见的匿名函数写法。 + // category 表示日志分类名,通常类似命名空间。 + return !new[] { "Microsoft.Hosting", "Microsoft.AspNetCore" }.Any(u => category.StartsWith(u)) && logLevel >= LogLevel.Information; + }); + + // 配置 Kestrel: + // Kestrel 是 ASP.NET Core 自带的 Web 服务器。 + // 这里统一调大超时时间,并取消请求体大小限制,适合文件上传和长连接场景。 + builder.WebHost.ConfigureKestrel(u => + { + u.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(30); + u.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(30); + // null 表示不限制请求体大小。 + // 实际生产中是否开放,要结合上传场景和安全策略决定。 + u.Limits.MaxRequestBodySize = null; + }); + } +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Properties/launchSettings.json b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Properties/launchSettings.json new file mode 100644 index 0000000..8d4f779 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5005" + // "sslPort": 44325 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Admin.NET.Web.Entry": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "http://localhost:5005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.en.resx b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.en.resx new file mode 100644 index 0000000..d555440 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.en.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Admin NET General Permission Development Platform + + + API Interfaces + + + Source Address + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.zh-CN.resx b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.zh-CN.resx new file mode 100644 index 0000000..4c25852 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Resources/Lang.zh-CN.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Admin.NET 通用权限开发平台 + + + API 接口 + + + 源码地址 + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/SingleFilePublish.cs b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/SingleFilePublish.cs new file mode 100644 index 0000000..7d1392e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/SingleFilePublish.cs @@ -0,0 +1,43 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion; +using System.Reflection; + +namespace Admin.NET.Web.Entry; + +/// +/// 解决单文件发布问题 +/// +public class SingleFilePublish : ISingleFilePublish +{ + /// + /// 解决单文件不能扫描的程序集 + /// + /// 可同时配置 + /// + public Assembly[] IncludeAssemblies() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return Array.Empty(); + } + + /// + /// 解决单文件不能扫描的程序集名称 + /// + /// 可同时配置 + /// + public string[] IncludeAssemblyNames() + { + // 需要 Furion 框架扫描哪些程序集就写上去即可 + return new[] + { + "Admin.NET.Application", + "Admin.NET.Core", + "Admin.NET.Web.Core", + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/UpdateScripts/1.0.0.sql b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/UpdateScripts/1.0.0.sql new file mode 100644 index 0000000..d1f760f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/UpdateScripts/1.0.0.sql @@ -0,0 +1,3 @@ +-- 1.0.0.sql +-- update +-- 2025-05-24 11:00:00 \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Home/Index.cshtml b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Home/Index.cshtml new file mode 100644 index 0000000..bc502bc --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Home/Index.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = ViewBag.Description; +} + +
+ +
\ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Shared/_Layout.cshtml b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..5c71d26 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/Shared/_Layout.cshtml @@ -0,0 +1,11 @@ + + + + + + @ViewData["Title"] - Furion + + + @RenderBody() + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewImports.cshtml b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewImports.cshtml new file mode 100644 index 0000000..ae2e7b9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Admin.NET.Web.Entry +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewStart.cshtml b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewStart.cshtml new file mode 100644 index 0000000..1af6e49 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.Development.json b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.Development.json new file mode 100644 index 0000000..c072d2b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ConfigurationScanDirectories": [ "Configuration", "" ] +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.json b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.json new file mode 100644 index 0000000..cd1c674 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/appsettings.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "ConfigurationScanDirectories": [ "Configuration", "" ] // 扫描配置文件json文件夹(自动合并该文件夹里面所有json文件) +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/ip2region.db b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/ip2region.db new file mode 100644 index 0000000..6cf58ef Binary files /dev/null and b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/ip2region.db differ diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/sensitive-words.txt b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/sensitive-words.txt new file mode 100644 index 0000000..901a560 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/sensitive-words.txt @@ -0,0 +1,4 @@ +装X|特么的|SB|屌爆了|你妹|马勒戈壁|蛋疼|买了个表|妈蛋|日了狗 +吃翔 +装13 +屁民 diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/web.config b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/web.config new file mode 100644 index 0000000..002dc98 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/web.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/images/logo.png b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/images/logo.png new file mode 100644 index 0000000..da79429 Binary files /dev/null and b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/images/logo.png differ diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Dto.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Dto.cs.vm new file mode 100644 index 0000000..69a9459 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Dto.cs.vm @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)输出参数 +/// +public class @(Model.ClassName)Dto +{ +@foreach (var column in Model.TableField){ +if(column.EffectType == "ForeignKey"){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public string @(column.PropertyName)FkColumn { get; set; } + @: +} +} +@foreach (var column in Model.TableField){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public @column.NetType @column.PropertyName { get; set; } + @: +} +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Entity.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Entity.cs.vm new file mode 100644 index 0000000..9e03652 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Entity.cs.vm @@ -0,0 +1,46 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +@if(Model.BaseClassName!=""){ +@:using Admin.NET.Core; +@:using SqlSugar; +} +namespace @(Model.NameSpace); + +/// +/// @(Model.Description) +/// +[Tenant("@(Model.ConfigId)")] +[SugarTable("@(Model.TableName)", "@(Model.Description)")] +public partial class @(Model.EntityName) @Model.BaseClassName +{ +@foreach (var column in Model.TableField) { + var propSuffix = ""; + if (column.IsPrimarykey && (Model.BaseClassName == "" || Model.BaseClassName != "" && column.DbColumnName.ToLower() != "id")) { + propSuffix = $", IsPrimaryKey = true, IsIdentity = {column.IsIdentity.ToString().ToLower()}"; + } + + if (column.DataType.TrimEnd('?') == "string") { + propSuffix += $", Length = {column.Length}"; + } else if (column.DataType.TrimEnd('?') == "decimal") { + propSuffix += $", Length = {column.Length}, DecimalDigits={column.DecimalDigits}"; + } + + if(!string.IsNullOrWhiteSpace(column.DefaultValue)){ + propSuffix +=$", DefaultValue = \"{column.DefaultValue}\""; + } + + @:/// + @:/// @column.ColumnDescription + @:/// + if(!column.IsNullable){ + @:[Required] + } + @:[SugarColumn(ColumnName = "@column.DbColumnName", ColumnDescription = "@column.ColumnDescription"@propSuffix)] + @:public virtual @column.DataType @column.PropertyName { get; set; } + @: +} +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Input.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Input.cs.vm new file mode 100644 index 0000000..b972c9f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Input.cs.vm @@ -0,0 +1,200 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using System.ComponentModel.DataAnnotations; +using Magicodes.ExporterAndImporter.Core; +using Magicodes.ExporterAndImporter.Excel; + +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)基础输入参数 +/// +public class @(Model.ClassName)BaseInput +{ +@foreach (var column in Model.PrimaryKeyFieldList.Concat(Model.AddUpdateFieldList)){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + @:public virtual @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)分页查询输入参数 +/// +public class Page@(Model.ClassName)Input : BasePageInput +{ +@foreach (var column in Model.TableField.Where(u => u.WhetherQuery == "Y")){ + if(column.NetType?.TrimEnd('?') == "DateTime" && column.QueryType == "~"){ + @:/// + @:/// @(column.ColumnComment)范围 + @:/// + @: public DateTime?[] @(column.PropertyName)Range { get; set; } + } else { + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + } + @: +} +@if (Model.ImportFieldList.Count > 0){ + var primaryKey = Model.PrimaryKeyFieldList.First(); + @:/// + @:/// 选中主键列表 + @:/// + @: public List<@(primaryKey.NetType)> SelectKeyList { get; set; } +} +} + +/// +/// @(Model.BusName)增加输入参数 +/// +public class Add@(Model.ClassName)Input +{ +@foreach (var column in Model.AddUpdateFieldList){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + if (column.NetType.TrimEnd('?').EndsWith("string") && column.ColumnLength > 0){ + @:[MaxLength(@column.ColumnLength, ErrorMessage = "@(column.ColumnComment)字符长度不能超过@(column.ColumnLength)")] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)删除输入参数 +/// +public class Delete@(Model.ClassName)Input +{ +@foreach (var column in Model.PrimaryKeyFieldList) { + @:/// + @:/// @column.ColumnComment + @:/// + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: +} +} + +/// +/// @(Model.BusName)更新输入参数 +/// +public class Update@(Model.ClassName)Input +{ + @foreach (var column in Model.PrimaryKeyFieldList.Concat(Model.AddUpdateFieldList)){ + @:/// + @:/// @column.ColumnComment + @:/// + if (column.EffectType is "EnumSelector" or "DictSelector") { + @:[Dict(@(column.EffectType == "EnumSelector" ? $"nameof({column.DictTypeCode})" : $"\"{column.DictTypeCode}\""), AllowNullValue=true)] + } + if (column.WhetherRequired == "Y" || column.ColumnKey == "True") { + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + } + if (column.NetType.TrimEnd('?').EndsWith("string") && column.ColumnLength > 0){ + @:[MaxLength(@column.ColumnLength, ErrorMessage = "@(column.ColumnComment)字符长度不能超过@(column.ColumnLength)")] + } + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + } +} + +/// +/// @(Model.BusName)主键查询输入参数 +/// +public class QueryById@(Model.ClassName)Input : Delete@(Model.ClassName)Input +{ +} + +@if (Model.DropdownFieldList.Count > 0) { +@:/// +@:/// 下拉数据输入参数 +@:/// +@:public class DropdownData@(Model.ClassName)Input +@:{ + @:/// + @:/// 是否用于分页查询 + @:/// + @:public bool FromPage { get; set; } +@:} +@: +} +@if (Model.HasSetStatus) { +@:/// +@:/// 设置状态输入参数 +@:/// +@:public class Set@(Model.ClassName)StatusInput : BaseStatusInput +@:{ + @foreach (var column in Model.PrimaryKeyFieldList.Where(u => u.PropertyName != "Id")) { + @:/// + @:/// @column.ColumnComment + @:/// + @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + } +@:} +@: +} +@if (Model.ImportFieldList.Count > 0){ +@:/// +@:/// @(Model.BusName)数据导入实体 +@:/// +@:[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)] +@:public class Import@(Model.ClassName)Input : BaseImportInput +@:{ + foreach (var column in Model.ImportFieldList){ + var headerName = (column.WhetherRequired == "Y" ? "*" : "") + column.ColumnComment; + if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector" || column.EffectType == "DictSelector") { + @:/// + @:/// @column.ColumnComment 关联值 + @:/// + @:[ImporterHeader(IsIgnore = true)] + @:[ExporterHeader(IsIgnore = true)] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + @: + @:/// + @:/// @column.ColumnComment 文本 + @:/// + if (column.EffectType == "DictSelector") { + @:[Dict(@($"\"{column.DictTypeCode}\""))] + } + @:[ImporterHeader(Name = "@(headerName)")] + @:[ExporterHeader("@(headerName)", Format = "@", Width = 25, IsBold = true)] + @:public string @column.ExtendedPropertyName { get; set; } + } else { + @:/// + @:/// @column.ColumnComment + @:/// + @:[ImporterHeader(Name = "@(headerName)")] + @:[ExporterHeader("@(headerName)", Format = "@", Width = 25, IsBold = true)] + @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; } + } + @: + } +@:} +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Output.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Output.cs.vm new file mode 100644 index 0000000..e1f2c11 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Output.cs.vm @@ -0,0 +1,71 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +using Magicodes.ExporterAndImporter.Core; +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)输出参数 +/// +public class @(Model.ClassName)Output +{ + @foreach (var column in Model.TableField){ + @:/// + @:/// @column.ColumnComment + @:/// + @:public @column.NetType @(column.PropertyName) { get; set; } + if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") { + @: + @:/// + @:/// @(column.ColumnComment) 描述 + @:/// + @:public string @(column.ExtendedPropertyName) { get; set; } + }else if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @: + @:/// + @:/// @(column.ColumnComment) 附件信息 + @:/// + @:public SysFile @(column.ExtendedPropertyName) { get; set; } + } + @: + } +} +@if (Model.ApiTreeFieldList.Count > 0) { +foreach(var column in Model.ApiTreeFieldList) { +@:/// +@:/// @column.ColumnComment 树选择器输出参数 +@:/// +@:public class Tree@(column.PropertyNameTrimEndId)Output : @(Model.ClassName) +@:{ + @:/// + @:/// 节点值 + @:/// + @:public @column.NetType Value { get; set; } + @: + @:/// + @:/// 节点文本 + @:/// + @:public string Label { get; set; } + @: + @:/// + @:/// 子集数据 + @:/// + @:public List Children { get; set; } +@:} +@: +} +} +@if (Model.ImportFieldList.Count > 0) { +@: +@:/// +@:/// @(Model.BusName)数据导入模板实体 +@:/// +@:public class Export@(Model.ClassName)Output : Import@(Model.ClassName)Input +@:{ +@: [ImporterHeader(IsIgnore = true)] +@: [ExporterHeader(IsIgnore = true)] +@: public override string Error { get; set; } +@:} +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/SeedData.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/SeedData.cs.vm new file mode 100644 index 0000000..2101c5b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/SeedData.cs.vm @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using @Model.EntityNameSpace; + +namespace @(Model.NameSpace); + +/// +/// @(Model.Description) 表种子数据 +/// +public class @(Model.SeedDataName) : ISqlSugarEntitySeedData<@(Model.EntityName)> +{ + /// + /// 种子数据 + /// + /// + public IEnumerable<@(Model.EntityName)> HasData() + { + return new List<@(Model.EntityName)> { + @foreach (var record in Model.RecordList) { + @:new() { @record }, + } + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Service.cs.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Service.cs.vm new file mode 100644 index 0000000..7005846 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Service.cs.vm @@ -0,0 +1,421 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core.Service; +using Microsoft.AspNetCore.Http; +using Furion.DatabaseAccessor; +using Furion.FriendlyException; +using Mapster; +using SqlSugar; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using @(Model.NameSpace).Entity; +namespace @(Model.NameSpace); + +/// +/// @(Model.BusName)服务 🧩 +/// +[ApiDescriptionSettings(@(Model.ProjectLastName)Const.GroupName, Order = 100)] +public partial class @(Model.ClassName)Service : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository<@(Model.ClassName)> _@(Model.LowerClassName)Rep; + @foreach(var kv in Model.InjectServiceMap) { + @:private readonly @(kv.Key) _@(kv.Value); + } + + public @(Model.ClassName)Service(SqlSugarRepository<@(Model.ClassName)> @(Model.LowerClassName)Rep@(Model.InjectServiceArgs)) + { + _@(Model.LowerClassName)Rep = @(Model.LowerClassName)Rep; + @foreach(var kv in Model.InjectServiceMap) { + @:_@(kv.Value) = @(kv.Value); + } + } + + /// + /// 分页查询@(Model.BusName) 🔖 + /// + /// + /// + [DisplayName("分页查询@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Page"), HttpPost] + public async Task> Page(Page@(Model.ClassName)Input input) + { + input.Keyword = input.Keyword?.Trim(); + var query = _@(Model.LowerClassName)Rep.AsQueryable() + @{ + string joinTableName = "u"; + var queryFields = Model.TableField.Where(u => u.WhetherQuery == "Y"); + // 关键字模糊查询 + if (queryFields.Any(u => u.QueryType == "like")) { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => @string.Join(" || ", queryFields.Where(u => u.QueryType == "like").Select(col => $"u.{col.PropertyName}.Contains(input.Keyword)"))) + } + + // 单字段模糊查询 + foreach(var column in queryFields.Where(u => u.QueryType == "like")) { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.@(column.PropertyName)), u => u.@(column.PropertyName).Contains(input.@(column.PropertyName).Trim())) + } + + // 字段组合查询 + foreach(var column in queryFields.Where(u => u.QueryType != "like")) { + if (column.NetType.TrimEnd('?') == "string") { + @:.WhereIF(!string.IsNullOrWhiteSpace(input.@(column.PropertyName)), u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?') == "int" || column.NetType.TrimEnd('?') == "long") { + @:.WhereIF(input.@(column.PropertyName) != null, u => u.@(column.PropertyName) @(column.QueryType) input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?').EndsWith("Enum")) { + @:.WhereIF(input.@(column.PropertyName).HasValue, u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } else if (column.NetType.TrimEnd('?') == "DateTime" && column.QueryType == "~") { + @:.WhereIF(input.@(column.PropertyName)Range?.Length == 2, u => u.@(column.PropertyName) >= input.@(column.PropertyName)Range[0] && u.@(column.PropertyName) <= input.@(column.PropertyName)Range[1]) + } else if (column.NetType.TrimEnd('?').EndsWith("bool")) { + @:.WhereIF(input.@(column.PropertyName).HasValue, u => u.@(column.PropertyName) == input.@(column.PropertyName)) + } + + } + // 联表 + if (Model.HasJoinTable) { + foreach (var column in Model.TableField.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")){ + joinTableName += ", " + column.LowerPropertyNameTrimEndId; + @:.LeftJoin<@column.FkEntityName>((@joinTableName) => u.@(column.PropertyName) == @(column.LowerPropertyNameTrimEndId).@(column.FkLinkColumnName)) + } + // 查询列表 + @:.Select((@joinTableName) => new @(Model.ClassName)Output + @:{ + foreach (var column in Model.TableField) { + if(column.NetType.TrimEnd('?').EndsWith("Enum")) { + @:@(column.PropertyName) = (@(column.NetType))u.@(column.PropertyName), + }else{ + @:@(column.PropertyName) = u.@(column.PropertyName), + } + if (column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") { + @:@(column.ExtendedPropertyName) = @column.GetDisplayColumn(column.LowerPropertyNameTrimEndId), + } + } + @:}); + } else { + // 无联表 + @:.Select<@(Model.ClassName)Output>(); + } + } + return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 获取@(Model.BusName)详情 ℹ️ + /// + /// + /// + [DisplayName("获取@(Model.BusName)详情")] + [ApiDescriptionSettings(Name = "Detail"), HttpGet] + public async Task<@(Model.ClassName)> Detail([FromQuery] QueryById@(Model.ClassName)Input input) + { + return await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")); + } + + /// + /// 增加@(Model.BusName) ➕ + /// + /// + /// + [DisplayName("增加@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(Add@(Model.ClassName)Input input) + { + var entity = input.Adapt<@(Model.ClassName)>(); + @foreach (var config in Model.TableUniqueConfigList) { + @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @(string.Join(" && ", @config.Columns.Select(x => $"u.{x} != null && u.{x} == input.{x}"))))) throw Oops.Oh("@(config.Message)已存在"); + } + return await _@(Model.LowerClassName)Rep.InsertAsync(entity) ? entity.Id : 0; + } + + /// + /// 更新@(Model.BusName) ✏️ + /// + /// + /// + [DisplayName("更新@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(Update@(Model.ClassName)Input input) + { + @{ + var primaryKeyWhere = Model.PrimaryKeysFormat(" && ", "u.{0} != input.{0}"); + foreach (var config in Model.TableUniqueConfigList) { + @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @primaryKeyWhere && @config.Format(" && ", "u.{0} != null && u.{0} == input.{0}"))) throw Oops.Oh("@(config.Message)已存在"); + } + } + var entity = input.Adapt<@(Model.ClassName)>(); + await _@(Model.LowerClassName)Rep.AsUpdateable(entity) + @if (Model.IgnoreUpdateFieldList.Count > 0) { + @:.IgnoreColumns(u => new { + foreach (var column in Model.IgnoreUpdateFieldList) { + @:u.@(column.PropertyName), + } + @:}) + } + .ExecuteCommandAsync(); + } + + /// + /// 删除@(Model.BusName) ❌ + /// + /// + /// + [DisplayName("删除@(Model.BusName)")] + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(Delete@(Model.ClassName)Input input) + { + var entity = await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")) ?? throw Oops.Oh(ErrorCodeEnum.D1002); +@{ + @if(Model.TableField.Where(x => x.ColumnName == "IsDelete").Any()) + { + @:await _@(Model.LowerClassName)Rep.FakeDeleteAsync(entity); //假删除 + } + else{ + @:await _@(Model.LowerClassName)Rep.DeleteAsync(entity); //真删除 + } +} + } + + /// + /// 批量删除@(Model.BusName) ❌ + /// + /// + /// + [DisplayName("批量删除@(Model.BusName)")] + [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost] + public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")]List input) + { + var exp = Expressionable.Create<@(Model.ClassName)>(); + foreach (var row in input) exp = exp.Or(it => @Model.PrimaryKeysFormat(" && ", "it.{0} == row.{0}")); + var list = await _@(Model.LowerClassName)Rep.AsQueryable().Where(exp.ToExpression()).ToListAsync(); +@{ + @if(Model.TableField.Where(x => x.ColumnName == "IsDelete").Any()) + { + @:return await _@(Model.LowerClassName)Rep.FakeDeleteAsync(list); //假删除 + } + else{ + @:return await _@(Model.LowerClassName)Rep.Context.Deleteable(list).ExecuteCommandAsync(); //真删除--返回受影响的行数 + } +} + } + @if (Model.HasSetStatus) { + @: + @:/// + @:/// 设置@(Model.BusName)状态 🚫 + @:/// + @:/// + @:/// + @:[DisplayName("设置@(Model.BusName)状态")] + @:[ApiDescriptionSettings(Name = "SetStatus"), HttpPost] + @:public async Task Set@(Model.ClassName)Status(Set@(Model.ClassName)StatusInput input) + @:{ + @:await _@(Model.LowerClassName)Rep.AsUpdateable().SetColumns(u => u.Status, input.Status).Where(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")).ExecuteCommandAsync(); + @:} + } + @foreach (var column in Model.UploadFieldList) { + @: + @:/// + @:/// 上传@(column.ColumnComment) ⬆️ + @:/// + @:/// + @:/// + @:[DisplayName("上传@(column.ColumnComment)")] + @:[ApiDescriptionSettings(Name = "Upload@(column.PropertyName)"), HttpPost] + @:public async Task Upload@(column.PropertyName)([Required] IFormFile file) + @:{ + @:return await _sysFileService.UploadFile(new UploadFileInput { File = file },"upload/@(Model.ClassName)/@(column.PropertyName)"); + @:} + } + @if (Model.DropdownFieldList.Count > 0) { + @: + @:/// + @:/// 获取下拉列表数据 🔖 + @:/// + @:/// + @:[DisplayName("获取下拉列表数据")] + @:[ApiDescriptionSettings(Name = "DropdownData"), HttpPost] + @:public async Task> DropdownData(DropdownData@(Model.ClassName)Input input) + @:{ + foreach (var column in Model.DropdownFieldList) { + @:var @(column.LowerPropertyName)Data = await _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>() + if (column.EffectType != "ApiTreeSelector") { + @:.InnerJoinIF<@Model.ClassName>(input.FromPage, (u, r) => u.@(column.FkLinkColumnName) == r.@(column.PropertyName)) + } + if (column.EffectType != "ApiTreeSelector") { + @:.Select(u => new { + @:Value = u.@(column.FkLinkColumnName), + @:Label = @column.GetDisplayColumn("u") + @:}).ToListAsync(); + } else { + @:.Select(u => new Tree@(column.PropertyNameTrimEndId)Output { + @:Value = u.@(column.FkLinkColumnName), + @:Label = @column.GetDisplayColumn("u") + @:}, true).ToTreeAsync(u => u.Children, u => u.@(column.PidColumn), @(column.WhetherRequired == "Y" ? "0" : "null")); + } + } + @:return new Dictionary + @:{ + foreach (var column in Model.DropdownFieldList) { + @:{ "@(column.LowerPropertyName)", @(column.LowerPropertyName)Data }, + } + @:}; + @:} + } + @if (Model.ImportFieldList.Count > 0) { + @: + @:/// + @:/// 导出@(Model.BusName)记录 🔖 + @:/// + @:/// + @:/// + @:[DisplayName("导出@(Model.BusName)记录")] + @:[ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify] + @:public async Task Export(Page@(Model.ClassName)Input input) + @:{ + @:var list = (await Page(input)).Items?.Adapt>() ?? new(); + @:if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.@(Model.PrimaryKeyFieldList.First().PropertyName))).ToList(); + var dictFields = Model.TableField.Where(x => x.WhetherImport == "Y" && x.EffectType == "DictSelector") ?? default; + foreach (var column in dictFields) { + @:var @(column.LowerPropertyName)DictMap = _sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = "@(column.DictTypeCode)" }).Result.ToDictionary(x => x.Value, x => x.Label); + } + if (dictFields.Count() > 0) { + @:list.ForEach(e => { + foreach (var column in dictFields) { + @:e.@(column.ExtendedPropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(e.@(column.PropertyName) ?? "", e.@(column.PropertyName)); + } + @:}); + } + @:return ExcelHelper.ExportTemplate(list, "@(Model.BusName)导出记录"); + @:} + @: + @:/// + @:/// 下载@(Model.BusName)数据导入模板 ⬇️ + @:/// + @:/// + @:[DisplayName("下载@(Model.BusName)数据导入模板")] + @:[ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify] + @:public IActionResult DownloadTemplate() + @:{ + var fieldsList = Model.ImportFieldList.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector").ToList(); + if (fieldsList.Any()) { + @:return ExcelHelper.ExportTemplate(new List(), "@(Model.BusName)导入模板", (_, info) => + @:{ + foreach (var column in fieldsList) { + var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList(); + @:if (nameof(Export@(Model.ClassName)Output.@column.ExtendedPropertyName) == info.Name) return _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Select(u => $"@(string.Join("-", columnList))").Distinct().ToList(); + } + @:return null; + @:}); + } else { + @:return ExcelHelper.ExportTemplate(new List(), "@(Model.BusName)导入模板"); + } + @:} + @: + @:private static readonly object _@(Model.LowerClassName)ImportLock = new object(); + @:/// + @:/// 导入@(Model.BusName)记录 💾 + @:/// + @:/// + @:[DisplayName("导入@(Model.BusName)记录")] + @:[ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork] + @:public IActionResult ImportData([Required] IFormFile file) + @:{ + @:lock (_@(Model.LowerClassName)ImportLock) + @:{ + var dictTableField = Model.TableField.Where(x => x.WhetherImport == "Y" && x.EffectType == "DictSelector") ?? default; + foreach (var column in dictTableField){ + @:var @(column.LowerPropertyName)DictMap = _sysDictTypeService.GetDataList(new GetDataDictTypeInput { Code = "@(column.DictTypeCode)" }).Result.ToDictionary(x => x.Label!, x => x.Value); + } + + @:var stream = ExcelHelper.ImportData(file, (list, markerErrorAction) => + @:{ + @:_sqlSugarClient.Utilities.PageEach(list, 2048, pageItems => + @:{ + foreach (var column in Model.ImportFieldList.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")) { + @:// 链接 @(column.ColumnComment) + @:var @(column.LowerPropertyName)LabelList = pageItems.Where(x => x.@column.ExtendedPropertyName != null).Select(x => x.@column.ExtendedPropertyName).Distinct().ToList(); + @:if (@(column.LowerPropertyName)LabelList.Any()) { + var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList(); + @:var @(column.LowerPropertyName)LinkMap = _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Where(u => @(column.LowerPropertyName)LabelList.Contains($"@(string.Join("-", columnList))")).ToList().ToDictionary(u => $"@(string.Join("-", columnList))", u => u.@(column.FkLinkColumnName) as @(column.NetType.TrimEnd('?') == "long" ? "long?": column.NetType)); + @:pageItems.ForEach(e => { + @:e.@(column.PropertyName) = @(column.LowerPropertyName)LinkMap.GetValueOrDefault(e.@column.ExtendedPropertyName ?? ""); + @:if (e.@(column.PropertyName) == null) e.Error = "@(column.ColumnComment)链接失败"; + @:}); + @:} + } + + if (dictTableField.Any()) { + @: + @:// 映射字典值 + @:foreach(var item in pageItems) { + @:System.Text.StringBuilder sbError = new System.Text.StringBuilder(); + foreach (var column in dictTableField) { + @:if (!string.IsNullOrWhiteSpace(item.@(column.ExtendedPropertyName))) { + @:item.@(column.PropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(item.@(column.ExtendedPropertyName)); + @:if (item.@(column.PropertyName) == null) sbError.AppendLine("@(column.ColumnComment)字典映射失败"); + @:} + } + @:item.Error = sbError.ToString(); + @:} + } + + @: + @:// 校验并过滤必填基本类型为null的字段 + @:var rows = pageItems.Where(x => { + @:if (!string.IsNullOrWhiteSpace(x.Error)) return false; + foreach (var column in Model.ImportFieldList.Where(x => x.WhetherRequired == "Y" && Regex.IsMatch(x.NetType, "(int|long|double|float|bool|Enum[?]?)"))){ + @:if (x.@(column.PropertyName) == null){ + @:x.Error = "@(column.ColumnComment)不能为空"; + @:return false; + @:} + } + @:return true; + @:}).Adapt>(); + + @{var updateFields = new List();} + @: + @:var storageable = _@(Model.LowerClassName)Rep.Context.Storageable(rows) + foreach (var column in Model.ImportFieldList){ + if (column.WhetherImport == "Y"){ + updateFields.Add(column.PropertyName); + } + if (column.WhetherRequired == "Y"){ + if(column.NetType.TrimEnd('?') == "string"){ + @:.SplitError(it => string.IsNullOrWhiteSpace(it.Item.@(column.PropertyName)), "@(column.ColumnComment)不能为空") + } else if(column.NetType.EndsWith('?') == true){ + @:.SplitError(it => it.Item.@(column.PropertyName) == null, "@(column.ColumnComment)不能为空") + }} + if (column.NetType?.TrimEnd('?') == "string" && column.ColumnLength > 0){ + @:.SplitError(it => it.Item.@(column.PropertyName)?.Length > @(column.ColumnLength), "@(column.ColumnComment)长度不能超过@(column.ColumnLength)个字符") + }} + if(Model.TableUniqueConfigList.Count>0){ + foreach (var config in Model.TableUniqueConfigList) { + @:.WhereColumns(it => new { @config.Format(", ", "it.{0}") }) + } + @:.SplitInsert(it=> !it.Any()) + @:.SplitUpdate(it=> it.Any()) + }else{ + @:.SplitInsert(_=> true) // 没有设置唯一键代表插入所有数据 + } + @:.ToStorage(); + @: + @:storageable.AsInsertable.ExecuteCommand();// 不存在插入 + @:storageable.AsUpdateable.UpdateColumns(it => new + @:{ + @foreach (var field in updateFields) + { + @: it.@(field), + } + @:}).ExecuteCommand();// 存在更新 + @: + @:// 标记错误信息 + @:markerErrorAction.Invoke(storageable, pageItems, rows); + @:}); + @:}); + @: + @:return stream; + @:} + @:} + } +} diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/api.ts.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/api.ts.vm new file mode 100644 index 0000000..3419568 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/api.ts.vm @@ -0,0 +1,59 @@ +import {useBaseApi} from '/@@/api/base'; + +// @(Model.BusName)接口服务 +export const use@(Model.ClassName)Api = () => { + const baseApi = useBaseApi("@(Model.LowerClassName)"); + return { + // 分页查询@(Model.BusName) + page: baseApi.page, + // 查看@(Model.BusName)详细 + detail: baseApi.detail, + // 新增@(Model.BusName) + add: baseApi.add, + // 更新@(Model.BusName) + update: baseApi.update, + @if (Model.HasSetStatus) { + @:// 设置@(Model.BusName)状态 + @:setStatus: baseApi.setStatus, + } + // 删除@(Model.BusName) + delete: baseApi.delete, + // 批量删除@(Model.BusName) + batchDelete: baseApi.batchDelete, + @if (Model.ImportFieldList.Count > 0) { + @:// 导出@(Model.BusName)数据 + @:exportData: baseApi.exportData, + @:// 导入@(Model.BusName)数据 + @:importData: baseApi.importData, + @:// 下载@(Model.BusName)数据导入模板 + @:downloadTemplate: baseApi.downloadTemplate, + } + @if (Model.DropdownFieldList.Count > 0) { + @:// 获取下拉列表数据 + @:getDropdownData: (fromPage: Boolean = false, cancel: boolean = false) => baseApi.dropdownData({ fromPage }, cancel), + } + @foreach (var column in Model.UploadFieldList) { + @:// 上传@(column.ColumnComment) + @:upload@(column.PropertyName): (params: any, cancel: boolean = false) => baseApi.uploadFile(params, 'upload@(column.PropertyName)', cancel), + } + } +} + +// @(Model.BusName)实体 +export interface @(Model.ClassName) { +@{ +var typeMap = new Dictionary() { + { "bool", "boolean" }, + { "int", "number" }, + { "long", "number" }, + { "double", "number" }, + { "float", "number" }, + { "decimal", "number" }, + { "byte", "number" } +}; +foreach (var column in Model.TableField) { + @:// @(column.ColumnComment) + @:@(column.LowerPropertyName)@(column.WhetherRequired == "Y" ? "?" : ""): @(Regex.IsMatch("@(column.DataType.Trim('?'))", ".*?Enum") ? "number" : typeMap.GetValueOrDefault(column.DataType.Trim('?').ToLower(), "string")); +} +} +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/data.data.ts.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/data.data.ts.vm new file mode 100644 index 0000000..3308776 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/data.data.ts.vm @@ -0,0 +1,159 @@ +import { h } from 'vue'; +import { BasicColumn, FormSchema } from '/@@/components/Table'; +@foreach (var column in Model.TableField){ +if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ +@:import { uploadFile } from '/@@/api/sys/admin'; +}else if(column.EffectType == "ForeignKey"){ +@:import { get@(column.FkEntityName)Dropdown } from '/@@/api/main/@(Model.ClassName)'; +}else if(column.EffectType == "DictSelector"){ +@:import { getDataList } from '/@@/api/sys/admin'; +}else if(column.EffectType == "ApiTreeSelector"){ +@:import { get@(column.FkEntityName)Tree } from '/@@/api/main/@(Model.ClassName)'; +}else if(column.EffectType == "ConstSelector"){ +@:import { codeToName, getSelector } from '/@@/utils/helper/constSelectorHelper'; +}else if(column.EffectType == "Switch"){ +@:import { Switch } from 'ant-design-vue'; +} +} +export const columns: BasicColumn[] = [ + @foreach (var column in Model.TableField){ + if(column.WhetherTable == "Y"){ + @:{ + @:title: '@column.ColumnComment', + @:dataIndex: '@column.LowerPropertyName', + @:sorter: true, +if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @:slots: { customRender: '@(column.LowerPropertyName)' }, +}else if(column.EffectType == "ForeignKey"){ + @:customRender: ({ record }) => { + @:return record.fk@(column.PropertyName).@(column.LowerFkDisplayColumnsList?.First()); + @:}, +}else if(column.EffectType == "Switch"){ + @:customRender: ({ record }) => { + @:return h(@(column.EffectType), { checked: record.@(column.LowerPropertyName) }); + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:customRender: ({ record }) => { + @:return codeToName(record.@(column.LowerPropertyName), '@(column.DictTypeCode)'); + @:}, +} + @:}, + } + + } +]; + +export const searchFormSchema: FormSchema[] = [ +@foreach (var column in Model.WhetherQueryList){ + @:{ + @:field: '@column.LowerPropertyName', + @:label: '@column.ColumnComment', + @:colProps: { span: 8 }, +if(column.EffectType == "ForeignKey"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: get@(column.FkEntityName)Dropdown, + @:labelField: 'label', + @:valueField: 'value', + @:}, +}else if(column.EffectType == "DictSelector"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: getDataList, + @:params: '@(column.DictTypeCode)', + @:fieldNames: { + @:label: 'label', + @:value: 'value', + @:}, + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:component: 'Select', + @:componentProps: { + @:options: getSelector('@(column.DictTypeCode)'), + @:fieldNames: { + @:label: 'name', + @:value: 'code', + @:}, + @:}, +}else if(column.EffectType == "ApiTreeSelector"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:api: get@(column.FkEntityName)Tree, + @:}, +} +else if(column.NetType?.TrimEnd('?') == "DateTime" && column.QueryType == "~"){ + @:component: 'RangePicker', + @:componentProps: { + @: valueFormat:"YYYY-MM-DD" + @:}, +} else { + @:component: 'Input', +} + + @:}, +} +]; + +export const formSchema: FormSchema[] = [ + @foreach (var column in Model.TableField){ + @:{ + @:label: '@column.ColumnComment', + @:field: '@column.LowerPropertyName', +if(column.EffectType == "ForeignKey"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: get@(column.FkEntityName)Dropdown, + @:labelField: 'label', + @:valueField: 'value', + @:}, +}else if(column.EffectType == "DictSelector"){ + @:component: 'ApiSelect', + @:componentProps: { + @:api: getDataList, + @:params: '@(column.DictTypeCode)', + @:fieldNames: { + @:label: 'label', + @:value: 'value', + @:}, + @:}, +}else if(column.EffectType == "ConstSelector"){ + @:component: 'Select', + @:componentProps: { + @:options: getSelector('@(column.DictTypeCode)'), + @:fieldNames: { + @:label: 'name', + @:value: 'code', + @:}, + @:}, +}else if(column.EffectType == "ApiTreeSelector"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:api: get@(column.FkEntityName)Tree, + @:}, +}else if(column.EffectType == "Switch"){ + @:component: '@(column.EffectType)', + @:componentProps: { + @:checkedChildren: '是', + @:unCheckedChildren: '否', + @:}, +}else{ + @:component: '@column.EffectType', +} + if(column.WhetherRequired == "Y"){ + @:required: true, + }else{ + @:required: false, + } + if(column.EffectType == "Upload" || column.EffectType == "Upload_SingleFile"){ + @:componentProps: { + @:maxNumber: 1, + @:api: uploadFile, + @:}, + } + if(column.LowerPropertyName == "id"){ + @:show: false, + } + @:colProps: { span: 12 }, + @:}, + } +]; diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/dataModal.vue.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/dataModal.vue.vm new file mode 100644 index 0000000..734b41d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/dataModal.vue.vm @@ -0,0 +1,71 @@ + + diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/editDialog.vue.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/editDialog.vue.vm new file mode 100644 index 0000000..8eddc15 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/editDialog.vue.vm @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/index.vue.vm b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/index.vue.vm new file mode 100644 index 0000000..e5da3c0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/index.vue.vm @@ -0,0 +1,326 @@ + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/upload/logo.png b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/upload/logo.png new file mode 100644 index 0000000..da79429 Binary files /dev/null and b/Admin.NET-v2/Admin.NET/Admin.NET.Web.Entry/wwwroot/upload/logo.png differ diff --git a/Admin.NET-v2/Admin.NET/Admin.NET.sln b/Admin.NET-v2/Admin.NET/Admin.NET.sln new file mode 100644 index 0000000..fffd0b1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Admin.NET.sln @@ -0,0 +1,106 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36705.20 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Application", "Admin.NET.Application\Admin.NET.Application.csproj", "{C3F5AEC5-ACEE-4109-94E3-3F981DC18268}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Core", "Admin.NET.Core\Admin.NET.Core.csproj", "{3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Web.Core", "Admin.NET.Web.Core\Admin.NET.Web.Core.csproj", "{8A42A864-A69E-40F7-975E-F2FA36E7DFEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Web.Entry", "Admin.NET.Web.Entry\Admin.NET.Web.Entry.csproj", "{11EA630B-4600-4236-A117-CE6C6CD67586}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Test", "Admin.NET.Test\Admin.NET.Test.csproj", "{57350E6A-B5A0-452C-9FD4-C69617C6DA30}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{662E0B8E-F23E-4C7D-80BD-CAA5707503CC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{76F70D22-8D53-468E-A3B6-1704666A1D71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.GoView", "Plugins\Admin.NET.Plugin.GoView\Admin.NET.Plugin.GoView.csproj", "{C4A288D5-0FAA-4F43-9072-B97635D7871D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.DingTalk", "Plugins\Admin.NET.Plugin.DingTalk\Admin.NET.Plugin.DingTalk.csproj", "{F6A002AD-CF7F-4771-8597-F12A50A93DAA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.ReZero", "Plugins\Admin.NET.Plugin.ReZero\Admin.NET.Plugin.ReZero.csproj", "{04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.ApprovalFlow", "Plugins\Admin.NET.Plugin.ApprovalFlow\Admin.NET.Plugin.ApprovalFlow.csproj", "{902A91A7-5EF0-4A63-BC2C-9B783DC00880}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.K3Cloud", "Plugins\Admin.NET.Plugin.K3Cloud\Admin.NET.Plugin.K3Cloud.csproj", "{72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin", "Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj", "{BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.HwPortal", "Plugins\Admin.NET.Plugin.HwPortal\Admin.NET.Plugin.HwPortal.csproj", "{3CDD5FAB-9A94-424F-B055-1DFF8018CEB1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3F5AEC5-ACEE-4109-94E3-3F981DC18268}.Release|Any CPU.Build.0 = Release|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD1A3ED-ED11-479D-BE32-6589D98A9ADC}.Release|Any CPU.Build.0 = Release|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A42A864-A69E-40F7-975E-F2FA36E7DFEE}.Release|Any CPU.Build.0 = Release|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11EA630B-4600-4236-A117-CE6C6CD67586}.Release|Any CPU.Build.0 = Release|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57350E6A-B5A0-452C-9FD4-C69617C6DA30}.Release|Any CPU.Build.0 = Release|Any CPU + {C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4A288D5-0FAA-4F43-9072-B97635D7871D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4A288D5-0FAA-4F43-9072-B97635D7871D}.Release|Any CPU.Build.0 = Release|Any CPU + {F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6A002AD-CF7F-4771-8597-F12A50A93DAA}.Release|Any CPU.Build.0 = Release|Any CPU + {04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04AB2E76-DE8B-4EFD-9F48-F8D4C0993106}.Release|Any CPU.Build.0 = Release|Any CPU + {902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Debug|Any CPU.Build.0 = Debug|Any CPU + {902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.ActiveCfg = Release|Any CPU + {902A91A7-5EF0-4A63-BC2C-9B783DC00880}.Release|Any CPU.Build.0 = Release|Any CPU + {72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72EB89AB-15F7-4F85-88DB-7C2EF7C3D588}.Release|Any CPU.Build.0 = Release|Any CPU + {BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4}.Release|Any CPU.Build.0 = Release|Any CPU + {3CDD5FAB-9A94-424F-B055-1DFF8018CEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CDD5FAB-9A94-424F-B055-1DFF8018CEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CDD5FAB-9A94-424F-B055-1DFF8018CEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CDD5FAB-9A94-424F-B055-1DFF8018CEB1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C4A288D5-0FAA-4F43-9072-B97635D7871D} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {F6A002AD-CF7F-4771-8597-F12A50A93DAA} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {04AB2E76-DE8B-4EFD-9F48-F8D4C0993106} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {902A91A7-5EF0-4A63-BC2C-9B783DC00880} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {72EB89AB-15F7-4F85-88DB-7C2EF7C3D588} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {BFE4764F-1FF8-47A7-B4AD-085F7D8CD6C4} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + {3CDD5FAB-9A94-424F-B055-1DFF8018CEB1} = {76F70D22-8D53-468E-A3B6-1704666A1D71} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3} + EndGlobalSection +EndGlobal diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Admin.NET.Plugin.ApprovalFlow.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Admin.NET.Plugin.ApprovalFlow.csproj new file mode 100644 index 0000000..fd050e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Admin.NET.Plugin.ApprovalFlow.csproj @@ -0,0 +1,26 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + true + PreserveNewest + PreserveNewest + true + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Configuration/ApprovalFlow.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Configuration/ApprovalFlow.json new file mode 100644 index 0000000..1453fe6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Configuration/ApprovalFlow.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "[openapi:ApprovalFlow]": { + "Group": "ApprovalFlow", + "Title": "审批流程", + "Description": "对业务实体数据的增删改操作进行流程审批。", + "Version": "1.0.0", + "Order": 100 + }, + "ApprovalFlow": { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Const/ApprovalFlowConst.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Const/ApprovalFlowConst.cs new file mode 100644 index 0000000..52952ac --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Const/ApprovalFlowConst.cs @@ -0,0 +1,19 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流程相关常量 +/// +[Const("审批流程相关常量")] +public class ApprovalFlowConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "ApprovalFlow"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlow.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlow.cs new file mode 100644 index 0000000..ec4ba3e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlow.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流程信息表 +/// +[SugarTable(null, "审批流程信息表")] +public class ApprovalFlow : EntityBaseOrgDel +{ + /// + /// 编号 + /// + [SugarColumn(ColumnDescription = "编号", Length = 32)] + [MaxLength(32)] + public string? Code { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 32)] + [MaxLength(32)] + public string Name { get; set; } + + /// + /// 表单 + /// + [SugarColumn(ColumnDescription = "表单", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormJson { get; set; } + + /// + /// 流程 + /// + [SugarColumn(ColumnDescription = "流程", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FlowJson { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public int? Status { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowRecord.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowRecord.cs new file mode 100644 index 0000000..6d1dff9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFlowRecord.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流流程记录 +/// +[SugarTable(null, "审批流流程记录")] +public class ApprovalFlowRecord : EntityBaseOrg +{ + /// + /// 表单名称 + /// + [SugarColumn(ColumnDescription = "表单名称", Length = 255)] + public string? FormName { get; set; } + + /// + /// 表单状态 + /// + [SugarColumn(ColumnDescription = "表单状态", Length = 32)] + public string? FormStatus { get; set; } + + /// + /// 表单触发 + /// + [SugarColumn(ColumnDescription = "表单触发", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormJson { get; set; } + + /// + /// 表单结果 + /// + [SugarColumn(ColumnDescription = "表单结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormResult { get; set; } + + /// + /// 流程结构 + /// + [SugarColumn(ColumnDescription = "流程结构", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FlowJson { get; set; } + + /// + /// 流程结果 + /// + [SugarColumn(ColumnDescription = "流程结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FlowResult { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalForm.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalForm.cs new file mode 100644 index 0000000..c272cab --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalForm.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流表单 +/// +[SugarTable(null, "审批流表单")] +public class ApprovalForm : EntityBaseOrg +{ + /// + /// 编号 + /// + [SugarColumn(ColumnDescription = "编号", Length = 32)] + public string? Code { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 32)] + public string? Name { get; set; } + + /// + /// 表单名称 + /// + [SugarColumn(ColumnDescription = "表单名称", Length = 32)] + public string? FormName { get; set; } + + /// + /// 表单属性 + /// + [SugarColumn(ColumnDescription = "表单属性", Length = 32)] + public string? FormType { get; set; } + + /// + /// 表单状态 + /// + [SugarColumn(ColumnDescription = "表单状态")] + public int? FormStatus { get; set; } + + /// + /// 表单结果 + /// + [SugarColumn(ColumnDescription = "表单结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormResult { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public int? Status { get; set; } + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 255)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFormRecord.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFormRecord.cs new file mode 100644 index 0000000..ec6dd74 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Entity/ApprovalFormRecord.cs @@ -0,0 +1,56 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流表单记录 +/// +[SugarTable(null, "审批流表单记录")] +public class ApprovalFormRecord : EntityBaseOrg +{ + /// + /// 流程Id + /// + [SugarColumn(ColumnDescription = "流程Id")] + public long? FlowId { get; set; } + + /// + /// 表单名称 + /// + [SugarColumn(ColumnDescription = "表单名称", Length = 32)] + public string? FormName { get; set; } + + /// + /// 表单类型 + /// + [SugarColumn(ColumnDescription = "表单类型", Length = 32)] + public string? FormType { get; set; } + + /// + /// 表单状态 + /// + [SugarColumn(ColumnDescription = "表单状态", Length = 11)] + public string? FormStatus { get; set; } + + /// + /// 修改前 + /// + [SugarColumn(ColumnDescription = "修改前", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormBefore { get; set; } + + /// + /// 修改后 + /// + [SugarColumn(ColumnDescription = "修改后", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormAfter { get; set; } + + /// + /// 表单结果 + /// + [SugarColumn(ColumnDescription = "表单结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? FormResult { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTypeEnum.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTypeEnum.cs new file mode 100644 index 0000000..3b165cb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Enum/FlowTypeEnum.cs @@ -0,0 +1,15 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 流程类型枚举 +/// +[Description("流程类型枚举")] +public enum FlowTypeEnum +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/GlobalUsings.cs new file mode 100644 index 0000000..904ea93 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/GlobalUsings.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core; +global using Furion; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.FriendlyException; +global using Mapster; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.DependencyInjection; +global using SqlSugar; +global using System; +global using System.Collections.Generic; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Middleware/ApprovalFlowMiddleware.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Middleware/ApprovalFlowMiddleware.cs new file mode 100644 index 0000000..9cba887 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Middleware/ApprovalFlowMiddleware.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.ApprovalFlow.Service; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 扩展审批流中间件 +/// +public static class ApprovalFlowMiddlewareExtensions +{ + /// + /// 使用审批流 + /// + /// + /// + public static IApplicationBuilder UseApprovalFlow(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} + +/// +/// 审批流中间件 +/// +public class ApprovalFlowMiddleware +{ + private readonly RequestDelegate _next; + private readonly SysApprovalService _sysApprovalService; + + public ApprovalFlowMiddleware(RequestDelegate next) + { + _next = next; + _sysApprovalService = App.GetRequiredService(); + } + + public async Task InvokeAsync(HttpContext context) + { + await _sysApprovalService.MatchApproval(context); + + // 调用下一个中间件 + await _next(context); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs new file mode 100644 index 0000000..a538a1c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/SeedData/SysMenuSeedData.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow; + +/// +/// 审批流程菜单表种子数据 +/// +public class SysMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysMenu{ Id=1310300010101, Pid=1300300000101, Title="审批流程", Path="/platform/approvalFlow", Name="approvalFlow", Component="/approvalFlow/index", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=2000 }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs new file mode 100644 index 0000000..d720384 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/ApprovalFlowService.cs @@ -0,0 +1,154 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json; + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +/// +/// 审批流程服务 +/// +[ApiDescriptionSettings(ApprovalFlowConst.GroupName, Order = 100)] +public class ApprovalFlowService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _approvalFlowRep; + + public ApprovalFlowService(SqlSugarRepository approvalFlowRep) + { + _approvalFlowRep = approvalFlowRep; + } + + /// + /// 分页查询审批流 + /// + /// + /// + [HttpPost] + [ApiDescriptionSettings(Name = "Page")] + public async Task> Page(ApprovalFlowInput input) + { + return await _approvalFlowRep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Code.Contains(input.Keyword.Trim()) || u.Name.Contains(input.Keyword.Trim()) || u.Remark.Contains(input.Keyword.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), u => u.Code.Contains(input.Code.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim())) + .WhereIF(!string.IsNullOrWhiteSpace(input.Remark), u => u.Remark.Contains(input.Remark.Trim())) + .Select() + .ToPagedListAsync(input.Page, input.PageSize); + } + + /// + /// 增加审批流 + /// + /// + /// + [ApiDescriptionSettings(Name = "Add"), HttpPost] + public async Task Add(AddApprovalFlowInput input) + { + var entity = input.Adapt(); + if (input.Code == null) + { + entity.Code = await LastCode(""); + } + await _approvalFlowRep.InsertAsync(entity); + return entity.Id; + } + + /// + /// 更新审批流 + /// + /// + /// + [ApiDescriptionSettings(Name = "Update"), HttpPost] + public async Task Update(UpdateApprovalFlowInput input) + { + var entity = input.Adapt(); + await _approvalFlowRep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); + } + + /// + /// 删除审批流 + /// + /// + /// + [ApiDescriptionSettings(Name = "Delete"), HttpPost] + public async Task Delete(DeleteApprovalFlowInput input) + { + var entity = await _approvalFlowRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002); + await _approvalFlowRep.FakeDeleteAsync(entity); // 假删除 + } + + /// + /// 获取审批流 + /// + /// + /// + public async Task GetDetail([FromQuery] QueryByIdApprovalFlowInput input) + { + return await _approvalFlowRep.GetByIdAsync(input.Id); + } + + /// + /// 根据编码获取审批流信息 + /// + /// + /// + public async Task GetInfo([FromQuery] string code) + { + return await _approvalFlowRep.GetFirstAsync(u => u.Code == code); + } + + /// + /// 获取审批流列表 + /// + /// + /// + public async Task> GetList([FromQuery] ApprovalFlowInput input) + { + return await _approvalFlowRep.AsQueryable().Select().ToListAsync(); + } + + /// + /// 获取今天创建的最大编号 + /// + /// + /// + private async Task LastCode(string prefix) + { + var today = DateTime.Now.Date; + var count = await _approvalFlowRep.AsQueryable().Where(u => u.CreateTime >= today).CountAsync(); + return prefix + DateTime.Now.ToString("yyMMdd") + string.Format("{0:d2}", count + 1); + } + + [HttpGet] + [ApiDescriptionSettings(Name = "FlowList")] + [DisplayName("获取审批流结构")] + public async Task FlowList([FromQuery] string code) + { + var result = await _approvalFlowRep.AsQueryable().Where(u => u.Code == code).Select().FirstAsync(); + var FlowJson = result.FlowJson != null ? JsonSerializer.Deserialize(result.FlowJson) : new ApprovalFlowItem(); + var FormJson = result.FormJson != null ? JsonSerializer.Deserialize(result.FormJson) : new ApprovalFormItem(); + return new + { + FlowJson, + FormJson + }; + } + + [HttpGet] + [ApiDescriptionSettings(Name = "FormRoutes")] + [DisplayName("获取审批流规则")] + public async Task> FormRoutes() + { + var results = await _approvalFlowRep.AsQueryable().Select().ToListAsync(); + var list = new List(); + foreach (var item in results) + { + var FormJson = item.FormJson != null ? JsonSerializer.Deserialize(item.FormJson) : new ApprovalFormItem(); + if (item.FormJson != null) list.Add(FormJson.Route); + } + return list; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowDto.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowDto.cs new file mode 100644 index 0000000..a8fd9a8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowDto.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +/// +/// 审批流输出参数 +/// +public class ApprovalFlowDto +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 编号 + /// + public string? Code { get; set; } + + /// + /// 名称 + /// + public string? Name { get; set; } + + /// + /// 表单 + /// + public string? FormJson { get; set; } + + /// + /// 流程 + /// + public string? FlowJson { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } + + /// + /// 创建者部门Id + /// + public long? CreateOrgId { get; set; } + + /// + /// 创建者部门名称 + /// + public string? CreateOrgName { get; set; } + + /// + /// 软删除 + /// + public bool IsDelete { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs new file mode 100644 index 0000000..dc9328f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowInput.cs @@ -0,0 +1,142 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +/// +/// 审批流基础输入参数 +/// +public class ApprovalFlowBaseInput +{ + /// + /// 编号 + /// + public virtual string? Code { get; set; } + + /// + /// 名称 + /// + public virtual string? Name { get; set; } + + /// + /// 表单 + /// + public virtual string? FormJson { get; set; } + + /// + /// 流程 + /// + public virtual string? FlowJson { get; set; } + + /// + /// 备注 + /// + public virtual string? Remark { get; set; } + + /// + /// 创建时间 + /// + public virtual DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public virtual DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public virtual long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public virtual string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public virtual long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public virtual string? UpdateUserName { get; set; } + + /// + /// 创建者部门Id + /// + public virtual long? CreateOrgId { get; set; } + + /// + /// 创建者部门名称 + /// + public virtual string? CreateOrgName { get; set; } + + /// + /// 软删除 + /// + public virtual bool IsDelete { get; set; } +} + +/// +/// 审批流分页查询输入参数 +/// +public class ApprovalFlowInput : BasePageInput +{ + /// + /// 编号 + /// + public string? Code { get; set; } + + /// + /// 名称 + /// + public string? Name { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } +} + +/// +/// 审批流增加输入参数 +/// +public class AddApprovalFlowInput : ApprovalFlowBaseInput +{ + /// + /// 软删除 + /// + [Required(ErrorMessage = "软删除不能为空")] + public override bool IsDelete { get; set; } +} + +/// +/// 审批流删除输入参数 +/// +public class DeleteApprovalFlowInput : BaseIdInput +{ +} + +/// +/// 审批流更新输入参数 +/// +public class UpdateApprovalFlowInput : ApprovalFlowBaseInput +{ + /// + /// 主键Id + /// + [Required(ErrorMessage = "主键Id不能为空")] + public long Id { get; set; } +} + +/// +/// 审批流主键查询输入参数 +/// +public class QueryByIdApprovalFlowInput : DeleteApprovalFlowInput +{ +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs new file mode 100644 index 0000000..e5bf95f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowItem.cs @@ -0,0 +1,96 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json.Serialization; + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +public class ApprovalFlowItem +{ + [JsonPropertyName("nodes")] + public List Nodes { get; set; } + + [JsonPropertyName("edges")] + public List Edges { get; set; } +} + +public class ApprovalFlowNodeItem +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("x")] + public float X { get; set; } + + [JsonPropertyName("y")] + public float Y { get; set; } + + [JsonPropertyName("properties")] + public FlowProperties Properties { get; set; } + + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public FlowTextItem Text { get; set; } +} + +public class ApprovalFlowEdgeItem +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("sourceNodeId")] + public string SourceNodeId { get; set; } + + [JsonPropertyName("targetNodeId")] + public string TargetNodeId { get; set; } + + [JsonPropertyName("startPoint")] + public FlowEdgePointItem StartPoint { get; set; } + + [JsonPropertyName("endPoint")] + public FlowEdgePointItem EndPoint { get; set; } + + [JsonPropertyName("properties")] + public FlowProperties Properties { get; set; } + + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public FlowTextItem Text { get; set; } + + [JsonPropertyName("pointsList")] + public List PointsList { get; set; } +} + +public class FlowProperties +{ +} + +public class FlowTextItem +{ + [JsonPropertyName("x")] + public float X { get; set; } + + [JsonPropertyName("y")] + public float Y { get; set; } + + [JsonPropertyName("value")] + public string Value { get; set; } +} + +public class FlowEdgePointItem +{ + [JsonPropertyName("x")] + public float X { get; set; } + + [JsonPropertyName("y")] + public float Y { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs new file mode 100644 index 0000000..81f4b49 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFlowOutput.cs @@ -0,0 +1,88 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +/// +/// 审批流输出参数 +/// +public class ApprovalFlowOutput +{ + /// + /// 主键Id + /// + public long Id { get; set; } + + /// + /// 编号 + /// + public string? Code { get; set; } + + /// + /// 名称 + /// + public string? Name { get; set; } + + /// + /// 表单 + /// + public string? FormJson { get; set; } + + /// + /// 流程 + /// + public string? FlowJson { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 创建者姓名 + /// + public string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } + + /// + /// 修改者姓名 + /// + public string? UpdateUserName { get; set; } + + /// + /// 创建者部门Id + /// + public long? CreateOrgId { get; set; } + + /// + /// 创建者部门名称 + /// + public string? CreateOrgName { get; set; } + + /// + /// 软删除 + /// + public bool IsDelete { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFormItem.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFormItem.cs new file mode 100644 index 0000000..37cecad --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/ApprovalFlow/Dto/ApprovalFormItem.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using System.Text.Json.Serialization; + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +public class ApprovalFormItem +{ + [JsonPropertyName("configId")] + public string ConfigId { get; set; } + + [JsonPropertyName("tableName")] + public string TableName { get; set; } + + [JsonPropertyName("entityName")] + public string EntityName { get; set; } + + [JsonPropertyName("typeName")] + public string TypeName { get; set; } + + [JsonPropertyName("route")] + public string Route => EntityName[..1].ToLower() + EntityName[1..] + "/" + TypeName; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/SysApproval/SysApprovalService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/SysApproval/SysApprovalService.cs new file mode 100644 index 0000000..ec62cc7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Service/SysApproval/SysApprovalService.cs @@ -0,0 +1,81 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Http; + +namespace Admin.NET.Plugin.ApprovalFlow.Service; + +public class SysApprovalService : ITransient +{ + private readonly SqlSugarRepository _approvalFlowRep; + private readonly SqlSugarRepository _approvalFormRep; + private readonly ApprovalFlowService _approvalFlowService; + + public SysApprovalService(SqlSugarRepository approvalFlowRep, SqlSugarRepository approvalFormRep, ApprovalFlowService approvalFlowService) + { + _approvalFlowRep = approvalFlowRep; + _approvalFormRep = approvalFormRep; + _approvalFlowService = approvalFlowService; + } + + /// + /// 匹配审批流程 + /// + /// + /// + [NonAction] + public async Task MatchApproval(HttpContext context) + { + var request = context.Request; + var response = context.Response; + + var path = request.Path.ToString().Split("/"); + + var method = request.Method; + var qs = request.QueryString; + var h = request.Headers; + var b = request.Body; + + var requestHeaders = request.Headers; + var responseHeaders = response.Headers; + + var serviceName = path[1]; + if (serviceName.StartsWith("api")) + { + if (path.Length > 3) + { + var funcName = path[2]; + var typeName = path[3]; + + var list = await _approvalFlowService.FormRoutes(); + if (list.Any(u => u.Contains(funcName) && u.Contains(typeName))) + { + var approvalFlow = new ApprovalFlowRecord + { + FormName = funcName, + CreateTime = DateTime.Now, + }; + + // 判断是否需要审批 + await _approvalFlowRep.InsertAsync(approvalFlow); + + var approvalForm = new ApprovalFormRecord + { + FlowId = approvalFlow.Id, + FormName = funcName, + FormType = typeName, + CreateTime = DateTime.Now, + }; + + // 判断是否需要审批 + await _approvalFormRep.InsertAsync(approvalForm); + } + } + } + + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Startup.cs new file mode 100644 index 0000000..f8d7f83 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ApprovalFlow/Startup.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Admin.NET.Plugin.ApprovalFlow; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseApprovalFlow(); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Admin.NET.Plugin.DingTalk.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Admin.NET.Plugin.DingTalk.csproj new file mode 100644 index 0000000..8d66519 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Admin.NET.Plugin.DingTalk.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Configuration/DingTalk.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Configuration/DingTalk.json new file mode 100644 index 0000000..4efc809 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Configuration/DingTalk.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "[openapi:DingTalk]": { + "Group": "DingTalk", + "Title": "钉钉开放平台", + "Description": "集成钉钉开放平台", + "Version": "1.0.0", + "Order": 90 + }, + "DingTalk": { + "AppId": "", + "AgentId": "", + "ClientId": "xxxx", // 原 AppKey 和 SuiteKey + "ClientSecret": "xxxx" // 原 AppSecret 和 SuiteSecret + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Const/DingTalkConst.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Const/DingTalkConst.cs new file mode 100644 index 0000000..4547390 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Const/DingTalkConst.cs @@ -0,0 +1,49 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉相关常量 +/// +[Const("钉钉相关常量")] +public class DingTalkConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "DingTalk"; + + /// + /// 姓名 + /// + public const string NameField = "sys00-name"; + + /// + /// 手机号 + /// + public const string MobileField = "sys00-mobile"; + + /// + /// 工号 + /// + public const string JobNumberField = "sys00-jobNumber"; + + /// + /// 主部门Id + /// + public const string DeptId = "sys00-mainDeptId"; + + /// + /// 主部门 + /// + public const string Dept = "sys00-mainDept"; + + /// + /// 职位 + /// + public const string Position = "sys00-position"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkDept.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkDept.cs new file mode 100644 index 0000000..3cae568 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkDept.cs @@ -0,0 +1,47 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉部门信息 +/// +[SugarTable("ding_talk_dept", "钉钉部门表")] +public class DingTalkDept +{ + /// + /// 部门id + /// + [SugarColumn(ColumnName = "Id", ColumnDescription = "部门id", IsPrimaryKey = true, IsIdentity = false)] + [Required] + public long dept_id { get; set; } + + /// + /// 上级部门id + /// + [SugarColumn(ColumnDescription = "上级部门id")] + [Required] + public virtual long parent_id { get; set; } + + /// + /// 部门名 + /// + [SugarColumn(ColumnDescription = "部门名", Length = 64)] + [MaxLength(64)] + public string? name { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)] + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public virtual DateTime? UpdateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkRoleUser.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkRoleUser.cs new file mode 100644 index 0000000..8871f2e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkRoleUser.cs @@ -0,0 +1,49 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉角色信息 +/// +[SugarTable(null, "钉钉角色表")] +public class DingTalkRoleUser : EntityBaseDel +{ + /// + /// 钉钉用户id + /// + [SugarColumn(ColumnDescription = "钉钉用户id", Length = 64)] + [Required, MaxLength(64)] + public virtual string? DingTalkUserId { get; set; } + + /// + /// 角色组id + /// + [SugarColumn(ColumnDescription = "角色组id")] + [Required] + public virtual long groupId { get; set; } + + /// + /// 角色组名称 + /// + [SugarColumn(ColumnDescription = "角色组名", Length = 64)] + [MaxLength(64)] + public string? groupName { get; set; } + + /// + /// 角色id + /// + [SugarColumn(ColumnDescription = "角色id")] + [Required] + public virtual long roleId { get; set; } + + /// + /// 角色名 + /// + [SugarColumn(ColumnDescription = "角色名", Length = 64)] + [MaxLength(64)] + public string? roleName { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkUser.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkUser.cs new file mode 100644 index 0000000..fe2f013 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkUser.cs @@ -0,0 +1,97 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉用户表 +/// +[SugarTable(null, "钉钉用户表")] +public class DingTalkUser : EntityBase +{ + /// + /// 系统用户Id + /// + [SugarColumn(ColumnDescription = "系统用户Id")] + public long SysUserId { get; set; } + + /// + /// 系统用户 + /// + [SugarColumn(IsIgnore = true)] + [Navigate(NavigateType.OneToOne, nameof(SysUserId))] + [JsonIgnore] + public SysUser SysUser { get; set; } + + /// + /// 钉钉用户id + /// + [SugarColumn(ColumnDescription = "钉钉用户id", Length = 64)] + [Required, MaxLength(64)] + public virtual string? DingTalkUserId { get; set; } + + /// + /// UnionId + /// + [SugarColumn(ColumnDescription = "UnionId", Length = 64)] + [MaxLength(64)] + public string? UnionId { get; set; } + + /// + /// 用户名 + /// + [SugarColumn(ColumnDescription = "用户名", Length = 64)] + [MaxLength(64)] + public string? Name { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Mobile { get; set; } + + /// + /// 性别 + /// + [SugarColumn(ColumnDescription = "性别")] + public int? Sex { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 256)] + [MaxLength(256)] + public string? Avatar { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 16)] + [MaxLength(16)] + public string? JobNumber { get; set; } + + /// + /// 主部门Id + /// + [SugarColumn(ColumnDescription = "主部门Id", Length = 16)] + [MaxLength(16)] + public string? DeptId { get; set; } + + /// + /// 主部门 + /// + [SugarColumn(ColumnDescription = "主部门", Length = 16)] + [MaxLength(16)] + public string? Dept { get; set; } + + /// + /// 职位 + /// + [SugarColumn(ColumnDescription = "职位", Length = 16)] + [MaxLength(16)] + public string? Position { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowConfig.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowConfig.cs new file mode 100644 index 0000000..a363b05 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowConfig.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +[SugarTable("ding_talk_wokerflow_config", "审批配置表")] +public class DingTalkWokerflowConfig +{ + /// + /// 审批名 + /// + [SugarColumn(ColumnDescription = "审批名", IsPrimaryKey = true, IsIdentity = false)] + public string WorkflowName { get; set; } + + /// + /// 审批单Id + /// + [SugarColumn(ColumnDescription = "审批单Id")] + public string ProcessCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowLog.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowLog.cs new file mode 100644 index 0000000..4735218 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Entity/DingTalkWokerflowLog.cs @@ -0,0 +1,86 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +[SugarTable("ding_talk_wokerflow_log", "钉钉审批日志")] +public class DingTalkWokerflowLog +{ + /// + /// 审批实例ID + /// + [SugarColumn(ColumnDescription = "审批实例ID", IsPrimaryKey = true, IsIdentity = false)] + public string instanceId { get; set; } + + /// + /// 审批单号 + /// + [SugarColumn(ColumnDescription = "审批单号")] + public string? WorkflowId { get; set; } + + /// + /// 来源单据 + /// + [SugarColumn(ColumnDescription = "来源单据")] + public string SourceDocument { get; set; } + + /// + /// 审批完成时间 + /// + [SugarColumn(ColumnDescription = "审批完成时间")] + public DateTime? EndTime { get; set; } + + /// + /// 其他信息 + /// + [SugarColumn(ColumnDescription = "其他信息", IsJson = true)] + public Dictionary? other_info { get; set; } + + /// + /// 是否回传结果给第三方 + /// + [SugarColumn(ColumnDescription = "是否回传结果")] + public bool? isReturn { get; set; } + + /// + /// 审批状态 + /// + /// + /// RUNNING:审批中 TERMINATED:已撤销 COMPLETED:审批完成 + /// /// + [SugarColumn(ColumnDescription = "审批状态")] + public string Status { get; set; } + + /// + /// 任务ID + /// + [SugarColumn(ColumnDescription = "任务ID")] + public long? taskId { get; set; } + + /// + /// 审批结果 agree:同意 refuse:拒绝 + /// + [SugarColumn(ColumnDescription = "审批结果")] + public string? Result { get; set; } + + /// + /// 创建者姓名 + /// + [SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)] + public string? CreateUserName { get; set; } + + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)] + public DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public virtual DateTime? UpdateTime { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Enum/DingTalkConversationTypeEnum.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Enum/DingTalkConversationTypeEnum.cs new file mode 100644 index 0000000..265564d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Enum/DingTalkConversationTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉发送的会话类型枚举 +/// +[Description("钉钉发送的会话类型枚举")] +public enum DingTalkConversationTypeEnum +{ + /// + /// 单聊 + /// + [Description("单聊")] + SingleChat = 0, + + /// + /// 群聊 + /// + [Description("群聊")] + GroupChat = 1 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/GlobalUsings.cs new file mode 100644 index 0000000..0ba5fc5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/GlobalUsings.cs @@ -0,0 +1,21 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core; +global using Furion; +global using Furion.ConfigurableOptions; +global using Furion.DependencyInjection; +global using Furion.DynamicApiController; +global using Furion.FriendlyException; +global using Furion.HttpRemote; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.Options; +global using Newtonsoft.Json; +global using SqlSugar; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Data; +global using System.Linq.Dynamic.Core; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkDeptJob.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkDeptJob.cs new file mode 100644 index 0000000..f151ad6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkDeptJob.cs @@ -0,0 +1,95 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.DingTalk; +using Furion.Schedule; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Admin.NET.Plugin.Job; + +/// +/// 同步钉钉角色job,自动同步触发器请在web页面按需求设置 +/// +[JobDetail("SyncDingTalkDeptJob", Description = "同步钉钉部门", GroupName = "default", Concurrent = false)] +[Daily(TriggerId = "SyncDingTalkDeptTrigger", Description = "同步钉钉部门")] +public class SyncDingTalkDeptJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IDingTalkApi _dingTalkApi; + private readonly ILogger _logger; + private readonly SqlSugarRepository _dingTalkDeptRep; + + public SyncDingTalkDeptJob( + IServiceScopeFactory scopeFactory, + IDingTalkApi dingTalkApi, + SqlSugarRepository dingTalkDeptRep, + ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _dingTalkApi = dingTalkApi; + _dingTalkDeptRep = dingTalkDeptRep; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + var _dingTalkOptions = serviceScope.ServiceProvider.GetRequiredService>(); + + // 获取Token + var tokenRes = await _dingTalkApi.GetDingTalkToken(_dingTalkOptions.Value.ClientId, _dingTalkOptions.Value.ClientSecret); + if (tokenRes.ErrCode != 0) + throw Oops.Oh(tokenRes.ErrMsg); + + var dingTalkDeptList = new List(); + // 获取部门列表 + var deptIdsRes = await _dingTalkApi.GetDingTalkDept(tokenRes.AccessToken, new GetDingTalkDeptInput + { dept_id = 1 }); + if (deptIdsRes.ErrCode != 0) + { + _logger.LogError(deptIdsRes.ErrMsg); + throw Oops.Oh(deptIdsRes.ErrMsg); + } + dingTalkDeptList.AddRange(deptIdsRes.Result.Select(d => new DingTalkDept + { + dept_id = d.dept_id, + name = d.name, + parent_id = d.parent_id + })); + foreach (var item in deptIdsRes.Result) + { + dingTalkDeptList.AddRange(await GetDingTalkDeptList(tokenRes.AccessToken, item.dept_id)); + } + await _dingTalkDeptRep.InsertOrUpdateAsync(dingTalkDeptList); + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("【" + DateTime.Now + "】同步钉钉部门"); + Console.ForegroundColor = originColor; + } + + private async Task> GetDingTalkDeptList(string token, long dept_id) + { + List listTemp = new List(); + var deptIdsRes = await _dingTalkApi.GetDingTalkDept(token, new GetDingTalkDeptInput + { dept_id = dept_id }); + if (deptIdsRes.ErrCode != 0) + { + return null; + } + listTemp.AddRange(deptIdsRes.Result.Select(x => new DingTalkDept + { + dept_id = x.dept_id, + name = x.name, + parent_id = x.parent_id + })); + foreach (var item in deptIdsRes.Result) + { + listTemp.AddRange(await GetDingTalkDeptList(token, item.dept_id)); + } + return listTemp; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkRoleJob.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkRoleJob.cs new file mode 100644 index 0000000..b99a1d1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkRoleJob.cs @@ -0,0 +1,140 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.DingTalk; +using Furion.Schedule; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Admin.NET.Plugin.Job; + +/// +/// 同步钉钉角色job,自动同步触发器请在web页面按需求设置 +/// +[JobDetail("SyncDingTalkRoleJob", Description = "同步钉钉角色", GroupName = "default", Concurrent = false)] +public class SyncDingTalkRoleJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IDingTalkApi _dingTalkApi; + private readonly ILogger _logger; + + public SyncDingTalkRoleJob(IServiceScopeFactory scopeFactory, IDingTalkApi dingTalkApi, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _dingTalkApi = dingTalkApi; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + var _dingTalkRoleRepo = serviceScope.ServiceProvider.GetRequiredService>(); + var _dingTalkOptions = serviceScope.ServiceProvider.GetRequiredService>(); + + // 获取Token + var tokenRes = await _dingTalkApi.GetDingTalkToken(_dingTalkOptions.Value.ClientId, _dingTalkOptions.Value.ClientSecret); + if (tokenRes.ErrCode != 0) + throw Oops.Oh(tokenRes.ErrMsg); + + var dingTalkRoleUserList = new List(); + // 获取角色列表 + var roleIdsRes = await _dingTalkApi.GetDingTalkRoleList(tokenRes.AccessToken, new GetDingTalkCurrentRoleListInput + { }); + if (roleIdsRes.Success) + { + _logger.LogError(roleIdsRes.ErrMsg); + throw Oops.Oh(roleIdsRes.ErrMsg); + } + foreach (var item in roleIdsRes.Result.list) + { + foreach (var role_item in item.roles) + { + // 根据角色id获取指定角色的员工列表 + var role_user = await _dingTalkApi.GetDingTalkRoleSimplelist( + tokenRes.AccessToken, + new GetDingTalkCurrentRoleSimplelistInput() + { + role_id = role_item.id, + } + ); + + if (role_user.Success) + { + _logger.LogError(role_user.ErrMsg); + break; + } + var tempList = role_user.Result.list.Select(u => new DingTalkRoleUser + { + DingTalkUserId = u.userid, + groupId = item.groupId, + groupName = item.name, + roleId = role_item.id, + roleName = role_item.name + }).ToList(); + if (tempList?.Count > 0) + { + dingTalkRoleUserList.AddRange(tempList); + } + } + } + + // 判断新增还是更新 + var sysDingTalkRoleList = await _dingTalkRoleRepo.AsQueryable().ToListAsync(); + // 需要更新的用户Id + var uDingTalkRole = dingTalkRoleUserList.Where(u => sysDingTalkRoleList.Any(m => m.DingTalkUserId == u.DingTalkUserId && m.groupId == u.groupId)); + // 需要新增的用户Id + var iDingTalkRole = dingTalkRoleUserList.Where(u => !sysDingTalkRoleList.Any(m => m.DingTalkUserId == u.DingTalkUserId && m.groupId == u.groupId)); + // 需要删除的数据 + var dDingTalkRole = sysDingTalkRoleList.Where(u => !dingTalkRoleUserList.Any(m => m.DingTalkUserId == u.DingTalkUserId && m.groupId == u.groupId)).ToList(); + // 新增钉钉角色 + var iUser = iDingTalkRole.Select(res => new DingTalkRoleUser + { + DingTalkUserId = res.DingTalkUserId, + groupId = res.groupId, + groupName = res.groupName, + roleId = res.roleId, + roleName = res.roleName, + }).ToList(); + if (iUser.Count > 0) + { + await _dingTalkRoleRepo.CopyNew().AsInsertable(iUser).ExecuteCommandAsync(); + } + + // 更新钉钉角色 + var uUser = uDingTalkRole.Select(res => new DingTalkRoleUser + { + Id = sysDingTalkRoleList.Where(u => u.DingTalkUserId == res.DingTalkUserId).Select(u => u.Id).FirstOrDefault(), + DingTalkUserId = res.DingTalkUserId, + groupId = res.groupId, + groupName = res.groupName, + roleId = res.roleId, + roleName = res.roleName + }).ToList(); + //添加需要删除的数据 + Parallel.ForEach(dDingTalkRole, user => user.IsDelete = true); + uUser.AddRange(dDingTalkRole); + if (uUser.Count > 0) + { + await _dingTalkRoleRepo.CopyNew().AsUpdateable(uUser).UpdateColumns(u => new + { + u.DingTalkUserId, + u.groupId, + u.groupName, + u.roleId, + u.roleName, + u.UpdateTime, + u.UpdateUserName, + u.UpdateUserId, + u.IsDelete + }).ExecuteCommandAsync(); + } + + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("【" + DateTime.Now + "】同步钉钉角色"); + Console.ForegroundColor = originColor; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkUserJob.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkUserJob.cs new file mode 100644 index 0000000..650e263 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncDingTalkUserJob.cs @@ -0,0 +1,177 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.DingTalk; +using Furion.Schedule; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Admin.NET.Plugin.Job; + +/// +/// 同步钉钉用户job +/// +[JobDetail("SyncDingTalkUserJob", Description = "同步钉钉用户", GroupName = "default", Concurrent = false)] +[Daily(TriggerId = "SyncDingTalkUserTrigger", Description = "同步钉钉用户")] +public class SyncDingTalkUserJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IDingTalkApi _dingTalkApi; + private readonly ILogger _logger; + + public SyncDingTalkUserJob(IServiceScopeFactory scopeFactory, IDingTalkApi dingTalkApi, ILoggerFactory loggerFactory) + { + _scopeFactory = scopeFactory; + _dingTalkApi = dingTalkApi; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + var _sysUserRep = serviceScope.ServiceProvider.GetRequiredService>(); + var _dingTalkUserRepo = serviceScope.ServiceProvider.GetRequiredService>(); + var _dingTalkOptions = serviceScope.ServiceProvider.GetRequiredService>(); + + // 获取Token + var tokenRes = await _dingTalkApi.GetDingTalkToken(_dingTalkOptions.Value.ClientId, _dingTalkOptions.Value.ClientSecret); + if (tokenRes.ErrCode != 0) + throw Oops.Oh(tokenRes.ErrMsg); + + var dingTalkUserList = new List(); + var offset = 0; + while (offset >= 0) + { + // 获取用户Id列表 + var userIdsRes = await _dingTalkApi.GetDingTalkCurrentEmployeesList(tokenRes.AccessToken, new GetDingTalkCurrentEmployeesListInput + { + StatusList = "2,3,5,-1", + Size = 50, + Offset = offset + }); + if (!userIdsRes.Success) + { + _logger.LogError(userIdsRes.ErrMsg); + break; + } + // 根据用户Id获取花名册 + var rosterRes = await _dingTalkApi.GetDingTalkCurrentEmployeesRosterList( + tokenRes.AccessToken, + new GetDingTalkCurrentEmployeesRosterListInput() + { + UserIdList = string.Join(",", userIdsRes.Result.DataList), + FieldFilterList = + $"{DingTalkConst.NameField},{DingTalkConst.JobNumberField},{DingTalkConst.MobileField},{DingTalkConst.DeptId},{DingTalkConst.Dept},{DingTalkConst.Position}", + AgentId = _dingTalkOptions.Value.AgentId + } + ); + if (!rosterRes.Success) + { + _logger.LogError(rosterRes.ErrMsg); + break; + } + dingTalkUserList.AddRange(rosterRes.Result); + if (userIdsRes.Result.NextCursor == null) + { + break; + } + // 保存分页游标 + offset = (int)userIdsRes.Result.NextCursor; + } + + // 判断新增还是更新 + var sysDingTalkUserIdList = await _dingTalkUserRepo.AsQueryable().Select(u => new + { + u.Id, + u.DingTalkUserId + }).ToListAsync(); + + var uDingTalkUser = dingTalkUserList.Where(u => sysDingTalkUserIdList.Any(m => m.DingTalkUserId == u.UserId)); // 需要更新的用户Id + var iDingTalkUser = dingTalkUserList.Where(u => !sysDingTalkUserIdList.Any(m => m.DingTalkUserId == u.UserId)); // 需要新增的用户Id + + // 新增钉钉用户 + var iUser = iDingTalkUser.Select(res => new DingTalkUser + { + DingTalkUserId = res.UserId, + Name = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.NameField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Mobile = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.MobileField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + JobNumber = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.JobNumberField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + DeptId = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.DeptId).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Dept = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.Dept).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Position = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.Position).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + }).ToList(); + if (iUser.Count > 0) + { + await _dingTalkUserRepo.CopyNew().AsInsertable(iUser).ExecuteCommandAsync(); + } + + // 更新钉钉用户 + var uUser = uDingTalkUser.Select(res => new DingTalkUser + { + Id = sysDingTalkUserIdList.Where(u => u.DingTalkUserId == res.UserId).Select(u => u.Id).FirstOrDefault(), + DingTalkUserId = res.UserId, + Name = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.NameField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Mobile = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.MobileField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + JobNumber = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.JobNumberField).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + DeptId = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.DeptId).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Dept = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.Dept).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + Position = res.FieldDataList.Where(u => u.FieldCode == DingTalkConst.Position).Select(u => u.FieldValueList.Select(m => m.Value).FirstOrDefault()).FirstOrDefault(), + }).ToList(); + if (uUser.Count > 0) + { + await _dingTalkUserRepo.CopyNew().AsUpdateable(uUser).UpdateColumns(u => new + { + u.DingTalkUserId, + u.Name, + u.Mobile, + u.JobNumber, + u.DeptId, + u.Dept, + u.Position, + u.UpdateTime, + u.UpdateUserName, + u.UpdateUserId, + }).ExecuteCommandAsync(); + } + + // 通过系统用户账号(工号),更新钉钉用户表里面的系统用户Id + var sysUser = await _sysUserRep.AsQueryable() + .Select(u => new + { + u.Id, + u.Account + }).ToListAsync(); + var sysDingTalkUser = await _dingTalkUserRepo.AsQueryable() + .Where(u => sysUser.Any(m => m.Account == u.JobNumber)) + .Select(u => new + { + u.Id, + u.JobNumber, + u.Mobile, + u.DeptId, + u.Dept, + u.Position + }).ToListAsync(); + var uSysDingTalkUser = sysDingTalkUser.Select(u => new DingTalkUser + { + Id = u.Id, + SysUserId = sysUser.Where(m => m.Account == u.JobNumber).Select(u => u.Id).FirstOrDefault(), + }).ToList(); + + await _dingTalkUserRepo.CopyNew().AsUpdateable(uSysDingTalkUser).UpdateColumns(u => new + { + u.SysUserId, + u.UpdateTime, + u.UpdateUserName, + u.UpdateUserId, + }).ExecuteCommandAsync(); + + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("【" + DateTime.Now + "】同步钉钉用户"); + Console.ForegroundColor = originColor; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncWokerflowLogJob.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncWokerflowLogJob.cs new file mode 100644 index 0000000..1fdbf13 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Job/SyncWokerflowLogJob.cs @@ -0,0 +1,98 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.DingTalk; +using Furion.Schedule; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Admin.NET.Plugin.Job; + +/// +/// 同步钉钉角色job,自动同步触发器请在web页面按需求设置 +/// +[JobDetail( + "SyncWokerflowLogJob", + Description = "同步钉钉审批状态", + GroupName = "default", + Concurrent = false +)] +[Daily(TriggerId = "SyncWokerflowLogTrigger", Description = "同步钉钉审批状态")] +public class SyncWokerflowLogJob : IJob +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IDingTalkApi _dingTalkApi; + private readonly ILogger _logger; + private readonly SqlSugarRepository _dingTalkDeptRep; + private readonly SqlSugarRepository _dingTalkWokerflowLogRep; + + public SyncWokerflowLogJob( + IServiceScopeFactory scopeFactory, + IDingTalkApi dingTalkApi, + SqlSugarRepository dingTalkDeptRep, + SqlSugarRepository dingTalkWokerflowLogRep, + ILoggerFactory loggerFactory + ) + { + _scopeFactory = scopeFactory; + _dingTalkApi = dingTalkApi; + _dingTalkDeptRep = dingTalkDeptRep; + _dingTalkWokerflowLogRep = dingTalkWokerflowLogRep; + _logger = loggerFactory.CreateLogger(CommonConst.SysLogCategoryName); + } + + public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + using var serviceScope = _scopeFactory.CreateScope(); + var _dingTalkOptions = serviceScope.ServiceProvider.GetRequiredService< + IOptions + >(); + + // 获取Token + var tokenRes = await _dingTalkApi.GetDingTalkToken( + _dingTalkOptions.Value.ClientId, + _dingTalkOptions.Value.ClientSecret + ); + if (tokenRes.ErrCode != 0) + throw Oops.Oh(tokenRes.ErrMsg); + + var dingTalkDeptList = new List(); + // 获取未完成审批列表 + List flow_list = await _dingTalkWokerflowLogRep.GetListAsync(t => + t.Status == "RUNNING" + ); + List update_list = new List(); + if (flow_list?.Count > 0) + { + foreach (var item in flow_list) + { + var flow = await _dingTalkApi.GetProcessInstances( + tokenRes.AccessToken, + item.instanceId + ); + if (flow.Result.Status != item.Status) + { + item.Status = flow.Result.Status; + item.UpdateTime = DateTime.Now; + item.WorkflowId = flow.Result.BusinessId; + item.taskId = flow + .Result.Tasks.FirstOrDefault(t => t.Status == "RUNNING") + ?.TaskId; + update_list.Add(item); + } + } + + if (update_list.Count > 0) + { + await _dingTalkWokerflowLogRep.UpdateRangeAsync(update_list); + } + var originColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine("【" + DateTime.Now + "】同步钉钉审批记录状态"); + Console.ForegroundColor = originColor; + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Option/DingTalkOptions.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Option/DingTalkOptions.cs new file mode 100644 index 0000000..9509de3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Option/DingTalkOptions.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public sealed class DingTalkOptions : IConfigurableOptions +{ + /// + /// AppId + /// + public string AppId { get; set; } + + /// + /// AgentId + /// + public string AgentId { get; set; } + + /// + /// 原 AppKey 和 SuiteKey + /// + public string ClientId { get; set; } + + /// + /// 原 AppSecret 和 SuiteSecret + /// + public string ClientSecret { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/DingTalkService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/DingTalkService.cs new file mode 100644 index 0000000..6633196 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/DingTalkService.cs @@ -0,0 +1,145 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk.Service; + +/// +/// 钉钉服务 🧩 +/// +[ApiDescriptionSettings(DingTalkConst.GroupName, Order = 100)] +public class DingTalkService : IDynamicApiController, IScoped +{ + private readonly IDingTalkApi _dingTalkApi; + private readonly DingTalkOptions _dingTalkOptions; + private readonly SqlSugarRepository _dingTalkWokerflowLogRep; + + public DingTalkService( + IDingTalkApi dingTalkApi, + IOptions dingTalkOptions, + SqlSugarRepository dingTalkWokerflowLogRep + ) + { + _dingTalkApi = dingTalkApi; + _dingTalkOptions = dingTalkOptions.Value; + _dingTalkWokerflowLogRep = dingTalkWokerflowLogRep; + } + + /// + /// 获取企业内部应用的access_token + /// + /// + [DisplayName("获取企业内部应用的access_token")] + public async Task GetDingTalkToken() + { + var tokenRes = await _dingTalkApi.GetDingTalkToken( + _dingTalkOptions.ClientId, + _dingTalkOptions.ClientSecret + ); + if (tokenRes.ErrCode != 0) + { + throw Oops.Oh(tokenRes.ErrMsg); + } + return tokenRes; + } + + /// + /// 获取在职员工列表 🔖 + /// + /// + /// + /// + [HttpPost, DisplayName("获取在职员工列表")] + public async Task< + DingTalkBaseResponse + > GetDingTalkCurrentEmployeesList( + string access_token, + [Required] GetDingTalkCurrentEmployeesListInput input + ) + { + return await _dingTalkApi.GetDingTalkCurrentEmployeesList(access_token, input); + } + + /// + /// 获取员工花名册字段信息 🔖 + /// + /// + /// + /// + [HttpPost, DisplayName("获取员工花名册字段信息")] + public async Task< + DingTalkBaseResponse> + > GetDingTalkCurrentEmployeesRosterList( + string access_token, + [Required] GetDingTalkCurrentEmployeesRosterListInput input + ) + { + return await _dingTalkApi.GetDingTalkCurrentEmployeesRosterList(access_token, input); + } + + /// + /// 发送钉钉互动卡片 🔖 + /// + /// + /// + /// + [DisplayName("给指定用户发送钉钉互动卡片")] + [Obsolete] + public async Task DingTalkSendInteractiveCards( + string token, + DingTalkSendInteractiveCardsInput input + ) + { + return await _dingTalkApi.DingTalkSendInteractiveCards(token, input); + } + + /// + /// 创建并投放钉钉消息卡片 🔖 + /// + /// + /// + /// + [DisplayName("给指定用户发送钉钉消息卡片")] + public async Task DingTalkCreateAndDeliver( + string token, + DingTalkCreateAndDeliverInput input + ) + { + return await _dingTalkApi.DingTalkCreateAndDeliver(token, input); + } + + [DisplayName("用于发起OA审批实例")] + public async Task DingTalkWorkflowProcessInstances( + string token, + DingTalkWorkflowProcessInstancesInput input + ) + { + var temp = await _dingTalkApi.DingTalkWorkflowProcessInstances(token, input); + return temp; + } + + [DisplayName("查询审批实例")] + public async Task DingTalkWorkflowProcessInstances( + string token, + string input + ) + { + var temp = await _dingTalkApi.GetProcessInstances(token, input); + DingTalkWokerflowLog flow = await _dingTalkWokerflowLogRep.GetFirstAsync(t => + t.Status == "RUNNING" && t.instanceId == input + ); + + if ((flow != null) && (temp.Result.Status != flow.Status)) + { + flow.Status = temp.Result.Status; + flow.UpdateTime = DateTime.Now; + flow.WorkflowId = temp.Result.BusinessId; + flow.Result = temp.Result.Result; + flow.taskId = temp.Result.Tasks.FirstOrDefault(t => t.Status == "RUNNING")?.TaskId; + await _dingTalkWokerflowLogRep.UpdateAsync(flow); + } + return temp; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkBaseResponse.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkBaseResponse.cs new file mode 100644 index 0000000..70189a3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkBaseResponse.cs @@ -0,0 +1,41 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 钉钉基础响应结果 +/// +/// Data +public class DingTalkBaseResponse +{ + /// + /// 返回结果 + /// + public T Result { get; set; } + + /// + /// 返回码 + /// + public int ErrCode { get; set; } + + /// + /// 返回码描述。 + /// + public string ErrMsg { get; set; } + + /// + /// 是否调用成功 + /// + public bool Success { get; set; } + + /// + /// 请求Id + /// + [Newtonsoft.Json.JsonProperty("request_id")] + [System.Text.Json.Serialization.JsonPropertyName("request_id")] + public string RequestId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCardData.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCardData.cs new file mode 100644 index 0000000..e683c5d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCardData.cs @@ -0,0 +1,36 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 卡片公有数据 +/// +public class DingTalkCardData +{ + /// + /// 卡片模板内容替换参数,普通文本类型。 + /// + public DingTalkCardParamMap CardParamMap { get; set; } + + /// + /// 卡片模板内容替换参数,多媒体类型。 + /// + public string CardMediaIdParamMap { get; set; } +} + +/// +/// 卡片模板内容替换参数 +/// +public class DingTalkCardParamMap +{ + /// + /// 片模板内容替换参数 + /// + [Newtonsoft.Json.JsonProperty("sys_full_json_obj")] + [System.Text.Json.Serialization.JsonPropertyName("sys_full_json_obj")] + public string SysFullJsonObj { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverInput.cs new file mode 100644 index 0000000..f07ae21 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverInput.cs @@ -0,0 +1,316 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkCreateAndDeliverInput +{ + /// + /// 卡片创建者的userId + /// + public string? userId { get; set; } + + /// + /// 卡片内容模板ID + /// + [Required] + public string cardTemplateId { get; set; } + + /// + /// 外部卡片实例Id + /// + [Required] + public string outTrackId { get; set; } + + /// + /// 卡片回调的类型:STREAM:stream模式 HTTP:http模式 + /// + public string? callbackType { get; set; } + + /// + /// 卡片回调HTTP模式时的路由 Key,用于查询注册的 callbackUrl。 + /// + public string? callbackRouteKey { get; set; } + + /// + /// 卡片数据 + /// + [Required] + public DingTalk_CardData cardData { get; set; } + + /// + /// 用户的私有数据 + /// + public PrivateData? crivateData { get; set; } + + /// + /// 动态数据源配置 + /// + public OpenDynamicDataConfig? openDynamicDataConfig { get; set; } + + /// + /// IM单聊酷应用场域信息 + /// + public OpenSpaceModel? imSingleOpenSpaceModel { get; set; } + + /// + /// IM群聊场域信息。 + /// + public OpenSpaceModel? imGroupOpenSpaceModel { get; set; } + + /// + /// IM机器人单聊场域信息。 + /// + public OpenSpaceModel? imRobotOpenSpaceModel { get; set; } + + /// + /// 协作场域信息 + /// + public OpenSpaceModel? coFeedOpenSpaceModel { get; set; } + + /// + /// 吊顶场域信息 + /// + public OpenSpaceModel? topOpenSpaceModel { get; set; } + + /// + /// 表示场域及其场域id + /// + /// + /// 其格式为dtv1.card//spaceType1.spaceId1;spaceType2.spaceId2_1;spaceType2.spaceId2_2;spaceType3.spaceId3 + /// + [Required] + public string openSpaceId { get; set; } + + /// + /// 单聊酷应用场域投放参数。 + /// + public DingTalkOpenDeliverModel? imSingleOpenDeliverModel { get; set; } + + /// + /// 群聊投放参数。 + /// + public DingTalkOpenDeliverModel? imGroupOpenDeliverModel { get; set; } + + /// + /// IM机器人单聊投放参数。 + /// + public DingTalkOpenDeliverModel? imRobotOpenDeliverModel { get; set; } + + /// + /// 吊顶投放参数。 + /// + public DingTalkOpenDeliverModel? topOpenDeliverModel { get; set; } + + /// + /// 协作投放参数。 + /// + public DingTalkOpenDeliverModel? coFeedOpenDeliverModel { get; set; } + + /// + /// 文档投放参数 + /// + public DingTalkOpenDeliverModel? docOpenDeliverModel { get; set; } + + /// + /// 用户userId类型:1(默认):userId模式 2:unionId模式 + /// + public int UserIdType { get; set; } +} + +public class DingTalk_CardData +{ + public DingTalk_CardParamMap cardParamMap { get; set; } +} + +/// +/// 卡片模板内容替换参数 +/// +public class DingTalk_CardParamMap +{ + /// + /// 片模板内容替换参数 + /// + [Newtonsoft.Json.JsonProperty("sys_full_json_obj")] + [System.Text.Json.Serialization.JsonPropertyName("sys_full_json_obj")] + public string sysFullJsonObj { get; set; } +} + +public class PrivateData +{ + public Dictionary key { get; set; } = new Dictionary(); +} + +public class OpenDynamicDataConfig +{ + /// + /// 动态数据源配置列表。 + /// + public List? dynamicDataSourceConfigs { get; set; } +} + +public class DynamicDataSourceConfig +{ + /// + /// 数据源的唯一 ID, 调用方指定。 + /// + public string? dynamicDataSourceId { get; set; } + + /// + /// 回调数据源时回传的固定参数。 示例 + /// + public Dictionary? constParams { get; set; } + + /// + /// 数据源拉取配置。 + /// + public PullConfig? pullConfig { get; set; } +} + +public class PullConfig +{ + /// + /// 拉取策略,可选值:NONE:不拉取,无动态数据 INTERVAL:间隔拉取ONCE:只拉取一次 + /// + public string pullStrategy { get; set; } + + /// + /// 拉取的间隔时间。 + /// + public int interval { get; set; } + + /// + /// 拉取的间隔时间的单位, 可选值:SECONDS:秒 MINUTES:分钟 HOURS:小时 DAYS:天 + /// + public string timeUnit { get; set; } +} + +public class OpenSpaceModel +{ + /// + /// 吊顶场域属性,通过增加spaeType使卡片支持吊顶场域。 + /// + public string? spaceType { get; set; } + + /// + /// 卡片标题。 + /// + public string? title { get; set; } + + /// + /// 酷应用编码。 + /// + public string? coolAppCode { get; set; } + + /// + /// 是否支持转发, 默认false。 + /// + public bool? supportForward { get; set; } + + /// + /// 支持国际化的LastMessage。 + /// + public Dictionary? lastMessageI18n { get; set; } + + /// + /// 支持卡片消息可被搜索字段。 + /// + public SearchSupport? searchSupport { get; set; } + + /// + /// 通知信息。 + /// + public Notification? notification { get; set; } +} + +public class SearchSupport +{ + /// + /// 类型的icon,供搜索展示使用。 + /// + public string searchIcon { get; set; } + + /// + /// 卡片类型名。 + /// + public string searchTypeName { get; set; } + + /// + /// 供消息展示与搜索的字段。 + /// + public string searchDesc { get; set; } +} + +public class Notification +{ + /// + /// 供消息展示与搜索的字段。 + /// + public string alertContent { get; set; } + + /// + /// 是否关闭推送通知:true:关闭 false:不关闭 + /// + public bool notificationOff { get; set; } +} + +public class DingTalkOpenDeliverModel +{ + /// + /// 用于发送卡片的机器人编码。 + /// + public string robotCode { get; set; } + + /// + /// 消息@人。格式:{"key":"value"}。key:用户的userId value:用户名 + /// + public Dictionary atUserIds { get; set; } + + /// + /// 指定接收人的userId。 + /// + public List recipients { get; set; } + + /// + /// 扩展字段,示例如下:{"key":"value"} + /// + public Dictionary extension { get; set; } + + /// + /// IM机器人单聊若未设置其他投放属性,需设置spaeType为IM_ROBOT。 + /// + public string spaceType { get; set; } + + /// + /// 过期时间戳。若使用topOpenDeliverModel对象,则该字段必填。 + /// + public long expiredTimeMillis { get; set; } + + /// + /// 可以查看该吊顶卡片的userId。 + /// + public List userIds { get; set; } + + /// + /// 可以查看该吊顶卡片的设备:android|ios|win|mac。 + /// + public List platforms { get; set; } + + /// + /// 业务标识。 + /// + public string bizTag { get; set; } + + /// + /// 协作场域下的排序时间。 + /// + public long gmtTimeLine { get; set; } + + /// + /// 员工userId信息 + /// + public string userId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverOutput.cs new file mode 100644 index 0000000..159468a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkCreateAndDeliverOutput.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkCreateAndDeliverOutput +{ + /// + /// 返回结果 + /// + public bool success { get; set; } + + /// + /// 创建卡片结果 + /// + public DingTalkCreateAndDeliverResult result { get; set; } + + public string code { get; set; } + public string requestid { get; set; } + public string message { get; set; } +} + +public class DingTalkCreateAndDeliverResult +{ + /// + /// 用于业务方后续查看已读列表的查询key + /// + public string processQueryKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkDeptOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkDeptOutput.cs new file mode 100644 index 0000000..98a359f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkDeptOutput.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkDeptOutput +{ + /// + /// 上级部门Id + /// + [JsonProperty("parent_id")] + [System.Text.Json.Serialization.JsonPropertyName("parent_id")] + public long parent_id { get; set; } + + /// + /// 部门名 + /// + [JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string name { get; set; } + + /// + /// 部门Id + /// + [JsonProperty("dept_id")] + [System.Text.Json.Serialization.JsonPropertyName("dept_id")] + public long dept_id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpFieldDataVo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpFieldDataVo.cs new file mode 100644 index 0000000..478a489 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpFieldDataVo.cs @@ -0,0 +1,38 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkEmpFieldDataVo +{ + /// + /// 字段名称 + /// + [Newtonsoft.Json.JsonProperty("field_name")] + [System.Text.Json.Serialization.JsonPropertyName("field_name")] + public string FieldName { get; set; } + + /// + /// 字段标识 + /// + [Newtonsoft.Json.JsonProperty("field_code")] + [System.Text.Json.Serialization.JsonPropertyName("field_code")] + public string FieldCode { get; set; } + + /// + /// 分组标识 + /// + [Newtonsoft.Json.JsonProperty("group_id")] + [System.Text.Json.Serialization.JsonPropertyName("group_id")] + public string GroupId { get; set; } + + /// + /// + /// + [Newtonsoft.Json.JsonProperty("field_value_list")] + [System.Text.Json.Serialization.JsonPropertyName("field_value_list")] + public List FieldValueList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpRosterFieldVo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpRosterFieldVo.cs new file mode 100644 index 0000000..46979f6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkEmpRosterFieldVo.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkEmpRosterFieldVo +{ + /// + /// 企业的corpid + /// + [Newtonsoft.Json.JsonProperty("corp_id")] + [System.Text.Json.Serialization.JsonPropertyName("corp_id")] + public string CorpId { get; set; } + + /// + /// 返回的字段信息列表 + /// + [Newtonsoft.Json.JsonProperty("field_data_list")] + [System.Text.Json.Serialization.JsonPropertyName("field_data_list")] + public List FieldDataList { get; set; } + + /// + /// 员工的userid + /// + [Newtonsoft.Json.JsonProperty("userid")] + [System.Text.Json.Serialization.JsonPropertyName("userid")] + public string UserId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkFieldValueVo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkFieldValueVo.cs new file mode 100644 index 0000000..2b9accb --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkFieldValueVo.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkFieldValueVo +{ + /// + /// 第几条的明细标识,下标从0开始 + /// + [Newtonsoft.Json.JsonProperty("item_index")] + [System.Text.Json.Serialization.JsonPropertyName("item_index")] + public int ItemIndex { get; set; } + + /// + /// 字段展示值,选项类型字段对应选项的value + /// + [Newtonsoft.Json.JsonProperty("label")] + [System.Text.Json.Serialization.JsonPropertyName("label")] + public string Label { get; set; } + + /// + /// 字段取值,选项类型字段对应选项的key + /// + [Newtonsoft.Json.JsonProperty("value")] + [System.Text.Json.Serialization.JsonPropertyName("value")] + public string Value { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkGetProcessInstancesOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkGetProcessInstancesOutput.cs new file mode 100644 index 0000000..63fa83a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkGetProcessInstancesOutput.cs @@ -0,0 +1,74 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkGetProcessInstancesOutput +{ + public ResultData Result { get; set; } + public bool Success { get; set; } +} + +public class OperationRecord +{ + public DateTime? Date { get; set; } + public string Result { get; set; } + public List Images { get; set; } // 图片可能是字符串 URL 或对象 + public string ShowName { get; set; } + public string Type { get; set; } + public string UserId { get; set; } +} + +// 表格行中的子项(用于 TableField 的解析) +public class TableRowItem +{ + public string BizAlias { get; set; } + public string Label { get; set; } + public string Value { get; set; } + public string Key { get; set; } + public bool Mask { get; set; } +} + +// 完整的一行表格数据 +public class TableRow +{ + public List RowValue { get; set; } + public string RowNumber { get; set; } +} + +public class TaskItem +{ + public string Result { get; set; } + public string ActivityId { get; set; } + public string PcUrl { get; set; } + public DateTime? CreateTime { get; set; } + public string MobileUrl { get; set; } + public string UserId { get; set; } + public long TaskId { get; set; } + public string Status { get; set; } +} + +public class ResultData +{ + public List AttachedProcessInstanceIds { get; set; } + public string BusinessId { get; set; } + public string Title { get; set; } + public string OriginatorDeptId { get; set; } + public List OperationRecords { get; set; } + public List FormComponentValues { get; set; } + + /// + /// 审批结果 agree:同意 refuse:拒绝 + /// + public string Result { get; set; } + + public string BizAction { get; set; } + public DateTime? CreateTime { get; set; } + public string OriginatorUserId { get; set; } + public List Tasks { get; set; } + public string OriginatorDeptName { get; set; } + public string Status { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListOutput.cs new file mode 100644 index 0000000..cfaeca9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListOutput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkRoleListOutput +{ + /// + /// 是否还有更多数据 + /// + [JsonProperty("hasMore")] + [System.Text.Json.Serialization.JsonPropertyName("hasMore")] + public bool hasMore { get; set; } + + /// + /// 角色组列表 + /// + [JsonProperty("list")] + [System.Text.Json.Serialization.JsonPropertyName("list")] + public List list { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListResult.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListResult.cs new file mode 100644 index 0000000..2775ea4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleListResult.cs @@ -0,0 +1,22 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkRoleListResult +{ + [JsonProperty("groupId")] + [System.Text.Json.Serialization.JsonPropertyName("groupId")] + public long groupId { get; set; } + + [JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string name { get; set; } + + [JsonProperty("roles")] + [System.Text.Json.Serialization.JsonPropertyName("roles")] + public List roles { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleResult.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleResult.cs new file mode 100644 index 0000000..0040e54 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleResult.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkRoleResult +{ + [JsonProperty("id")] + [System.Text.Json.Serialization.JsonPropertyName("id")] + public long id { get; set; } + + [JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string name { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistOutput.cs new file mode 100644 index 0000000..c9ac44b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistOutput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkRoleSimplelistOutput +{ + /// + /// 是否还有更多数据 + /// + [JsonProperty("hasMore")] + [System.Text.Json.Serialization.JsonPropertyName("hasMore")] + public bool hasMore { get; set; } + + /// + /// 角色组列表 + /// + [JsonProperty("list")] + [System.Text.Json.Serialization.JsonPropertyName("list")] + public List list { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistResult.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistResult.cs new file mode 100644 index 0000000..546d28f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkRoleSimplelistResult.cs @@ -0,0 +1,18 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkRoleSimplelistResult +{ + [JsonProperty("userid")] + [System.Text.Json.Serialization.JsonPropertyName("userid")] + public string userid { get; set; } + + [JsonProperty("name")] + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string name { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsInput.cs new file mode 100644 index 0000000..9f17d02 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsInput.cs @@ -0,0 +1,113 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkSendInteractiveCardsInput +{ + /// + /// 互动卡片的消息模板Id + /// + [Required(ErrorMessage = "互动卡片的消息模板Id必填!")] + public string? CardTemplateId { get; set; } + + /// + /// 群Id + /// + /// + /// 1、基于群模板创建的群。 + /// 企业内部应用,调用创建群接口获取open_conversation_id参数值。 + /// 2、安装群聊酷应用的群。 + /// 企业内部应用,通过群内安装酷应用事件获取回调参数OpenConversationId参数值。 + /// + public string OpenConversationId { get; set; } + + /// + /// 接收人userId列表 + /// + /// + /// 单聊:receiverUserIdList填写用户ID,最大值20。 + /// 群聊:receiverUserIdList填写用户ID,表示当前对应ID的群内用户可见 + /// receiverUserIdList参数不填写,表示当前群内所有用户可见 + /// + [Required(ErrorMessage = "接收人userId列表必填!")] + public List? ReceiverUserIdList { get; set; } + + /// + /// 唯一标示卡片的外部编码 + /// + [Required(ErrorMessage = "唯一标示卡片的外部编码必填!")] + public string? OutTrackId { get; set; } + + /// + /// 机器人的编码 + /// + public string RobotCode { get; set; } + + /// + /// 发送的会话类型 + /// + [Required(ErrorMessage = "会话类型必填!")] + public DingTalkConversationTypeEnum? ConversationType { get; set; } + + /// + /// 卡片回调时的路由Key,用于查询注册的callbackUrl + /// + public string CallbackRouteKey { get; set; } + + /// + /// 卡片公有数据 + /// + [Required(ErrorMessage = "卡片公有数据必填!")] + public DingTalkCardData CardData { get; set; } +} + +public class GetDingTalkCardMessageReadStatusInput +{ + /// + /// 机器人的编码 + /// + public string RobotCode { set; get; } + + /// + /// 消息唯一标识,可通过批量发送人与机器人会话中机器人消息接口返回参数中processQueryKey字段获取。 + /// + public string ProcessQueryKey { set; get; } +} + +public class GetDingTalkCardMessageReadStatusOutput +{ + /// + /// 消息发送状态,SUCCESS:成功、RECALLED:已撤回、PROCESSING: 处理中 + /// + public string SendStatus { get; set; } + + /// + /// + /// + public DingTalkCardMessageReadInfoList MessageReadInfoList { get; set; } +} + +/// +/// 钉钉卡片消息已读情况 +/// +public class DingTalkCardMessageReadInfoList +{ + /// + /// 消息接收者名称 + /// + public string Name { set; get; } + + /// + /// 消息接收者的userId + /// + public string UserId { set; get; } + + /// + /// 已读状态,READ:已读、UNREAD:未读 + /// + public string ReadStatus { set; get; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsOutput.cs new file mode 100644 index 0000000..a592150 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsOutput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 发送钉钉互动卡片返回 +/// +public class DingTalkSendInteractiveCardsOutput +{ + /// + /// 返回结果 + /// + public bool Success { get; set; } + + /// + /// 创建卡片结果 + /// + public DingTalkSendInteractiveCardsResult Result { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsResult.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsResult.cs new file mode 100644 index 0000000..42de98b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkSendInteractiveCardsResult.cs @@ -0,0 +1,15 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkSendInteractiveCardsResult +{ + /// + /// 用于业务方后续查看已读列表的查询key + /// + public string ProcessQueryKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesInput.cs new file mode 100644 index 0000000..4186e4d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesInput.cs @@ -0,0 +1,105 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkWorkflowProcessInstancesInput +{ + /// + /// 发起人用户ID + /// + public string OriginatorUserId { get; set; } + + /// + /// 审批模板的流程编码 + /// + public string ProcessCode { get; set; } + + /// + /// 部门ID + /// + public long DeptId { get; set; } + + /// + /// 微应用AgentId + /// + public long MicroappAgentId { get; set; } + + /// + /// 审批人列表(支持多节点) + /// + public List Approvers { get; set; } + + /// + /// 抄送人列表 + /// + public List CcList { get; set; } + + /// + /// 抄送位置:START(开始),MIDDLE(中间),END(结束) + /// + public string CcPosition { get; set; } + + /// + /// 目标动态选择办理人(用于会签或或签等场景) + /// + public List TargetSelectActioners { get; set; } + + /// + /// 表单组件值列表 + /// + public List FormComponentValues { get; set; } + + /// + /// 请求ID,用于幂等控制 + /// + public string RequestId { get; set; } +} + +/// +/// 审批人信息 +/// +public class Approver +{ + /// + /// 节点类型:AGREE(同意),REFUSE(拒绝)等 + /// + public string ActionType { get; set; } + + /// + /// 该节点的审批人用户ID列表 + /// + public List UserIds { get; set; } +} + +/// +/// 动态选择办理人 +/// +public class TargetSelectActioner +{ + /// + /// 办理人Key,对应表单中的人员选择控件的key + /// + public string ActionerKey { get; set; } + + /// + /// 该控件选中的用户ID列表 + /// + public List ActionerUserIds { get; set; } +} + +/// +/// 表单组件值 +/// +public class FormComponentValue +{ + public string ComponentType { get; set; } + public string Name { get; set; } + public string BizAlias { get; set; } + public string Id { get; set; } + public string Value { get; set; } + public string ExtValue { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesOutput.cs new file mode 100644 index 0000000..bca716b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/DingTalkWorkflowProcessInstancesOutput.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class DingTalkWorkflowProcessInstancesOutput +{ + /// + /// 请求Id + /// + [Newtonsoft.Json.JsonProperty("request_id")] + [System.Text.Json.Serialization.JsonPropertyName("request_id")] + public string RequestId { get; set; } + + public string code { get; set; } + public string message { get; set; } + + /// + /// 是否还有更多数据 + /// + [JsonProperty("instanceId")] + [System.Text.Json.Serialization.JsonPropertyName("instanceId")] + public string instanceId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListInput.cs new file mode 100644 index 0000000..138604d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListInput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +/// +/// 获取在职员工列表参数 +/// +public class GetDingTalkCurrentEmployeesListInput +{ + /// + /// 在职员工状态筛选,可以查询多个状态。不同状态之间使用英文逗号分隔。2:试用期、3:正式、5:待离职、-1:无状态 + /// + [Newtonsoft.Json.JsonProperty("status_list")] + [System.Text.Json.Serialization.JsonPropertyName("status_list")] + public string StatusList { get; set; } + + /// + /// 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值。 + /// + public int Offset { get; set; } + + /// + /// 分页大小,最大50。 + /// + public int Size { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListOutput.cs new file mode 100644 index 0000000..9bbd022 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesListOutput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkCurrentEmployeesListOutput +{ + /// + /// 查询到的员工userId列表 + /// + [Newtonsoft.Json.JsonProperty("data_list")] + [System.Text.Json.Serialization.JsonPropertyName("data_list")] + public List DataList { get; set; } + + /// + /// 下一次分页调用的offset值,当返回结果里没有next_cursor时,表示分页结束。 + /// + [Newtonsoft.Json.JsonProperty("next_cursor")] + [System.Text.Json.Serialization.JsonPropertyName("next_cursor")] + public int? NextCursor { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesRosterListInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesRosterListInput.cs new file mode 100644 index 0000000..ce81388 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentEmployeesRosterListInput.cs @@ -0,0 +1,31 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkCurrentEmployeesRosterListInput +{ + /// + /// 员工的userId列表,多个userid之间使用逗号分隔,一次最多支持传100个值。 + /// + [Newtonsoft.Json.JsonProperty("userid_list")] + [System.Text.Json.Serialization.JsonPropertyName("userid_list")] + public string UserIdList { get; set; } + + /// + /// 需要获取的花名册字段field_code值列表,多个字段之间使用逗号分隔,一次最多支持传100个值。 + /// + [Newtonsoft.Json.JsonProperty("field_filter_list")] + [System.Text.Json.Serialization.JsonPropertyName("field_filter_list")] + public string FieldFilterList { get; set; } + + /// + /// 应用的AgentId + /// + [Newtonsoft.Json.JsonProperty("agentid")] + [System.Text.Json.Serialization.JsonPropertyName("agentid")] + public string AgentId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleListInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleListInput.cs new file mode 100644 index 0000000..df6b2ad --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleListInput.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkCurrentRoleListInput +{ + /// + /// 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值。 + /// + public int? Offset { get; set; } + + /// + /// 分页大小,最大50。 + /// + public int? Size { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleSimplelistInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleSimplelistInput.cs new file mode 100644 index 0000000..fa5a30f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkCurrentRoleSimplelistInput.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkCurrentRoleSimplelistInput +{ + /// + /// 角色id + /// + public long role_id { get; set; } + + /// + /// 分页游标,从0开始。根据返回结果里的next_cursor是否为空来判断是否还有下一页,且再次调用时offset设置成next_cursor的值。 + /// + public int? Offset { get; set; } + + /// + /// 分页大小,最大50。 + /// + public int? Size { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkDeptInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkDeptInput.cs new file mode 100644 index 0000000..1c219af --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkDeptInput.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkDeptInput +{ + public long dept_id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkToken.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkToken.cs new file mode 100644 index 0000000..94567bd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/Dto/GetDingTalkToken.cs @@ -0,0 +1,34 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public class GetDingTalkTokenOutput +{ + /// + /// 生成的access_token + /// + [Newtonsoft.Json.JsonProperty("access_token")] + [System.Text.Json.Serialization.JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + /// + /// access_token的过期时间,单位秒 + /// + [Newtonsoft.Json.JsonProperty("expires_in")] + [System.Text.Json.Serialization.JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + /// + /// 返回码描述 + /// + public string ErrMsg { get; set; } + + /// + /// 返回码 + /// + public int ErrCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs new file mode 100644 index 0000000..67713f4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs @@ -0,0 +1,156 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.DingTalk; + +public interface IDingTalkApi : IHttpDeclarative +{ + /// + /// 获取企业内部应用的access_token + /// + /// 应用的唯一标识key + /// 应用的密钥。AppKey和AppSecret可在钉钉开发者后台的应用详情页面获取。 + /// + [Get("https://oapi.dingtalk.com/gettoken")] + Task GetDingTalkToken([Query] string appkey, [Query] string appsecret); + + /// + /// 获取在职员工列表 + /// + /// 调用该接口的应用凭证 + /// + /// + [Post("https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/queryonjob")] + Task< + DingTalkBaseResponse + > GetDingTalkCurrentEmployeesList( + [Query] string access_token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + GetDingTalkCurrentEmployeesListInput input + ); + + /// + /// 获取员工花名册字段信息 + /// + /// 调用该接口的应用凭证 + /// + /// + [Post("https://oapi.dingtalk.com/topapi/smartwork/hrm/employee/v2/list")] + Task< + DingTalkBaseResponse> + > GetDingTalkCurrentEmployeesRosterList( + [Query] string access_token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + GetDingTalkCurrentEmployeesRosterListInput input + ); + + /// + /// 发送钉钉互动卡片 + /// + /// 调用该接口的访问凭证 + /// + /// + /// + /// 钉钉官方文档显示接口不再支持新应用接入, 已接入的应用可继续调用 + /// 推荐更新接口https://open.dingtalk.com/document/orgapp/create-and-deliver-cards?spm=ding_open_doc.document.0.0.67fc50988Pf0mc + /// + [Post("https://api.dingtalk.com/v1.0/im/interactiveCards/send")] + [Obsolete] + Task DingTalkSendInteractiveCards( + [Header("x-acs-dingtalk-access-token")] string token, + [Body(ContentType = "application/json", UseStringContent = true)] + DingTalkSendInteractiveCardsInput input + ); + + /// + /// 获取钉钉卡片消息读取状态 + /// + /// + /// + /// + [Get("https://api.dingtalk.com/v1.0/robot/oToMessages/readStatus")] + Task GetDingTalkCardMessageReadStatus( + [Header("x-acs-dingtalk-access-token")] string token, + [Query] GetDingTalkCardMessageReadStatusInput input + ); + + /// + /// 获取角色列表 + /// + /// 调用该接口的应用凭证 + /// + /// + [Post("https://oapi.dingtalk.com/topapi/role/list")] + Task> GetDingTalkRoleList( + [Query] string access_token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + GetDingTalkCurrentRoleListInput input + ); + + /// + /// 获取指定角色的员工列表 + /// + /// 调用该接口的应用凭证 + /// + /// + [Post("https://oapi.dingtalk.com/topapi/role/simplelist")] + Task> GetDingTalkRoleSimplelist( + [Query] string access_token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + GetDingTalkCurrentRoleSimplelistInput input + ); + + /// + /// 创建并投放钉钉消息卡片 + /// + /// + /// + /// + [Post("https://api.dingtalk.com/v1.0/card/instances/createAndDeliver")] + Task DingTalkCreateAndDeliver( + [Header("x-acs-dingtalk-access-token")] string token, + [Body(ContentType = "application/json", UseStringContent = true)] + DingTalkCreateAndDeliverInput input + ); + + /// + /// 获取部门列表列表 + /// + /// 调用该接口的应用凭证 + /// + /// + [Post("https://oapi.dingtalk.com/topapi/v2/department/listsub")] + Task>> GetDingTalkDept( + [Query] string access_token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + GetDingTalkDeptInput input + ); + + /// + /// 发起审批实例 + /// + /// + /// + /// + [Post("https://api.dingtalk.com/v1.0/workflow/processInstances")] + Task DingTalkWorkflowProcessInstances( + [Header("x-acs-dingtalk-access-token")] string token, + [Body(ContentType = "application/json", UseStringContent = true), Required] + DingTalkWorkflowProcessInstancesInput input + ); + + /// + /// 查询审批实例 + /// + /// + /// + /// + [Get("https://api.dingtalk.com/v1.0/workflow/processInstances")] + Task GetProcessInstances( + [Header("x-acs-dingtalk-access-token")] string token, + [Query] string processInstanceId + ); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs new file mode 100644 index 0000000..095924e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Plugin.DingTalk; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddConfigurableOptions(); + + services.AddHttpRemote(builder => + { + builder.AddHttpDeclarative(); + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Admin.NET.Plugin.GoView.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Admin.NET.Plugin.GoView.csproj new file mode 100644 index 0000000..8d66519 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Admin.NET.Plugin.GoView.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Configuration/GoView.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Configuration/GoView.json new file mode 100644 index 0000000..325f598 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Configuration/GoView.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "[openapi:GoView]": { + "Group": "GoView", + "Title": "GoView 大屏可视化", + "Description": "GoView 是一个高效的拖拽式低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可制作数据大屏,减少心智负担。", + "Version": "2.2.8", + "Order": 95 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Const/GoViewConst.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Const/GoViewConst.cs new file mode 100644 index 0000000..9964df0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Const/GoViewConst.cs @@ -0,0 +1,19 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView; + +/// +/// GoView 相关常量 +/// +[Const("GoView 相关常量")] +public class GoViewConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "GoView"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewPro.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewPro.cs new file mode 100644 index 0000000..95fe4d5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewPro.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView; + +/// +/// GoView 项目表 +/// +[SugarTable(null, "GoView 项目表")] +[SysTable] +public class GoViewPro : EntityBaseTenant +{ + /// + /// 项目名称 + /// + [SugarColumn(ColumnDescription = "项目名称", Length = 64)] + [Required, MaxLength(64)] + public string ProjectName { get; set; } + + /// + /// 项目状态 + /// + [SugarColumn(ColumnDescription = "项目状态")] + public GoViewProStateEnum StateEnum { get; set; } = GoViewProStateEnum.UnPublish; + + /// + /// 预览图片Url + /// + [SugarColumn(ColumnDescription = "预览图片Url", Length = 1024)] + [MaxLength(1024)] + public string? IndexImage { get; set; } + + /// + /// 项目备注 + /// + [SugarColumn(ColumnDescription = "项目备注", Length = 512)] + [MaxLength(512)] + public string? Remarks { get; set; } + + ///// + ///// 项目数据 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(Id))] + //public GoViewProData GoViewProData { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewProData.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewProData.cs new file mode 100644 index 0000000..2029987 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Entity/GoViewProData.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView; + +/// +/// GoView 项目数据表 +/// +[SugarTable(null, "GoView 项目数据表")] +[SysTable] +public class GoViewProData : EntityBaseTenant +{ + /// + /// 项目内容 + /// + [SugarColumn(ColumnDescription = "项目内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Content { get; set; } + + /// + /// 预览图片 + /// + [SugarColumn(ColumnDescription = "预览图片", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? IndexImageData { get; set; } + + /// + /// 背景图片 + /// + [SugarColumn(ColumnDescription = "背景图片", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? BackGroundImageData { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Enum/GoViewProStateEnum.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Enum/GoViewProStateEnum.cs new file mode 100644 index 0000000..4994935 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Enum/GoViewProStateEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView; + +/// +/// GoView 项目状态 +/// +[Description("GoView 项目状态")] +public enum GoViewProStateEnum +{ + /// + /// 未发布 + /// + [Description("未发布")] + UnPublish = -1, + + /// + /// 已发布 + /// + [Description("已发布")] + Published = 1, +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/GlobalUsings.cs new file mode 100644 index 0000000..e51078d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/GlobalUsings.cs @@ -0,0 +1,27 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Admin.NET.Core; +global using Admin.NET.Core.Service; +global using Furion; +global using Furion.DatabaseAccessor; +global using Furion.DataValidation; +global using Furion.DynamicApiController; +global using Furion.FriendlyException; +global using Furion.JsonSerialization; +global using Furion.UnifyResult; +global using Mapster; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Newtonsoft.Json; +global using SqlSugar; +global using System.Collections; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Data; +global using System.Linq.Dynamic.Core; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProInput.cs new file mode 100644 index 0000000..34556f9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProInput.cs @@ -0,0 +1,81 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// GoView 新增项目 +/// +public class GoViewProCreateInput +{ + /// + /// 项目名称 + /// + public string ProjectName { get; set; } + + /// + /// 项目备注 + /// + public string Remarks { get; set; } + + /// + /// 预览图片url + /// + public string IndexImage { get; set; } +} + +/// +/// GoView 编辑项目 +/// +public class GoViewProEditInput +{ + /// + /// 项目Id + /// + public long Id { get; set; } + + /// + /// 项目名称 + /// + public string ProjectName { get; set; } + + /// + /// 预览图片url + /// + public string IndexImage { get; set; } +} + +/// +/// GoView 修改项目发布状态 +/// +public class GoViewProPublishInput +{ + /// + /// 项目Id + /// + public long Id { get; set; } + + /// + /// 项目状态 + /// + public GoViewProStateEnum StateEnum { get; set; } +} + +/// +/// GoView 保存项目数据 +/// +public class GoViewProSaveDataInput +{ + /// + /// 项目Id + /// + public long ProjectId { get; set; } + + /// + /// 项目内容 + /// + public string Content { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProOutput.cs new file mode 100644 index 0000000..6f96760 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/Dto/GoViewProOutput.cs @@ -0,0 +1,132 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// GoView 项目 Item +/// +public class GoViewProItemOutput +{ + /// + /// 项目Id + /// + public long Id { get; set; } + + /// + /// 项目名称 + /// + public string ProjectName { get; set; } + + /// + /// 项目状态 + /// + public GoViewProStateEnum StateEnum { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 预览图片url + /// + public string IndexImage { get; set; } + + /// + /// 背景图片url + /// + public string BackGroundImage { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 项目备注 + /// + public string Remarks { get; set; } +} + +/// +/// GoView 项目详情 +/// +public class GoViewProDetailOutput : GoViewProItemOutput +{ + /// + /// 项目内容 + /// + public string Content { get; set; } +} + +/// +/// GoView 新增项目输出 +/// +public class GoViewProCreateOutput +{ + /// + /// 项目Id + /// + public long Id { get; set; } +} + +/// +/// GoView 上传项目输出 +/// +public class GoViewProUploadOutput +{ + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 仓储名称 + /// + public string BucketName { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 创建者Id + /// + public long? CreateUserId { get; set; } + + /// + /// 文件名称 + /// + public string FileName { get; set; } + + /// + /// 文件大小KB + /// + public int FileSize { get; set; } + + /// + /// 文件后缀 + /// + public string FileSuffix { get; set; } + + /// + /// 文件 Url + /// + [JsonProperty("fileurl")] + public string FileUrl { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + /// + /// 修改者Id + /// + public long? UpdateUserId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/GoViewProService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/GoViewProService.cs new file mode 100644 index 0000000..ece05fa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/GoViewProService.cs @@ -0,0 +1,323 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// 项目管理服务 🧩 +/// +[UnifyProvider("GoView")] +[ApiDescriptionSettings(GoViewConst.GroupName, Module = "goview", Name = "project", Order = 100, Description = "项目管理")] +public class GoViewProService : IDynamicApiController +{ + private readonly SqlSugarRepository _goViewProRep; + private readonly SqlSugarRepository _goViewProDataRep; + + public GoViewProService(SqlSugarRepository goViewProjectRep, + SqlSugarRepository goViewProjectDataRep) + { + _goViewProRep = goViewProjectRep; + _goViewProDataRep = goViewProjectDataRep; + } + + /// + /// 获取项目列表 🔖 + /// + /// + /// + /// + [DisplayName("获取项目列表")] + public async Task> GetList([FromQuery] int page = 1, [FromQuery] int limit = 12) + { + var res = await _goViewProRep.AsQueryable() + .Select(u => new GoViewProItemOutput(), true) + .ToPagedListAsync(page, limit); + return res.Items.ToList(); + } + + /// + /// 新增项目 🔖 + /// + /// + /// + [ApiDescriptionSettings(Name = "Create")] + [DisplayName("新增项目")] + public async Task Create(GoViewProCreateInput input) + { + var project = await _goViewProRep.AsInsertable(input.Adapt()).ExecuteReturnEntityAsync(); + return new GoViewProCreateOutput + { + Id = project.Id + }; + } + + /// + /// 修改项目 🔖 + /// + /// + /// + [DisplayName("修改项目")] + public async Task Edit(GoViewProEditInput input) + { + await _goViewProRep.AsUpdateable(input.Adapt()).IgnoreColumns(true).ExecuteCommandAsync(); + } + + /// + /// 删除项目 🔖 + /// + [ApiDescriptionSettings(Name = "Delete")] + [DisplayName("删除项目")] + [UnitOfWork] + public async Task Delete([FromQuery] string ids) + { + var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(u => Convert.ToInt64(u)).ToList(); + await _goViewProRep.AsDeleteable().Where(u => idList.Contains(u.Id)).ExecuteCommandAsync(); + await _goViewProDataRep.AsDeleteable().Where(u => idList.Contains(u.Id)).ExecuteCommandAsync(); + } + + /// + /// 修改发布状态 🔖 + /// + [HttpPut] + [DisplayName("修改发布状态")] + public async Task Publish(GoViewProPublishInput input) + { + await _goViewProRep.AsUpdateable() + .SetColumns(u => new GoViewPro + { + StateEnum = input.StateEnum + }) + .Where(u => u.Id == input.Id) + .ExecuteCommandAsync(); + } + + /// + /// 获取项目数据 🔖 + /// + /// + /// + [AllowAnonymous] + [ApiDescriptionSettings(Name = "GetData")] + [DisplayName("获取项目数据")] + public async Task GetData([FromQuery] long projectId) + { + var projectData = await _goViewProDataRep.GetByIdAsync(projectId); + if (projectData == null) return null; + + var project = await _goViewProRep.GetByIdAsync(projectId); + var projectDetail = project.Adapt(); + projectDetail.Content = projectData.Content; + + return projectDetail; + } + + /// + /// 保存项目数据 🔖 + /// + [ApiDescriptionSettings(Name = "save/data")] + [DisplayName("保存项目数据")] + public async Task SaveData([FromForm] GoViewProSaveDataInput input) + { + if (await _goViewProDataRep.IsAnyAsync(u => u.Id == input.ProjectId)) + { + await _goViewProDataRep.AsUpdateable() + .SetColumns(u => new GoViewProData + { + Content = input.Content + }) + .Where(u => u.Id == input.ProjectId) + .ExecuteCommandAsync(); + } + else + { + await _goViewProDataRep.InsertAsync(new GoViewProData + { + Id = input.ProjectId, + Content = input.Content, + }); + } + } + + /// + /// 上传预览图 🔖 + /// + [DisplayName("上传预览图")] + public async Task Upload(IFormFile @object) + { + /* + * 前端逻辑(useSync.hook.ts 的 dataSyncUpdate 方法): + * 如果 FileUrl 不为空,使用 FileUrl + * 否则使用 GetOssInfo 接口获取到的 BucketUrl 和 FileName 进行拼接 + */ + + // 文件名格式示例 13414795568325_index_preview.png + var fileNameSplit = @object.FileName.Split('_'); + var idStr = fileNameSplit[0]; + if (!long.TryParse(idStr, out var id)) return new GoViewProUploadOutput(); + + // 将预览图转换成 Base64 + var ms = new MemoryStream(); + await @object.CopyToAsync(ms); + var base64Image = Convert.ToBase64String(ms.ToArray()); + + // 保存 + if (await _goViewProDataRep.IsAnyAsync(u => u.Id == id)) + { + await _goViewProDataRep.AsUpdateable() + .SetColumns(u => new GoViewProData + { + IndexImageData = base64Image + }) + .Where(u => u.Id == id) + .ExecuteCommandAsync(); + } + else + { + await _goViewProDataRep.InsertAsync(new GoViewProData + { + Id = id, + IndexImageData = base64Image, + }); + } + + var output = new GoViewProUploadOutput + { + Id = id, + BucketName = null, + CreateTime = null, + CreateUserId = null, + FileName = null, + FileSize = 0, + FileSuffix = "png", + FileUrl = $"api/goview/project/getIndexImage/{id}", + UpdateTime = null, + UpdateUserId = null + }; + + #region 使用 SysFileService 方式(已注释) + + ////删除已存在的预览图 + //var uploadFileName = Path.GetFileNameWithoutExtension(@object.FileName); + //var existFiles = await _fileRep.GetListAsync(u => u.FileName == uploadFileName); + //foreach (var f in existFiles) + // await _fileService.DeleteFile(new DeleteFileInput { Id = f.Id }); + + ////保存预览图 + //var result = await _fileService.UploadFile(@object, ""); + //var file = await _fileRep.GetByIdAsync(result.Id); + //int.TryParse(file.SizeKb, out var size); + + ////本地存储,使用拼接的地址 + //var fileUrl = file.BucketName == "Local" ? $"{file.FilePath}/{file.Id}{file.Suffix}" : file.Url; + + //var output = new ProjectUploadOutput + //{ + // Id = file.Id, + // BucketName = file.BucketName, + // CreateTime = file.CreateTime, + // CreateUserId = file.CreateUserId, + // FileName = $"{file.FileName}{file.Suffix}", + // FileSize = size, + // FileSuffix = file.Suffix?[1..], + // FileUrl = fileUrl, + // UpdateTime = null, + // UpdateUserId = null + //}; + + #endregion 使用 SysFileService 方式(已注释) + + return output; + } + + /// + /// 获取预览图 🔖 + /// + /// + [AllowAnonymous] + [NonUnify] + [ApiDescriptionSettings(Name = "GetIndexImage")] + [DisplayName("获取预览图")] + public async Task GetIndexImage(long id) + { + var projectData = await _goViewProDataRep.AsQueryable().IgnoreColumns(u => u.Content).FirstAsync(u => u.Id == id); + if (projectData?.IndexImageData == null) + return new NoContentResult(); + + var bytes = Convert.FromBase64String(projectData.IndexImageData); + return new FileStreamResult(new MemoryStream(bytes), "image/png"); + } + + /// + /// 上传背景图 + /// + [DisplayName("上传背景图")] + public async Task UploadBackGround(IFormFile @object) + { + // 文件名格式示例 13414795568325_index_preview.png + var fileNameSplit = @object.FileName.Split('_'); + var idStr = fileNameSplit[0]; + if (!long.TryParse(idStr, out var id)) return new GoViewProUploadOutput(); + + // 将预览图转换成 Base64 + var ms = new MemoryStream(); + await @object.CopyToAsync(ms); + var base64Image = Convert.ToBase64String(ms.ToArray()); + + // 保存 + if (await _goViewProDataRep.IsAnyAsync(u => u.Id == id)) + { + await _goViewProDataRep.AsUpdateable() + .SetColumns(u => new GoViewProData + { + BackGroundImageData = base64Image + }) + .Where(u => u.Id == id) + .ExecuteCommandAsync(); + } + else + { + await _goViewProDataRep.InsertAsync(new GoViewProData + { + Id = id, + BackGroundImageData = base64Image, + }); + } + + var output = new GoViewProUploadOutput + { + Id = id, + BucketName = null, + CreateTime = null, + CreateUserId = null, + FileName = null, + FileSize = 0, + FileSuffix = "png", + FileUrl = $"api/goview/project/getBackGroundImage/{id}", + UpdateTime = null, + UpdateUserId = null + }; + + return output; + } + + /// + /// 获取背景图 + /// + /// + [AllowAnonymous] + [NonUnify] + [ApiDescriptionSettings(Name = "GetBackGroundImage")] + [DisplayName("获取背景图")] + public async Task GetBackGroundImage(long id) + { + var projectData = await _goViewProDataRep.AsQueryable().IgnoreColumns(u => u.Content).FirstAsync(u => u.Id == id); + if (projectData?.BackGroundImageData == null) + return new NoContentResult(); + + var bytes = Convert.FromBase64String(projectData.BackGroundImageData); + return new FileStreamResult(new MemoryStream(bytes), "image/png"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginInput.cs new file mode 100644 index 0000000..b777697 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginInput.cs @@ -0,0 +1,30 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// 登录输入 +/// +public class GoViewLoginInput +{ + /// + /// 用户名 + /// + [Required(ErrorMessage = "用户名不能为空")] + public string Username { get; set; } + + /// + /// 密码 + /// + [Required(ErrorMessage = "密码不能为空")] + public string Password { get; set; } + + /// + /// 租户 + /// + public long? TenantId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginOutput.cs new file mode 100644 index 0000000..029671e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewLoginOutput.cs @@ -0,0 +1,60 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// 登录输出 +/// +public class GoViewLoginOutput +{ + /// + /// 用户信息 + /// + public GoViewLoginUserInfo Userinfo { get; set; } + + /// + /// Token + /// + public GoViewLoginToken Token { get; set; } +} + +/// +/// 登录 Token +/// +public class GoViewLoginToken +{ + /// + /// Token 名 + /// + public string TokenName { get; set; } = "Authorization"; + + /// + /// Token 值 + /// + public string TokenValue { get; set; } +} + +/// +/// 用户信息 +/// +public class GoViewLoginUserInfo +{ + /// + /// 用户 Id + /// + public string Id { get; set; } + + /// + /// 用户名 + /// + public string Username { get; set; } + + /// + /// 昵称 + /// + public string Nickname { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewOssUrlOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewOssUrlOutput.cs new file mode 100644 index 0000000..bf21d3c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/Dto/GoViewOssUrlOutput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// 获取 OSS 上传接口输出 +/// +public class GoViewOssUrlOutput +{ + /// + /// 桶名 + /// + public string BucketName { get; set; } + + /// + /// BucketURL 地址 + /// + public string BucketURL { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/GoViewSysService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/GoViewSysService.cs new file mode 100644 index 0000000..e3449b2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewSys/GoViewSysService.cs @@ -0,0 +1,87 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView.Service; + +/// +/// 系统登录服务 🧩 +/// +[UnifyProvider("GoView")] +[ApiDescriptionSettings(GoViewConst.GroupName, Module = "goview", Name = "sys", Order = 100, Description = "系统登录")] +public class GoViewSysService : IDynamicApiController +{ + private readonly SysAuthService _sysAuthService; + private readonly SqlSugarRepository _sysUserRep; + private readonly SysCacheService _sysCacheService; + + public GoViewSysService(SysAuthService sysAuthService, + SqlSugarRepository sysUserRep, + SysCacheService sysCacheService) + { + _sysAuthService = sysAuthService; + _sysUserRep = sysUserRep; + _sysCacheService = sysCacheService; + } + + /// + /// GoView 登录 🔖 + /// + /// + [AllowAnonymous] + [DisplayName("GoView 登录")] + public async Task Login(GoViewLoginInput input) + { + // 设置默认租户 + input.TenantId ??= SqlSugarConst.DefaultTenantId; + + _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}", false); + + input.Password = CryptogramUtil.SM2Encrypt(input.Password); + var loginResult = await _sysAuthService.Login(new LoginInput() + { + Account = input.Username, + Password = input.Password, + }); + + _sysCacheService.Remove($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}"); + + var sysUser = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Account.Equals(input.Username)); + return new GoViewLoginOutput() + { + Userinfo = new GoViewLoginUserInfo + { + Id = sysUser.Id.ToString(), + Username = sysUser.Account, + Nickname = sysUser.NickName, + }, + Token = new GoViewLoginToken + { + TokenValue = $"Bearer {loginResult.AccessToken}" + } + }; + } + + /// + /// GoView 退出 🔖 + /// + [DisplayName("GoView 退出")] + public void GetLogout() + { + _sysAuthService.Logout(); + } + + /// + /// 获取 OSS 上传接口 🔖 + /// + /// + [AllowAnonymous] + [ApiDescriptionSettings(Name = "GetOssInfo")] + [DisplayName("获取 OSS 上传接口")] + public Task GetOssInfo() + { + return Task.FromResult(new GoViewOssUrlOutput { BucketURL = "" }); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Startup.cs new file mode 100644 index 0000000..ab7a81d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Startup.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Plugin.GoView; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + // 注册 GoView 规范化处理提供器 + services.AddUnifyProvider("GoView"); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs new file mode 100644 index 0000000..a8970aa --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs @@ -0,0 +1,133 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.GoView; + +/// +/// GoView 规范化结果 +/// +[UnifyModel(typeof(GoViewResult<>))] +public class GoViewResultProvider : IUnifyResultProvider +{ + /// + /// JWT 授权异常返回值 + /// + /// + /// + /// + public IActionResult OnAuthorizeException(DefaultHttpContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, errors: metadata.Errors)); + } + + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, errors: metadata.Errors)); + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data)); + } + + /// + /// 验证失败返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode ?? StatusCodes.Status400BadRequest, data: metadata.Data, errors: metadata.ValidationResult)); + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + UnifyContext.SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + await context.Response.WriteAsJsonAsync(RESTfulResult(886, errors: "401 登录已过期,请重新登录"), + App.GetOptions()?.JsonSerializerOptions); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "403 禁止访问,没有权限"), + App.GetOptions()?.JsonSerializerOptions); + break; + + default: break; + } + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + private static GoViewResult RESTfulResult(int statusCode, bool succeeded = default, object data = default, object errors = default) + { + return new GoViewResult + { + Code = statusCode, + Msg = errors is null or string ? (errors + "") : JSON.Serialize(errors), + Data = data, + Count = data != null && data.GetType().IsGenericType && data.GetType().GetGenericTypeDefinition() == typeof(List<>) ? ((IList)data).Count : null + }; + } +} + +/// +/// GoView 返回结果 +/// +/// +public class GoViewResult +{ + /// + /// 状态码 + /// + public int Code { get; set; } + + /// + /// 信息 + /// + public string Msg { get; set; } + + /// + /// 数据 + /// + public T Data { get; set; } + + /// + /// 总数 + /// + public int? Count { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Admin.NET.Plugin.HwPortal.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Admin.NET.Plugin.HwPortal.csproj new file mode 100644 index 0000000..e7dcf7b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Admin.NET.Plugin.HwPortal.csproj @@ -0,0 +1,31 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET hw-portal 模块 + + + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalAjaxResult.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalAjaxResult.cs new file mode 100644 index 0000000..275ebd9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalAjaxResult.cs @@ -0,0 +1,442 @@ +// ============================================================================ +// 【文件说明】HwPortalAjaxResult.cs - Ajax 统一响应结果类 +// ============================================================================ +// 这个类用于统一所有 API 接口的返回格式。 +// 在 Java 若依(RuoYi)框架中,有一个 AjaxResult 类做同样的事情。 +// +// 统一返回格式的好处: +// 1. 前端可以用同一套逻辑处理所有接口响应 +// 2. 便于统一添加日志、监控、错误处理 +// 3. 接口文档更清晰,返回结构可预测 +// +// 返回的 JSON 结构示例: +// 成功:{ "code": 200, "msg": "操作成功", "data": { ... } } +// 失败:{ "code": 500, "msg": "操作失败", "data": null } +// 警告:{ "code": 301, "msg": "警告信息", "data": null } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// Ajax 统一响应结果类。 +/// +/// 【设计模式 - 静态工厂方法模式】 +/// 这个类大量使用了"静态工厂方法"设计模式: +/// - Success() 返回成功结果 +/// - Error() 返回失败结果 +/// - Warn() 返回警告结果 +/// +/// 对比 Java: +/// Java 中你可能会写: +/// AjaxResult result = new AjaxResult(); +/// result.put("code", 200); +/// result.put("msg", "操作成功"); +/// +/// C# 这里用静态工厂方法更优雅: +/// return HwPortalAjaxResult.Success("操作成功", data); +/// +/// 好处: +/// 1. 代码更简洁,一行搞定 +/// 2. 避免忘记设置 code/msg +/// 3. 可以在工厂方法里统一加日志、校验等逻辑 +/// +/// +public class HwPortalAjaxResult +{ + /// + /// 返回码字段名。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了,不能修改。 + /// + /// 对比 Java: + /// Java 写法:public static final String CODE_TAG = "code"; + /// C# 写法:public const string CodeTag = "code"; + /// + /// const 和 readonly 的区别: + /// - const:编译期确定,值直接嵌入调用处(类似宏替换) + /// - readonly:运行时确定,可以在构造函数中赋值 + /// + /// 这里用 const 是因为字段名永远不会变,编译期确定更高效。 + /// + /// 与原 Java AjaxResult 的 CODE_TAG 对齐。 + /// + public const string CodeTag = "code"; + + /// + /// 返回消息字段名。 + /// 与原 Java AjaxResult 的 MSG_TAG 对齐。 + /// + public const string MsgTag = "msg"; + + /// + /// 返回数据字段名。 + /// 与原 Java AjaxResult 的 DATA_TAG 对齐。 + /// + public const string DataTag = "data"; + + /// + /// 成功状态码。 + /// HTTP 200 OK 的含义是"请求成功",这里借用这个惯例。 + /// + public const int SuccessCode = 200; + + /// + /// 警告状态码。 + /// RuoYi 默认使用 301 作为 warn(HTTP 301 是重定向,但这里只是借用数字)。 + /// 警告通常表示"操作部分成功"或"需要用户注意但不阻断"的情况。 + /// + public const int WarnCode = 301; + + /// + /// 错误状态码。 + /// HTTP 500 Internal Server Error 的含义是"服务器内部错误"。 + /// 这里用于表示"操作失败"的业务错误。 + /// + public const int ErrorCode = 500; + + /// + /// 状态码。 + /// + /// 【C# 语法知识点 - 自动属性】 + /// public int Code { get; set; } + /// 这是 C# 的"自动属性"语法,编译器自动生成后备字段和 get/set 方法。 + /// + /// 对比 Java: + /// Java 需要手写 getter/setter: + /// private int code; + /// public int getCode() { return code; } + /// public void setCode(int code) { this.code = code; } + /// + /// + public int Code { get; set; } + + /// + /// 返回消息。 + /// + public string Msg { get; set; } + + /// + /// 返回数据。 + /// + /// 【C# 语法知识点 - object 类型】 + /// object 是 C# 的"根类型",所有类型都继承自 object。 + /// 类似 Java 的 Object,但 C# 的 object 可以是值类型(int, bool 等)。 + /// + /// 为什么用 object? + /// 因为 Data 可以是任何类型: + /// - 单个对象:{ "id": 1, "name": "张三" } + /// - 列表:[{ "id": 1 }, { "id": 2 }] + /// - 字符串:"操作成功" + /// - null:没有数据 + /// + /// 使用 object 可以容纳所有可能的数据类型。 + /// + /// + public object Data { get; set; } + + /// + /// 无参构造函数。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// 构造函数名称必须与类名相同,没有返回类型。 + /// + /// 对比 Java: + /// Java 和 C# 的构造函数语法几乎一致。 + /// + /// 为什么需要无参构造函数? + /// 某些序列化框架(如 JSON 反序列化)需要无参构造函数来创建对象, + /// 然后再通过属性赋值。 + /// + /// + public HwPortalAjaxResult() + { + } + + /// + /// 带参数的构造函数。 + /// + /// 【C# 语法知识点 - 可选参数 data = null】 + /// object data = null 中的 = null 是"默认参数值"。 + /// 调用时可以不传这个参数,自动使用默认值 null。 + /// + /// 对比 Java: + /// Java 不支持可选参数,需要写多个重载构造函数: + /// public HwPortalAjaxResult(int code, String msg) { this(code, msg, null); } + /// public HwPortalAjaxResult(int code, String msg, Object data) { ... } + /// + /// C# 一个构造函数就搞定了,更简洁。 + /// + /// + /// 状态码 + /// 返回消息 + /// 返回数据,默认为 null + public HwPortalAjaxResult(int code, string msg, object data = null) + { + Code = code; + Msg = msg; + Data = data; + } + + /// + /// 返回成功结果(默认消息)。 + /// + /// 【C# 语法知识点 - 静态工厂方法】 + /// public static HwPortalAjaxResult Success() + /// 这是一个静态方法,返回类本身的实例。 + /// + /// 为什么用静态工厂方法而不是直接 new? + /// 1. 命名更清晰:Success() 比 new AjaxResult(200, "操作成功") 更易读 + /// 2. 可以缓存常用实例(这里没做,但可以扩展) + /// 3. 可以返回子类(这里没用到,但这是工厂模式的优势) + /// + /// 对比 Java: + /// Java 的静态工厂方法写法一样: + /// public static AjaxResult success() { return new AjaxResult(200, "操作成功"); } + /// + /// + /// 成功结果实例 + public static HwPortalAjaxResult Success() + { + // 方法重载链:调用带消息参数的版本 + return Success("操作成功"); + } + + /// + /// 返回成功结果(带数据)。 + /// + /// 返回数据 + /// 成功结果实例 + public static HwPortalAjaxResult Success(object data) + { + return Success("操作成功", data); + } + + /// + /// 返回成功结果(带自定义消息)。 + /// + /// 返回消息 + /// 成功结果实例 + public static HwPortalAjaxResult Success(string msg) + { + return Success(msg, null); + } + + /// + /// 返回成功结果(完整参数)。 + /// + /// 【C# 语法知识点 - 对象初始化器】 + /// new HwPortalAjaxResult { Code = ..., Msg = ..., Data = ... } + /// 这是"对象初始化器"语法,可以在创建对象时直接给属性赋值。 + /// + /// 对比 Java: + /// Java 需要这样写: + /// HwPortalAjaxResult result = new HwPortalAjaxResult(); + /// result.setCode(SuccessCode); + /// result.setMsg(msg); + /// result.setData(data); + /// return result; + /// + /// C# 一行搞定,更简洁。 + /// + /// + /// 返回消息 + /// 返回数据 + /// 成功结果实例 + public static HwPortalAjaxResult Success(string msg, object data) + { + return new HwPortalAjaxResult + { + Code = SuccessCode, + Msg = msg, + Data = data + }; + } + + /// + /// 返回警告结果(仅消息)。 + /// + /// 警告消息 + /// 警告结果实例 + public static HwPortalAjaxResult Warn(string msg) + { + return Warn(msg, null); + } + + /// + /// 返回警告结果(带数据)。 + /// + /// 警告消息 + /// 返回数据 + /// 警告结果实例 + public static HwPortalAjaxResult Warn(string msg, object data) + { + return new HwPortalAjaxResult + { + Code = WarnCode, + Msg = msg, + Data = data + }; + } + + /// + /// 返回错误结果(默认消息)。 + /// + /// 错误结果实例 + public static HwPortalAjaxResult Error() + { + return Error("操作失败"); + } + + /// + /// 返回错误结果(自定义消息)。 + /// + /// 错误消息 + /// 错误结果实例 + public static HwPortalAjaxResult Error(string msg) + { + return Error(msg, null); + } + + /// + /// 返回错误结果(带数据)。 + /// + /// 错误消息 + /// 返回数据 + /// 错误结果实例 + public static HwPortalAjaxResult Error(string msg, object data) + { + return new HwPortalAjaxResult + { + Code = ErrorCode, + Msg = msg, + Data = data + }; + } + + /// + /// 返回错误结果(自定义状态码)。 + /// + /// 自定义状态码 + /// 错误消息 + /// 错误结果实例 + public static HwPortalAjaxResult Error(int code, string msg) + { + return new HwPortalAjaxResult + { + Code = code, + Msg = msg, + Data = null + }; + } + + /// + /// 根据影响行数返回成功或失败结果。 + /// + /// 【业务场景】 + /// 在数据库操作中,增删改会返回"影响行数": + /// - rows > 0:表示操作成功,有数据被修改 + /// - rows = 0:表示操作失败,没有数据被修改 + /// + /// 这个方法把"影响行数"转换为"响应结果",简化控制器代码: + /// return ToAjax(await service.Insert(input)); + /// + /// 对比 Java 若依: + /// 若依的 BaseController.toAjax(int rows) 做同样的事情。 + /// + /// + /// 数据库影响行数 + /// 成功或失败结果 + public static HwPortalAjaxResult FromRows(int rows) + { + // 【三元运算符】 + // condition ? valueIfTrue : valueIfFalse + // 和 Java 完全一样的语法。 + // + // Why:RuoYi 的 BaseController.toAjax(int rows) 只根据影响行数返回成功/失败, + // 不会把 rows 本身塞进 data;这里必须保持同一口径,否则前端判断会漂移。 + return rows > 0 ? Success() : Error(); + } + + /// + /// 判断是否成功。 + /// + /// 是否成功 + public bool IsSuccess() + { + return Code == SuccessCode; + } + + /// + /// 判断是否警告。 + /// + /// 是否警告 + public bool IsWarn() + { + return Code == WarnCode; + } + + /// + /// 判断是否错误。 + /// + /// 是否错误 + public bool IsError() + { + return Code == ErrorCode; + } + + /// + /// 链式设置字段值(兼容原 Java AjaxResult 的 put 方法)。 + /// + /// 【C# 语法知识点 - 链式调用】 + /// 这个方法返回 this,可以链式调用: + /// result.Put("code", 200).Put("msg", "成功"); + /// + /// 对比 Java: + /// Java 若依的 AjaxResult 继承自 HashMap,可以: + /// result.put("code", 200).put("msg", "成功"); + /// + /// C# 这里用强类型对象实现,只对常见字段做兼容。 + /// + /// + /// 【C# 语法知识点 - StringComparison.OrdinalIgnoreCase】 + /// OrdinalIgnoreCase 表示"忽略大小写的序号比较"。 + /// 这样 "Code" 和 "code" 会被视为相同,提高容错性。 + /// + /// 对比 Java: + /// Java 用 equalsIgnoreCase() 方法: + /// key.equalsIgnoreCase(CodeTag) + /// + /// + /// 字段名 + /// 字段值 + /// 当前实例,支持链式调用 + public HwPortalAjaxResult Put(string key, object value) + { + // 【字符串比较的最佳实践】 + // string.Equals(a, b, StringComparison.OrdinalIgnoreCase) + // 比 a.ToLower() == b.ToLower() 更高效,不会产生新字符串对象。 + if (string.Equals(key, CodeTag, StringComparison.OrdinalIgnoreCase)) + { + // 【类型转换】 + // Convert.ToInt32 可以处理多种类型:string, long, double 等。 + // CultureInfo.InvariantCulture 表示使用"不变的格式规则", + // 避免不同地区的数字格式差异(如某些地区用逗号作小数点)。 + Code = Convert.ToInt32(value, CultureInfo.InvariantCulture); + return this; + } + + if (string.Equals(key, MsgTag, StringComparison.OrdinalIgnoreCase)) + { + Msg = Convert.ToString(value, CultureInfo.InvariantCulture); + return this; + } + + if (string.Equals(key, DataTag, StringComparison.OrdinalIgnoreCase)) + { + Data = value; + } + + return this; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalBaseEntity.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalBaseEntity.cs new file mode 100644 index 0000000..6a1e176 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalBaseEntity.cs @@ -0,0 +1,97 @@ +// ============================================================================ +// 【文件说明】HwPortalBaseEntity.cs - 门户模块实体基类 +// ============================================================================ +// 这个文件定义了所有门户实体的"公共字段基类"。 +// 在 Java Spring Boot 项目中,你可能会用 @MappedSuperclass 注解一个 BaseEntity, +// 然后让其他实体类继承它。这里的概念完全一样! +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户模块实体基类。 +/// +/// 【C# 语法知识点 - abstract 关键字】 +/// abstract 表示"抽象类",和 Java 的 abstract class 含义完全一致: +/// - 不能直接 new 实例化,只能被继承 +/// - 可以包含抽象方法(没有实现体)和具体方法(有实现体) +/// - 子类必须实现所有抽象成员 +/// +/// +/// 【与 Java Spring Boot 的对比】 +/// Java: @MappedSuperclass public class BaseEntity { ... } +/// C#: public abstract class HwPortalBaseEntity { ... } +/// 两者目的相同:把公共字段抽取到父类,避免每个实体都重复定义 create_by、create_time 等字段。 +/// +/// +public abstract class HwPortalBaseEntity +{ + /// + /// 创建人。 + /// + /// 【C# 语法知识点 - 属性(Property)】 + /// public string CreateBy { get; set; } 是 C# 的"自动属性"语法。 + /// 编译器会自动生成一个私有的后备字段(backing field)和 get/set 方法。 + /// + /// 对比 Java: + /// Java 需要手写: + /// private String createBy; + /// public String getCreateBy() { return createBy; } + /// public void setCreateBy(String createBy) { this.createBy = createBy; } + /// + /// C# 只需要一行: + /// public string CreateBy { get; set; } + /// + /// 这就是 C# 的"语法糖"——让代码更简洁,编译器帮你生成繁琐的部分。 + /// + /// + [SugarColumn(ColumnName = "create_by")] + // 【C# 语法知识点 - 特性(Attribute)】 + // [SugarColumn(...)] 是 SqlSugar ORM 的特性,用于映射数据库列名。 + // 特性相当于 Java 的注解(Annotation),写法是方括号 [] 而不是 @。 + // + // Java 写法:@Column(name = "create_by") + // C# 写法:[SugarColumn(ColumnName = "create_by")] + // + // 这里为什么要写 ColumnName = "create_by"? + // 因为 C# 的命名规范是 PascalCase(首字母大写),但数据库列名是 snake_case(小写+下划线)。 + // 如果不指定映射,SqlSugar 会默认把 CreateBy 映射成 CreateBy 列(大写开头), + // 这在 MySQL(Linux 环境,区分大小写)下会报错"列不存在"。 + public string CreateBy { get; set; } + + /// + /// 创建时间。 + /// + /// 【C# 语法知识点 - 可空值类型 DateTime?】 + /// DateTime 是值类型(struct),不能为 null。 + /// 但数据库里的 create_time 可能是 NULL(比如老数据没填)。 + /// + /// DateTime? 是"可空 DateTime",等价于 Nullable<DateTime>。 + /// 它可以是具体的日期时间,也可以是 null。 + /// + /// 对比 Java: + /// Java 的 Date 本身就是引用类型,可以直接 null。 + /// C# 把日期作为值类型是为了性能,但这就需要 Nullable<T> 来支持 null。 + /// + /// + [SugarColumn(ColumnName = "create_time")] + public DateTime? CreateTime { get; set; } + + /// + /// 更新人。 + /// + [SugarColumn(ColumnName = "update_by")] + public string UpdateBy { get; set; } + + /// + /// 更新时间。 + /// + [SugarColumn(ColumnName = "update_time")] + public DateTime? UpdateTime { get; set; } + + /// + /// 备注信息。 + /// + [SugarColumn(ColumnName = "remark")] + public string Remark { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalContextHelper.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalContextHelper.cs new file mode 100644 index 0000000..42f299c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalContextHelper.cs @@ -0,0 +1,126 @@ +// ============================================================================ +// 【文件说明】HwPortalContextHelper.cs - 上下文帮助类 +// ============================================================================ +// 这个类提供一些"获取当前请求上下文信息"的静态方法。 +// 例如:当前时间、当前用户名、当前请求 IP 等。 +// +// 在 Java Spring Boot 中,你可能会这样做: +// - 获取用户:SecurityContextHolder.getContext().getAuthentication().getName() +// - 获取 IP:request.getRemoteAddr() +// +// 在 ASP.NET Core + Furion 中,方式略有不同,这个类封装了这些差异。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户上下文帮助类。 +/// +/// 【C# 语法知识点 - internal static class】 +/// internal:表示"程序集内部可见",只能被当前项目(.dll)内的代码访问。 +/// static:表示"静态类",不能被实例化,所有成员必须是静态的。 +/// +/// 为什么用 internal static? +/// - 这个类只是内部工具方法,不需要暴露给外部调用者 +/// - 静态类可以直接用类名调用方法,不需要 new 对象 +/// +/// 对比 Java: +/// Java 没有 internal 关键字,通常用 package-private(不写访问修饰符)来限制可见性。 +/// Java 的静态类通常用 private 构造函数 + 静态方法来模拟。 +/// +/// +internal static class HwPortalContextHelper +{ + /// + /// 获取当前时间。 + /// + /// 【为什么封装这个方法?】 + /// 看起来多此一举,直接 DateTime.Now 不就行了? + /// 但在单元测试时,这个封装就很有用: + /// - 你可以用 Mock 框架替换这个方法返回固定时间 + /// - 测试"创建时间是否正确"时,不需要真的等待时间流逝 + /// + /// 这叫"时间抽象",是可测试性设计的一部分。 + /// + /// + /// 当前本地时间 + public static DateTime Now() + { + // DateTime.Now 返回本地时间,DateTime.UtcNow 返回 UTC 时间。 + // 在服务器开发中,推荐用 UTC 时间存储,显示时再转本地时间。 + // 这里保持用本地时间是为了兼容原 Java 代码的行为。 + return DateTime.Now; + } + + /// + /// 获取当前用户名。 + /// + /// 【C# 语法知识点 - ?. 空条件运算符】 + /// App.User?.FindFirst(...) 中的 ?. 是"空条件运算符"。 + /// 含义:如果 App.User 不为 null,才调用后面的方法;否则直接返回 null。 + /// + /// 对比 Java: + /// Java 需要这样写: + /// if (App.User != null) { + /// Claim claim = App.User.FindFirst(...); + /// if (claim != null) { + /// return claim.getValue(); + /// } + /// } + /// + /// C# 可以一行搞定: + /// App.User?.FindFirst(ClaimConst.Account)?.Value + /// + /// 这就是 C# 的"空值传播"语法,大幅减少空指针检查代码。 + /// + /// + /// 【C# 语法知识点 - ?? 空合并运算符】 + /// ?? 是"空合并运算符":左边不为 null 就返回左边,否则返回右边。 + /// + /// 这行代码的完整逻辑是: + /// 1. 先尝试从 Account 声明获取用户名 + /// 2. 如果没有,尝试从 Identity.Name 获取 + /// 3. 如果都没有,返回默认值 "system" + /// + /// 对比 Java: + /// Java 需要多个 if-else 或三元运算符: + /// String name = account != null ? account : (identityName != null ? identityName : "system"); + /// + /// + /// 当前登录用户名,未登录则返回 "system" + public static string CurrentUserName() + { + // App.User 是 Furion 框架提供的静态属性,代表当前登录用户的 ClaimsPrincipal。 + // ClaimsPrincipal 类似 Java Spring Security 的 Authentication 对象。 + // + // ClaimConst.Account 是自定义的声明类型常量,代表"账号名"声明。 + // FindFirst() 方法查找指定类型的声明,返回 Claim 对象。 + // .Value 获取声明的值。 + return App.User?.FindFirst(ClaimConst.Account)?.Value + ?? App.User?.Identity?.Name + ?? "system"; + } + + /// + /// 获取当前请求的客户端 IP 地址。 + /// + /// HTTP 上下文对象 + /// IPv4 地址字符串,获取失败返回 null + public static string CurrentRequestIp(HttpContext httpContext) + { + // 【获取 IP 的完整链路】 + // httpContext.Connection:获取连接信息对象 + // .RemoteIpAddress:获取远程 IP 地址(可能是 IPv6) + // ?.MapToIPv4():将 IPv6 映射到 IPv4 格式(如果原来是 IPv4 则不变) + // ?.ToString():转换为字符串形式,如 "192.168.1.100" + // + // 为什么用 MapToIPv4()? + // 现代服务器可能监听 IPv6,客户端连接时 RemoteIpAddress 可能是 IPv6 格式。 + // 前端和日志系统通常期望 IPv4,所以这里做个转换。 + // + // 对比 Java Spring Boot: + // Java 获取 IP:request.getRemoteAddr() + // 但 Java 返回的是字符串,不需要手动转换格式。 + return httpContext?.Connection?.RemoteIpAddress?.MapToIPv4().ToString(); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalControllerBase.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalControllerBase.cs new file mode 100644 index 0000000..adaad21 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalControllerBase.cs @@ -0,0 +1,422 @@ +// ============================================================================ +// 【文件说明】HwPortalControllerBase.cs - 门户控制器基类 +// ============================================================================ +// 这个类是所有门户控制器的"公共基类"。 +// 在 Java Spring Boot 中,你可能会写一个 BaseController,提供公共方法。 +// 这里概念完全一样:把"返回成功"、"返回失败"、"分页"等公共逻辑封装起来。 +// +// 【继承关系】 +// HwPortalControllerBase : ControllerBase +// ↑ +// HwWebController, HwProductInfoController, ... (具体控制器) +// +// ControllerBase 是 ASP.NET Core 提供的控制器基类, +// 包含 Request、Response、User、HttpContext 等常用属性。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户控制器基类。 +/// +/// 【C# 语法知识点 - abstract 抽象类】 +/// abstract class 表示"抽象类",和 Java 的 abstract class 含义完全一致: +/// - 不能直接 new 实例化,只能被继承 +/// - 可以包含抽象方法(没有实现体)和具体方法(有实现体) +/// - 子类必须实现所有抽象成员 +/// +/// +/// 【C# 语法知识点 - ControllerBase】 +/// ControllerBase 是 ASP.NET Core 提供的控制器基类。 +/// 它提供了: +/// - Request:HTTP 请求对象 +/// - Response:HTTP 响应对象 +/// - User:当前登录用户信息 +/// - HttpContext:HTTP 上下文 +/// - Ok()、BadRequest()、NotFound() 等返回方法 +/// +/// 对比 Java Spring Boot: +/// Spring 的控制器通常继承或使用 @RestController 注解, +/// 方法参数可以注入 HttpServletRequest、HttpServletResponse 等。 +/// +/// ASP.NET Core 的方式更"面向对象": +/// 控制器本身就是请求上下文的载体,通过属性访问请求/响应。 +/// +/// +/// 【与 Java Spring Boot 的对比】 +/// Java 若依的 BaseController: +/// public class BaseController { +/// protected AjaxResult success() { ... } +/// protected AjaxResult error() { ... } +/// protected TableDataInfo getDataTable(List list) { ... } +/// } +/// +/// C# 这里的设计几乎一样,只是语法不同。 +/// +/// +[ApiController] +// 【C# 语法知识点 - 特性(Attribute)】 +// [ApiController] 是 ASP.NET Core 的特性,用于标记这是一个 API 控制器。 +// 它会自动启用: +// 1. 自动 HTTP 400 响应(模型验证失败时) +// 2. 绑定源推断(自动从 body/query/route 绑定参数) +// 3. 问题详情响应(错误时返回标准格式) +// +// 对比 Java Spring Boot: +// Java 用 @RestController 注解,功能类似: +// @RestController +// public class MyController { ... } +// +// C# 的特性用方括号 [],Java 的注解用 @。 +public abstract class HwPortalControllerBase : ControllerBase +{ + /// + /// 返回成功结果(带数据)。 + /// + /// 【C# 语法知识点 - protected 访问修饰符】 + /// protected 表示"受保护的",只能在当前类或子类中访问。 + /// + /// 为什么用 protected? + /// 这些方法是给子控制器用的,不应该被外部直接调用。 + /// + /// 对比 Java: + /// Java 的 protected 含义一样,但 C# 没有"包级访问"的概念。 + /// Java 的 protected 还允许同一个包内的类访问,C# 不行。 + /// + /// + /// 【方法重载】 + /// Success()、Success(data)、Success(msg)、Success(msg, data) + /// 这是"方法重载"(Overload):同名方法,参数不同。 + /// + /// 对比 Java: + /// Java 的方法重载语法完全一样。 + /// 但 C# 还支持"可选参数",可以减少重载数量: + /// protected HwPortalAjaxResult Success(string msg = "操作成功", object data = null) + /// + /// 这里保持多个重载是为了和原 Java 代码风格一致。 + /// + /// + /// 返回数据,默认为 null + /// 返回消息,默认为 "操作成功" + /// 成功结果 + protected HwPortalAjaxResult Success(object data = null, string msg = "操作成功") + { + return HwPortalAjaxResult.Success(msg, data); + } + + /// + /// 返回成功结果(仅消息)。 + /// + /// 返回消息 + /// 成功结果 + protected HwPortalAjaxResult Success(string msg) + { + return HwPortalAjaxResult.Success(msg); + } + + /// + /// 返回警告结果。 + /// + /// 警告消息 + /// 返回数据,默认为 null + /// 警告结果 + protected HwPortalAjaxResult Warn(string msg, object data = null) + { + return HwPortalAjaxResult.Warn(msg, data); + } + + /// + /// 返回错误结果。 + /// + /// 错误消息 + /// 错误码,默认为 500 + /// 错误结果 + protected HwPortalAjaxResult Error(string msg, int code = 500) + { + // 【三元运算符】 + // condition ? valueIfTrue : valueIfFalse + // 这里判断:如果 code 等于默认错误码,用简化方法;否则用带自定义码的方法。 + return code == HwPortalAjaxResult.ErrorCode + ? HwPortalAjaxResult.Error(msg) + : HwPortalAjaxResult.Error(code, msg); + } + + /// + /// 根据影响行数返回成功或失败结果。 + /// + /// 【业务场景】 + /// 在数据库操作中,增删改会返回"影响行数": + /// - rows > 0:表示操作成功,有数据被修改 + /// - rows = 0:表示操作失败,没有数据被修改 + /// + /// 这个方法把"影响行数"转换为"响应结果",简化控制器代码: + /// return ToAjax(await service.Insert(input)); + /// + /// 对比 Java 若依: + /// 若依的 BaseController.toAjax(int rows) 做同样的事情: + /// protected AjaxResult toAjax(int rows) { + /// return rows > 0 ? success() : error(); + /// } + /// + /// + /// 数据库影响行数 + /// 成功或失败结果 + protected HwPortalAjaxResult ToAjax(int rows) + { + return HwPortalAjaxResult.FromRows(rows); + } + + /// + /// 导出 Excel 文件。 + /// + /// 【C# 语法知识点 - 泛型方法 <T>】 + /// protected IActionResult ExportExcel<T>(...) + /// 这里的 <T> 是泛型参数,表示"这个方法可以处理任何类型的数据"。 + /// + /// 调用示例: + /// ExportExcel<HwWeb>(list, "官网数据"); // 显式指定类型 + /// ExportExcel(list, "官网数据"); // 类型推断,编译器自动识别 + /// + /// 对比 Java: + /// Java 的泛型方法写法类似: + /// protected <T> ResponseEntity exportExcel(List<T> source, String fileName) + /// + /// 但 Java 的泛型有"类型擦除",运行时 T 只是 Object。 + /// C# 的泛型在运行时是真实类型,性能更好。 + /// + /// + /// 数据类型 + /// 数据列表 + /// 文件名 + /// Excel 文件响应 + protected IActionResult ExportExcel(IReadOnlyList source, string fileName) + { + // ExcelHelper 是项目中的工具类,负责把列表导出为 Excel 文件。 + // 返回的 IActionResult 会被框架转换为文件下载响应。 + return ExcelHelper.ExportData(source, fileName); + } + + /// + /// 获取分页数据(内存分页)。 + /// + /// 【内存分页 vs 数据库分页】 + /// 这个方法做的是"内存分页":先查出所有数据,再在内存中分页。 + /// + /// 优点:简单,不依赖数据库的分页语法 + /// 缺点:数据量大时性能差 + /// + /// 适用场景:数据量小(几百条以内),或者已经做过数据库分页的二次分页。 + /// + /// + /// 【C# 语法知识点 - IReadOnlyList<T>】 + /// IReadOnlyList<T> 是"只读列表接口"。 + /// 它比 List<T> 更安全,因为调用者不能修改列表内容(不能 Add/Remove)。 + /// + /// 对比 Java: + /// Java 没有内置的只读列表接口,通常用 List<T> 或 Collections.unmodifiableList()。 + /// + /// + /// 数据类型 + /// 完整数据列表 + /// 分页后的数据包装对象 + protected HwPortalTableDataInfo GetDataTable(IReadOnlyList source) + { + // 【获取分页参数】 + // Request.Query 用来读取 URL 查询字符串,例如 ?pageNum=1&pageSize=20。 + // + // 对比 Java Spring Boot: + /// Java 通常用 @RequestParam 注解获取: + /// public Result list(@RequestParam(defaultValue = "1") int pageNum, + /// @RequestParam(defaultValue = "10") int pageSize) + /// + /// 这里直接从 Request.Query 读取,更灵活但需要手动处理默认值。 + int pageNum = ParsePositiveInt(Request.Query["pageNum"], 1); + int pageSize = ParsePositiveInt(Request.Query["pageSize"], source.Count == 0 ? 10 : source.Count); + + // 【计算跳过的记录数】 + // skip = 需要跳过多少条。 + // 这是最常见的内存分页算法: + // 第1页:skip = 0,取 1-10 条 + // 第2页:skip = 10,取 11-20 条 + // 第3页:skip = 20,取 21-30 条 + // + // Math.Max(0, ...) 确保不会出现负数(比如 pageNum = 0 的情况)。 + int skip = Math.Max(0, (pageNum - 1) * pageSize); + + // 【LINQ 分页操作】 + // source.Skip(skip).Take(pageSize).ToList() + // + // Skip(n):跳过前 n 条记录 + // Take(n):取接下来的 n 条记录 + // ToList():将结果转换为 List<T> + // + // 对比 Java Stream: + /// Java 写法: + /// list.stream().skip(skip).limit(pageSize).collect(Collectors.toList()); + /// + /// C# 的 LINQ 和 Java Stream 非常相似,都是函数式编程风格。 + IReadOnlyList rows = source.Skip(skip).Take(pageSize).ToList(); + + // 【对象初始化器】 + // new HwPortalTableDataInfo<T> { Total = ..., Rows = ... } + // 这是"对象初始化器"语法,可以在创建对象时直接给属性赋值。 + // + // 对比 Java: + /// Java 需要这样写: + /// HwPortalTableDataInfo<T> dto = new HwPortalTableDataInfo<>(); + /// dto.setTotal(source.size()); + /// dto.setRows(rows); + /// return dto; + /// + /// C# 一行搞定,更简洁。 + return new HwPortalTableDataInfo + { + Total = source.Count, + Rows = rows + }; + } + + /// + /// 获取不分页数据(返回全部)。 + /// + /// 数据类型 + /// 数据列表 + /// 数据包装对象(不分页) + protected HwPortalTableDataInfo GetDataTableWithoutPaging(IReadOnlyList source) + { + return new HwPortalTableDataInfo + { + Total = source.Count, + Rows = source + }; + } + + /// + /// 解析逗号分隔的长整型数组。 + /// + /// 【业务场景】 + /// 前端传来的删除请求通常是:DELETE /api/users/1,2,3,4,5 + /// 这里的 "1,2,3,4,5" 需要解析成 long[] 数组。 + /// + /// + /// 【C# 语法知识点 - static 静态方法】 + /// protected static 表示"受保护的静态方法"。 + /// 静态方法不依赖实例状态,可以直接通过类名调用。 + /// + /// 为什么用 static? + /// 这个方法不访问任何实例成员,只是做数据转换。 + /// 标记为 static 可以让编译器优化,也方便子类直接调用。 + /// + /// + /// 逗号分隔的字符串,如 "1,2,3" + /// 解析后的长整型数组 + protected static long[] ParseLongArray(string value) + { + // 【空值处理】 + // string.IsNullOrWhiteSpace(value) 检查: + // - null + // - 空字符串 "" + // - 纯空白字符串 " " + // + // 对比 Java: + /// Java 需要手动判断: + /// if (value == null || value.trim().isEmpty()) { ... } + if (string.IsNullOrWhiteSpace(value)) + { + // 【Array.Empty<T>()】 + // Array.Empty<long>() 返回一个共享的空数组实例。 + // 比 new long[0] 更高效,不会每次都创建新对象。 + // + // 这是一个小优化点:避免每次 new 空数组产生垃圾对象。 + return Array.Empty(); + } + + // 【LINQ 链式操作】 + // value.Split(...):按逗号分割字符串 + // .Select(long.Parse):把每个字符串解析成 long + // .ToArray():转换成数组 + // + // 对比 Java Stream: + /// Java 写法: + /// Arrays.stream(value.split(",")) + /// .map(Long::parseLong) + /// .toArray(Long[]::new); + /// + /// C# 的 LINQ 更简洁,而且返回的是 long[] 而不是 Long[](值类型数组,更高效)。 + /// + /// 【StringSplitOptions】 + /// RemoveEmptyEntries:移除空字符串(比如 "1,,2" 中的空项) + /// TrimEntries:去除每项的前后空白 + /// | 是"位或"运算符,用于组合多个枚举标志。 + return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(long.Parse) + .ToArray(); + } + + /// + /// 解析逗号分隔的字符串数组。 + /// + /// 逗号分隔的字符串 + /// 解析后的字符串数组 + protected static string[] ParseStringArray(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty(); + } + + return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + /// + /// 解析正整数(私有辅助方法)。 + /// + /// 【C# 语法知识点 - int.TryParse】 + /// int.TryParse 是"安全转换"方法: + /// - 转换成功:返回 true,out 参数得到转换结果 + /// - 转换失败:返回 false,out 参数得到默认值 0 + // 不会抛异常! + /// + /// 对比 Java: + /// Java 的 Integer.parseInt 失败会抛 NumberFormatException: + /// try { + /// int parsed = Integer.parseInt(value); + /// if (parsed > 0) return parsed; + /// } catch (NumberFormatException e) { + /// // 处理异常 + /// } + /// return fallback; + /// + /// C# 的 TryParse 更优雅,不需要 try-catch。 + /// + /// + /// 【C# 语法知识点 - out 参数】 + /// out 关键字表示"输出参数",方法必须给它赋值。 + /// + // 调用语法: + /// if (int.TryParse(value, out int parsed)) { ... } + /// + /// 这里 out int parsed 是"内联变量声明": + /// 在调用方法的同时声明变量,C# 7.0 引入的特性。 + /// + /// 对比 Java: + /// Java 没有 out 参数的概念,通常用返回值包装类: + /// OptionalInt result = parseIntSafe(value); + /// + /// + /// 字符串值 + /// 失败时的默认值 + /// 解析后的正整数,或默认值 + private static int ParsePositiveInt(string value, int fallback) + { + // int.TryParse(value, out int parsed) 尝试把字符串解析成整数。 + // && parsed > 0 确保是正整数。 + if (int.TryParse(value, out int parsed) && parsed > 0) + { + return parsed; + } + + // 解析失败或不是正整数时,返回默认值。 + return fallback; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalIpRateLimitAttribute.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalIpRateLimitAttribute.cs new file mode 100644 index 0000000..071801b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalIpRateLimitAttribute.cs @@ -0,0 +1,366 @@ +// ============================================================================ +// 【文件说明】HwPortalIpRateLimitAttribute.cs - IP限流特性 +// ============================================================================ +// 这是一个"限流过滤器",用于防止接口被恶意刷调用。 +// +// 【业务场景】 +// 比如用户留言接口,如果不限流,恶意用户可以1秒提交100次, +// 导致数据库被打爆、短信接口被刷爆。所以需要限制: +// "同一IP在60秒内最多提交10次"。 +// +// 【什么是 Attribute(特性)?】 +// 特性是 C# 的"声明式编程"机制,可以给代码元素(类、方法等)贴标签。 +// +// 对比 Java 注解: +// Java: @RateLimit(key = "contact", time = 60, count = 10) +// C#: [HwPortalIpRateLimit("contact", 60, 10)] +// +// 两者概念完全一样,只是: +// - Java 用 @ 符号,叫"注解"(Annotation) +// - C# 用 [] 方括号,叫"特性"(Attribute) +// +// 【什么是 Filter(过滤器)?】 +// 过滤器是 ASP.NET Core 的中间件机制,可以在请求到达控制器前后执行代码。 +// +// 执行顺序: +// 请求 -> 中间件 -> Filter(OnActionExecuting) -> 控制器方法 -> Filter(OnActionExecuted) -> 响应 +// +// 对比 Java Spring: +// Java Spring 用 HandlerInterceptor 或 AOP @Aspect 实现类似功能。 +// 若依框架的限流就是用 AOP 切面实现的。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// IP限流特性。 +/// +/// 【C# 语法知识点 - 特性定义】 +/// public sealed class HwPortalIpRateLimitAttribute : Attribute, IAsyncActionFilter +/// +/// 1. 继承 Attribute:这是定义特性的基础要求 +/// 2. 实现 IAsyncActionFilter:这是 ASP.NET Core 的过滤器接口 +/// 3. sealed 关键字:密封类,不能被继承(特性通常不需要继承) +/// +/// 对比 Java: +/// Java 定义注解用 @interface 关键字: +/// @Target(ElementType.METHOD) +/// @Retention(RetentionPolicy.RUNTIME) +/// public @interface RateLimit { +/// String key(); +/// int time() default 60; +/// int count() default 10; +/// } +/// +/// C# 的特性是真正的类,可以有构造函数、属性、方法。 +/// Java 的注解更像接口,只能定义属性签名。 +/// +/// +/// 【AttributeUsage 特性】 +/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +/// +/// 这是"元特性"——给特性本身贴的特性: +/// - AttributeTargets.Method:只能用在方法上(不能用在类上) +/// - AllowMultiple = false:同一个方法只能贴一个 +/// - Inherited = true:子类继承父类方法时,特性也会继承 +/// +/// 对比 Java: +/// Java 用 @Target 和 @Retention 注解定义: +/// @Target(ElementType.METHOD) // 只能用在方法上 +/// @Retention(RetentionPolicy.RUNTIME) // 运行时保留 +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public sealed class HwPortalIpRateLimitAttribute : Attribute, IAsyncActionFilter +{ + /// + /// 限流键名(用于区分不同业务场景)。 + /// + public string Key { get; } + + /// + /// 时间窗口(秒)。 + /// + public int Time { get; } + + /// + /// 最大访问次数。 + /// + public int Count { get; } + + /// + /// 限流提示消息。 + /// + /// 【C# 语法知识点 - 属性默认值】 + /// public string Message { get; set; } = "访问过于频繁,请稍后再试"; + /// + /// = "..." 是属性的默认值初始化。 + /// 调用方可以覆盖:[HwPortalIpRateLimit("key", 60, 10, Message = "自定义消息")] + /// + /// 对比 Java: + /// Java 注解用 default 关键字: + /// String message() default "访问过于频繁,请稍后再试"; + /// + /// + public string Message { get; set; } = "访问过于频繁,请稍后再试"; + + /// + /// 构造函数。 + /// + /// 【C# 语法知识点 - 构造函数参数】 + /// public HwPortalIpRateLimitAttribute(string key, int time, int count) + /// + /// 使用方式: + /// [HwPortalIpRateLimit("contact", 60, 10)] + /// public async Task<HwPortalAjaxResult> AddContactUsInfo(...) { } + /// + /// 对比 Java: + /// Java 注解没有构造函数,使用时写属性名: + /// @RateLimit(key = "contact", time = 60, count = 10) + /// + /// C# 特性更接近普通类,构造函数参数是位置参数(必填), + /// 属性是命名参数(可选)。 + /// + /// + /// 【设计意图】 + /// 把限流配置固化在特性参数里,让控制器方法声明时就能看出限流策略: + /// - 一眼看出限流配置 + /// - 不用翻配置文件 + /// - 代码即文档 + /// + /// + /// 限流键名 + /// 时间窗口(秒) + /// 最大次数 + public HwPortalIpRateLimitAttribute(string key, int time, int count) + { + Key = key; + Time = time; + Count = count; + } + + /// + /// 异步过滤器执行方法。 + /// + /// 【C# 语法知识点 - IAsyncActionFilter 接口】 + /// Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + /// + /// 这是 ASP.NET Core 的过滤器核心接口: + /// - ActionExecutingContext:执行前上下文(可以拿到请求信息、修改结果) + /// - ActionExecutionDelegate next:继续执行的委托(调用它就放行) + /// - Task:异步方法返回类型 + /// + /// 执行流程: + /// 1. 框架调用 OnActionExecutionAsync + /// 2. 过滤器做前置处理(限流检查) + /// 3. 如果通过,调用 await next() 放行 + /// 4. 控制器方法执行 + /// 5. 过滤器做后置处理(本例没有) + /// + /// 对比 Java Spring: + /// Java 用 HandlerInterceptor 的三个方法: + /// preHandle() -> 对应 next() 之前的代码 + /// postHandle() -> 对应 next() 之后的代码 + /// afterCompletion() -> 完成后的清理 + /// + /// 或者用 AOP @Around: + /// @Around("切点表达式") + /// public Object around(ProceedingJoinPoint pjp) { + /// // 前置处理 + /// Object result = pjp.proceed(); // 相当于 await next() + /// // 后置处理 + /// return result; + /// } + /// + /// + /// 执行上下文 + /// 继续执行的委托 + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 【获取 HTTP 上下文】 + // context.HttpContext 包含当前请求的所有信息: + // - Request:请求对象(Headers、Query、Body、Cookies 等) + // - Response:响应对象 + // - User:当前登录用户(如果有) + // - RequestServices:DI 服务容器 + // + // 对比 Java Spring: + // Java 通常通过注入 HttpServletRequest: + // @Autowired private HttpServletRequest request; + // 或者在 Controller 方法参数中声明: + // public Result method(HttpServletRequest request) { ... } + HttpContext httpContext = context.HttpContext; + + // 【从 DI 容器获取服务】 + // httpContext.RequestServices.GetRequiredService<SysCacheService>() + // + // RequestServices 是"请求级 DI 容器"。 + // GetRequiredService<T>() 获取服务实例,如果不存在会抛异常。 + // + // 对比 Java Spring: + // Java 用 @Autowired 注入: + // @Autowired private RedisCache redisCache; + // + // C# 的写法更显式,从容器直接获取。 + // 两种方式本质一样:都是依赖注入。 + SysCacheService cacheService = httpContext.RequestServices.GetRequiredService(); + + // 获取请求 IP。 + string requestIp = HwPortalContextHelper.CurrentRequestIp(httpContext); + + // 获取当前方法名(用于日志和区分不同接口)。 + // context.ActionDescriptor.DisplayName 是方法的完整名称。 + // ?? string.Empty 是空合并运算符:如果 DisplayName 为 null,就用空字符串。 + string actionName = context.ActionDescriptor.DisplayName ?? string.Empty; + + // 【构建缓存键】 + // 格式:hwportal:ratelimit:{key}:{ip}:{action} + // + // 为什么要拼接这么多信息? + // 1. key:区分不同业务场景(留言、搜索等) + // 2. ip:区分不同用户 + // 3. action:区分同一控制器的不同方法 + // + // 这样可以做到: + // - 同一IP访问不同接口,各自独立计数 + // - 同一接口不同IP,各自独立计数 + // + // 对比 Java 若依: + // 若依的限流键也是类似拼接: + // String key = config.getKey() + ":" + ip + ":" + methodName; + string cacheKey = $"hwportal:ratelimit:{Key}:{requestIp}:{actionName}"; + + // 时间窗口和最大次数的兜底处理。 + // 如果传入的值 <= 0,就用默认值 60。 + int windowSeconds = Time > 0 ? Time : 60; + int maxCount = Count > 0 ? Count : 60; + + // 【分布式锁】 + // 为什么需要锁? + // 限流必须原子性,避免并发请求同时穿透。 + // 比如当前计数=9,两个请求同时读取,都认为可以通过,结果变成11次。 + // + // BeginCacheLock 在缓存层获取分布式锁: + // - "lock:{cacheKey}":锁的键名 + // - 500:获取锁的超时时间(毫秒) + // - 5000:锁的持有时间(毫秒) + // - throwOnFailure: false:获取失败不抛异常,返回 null + // + // using 语法:自动释放锁(离开作用域时调用 Dispose) + // + // 对比 Java 若依: + // 若依用 Redis 的 SETNX 实现分布式锁: + // Boolean locked = redisCache.setCacheObject(lockKey, "1", 5, TimeUnit.SECONDS); + using IDisposable? cacheLock = cacheService.BeginCacheLock($"lock:{cacheKey}", 500, 5000, throwOnFailure: false); + + if (cacheLock == null) + { + // 获取锁失败,说明并发太高,直接拒绝。 + // 业务取舍:宁可错杀少量请求,也不能放开限流。 + // + // 【短路返回】 + // context.Result = ... 会直接设置响应结果,不再执行控制器方法。 + // 这就是"拦截"的实现方式。 + // + // ObjectResult 是 ASP.NET Core 的返回类型, + // 会自动把对象序列化成 JSON 响应。 + context.Result = new ObjectResult(HwPortalAjaxResult.Error(Message)); + return; + } + + // 【读取计数器】 + // 从缓存获取当前时间窗口的访问次数。 + // HwPortalRateLimitCounter 是内部类,包含 Count 和 ExpireAt。 + HwPortalRateLimitCounter counter = cacheService.Get(cacheKey); + DateTime now = DateTime.UtcNow; + + if (counter == null || counter.ExpireAt <= now) + { + // 【新建计数窗口】 + // 没有计数器,或窗口已过期,创建新窗口。 + // + // 为什么新窗口 Count = 1? + // 当前请求本身也算一次,所以从 1 开始。 + counter = new HwPortalRateLimitCounter + { + Count = 1, + ExpireAt = now.AddSeconds(windowSeconds) + }; + + // 写入缓存,设置过期时间。 + // 缓存到期后自动删除,不需要后台清理。 + cacheService.Set(cacheKey, counter, TimeSpan.FromSeconds(windowSeconds)); + + // 【放行】 + // await next() 调用下一个过滤器或控制器方法。 + // 如果前面已经 return,就说明请求被拦截了。 + await next(); + return; + } + + if (counter.Count >= maxCount) + { + // 【限流触发】 + // 计数器已达到上限,拒绝请求。 + // + // 注意:这里返回的是正常响应(code=500),不是抛异常。 + // 限流是"预期中的业务拒绝",不应该走全局异常处理。 + context.Result = new ObjectResult(HwPortalAjaxResult.Error(Message)); + return; + } + + // 【计数器+1】 + // 窗口内后续请求,只增加计数,不重置过期时间。 + // 这样是"固定窗口"限流,不是"滑动窗口"。 + counter.Count++; + + // 计算剩余时间。 + TimeSpan expire = counter.ExpireAt - now; + + // 更新缓存,用剩余时间作为过期时间。 + cacheService.Set(cacheKey, counter, expire > TimeSpan.Zero ? expire : TimeSpan.FromSeconds(windowSeconds)); + + // 放行请求。 + await next(); + } + + /// + /// 限流计数器(内部类)。 + /// + /// 【C# 语法知识点 - 嵌套类】 + /// private sealed class HwPortalRateLimitCounter + /// + /// 嵌套类定义在另一个类内部: + /// - private:只能在外部类中使用 + /// - sealed:不能被继承 + /// + /// 对比 Java: + /// Java 的内部类有两种: + /// - 静态嵌套类:static class Inner { } - 类似 C# 的嵌套类 + /// - 非静态内部类:class Inner { } - 可以访问外部类实例成员 + /// + /// C# 的嵌套类默认是"静态"的(不能访问外部类实例成员), + /// 除非显式传入外部类引用。 + /// + /// + /// 【为什么定义为嵌套类?】 + /// 1. 只在这个特性内使用,不需要暴露给外部 + /// 2. 封装性:外部代码不需要知道这个类的存在 + /// 3. 避免污染命名空间 + /// + /// + private sealed class HwPortalRateLimitCounter + { + /// + /// 访问次数。 + /// + public int Count { get; set; } + + /// + /// 窗口过期时间。 + /// + public DateTime ExpireAt { get; set; } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalTableDataInfo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalTableDataInfo.cs new file mode 100644 index 0000000..c7be5e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Common/HwPortalTableDataInfo.cs @@ -0,0 +1,95 @@ +// ============================================================================ +// 【文件说明】HwPortalTableDataInfo.cs - 分页数据返回包装类 +// ============================================================================ +// 这个类用于统一返回"表格分页数据"的格式。 +// 在 Java 若依(RuoYi)框架中,有一个 TableDataInfo 类做同样的事情。 +// 前端通常期望这样的 JSON 结构: +// { +// "code": 200, +// "msg": "查询成功", +// "total": 100, // 总记录数 +// "rows": [...] // 当前页的数据列表 +// } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 分页数据返回包装类。 +/// +/// 【C# 语法知识点 - 泛型类 <T>】 +/// public class HwPortalTableDataInfo<T> 中的 <T> 是泛型参数。 +/// 泛型允许你编写"类型无关"的代码,使用时再指定具体类型。 +/// +/// 例如: +/// - HwPortalTableDataInfo<HwWeb> 表示"包含 HwWeb 列表的分页数据" +/// - HwPortalTableDataInfo<HwProductInfo> 表示"包含 HwProductInfo 列表的分页数据" +/// +/// 对比 Java: +/// Java 的泛型写法几乎一样:public class TableDataInfo<T> { ... } +/// 但 Java 有"类型擦除",运行时 T 只是 Object。 +/// C# 的泛型在运行时是真实类型,性能更好。 +/// +/// +/// 【为什么需要这个类?】 +/// 这是"统一响应格式"的最佳实践: +/// - 前端不需要猜测返回格式,所有接口都返回相同结构 +/// - 前端分页组件可以直接读取 total 和 rows +/// - 错误处理统一:code 不为 200 时显示 msg +/// +/// +/// 数据行类型,例如 HwWeb、HwProductInfo 等 +public class HwPortalTableDataInfo +{ + /// + /// 状态码,200 表示成功。 + /// + /// 【C# 语法知识点 - 属性初始化器】 + /// public int Code { get; set; } = 200; + /// 这里的 = 200 是"属性默认值"。 + /// 如果创建对象时不赋值,Code 就自动是 200。 + /// + /// 对比 Java: + /// Java 需要在构造函数里赋默认值: + /// private int code = 200; + /// 或者: + /// public TableDataInfo() { this.code = 200; } + /// + /// + public int Code { get; set; } = 200; + + /// + /// 返回消息。 + /// + public string Msg { get; set; } = "查询成功"; + + /// + /// 总记录数(用于前端分页计算)。 + /// + /// 前端分页组件需要知道总共有多少条记录, + /// 才能计算总页数 = Math.Ceiling(total / pageSize)。 + /// + /// + public long Total { get; set; } + + /// + /// 当前页的数据列表。 + /// + /// 【C# 语法知识点 - IReadOnlyList<T>】 + /// IReadOnlyList<T> 是"只读列表接口"。 + /// 它比 List<T> 更安全,因为调用者不能修改列表内容(不能 Add/Remove)。 + /// + /// 对比 Java: + /// Java 没有内置的只读列表接口,通常用 List<T> 或 Collections.unmodifiableList()。 + /// C# 的 IReadOnlyList<T> 是接口级别的只读约束,更清晰。 + /// + /// + /// 【C# 语法知识点 - Array.Empty<T>()】 + /// = Array.Empty<T>() 是"空数组的单例"。 + /// 每次调用都返回同一个实例,比 new T[0] 更节省内存。 + /// + /// 这是一个小优化点:避免每次 new 空数组产生垃圾对象。 + /// + /// + public IReadOnlyList Rows { get; set; } = Array.Empty(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Const/HwPortalConstants.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Const/HwPortalConstants.cs new file mode 100644 index 0000000..4b0fe6c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Const/HwPortalConstants.cs @@ -0,0 +1,71 @@ +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户模块常量定义。 +/// +/// 【C# 语法知识点 - static class 静态类】 +/// public static class HwPortalConstants +/// +/// 静态类有以下特点: +/// 1. 不能实例化(不能 new) +/// 2. 所有成员必须是静态的 +/// 3. 是密封的(不能被继承) +/// +/// 对比 Java: +/// Java 没有静态类概念,通常用 final class + private 构造函数模拟: +/// public final class HwPortalConstants { +/// private HwPortalConstants() {} // 防止实例化 +/// public static final String PRODUCT_INFO_CONFIG_MODAL = "..."; +/// } +/// +/// C# 的 static class 更简洁,编译器自动阻止实例化和继承。 +/// +/// +/// 【命名约定】 +/// C# 常量命名约定: +/// - PascalCase(首字母大写) +/// - 有意义的名称 +/// - 避免缩写 +/// +/// Java 常量命名约定: +/// - UPPER_SNAKE_CASE(全大写下划线分隔) +/// +/// 这是两种语言的文化差异,功能上完全等价。 +/// +/// +public static class HwPortalConstants +{ + /// + /// 产品信息配置模式 1。 + /// 原 Java 常量:PRODUCT_INFO_CONFIG_MODAL_ONE。 + /// + public const string ProductInfoConfigModalOne = "1"; + + /// + /// 产品信息配置模式 2。 + /// 原 Java 常量:PRODUCT_INFO_CONFIG_MODAL_TWO。 + /// + public const string ProductInfoConfigModalTwo = "2"; + + /// + /// hw 官网当前额外使用的树形配置模式。 + /// 该值在原业务代码里被直接写死为 13,这里收口为常量便于复用。 + /// + public const string ProductInfoConfigModalTree = "13"; + + /// + /// 首页典型案例标记。 + /// + public const string HomeTypicalFlagYes = "1"; + + /// + /// 典型案例标记。 + /// + public const string TypicalFlagYes = "1"; + + /// + /// 门户配置类型“2”。 + /// 当前业务里用于切换另一套查询 SQL。 + /// + public const string PortalConfigTypeTwo = "2"; +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoController.cs new file mode 100644 index 0000000..71110f5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoController.cs @@ -0,0 +1,55 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/aboutUsInfo")] +public class HwAboutUsInfoController : HwPortalControllerBase +{ + private readonly HwAboutUsInfoService _service; + + public HwAboutUsInfoController(HwAboutUsInfoService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwAboutUsInfo input) + { + return GetDataTable(await _service.SelectHwAboutUsInfoList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwAboutUsInfo input) + { + return ExportExcel(await _service.SelectHwAboutUsInfoList(input), "关于我们数据"); + } + + [HttpGet("{aboutUsInfoId:long}")] + public async Task GetInfo(long aboutUsInfoId) + { + return Success(await _service.SelectHwAboutUsInfoByAboutUsInfoId(aboutUsInfoId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwAboutUsInfo input) + { + return ToAjax(await _service.InsertHwAboutUsInfo(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwAboutUsInfo input) + { + return ToAjax(await _service.UpdateHwAboutUsInfo(input)); + } + + [HttpDelete("{aboutUsInfoIds}")] + [Idempotent] + public async Task Remove(string aboutUsInfoIds) + { + return ToAjax(await _service.DeleteHwAboutUsInfoByAboutUsInfoIds(ParseLongArray(aboutUsInfoIds))); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoDetailController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoDetailController.cs new file mode 100644 index 0000000..584c38c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAboutUsInfoDetailController.cs @@ -0,0 +1,55 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/aboutUsInfoDetail")] +public class HwAboutUsInfoDetailController : HwPortalControllerBase +{ + private readonly HwAboutUsInfoDetailService _service; + + public HwAboutUsInfoDetailController(HwAboutUsInfoDetailService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwAboutUsInfoDetail input) + { + return GetDataTable(await _service.SelectHwAboutUsInfoDetailList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwAboutUsInfoDetail input) + { + return ExportExcel(await _service.SelectHwAboutUsInfoDetailList(input), "关于我们明细数据"); + } + + [HttpGet("{usInfoDetailId:long}")] + public async Task GetInfo(long usInfoDetailId) + { + return Success(await _service.SelectHwAboutUsInfoDetailByUsInfoDetailId(usInfoDetailId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwAboutUsInfoDetail input) + { + return ToAjax(await _service.InsertHwAboutUsInfoDetail(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwAboutUsInfoDetail input) + { + return ToAjax(await _service.UpdateHwAboutUsInfoDetail(input)); + } + + [HttpDelete("{usInfoDetailIds}")] + [Idempotent] + public async Task Remove(string usInfoDetailIds) + { + return ToAjax(await _service.DeleteHwAboutUsInfoDetailByUsInfoDetailIds(ParseLongArray(usInfoDetailIds))); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAnalyticsController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAnalyticsController.cs new file mode 100644 index 0000000..1fca1f2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwAnalyticsController.cs @@ -0,0 +1,94 @@ +namespace Admin.NET.Plugin.HwPortal; + +[Route("portal/analytics")] +public class HwAnalyticsController : HwPortalControllerBase +{ + private readonly HwAnalyticsService _service; + + public HwAnalyticsController(HwAnalyticsService service) + { + _service = service; + } + + // 这里故意用 POST,而不是 GET。 + // 原因是埋点采集本质上是“写入事件”,语义上就应该是 POST。 + // Spring Boot 里这个思路也一样,只是写法会是 @PostMapping("/collect")。 + [HttpPost("collect")] + [AllowAnonymous] + // [Consumes] 是 ASP.NET Core 的请求体内容类型约束。 + // 它告诉框架:这个接口允许 application/json 和 text/plain 两种 Content-Type。 + // 之所以要写这个,是因为原 ruoyi-portal 明确支持前端把 JSON 字符串当 text/plain 发过来。 + [Consumes("application/json", "text/plain")] + [HwPortalIpRateLimit("portal_analytics_collect", 60, 240)] + public async Task Collect() + { + // 这里没有直接写参数 [FromBody] AnalyticsCollectRequest request, + // 是因为我们要同时兼容 “真正 JSON 请求体” 和 “text/plain 包着 JSON 字符串” 两种历史行为。 + // 如果强行交给默认模型绑定,text/plain 场景很容易直接绑定失败。 + string body = await ReadRequestBodyAsync(); + if (string.IsNullOrWhiteSpace(body)) + { + return Error("请求体不能为空"); + } + + try + { + // JsonSerializer.Deserialize() 是 C# 常见的 JSON 反序列化写法。 + // 你可以把它理解成 Jackson 的 objectMapper.readValue(body, AnalyticsCollectRequest.class)。 + AnalyticsCollectRequest request = JsonSerializer.Deserialize(body, new JsonSerializerOptions + { + // PropertyNameCaseInsensitive = true 表示“属性名大小写不敏感”。 + // 这样前端传 eventType / EventType / eventtype 都能尽量兼容,减少历史字段命名不统一导致的问题。 + PropertyNameCaseInsensitive = true + }); + + // HttpContext 是 ASP.NET Core 里贯穿整个请求的核心上下文对象。 + // 类似于你在 Spring MVC 里能拿到的 HttpServletRequest / SecurityContext / RequestAttributes 的组合入口。 + await _service.Collect(request, HwPortalContextHelper.CurrentRequestIp(HttpContext), Request.Headers["User-Agent"].ToString()); + return Success(); + } + catch (Exception ex) + { + // 这里不把异常直接抛出去,而是转成兼容 AjaxResult。 + // 原因是这个接口在源模块里就是“失败返回 error(msg)”风格,迁移后要保持对外行为一致。 + return Error($"采集失败: {ex.Message}"); + } + } + + [HttpGet("dashboard")] + public async Task Dashboard([FromQuery] DateTime? statDate, [FromQuery(Name = "top")] int? top) + { + // DateTime? 里的问号表示“可空类型”。 + // Java 里你常见 Date/LocalDate 可以为 null;C# 的值类型默认不能为 null,所以要写成 DateTime? 才能接收空值。 + return Success(await _service.GetDashboard(statDate, top)); + } + + [HttpPost("refreshDaily")] + public async Task RefreshDaily([FromQuery] DateTime? statDate) + { + await _service.RefreshDailyStat(statDate); + return Success(null, "刷新成功"); + } + + private async Task ReadRequestBodyAsync() + { + // Request.EnableBuffering() 的作用是“允许请求体被重复读取”。 + // 这一点和 Java Servlet 最大的差异之一在于: + // Java 里 InputStream 一般读一次就没了,ASP.NET Core 这里也一样,所以如果你想自己读取后还让后续组件继续用,就必须先开启缓冲。 + Request.EnableBuffering(); + + // Position = 0 表示把流指针移回开头。 + // 你可以把它理解成“从头重新读一遍文件/流”。 + Request.Body.Position = 0; + + // using StreamReader reader = new(...) 是 C# 的资源释放语法。 + // Java 里你可以类比 try-with-resources。 + // leaveOpen: true 的意思是“Reader 用完后,不要顺手把底层 Request.Body 也关掉”,否则后续框架再访问请求体就会出问题。 + using StreamReader reader = new(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + string body = await reader.ReadToEndAsync(); + + // 读完后把流指针再拨回起点,这是为了保证后续如果别的组件还想读请求体,不会读到空内容。 + Request.Body.Position = 0; + return body; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwContactUsInfoController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwContactUsInfoController.cs new file mode 100644 index 0000000..de49c59 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwContactUsInfoController.cs @@ -0,0 +1,55 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/contactUsInfo")] +public class HwContactUsInfoController : HwPortalControllerBase +{ + private readonly HwContactUsInfoService _service; + + public HwContactUsInfoController(HwContactUsInfoService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwContactUsInfo input) + { + return GetDataTable(await _service.SelectHwContactUsInfoList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwContactUsInfo input) + { + return ExportExcel(await _service.SelectHwContactUsInfoList(input), "联系我们数据"); + } + + [HttpGet("{contactUsInfoId:long}")] + public async Task GetInfo(long contactUsInfoId) + { + return Success(await _service.SelectHwContactUsInfoByContactUsInfoId(contactUsInfoId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwContactUsInfo input) + { + return ToAjax(await _service.InsertHwContactUsInfo(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwContactUsInfo input) + { + return ToAjax(await _service.UpdateHwContactUsInfo(input)); + } + + [HttpDelete("{contactUsInfoIds}")] + [Idempotent] + public async Task Remove(string contactUsInfoIds) + { + return ToAjax(await _service.DeleteHwContactUsInfoByContactUsInfoIds(ParseLongArray(contactUsInfoIds))); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigController.cs new file mode 100644 index 0000000..e24b328 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigController.cs @@ -0,0 +1,63 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/portalConfig")] +public class HwPortalConfigController : HwPortalControllerBase +{ + private readonly HwPortalConfigService _configService; + private readonly HwPortalConfigTypeService _configTypeService; + + public HwPortalConfigController(HwPortalConfigService configService, HwPortalConfigTypeService configTypeService) + { + _configService = configService; + _configTypeService = configTypeService; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwPortalConfig input) + { + return GetDataTable(await _configService.SelectHwPortalConfigJoinList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwPortalConfig input) + { + return ExportExcel(await _configService.SelectHwPortalConfigList(input), "门户配置数据"); + } + + [HttpGet("{portalConfigId:long}")] + public async Task GetInfo(long portalConfigId) + { + return Success(await _configService.SelectHwPortalConfigByPortalConfigId(portalConfigId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwPortalConfig input) + { + return ToAjax(await _configService.InsertHwPortalConfig(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwPortalConfig input) + { + return ToAjax(await _configService.UpdateHwPortalConfig(input)); + } + + [HttpDelete("{portalConfigIds}")] + [Idempotent] + public async Task Remove(string portalConfigIds) + { + return ToAjax(await _configService.DeleteHwPortalConfigByPortalConfigIds(ParseLongArray(portalConfigIds))); + } + + [HttpGet("portalConfigTypeTree")] + public async Task PortalConfigTypeTree([FromQuery] HwPortalConfigType input) + { + return Success(await _configTypeService.SelectPortalConfigTypeTreeList(input)); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigTypeController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigTypeController.cs new file mode 100644 index 0000000..ab3f75d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalConfigTypeController.cs @@ -0,0 +1,55 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/portalConfigType")] +public class HwPortalConfigTypeController : HwPortalControllerBase +{ + private readonly HwPortalConfigTypeService _service; + + public HwPortalConfigTypeController(HwPortalConfigTypeService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task List([FromQuery] HwPortalConfigType input) + { + return Success(await _service.SelectHwPortalConfigTypeList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwPortalConfigType input) + { + return ExportExcel(await _service.SelectHwPortalConfigTypeList(input), "门户配置分类数据"); + } + + [HttpGet("{configTypeId:long}")] + public async Task GetInfo(long configTypeId) + { + return Success(await _service.SelectHwPortalConfigTypeByConfigTypeId(configTypeId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwPortalConfigType input) + { + return ToAjax(await _service.InsertHwPortalConfigType(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwPortalConfigType input) + { + return ToAjax(await _service.UpdateHwPortalConfigType(input)); + } + + [HttpDelete("{configTypeIds}")] + [Idempotent] + public async Task Remove(string configTypeIds) + { + return ToAjax(await _service.DeleteHwPortalConfigTypeByConfigTypeIds(ParseLongArray(configTypeIds))); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalController.cs new file mode 100644 index 0000000..fe9a4f2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwPortalController.cs @@ -0,0 +1,315 @@ +// ============================================================================ +// 【文件说明】HwPortalController.cs - 门户主控制器 +// ============================================================================ +// 这是门户模块的主控制器,提供官网前台所需的各种数据查询接口。 +// +// 【业务场景】 +// 官网前台需要展示: +// - 首页配置:轮播图、核心优势、合作伙伴等 +// - 产品中心:产品列表、产品详情 +// - 案例中心:案例列表、案例详情 +// - 联系我们:用户留言提交 +// - 关于我们:公司介绍 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @RestController +// @RequestMapping("/portal/portal") +// public class HwPortalController { ... } +// +// ASP.NET Core: +// [ApiController] +// [Route("portal/portal")] +// public class HwPortalController : ControllerBase { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户主控制器。 +/// +/// 【控制器职责】 +/// 这个控制器是官网前台的"数据入口",提供: +/// 1. 配置数据查询:首页配置、配置类型 +/// 2. 产品数据查询:产品列表、产品详情 +/// 3. 案例数据查询:案例列表、案例详情 +/// 4. 用户留言提交:联系我们表单 +/// 5. 公司信息查询:关于我们 +/// +/// 所有接口都是 [AllowAnonymous],因为官网前台用户不需要登录。 +/// +/// +/// 【多服务依赖】 +/// 这个控制器注入了 8 个服务,这是"服务聚合"模式: +/// - 控制器协调多个服务完成复杂业务 +/// - 每个服务专注一个领域 +/// - 控制器不包含业务逻辑,只做服务调用和结果组装 +/// +/// 对比 Java Spring Boot: +/// Java 的写法完全一样,只是注解不同: +/// @Autowired private HwPortalConfigService configService; +/// @Autowired private HwProductInfoService productInfoService; +/// ... +/// +/// +[AllowAnonymous] +[Route("portal/portal")] +public class HwPortalController : HwPortalControllerBase +{ + /// + /// 门户配置服务。 + /// + private readonly HwPortalConfigService _configService; + + /// + /// 配置类型服务。 + /// + private readonly HwPortalConfigTypeService _configTypeService; + + /// + /// 案例信息服务。 + /// + private readonly HwProductCaseInfoService _caseInfoService; + + /// + /// 联系我们信息服务。 + /// + private readonly HwContactUsInfoService _contactUsInfoService; + + /// + /// 产品信息服务。 + /// + private readonly HwProductInfoService _productInfoService; + + /// + /// 产品明细服务。 + /// + private readonly HwProductInfoDetailService _productInfoDetailService; + + /// + /// 关于我们信息服务。 + /// + private readonly HwAboutUsInfoService _aboutUsInfoService; + + /// + /// 关于我们明细服务。 + /// + private readonly HwAboutUsInfoDetailService _aboutUsInfoDetailService; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【构造函数注入的优势】 + /// 1. 依赖关系明确:看构造函数就知道需要哪些服务 + /// 2. 便于测试:可以传入 mock 对象 + /// 3. 不可变:readonly 字段保证服务实例不被替换 + /// + /// 对比 Java Spring: + /// Java 推荐的写法也是构造函数注入: + /// @Autowired + /// public HwPortalController( + /// HwPortalConfigService configService, + /// HwPortalConfigTypeService configTypeService, + /// ... + /// ) { + /// this.configService = configService; + /// ... + /// } + /// + /// C# 和 Java 的最佳实践是一致的。 + /// + /// + public HwPortalController( + HwPortalConfigService configService, + HwPortalConfigTypeService configTypeService, + HwProductCaseInfoService caseInfoService, + HwContactUsInfoService contactUsInfoService, + HwProductInfoService productInfoService, + HwProductInfoDetailService productInfoDetailService, + HwAboutUsInfoService aboutUsInfoService, + HwAboutUsInfoDetailService aboutUsInfoDetailService) + { + _configService = configService; + _configTypeService = configTypeService; + _caseInfoService = caseInfoService; + _contactUsInfoService = contactUsInfoService; + _productInfoService = productInfoService; + _productInfoDetailService = productInfoDetailService; + _aboutUsInfoService = aboutUsInfoService; + _aboutUsInfoDetailService = aboutUsInfoDetailService; + } + + /// + /// 查询门户配置列表(分页)。 + /// + /// 查询条件 + /// 分页数据 + [HttpGet("getPortalConfigList")] + public async Task> GetPortalConfigList([FromQuery] HwPortalConfig input) + { + // [FromQuery] 表示从 URL 查询字符串绑定参数。 + // 例如:GET portal/portal/getPortalConfigList?portalConfigType=2 + // 框架会自动把 portalConfigType=2 绑定到 input.PortalConfigType。 + return GetDataTable(await _configService.SelectHwPortalConfigList(input)); + } + + /// + /// 查询配置类型列表(不分页)。 + /// + /// 查询条件 + /// 配置类型列表 + [HttpGet("getPortalConfigTypeList")] + public async Task> GetPortalConfigTypeList([FromQuery] HwPortalConfigType input) + { + // GetDataTableWithoutPaging 是基类方法,返回全部数据不做分页。 + return GetDataTableWithoutPaging(await _configTypeService.SelectHwPortalConfigTypeList(input)); + } + + /// + /// 查询配置类型列表(另一种查询)。 + /// + /// 查询条件 + /// 配置类型列表 + [HttpGet("selectConfigTypeList")] + public async Task> SelectConfigTypeList([FromQuery] HwPortalConfigType input) + { + return GetDataTableWithoutPaging(await _configTypeService.SelectConfigTypeList(input)); + } + + /// + /// 查询首页案例标题列表。 + /// + /// 查询条件 + /// 案例标题列表 + [HttpGet("getHomeCaseTitleList")] + public async Task> GetHomeCaseTitleList([FromQuery] HwPortalConfigType input) + { + return GetDataTable(await _configTypeService.SelectHwPortalConfigTypeList(input)); + } + + /// + /// 查询首页典型案例信息。 + /// + /// 查询条件 + /// 典型案例信息 + [HttpGet("getTypicalHomeCaseInfo")] + public async Task GetTypicalHomeCaseInfo([FromQuery] HwProductCaseInfo input) + { + return Success(await _caseInfoService.GetTypicalHomeCaseInfo(input)); + } + + /// + /// 提交联系我们信息(用户留言)。 + /// + /// 【业务场景】 + /// 用户在官网"联系我们"页面填写表单,提交留言。 + /// 系统记录用户信息、留言内容、IP 地址等。 + /// + /// + /// 联系我们信息 + /// 操作结果 + [HttpPost("addContactUsInfo")] + [Idempotent] + public async Task AddContactUsInfo([FromBody] HwContactUsInfo input) + { + // 【获取客户端 IP】 + // HwPortalContextHelper.CurrentRequestIp(HttpContext) 获取请求的客户端 IP。 + // HttpContext 包含当前 HTTP 请求的所有信息: + // - Request:请求对象(Headers、Query、Body 等) + // - Response:响应对象 + // - User:当前登录用户(如果有) + // - Connection:连接信息(IP、端口等) + // + // 对比 Java Spring: + // Java 通常通过注入 HttpServletRequest 获取 IP: + // @Autowired private HttpServletRequest request; + // String ip = request.getRemoteAddr(); + // + // C# 通过 HttpContext 直接访问,更简洁。 + input.UserIp = HwPortalContextHelper.CurrentRequestIp(HttpContext); + + return ToAjax(await _contactUsInfoService.InsertHwContactUsInfo(input)); + } + + /// + /// 查询产品中心产品列表(含明细)。 + /// + /// 查询条件 + /// 产品列表(含明细) + [HttpGet("getProductCenterProductInfos")] + public async Task GetProductCenterProductInfos([FromQuery] HwProductInfo input) + { + // SelectHwProductInfoJoinDetailList 返回产品及其明细列表。 + // 这是一个复杂的关联查询,在 Service 层处理。 + return Success(await _productInfoService.SelectHwProductInfoJoinDetailList(input)); + } + + /// + /// 查询产品明细列表。 + /// + /// 查询条件 + /// 产品明细列表 + [HttpGet("getProductCenterProductDetailInfos")] + public async Task GetProductCenterProductDetailInfos([FromQuery] HwProductInfoDetail input) + { + return Success(await _productInfoDetailService.SelectHwProductInfoDetailList(input)); + } + + /// + /// 查询案例中心案例列表。 + /// + /// 查询条件 + /// 案例列表 + [HttpGet("getCaseCenterCaseInfos")] + public async Task GetCaseCenterCaseInfos([FromQuery] HwProductCaseInfo input) + { + return Success(await _caseInfoService.SelectHwProductCaseInfoList(input)); + } + + /// + /// 查询案例详情。 + /// + /// 【路由参数】 + /// [HttpGet("getCaseCenterCaseInfo/{caseInfoId:long}")] 中的 {caseInfoId:long} 是路由参数。 + /// :long 是路由约束,确保 caseInfoId 是长整型。 + /// + /// 调用示例:GET portal/portal/getCaseCenterCaseInfo/123 + /// 框架会把 123 绑定到 caseInfoId 参数。 + /// + /// 对比 Java Spring Boot: + /// Java: @GetMapping("/getCaseCenterCaseInfo/{caseInfoId}") + /// public Result getCaseCenterCaseInfo(@PathVariable Long caseInfoId) + /// + /// C# 的路由约束更强大,可以在路由模板中定义类型验证。 + /// + /// + /// 案例ID + /// 案例详情 + [HttpGet("getCaseCenterCaseInfo/{caseInfoId:long}")] + public async Task GetCaseCenterCaseInfo(long caseInfoId) + { + return Success(await _caseInfoService.SelectHwProductCaseInfoByCaseInfoId(caseInfoId)); + } + + /// + /// 查询关于我们信息。 + /// + /// 查询条件 + /// 关于我们信息 + [HttpGet("getAboutUsInfo")] + public async Task GetAboutUsInfo([FromQuery] HwAboutUsInfo input) + { + return Success(await _aboutUsInfoService.SelectHwAboutUsInfoList(input)); + } + + /// + /// 查询关于我们明细列表。 + /// + /// 查询条件 + /// 关于我们明细列表 + [HttpGet("getAboutUsInfoDetails")] + public async Task GetAboutUsInfoDetails([FromQuery] HwAboutUsInfoDetail input) + { + return Success(await _aboutUsInfoDetailService.SelectHwAboutUsInfoDetailList(input)); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductCaseInfoController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductCaseInfoController.cs new file mode 100644 index 0000000..ded3f06 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductCaseInfoController.cs @@ -0,0 +1,63 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/productCaseInfo")] +public class HwProductCaseInfoController : HwPortalControllerBase +{ + private readonly HwProductCaseInfoService _caseInfoService; + private readonly HwPortalConfigTypeService _configTypeService; + + public HwProductCaseInfoController(HwProductCaseInfoService caseInfoService, HwPortalConfigTypeService configTypeService) + { + _caseInfoService = caseInfoService; + _configTypeService = configTypeService; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwProductCaseInfo input) + { + return GetDataTable(await _caseInfoService.SelectHwProductCaseInfoList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwProductCaseInfo input) + { + return ExportExcel(await _caseInfoService.SelectHwProductCaseInfoList(input), "案例信息数据"); + } + + [HttpGet("{caseInfoId:long}")] + public async Task GetInfo(long caseInfoId) + { + return Success(await _caseInfoService.SelectHwProductCaseInfoByCaseInfoId(caseInfoId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwProductCaseInfo input) + { + return ToAjax(await _caseInfoService.InsertHwProductCaseInfo(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwProductCaseInfo input) + { + return ToAjax(await _caseInfoService.UpdateHwProductCaseInfo(input)); + } + + [HttpDelete("{caseInfoIds}")] + [Idempotent] + public async Task Remove(string caseInfoIds) + { + return ToAjax(await _caseInfoService.DeleteHwProductCaseInfoByCaseInfoIds(ParseLongArray(caseInfoIds))); + } + + [HttpGet("portalConfigTypeTree")] + public async Task PortalConfigTypeTree([FromQuery] HwPortalConfigType input) + { + return Success(await _configTypeService.SelectPortalConfigTypeTreeList(input)); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoController.cs new file mode 100644 index 0000000..056700d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoController.cs @@ -0,0 +1,169 @@ +// ============================================================================ +// 【文件说明】HwProductInfoController.cs - 产品信息控制器 +// ============================================================================ +// 这个控制器负责处理产品信息相关的 HTTP 请求。 +// +// 【RESTful API 设计】 +// - GET portal/productInfo/list -> 查询列表 +// - GET portal/productInfo/{productInfoId} -> 查询详情 +// - POST portal/productInfo -> 新增 +// - PUT portal/productInfo -> 更新 +// - DELETE portal/productInfo/{productInfoIds} -> 批量删除 +// - GET portal/productInfo/portalConfigTypeTree -> 查询配置类型树 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品信息控制器。 +/// +/// 【控制器职责】 +/// 控制器只负责"请求处理",不包含业务逻辑: +/// 1. 接收 HTTP 请求 +/// 2. 调用 Service 处理业务 +/// 3. 返回统一格式的响应 +/// +/// 业务逻辑在 HwProductInfoService 中实现。 +/// +/// +[AllowAnonymous] +[Route("portal/productInfo")] +public class HwProductInfoController : HwPortalControllerBase +{ + /// + /// 产品信息服务。 + /// + private readonly HwProductInfoService _productInfoService; + + /// + /// 配置类型服务。 + /// + private readonly HwPortalConfigTypeService _configTypeService; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【多服务注入】 + /// 这个控制器需要两个服务: + /// - HwProductInfoService:处理产品信息 + /// - HwPortalConfigTypeService:处理配置类型 + /// + /// ASP.NET Core 的 DI 容器会自动创建并注入这些服务。 + /// + /// + /// 产品信息服务 + /// 配置类型服务 + public HwProductInfoController(HwProductInfoService productInfoService, HwPortalConfigTypeService configTypeService) + { + _productInfoService = productInfoService; + _configTypeService = configTypeService; + } + + /// + /// 查询产品列表(分页)。 + /// + /// 查询条件 + /// 分页数据 + [HttpGet("list")] + public async Task> List([FromQuery] HwProductInfo input) + { + // GetDataTable 是基类方法,做内存分页。 + // 如果数据量大,应该在数据库层面分页(SQL LIMIT/OFFSET)。 + return GetDataTable(await _productInfoService.SelectHwProductInfoJoinList(input)); + } + + /// + /// 导出产品信息到 Excel。 + /// + /// 查询条件 + /// Excel 文件 + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwProductInfo input) + { + return ExportExcel(await _productInfoService.SelectHwProductInfoList(input), "产品信息数据"); + } + + /// + /// 根据产品ID查询详情。 + /// + /// 产品ID + /// 产品详情 + [HttpGet("{productInfoId:long}")] + public async Task GetInfo(long productInfoId) + { + return Success(await _productInfoService.SelectHwProductInfoByProductInfoId(productInfoId)); + } + + /// + /// 新增产品。 + /// + /// 产品数据 + /// 操作结果 + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwProductInfo input) + { + return ToAjax(await _productInfoService.InsertHwProductInfo(input)); + } + + /// + /// 更新产品。 + /// + /// 产品数据 + /// 操作结果 + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwProductInfo input) + { + return ToAjax(await _productInfoService.UpdateHwProductInfo(input)); + } + + /// + /// 批量删除产品。 + /// + /// 逗号分隔的产品ID + /// 操作结果 + [HttpDelete("{productInfoIds}")] + [Idempotent] + public async Task Remove(string productInfoIds) + { + return ToAjax(await _productInfoService.DeleteHwProductInfoByProductInfoIds(ParseLongArray(productInfoIds))); + } + + /// + /// 查询配置类型树。 + /// + /// 查询条件 + /// 配置类型树 + [HttpGet("portalConfigTypeTree")] + public async Task PortalConfigTypeTree([FromQuery] HwPortalConfigType input) + { + return Success(await _configTypeService.SelectPortalConfigTypeTreeList(input)); + } + + /// + /// 解析逗号分隔的长整型数组(私有方法)。 + /// + /// 【C# 语法知识点 - 表达式体方法】 + /// private static long[] ParseLongArray(string value) => ... + // + /// 这是"表达式体方法"(Expression-bodied method)语法。 + /// 等价于: + /// private static long[] ParseLongArray(string value) { + /// return value.Split(...).Select(...).ToArray(); + /// } + /// + /// 当方法体只有一行 return 语句时,可以用 => 简化。 + /// + /// 对比 Java: + /// Java 没有这个语法,但 Java 14+ 有类似的表达式体方法: + /// private static long[] parseLongArray(String value) -> + /// Arrays.stream(value.split(",")).mapToLong(Long::parseLong).toArray(); + /// + /// + /// 逗号分隔的字符串 + /// 长整型数组 + private static long[] ParseLongArray(string value) => + value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoDetailController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoDetailController.cs new file mode 100644 index 0000000..2dc829f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwProductInfoDetailController.cs @@ -0,0 +1,55 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/productInfoDetail")] +public class HwProductInfoDetailController : HwPortalControllerBase +{ + private readonly HwProductInfoDetailService _service; + + public HwProductInfoDetailController(HwProductInfoDetailService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task List([FromQuery] HwProductInfoDetail input) + { + return Success(await _service.SelectHwProductInfoDetailList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwProductInfoDetail input) + { + return ExportExcel(await _service.SelectHwProductInfoDetailList(input), "产品明细数据"); + } + + [HttpGet("{productInfoDetailId:long}")] + public async Task GetInfo(long productInfoDetailId) + { + return Success(await _service.SelectHwProductInfoDetailByProductInfoDetailId(productInfoDetailId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwProductInfoDetail input) + { + return ToAjax(await _service.InsertHwProductInfoDetail(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwProductInfoDetail input) + { + return ToAjax(await _service.UpdateHwProductInfoDetail(input)); + } + + [HttpDelete("{productInfoDetailIds}")] + [Idempotent] + public async Task Remove(string productInfoDetailIds) + { + return ToAjax(await _service.DeleteHwProductInfoDetailByProductInfoDetailIds(ParseLongArray(productInfoDetailIds))); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs new file mode 100644 index 0000000..0e58abe --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs @@ -0,0 +1,19 @@ +namespace Admin.NET.Plugin.HwPortal; + +[Route("portal/search/admin")] +public class HwSearchAdminController : HwPortalControllerBase +{ + private readonly IHwSearchRebuildService _service; + + public HwSearchAdminController(IHwSearchRebuildService service) + { + _service = service; + } + + [HttpPost("rebuild")] + public async Task Rebuild() + { + await _service.RebuildAllAsync(); + return Success("重建完成"); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs new file mode 100644 index 0000000..eed8ec3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs @@ -0,0 +1,52 @@ +namespace Admin.NET.Plugin.HwPortal; + +// [AllowAnonymous] 是 ASP.NET Core/Furion 里的匿名放行特性。 +// 你可以把它理解成 Spring Security 里“这个接口不要求登录”的声明式配置。 +// 和 Spring 常见的配置类放行不同,这里直接写在控制器上,阅读时更集中。 +[AllowAnonymous] +[Route("portal/search")] +public class HwSearchController : HwPortalControllerBase +{ + // readonly 字段表示“构造完成后不允许再被重新赋值”。 + // 这和 Java 里把依赖字段声明成 final 的意图一致:依赖一旦注入完成,就不要在运行期乱改。 + private readonly HwSearchService _service; + + public HwSearchController(HwSearchService service) + { + // 这就是 C#/ASP.NET Core 最常见的“构造函数注入”。 + // 对应 Java Spring 你可以理解成: + // 1. 以前常见的 @Autowired 字段注入 + // 2. 更推荐的 @RequiredArgsConstructor / 构造器注入 + // 这里只是 C# 没有 Lombok,直接手写构造函数而已。 + _service = service; + } + + // [HttpGet] 表示这个方法处理 GET 请求。 + // Java Spring 里对应 @GetMapping。 + // 这里没有写路径,表示沿用类上的基础路由,也就是 /portal/search。 + [HttpGet] + // 这是我们自定义的限流特性。 + // 注意它不是“只做标记”,而是自己实现了 IAsyncActionFilter,框架执行到这里会先跑限流逻辑,再决定要不要进入方法体。 + [HwPortalIpRateLimit("portal_search", 60, 120)] + public async Task Search([FromQuery] string keyword, [FromQuery] int? pageNum, [FromQuery] int? pageSize) + { + // async + await 是 C# 异步编程的核心写法。 + // 你可以先把它理解成“这个方法里要等待数据库/IO,但等待时不想卡死线程”。 + // Java 里它不等于 new Thread,也不等于 CompletableFuture 全套写法,更像框架层帮你把异步 IO 写法简化了。 + + // [FromQuery] 表示参数从 URL 查询串里绑定,例如 ?keyword=轮胎&pageNum=1。 + // Spring Boot 里通常你会写 @RequestParam;ASP.NET Core 则更常用 [FromQuery] 明确绑定来源。 + return Success(await _service.Search(keyword, pageNum, pageSize)); + } + + // 这里写了 "edit",所以完整路由会变成 /portal/search/edit。 + // 这和 Spring 的 @GetMapping("/edit") 完全是同一类路由声明思路。 + [HttpGet("edit")] + [HwPortalIpRateLimit("portal_search_edit", 60, 120)] + public async Task EditSearch([FromQuery] string keyword, [FromQuery] int? pageNum, [FromQuery] int? pageSize) + { + // 展示端和编辑端共用同一套搜索主逻辑,只是编辑端需要多返回 editRoute。 + // 这种“控制器只做参数接线,真正差异交给 Service”的写法,是为了让 API 层保持薄,不把业务判断写散。 + return Success(await _service.SearchForEdit(keyword, pageNum, pageSize)); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWeb1Controller.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWeb1Controller.cs new file mode 100644 index 0000000..94dab2a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWeb1Controller.cs @@ -0,0 +1,59 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/hwWeb1")] +public class HwWeb1Controller : HwPortalControllerBase +{ + private readonly HwWeb1Service _service; + + public HwWeb1Controller(HwWeb1Service service) + { + _service = service; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwWeb1 input) + { + return GetDataTableWithoutPaging(await _service.SelectHwWebList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwWeb1 input) + { + return ExportExcel(await _service.SelectHwWebList(input), "官网详情页数据"); + } + + [HttpGet("{webCode:long}")] + public async Task GetInfo(long webCode) + { + return Success(await _service.SelectHwWebByWebcode(webCode)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwWeb1 input) + { + return ToAjax(await _service.InsertHwWeb(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwWeb1 input) + { + return ToAjax(await _service.UpdateHwWeb(input)); + } + + [HttpDelete("{webIds}")] + [Idempotent] + public async Task Remove(string webIds) + { + return ToAjax(await _service.DeleteHwWebByWebIds(ParseLongArray(webIds))); + } + + [HttpGet("getHwWeb1List")] + public async Task GetHwWeb1List([FromQuery] HwWeb1 input) + { + return Success(await _service.SelectHwWebList(input)); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebController.cs new file mode 100644 index 0000000..2844ab5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebController.cs @@ -0,0 +1,313 @@ +// ============================================================================ +// 【文件说明】HwWebController.cs - 官网页面控制器 +// ============================================================================ +// 这个控制器负责处理官网页面相关的 HTTP 请求。 +// +// 【ASP.NET Core 控制器基础】 +// 控制器是 MVC 架构中的"C",负责: +// 1. 接收 HTTP 请求 +// 2. 调用业务逻辑(Service 层) +// 3. 返回 HTTP 响应 +// +// 【路由规则】 +// [Route("portal/hwWeb")] 定义了路由前缀: +// - GET portal/hwWeb/list -> List() 方法 +// - GET portal/hwWeb/{webCode} -> GetInfo() 方法 +// - POST portal/hwWeb -> Add() 方法 +// - PUT portal/hwWeb -> Edit() 方法 +// - DELETE portal/hwWeb/{webIds} -> Remove() 方法 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @RestController +// @RequestMapping("/portal/hwWeb") +// public class HwWebController { ... } +// +// ASP.NET Core: +// [ApiController] +// [Route("portal/hwWeb")] +// public class HwWebController : ControllerBase { ... } +// +// 两者概念相同,只是注解/特性的写法不同。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 官网页面控制器。 +/// +/// 【C# 语法知识点 - 特性组合】 +/// [AllowAnonymous] + [Route("portal/hwWeb")] 是多个特性组合使用。 +/// +/// [AllowAnonymous] 表示"允许匿名访问",不需要登录就能调用这些接口。 +/// 这对于官网前台是必要的,因为访问官网的用户通常不需要登录。 +/// +/// 对比 Java Spring Security: +/// Java: @PermitAll 或在 SecurityConfig 中配置 permitAll() +/// C#: [AllowAnonymous] 特性 +/// +/// +/// 【RESTful API 设计】 +/// 这个控制器遵循 RESTful 风格: +/// - GET:查询操作 +/// - POST:新增操作 +/// - PUT:更新操作 +/// - DELETE:删除操作 +/// +/// 对比 Java Spring Boot: +/// Java: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping +/// C#: [HttpGet], [HttpPost], [HttpPut], [HttpDelete] +/// +/// 写法几乎一样,只是特性名称略有不同。 +/// +/// +[AllowAnonymous] +[Route("portal/hwWeb")] +public class HwWebController : HwPortalControllerBase +{ + /// + /// 页面服务实例。 + /// + /// 【C# 语法知识点 - readonly 字段】 + /// readonly 表示"只读字段",只能在构造函数中赋值,之后不能修改。 + /// + /// 为什么用 readonly? + /// 1. 防止意外修改:服务实例不应该被替换 + /// 2. 线程安全:readonly 字段天然线程安全 + /// 3. 编译器优化:编译器可以对 readonly 字段做优化 + /// + /// 对比 Java: + /// Java 通常用 final 关键字: + /// private final HwWebService service; + /// + /// + /// 【命名约定】 + /// _service 是 C# 的私有字段命名约定(下划线前缀)。 + /// 也可以用 service(无前缀),但 _service 更常见于依赖注入字段。 + /// + /// + private readonly HwWebService _service; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数依赖注入】 + /// public HwWebController(HwWebService service) + /// + /// 这是 ASP.NET Core 最核心的设计模式:依赖注入(DI)。 + /// + /// 工作原理: + /// 1. 应用启动时,框架扫描所有服务并注册到 DI 容器 + /// 2. 当请求到达控制器时,框架自动创建所需的服务实例 + /// 3. 通过构造函数把服务实例"注入"进来 + /// + /// 对比 Java Spring Boot: + /// Java 有三种注入方式: + /// // 1. 字段注入(不推荐) + /// @Autowired + /// private HwWebService service; + /// + /// // 2. Setter 注入(较少用) + /// @Autowired + /// public void setService(HwWebService service) { this.service = service; } + /// + /// // 3. 构造函数注入(推荐,和 C# 一样) + /// @Autowired + /// public HwWebController(HwWebService service) { this.service = service; } + /// + /// C# ASP.NET Core 只推荐构造函数注入,这是最佳实践。 + /// + /// + /// 页面服务实例(由框架自动注入) + public HwWebController(HwWebService service) + { + _service = service; + } + + /// + /// 查询页面列表。 + /// + /// 【HTTP 路由】 + /// [HttpGet("list")] 定义了路由:GET portal/hwWeb/list + /// + /// 对比 Java Spring Boot: + /// Java: @GetMapping("/list") 或 @GetMapping("list") + /// C#: [HttpGet("list")] + /// + /// + /// 【C# 语法知识点 - async/await 异步编程】 + /// public async Task<HwPortalTableDataInfo<HwWeb>> List(...) + /// + /// async/await 是 C# 的异步编程语法: + /// - async:标记方法为异步方法 + /// - await:等待异步操作完成 + /// - Task<T>:表示一个返回 T 类型的异步操作 + /// + /// 为什么用异步? + /// 数据库操作是 I/O 密集型,使用异步可以: + /// 1. 不阻塞线程:等待数据库时,线程可以处理其他请求 + /// 2. 提高吞吐量:同样的线程数可以处理更多请求 + /// + /// 对比 Java: + /// Java 的异步写法: + /// public CompletableFuture<TableDataInfo<HwWeb>> list(...) { + /// return CompletableFuture.supplyAsync(() -> service.selectList(input)); + /// } + /// + /// 或者用 Spring 的 @Async 注解。 + /// + /// C# 的 async/await 更简洁,是语言级别的支持。 + /// + /// + /// + /// 查询参数。 + /// + /// 【C# 语法知识点 - [FromQuery] 特性】 + /// [FromQuery] 表示从 URL 查询字符串绑定参数。 + /// 例如:GET portal/hwWeb/list?webCode=100 + /// 框架会自动把 webCode=100 绑定到 input.WebCode 属性。 + /// + /// 对比 Java Spring Boot: + /// Java: public Result list(@RequestParam Map<String, String> params) + /// 或者:public Result list(HwWeb input) // Spring 会自动绑定 + /// + /// + /// 分页数据包装对象 + [HttpGet("list")] + public async Task> List([FromQuery] HwWeb input) + { + // await 表示"等待异步操作完成"。 + // GetDataTableWithoutPaging 是基类方法,不做分页,返回全部数据。 + return GetDataTableWithoutPaging(await _service.SelectHwWebList(input)); + } + + /// + /// 导出页面数据到 Excel。 + /// + /// 【幂等性设计】 + /// [Idempotent] 是 Furion 框架的特性,表示"幂等操作"。 + /// + /// 什么是幂等? + /// 多次执行相同的请求,结果和执行一次一样。 + /// + /// 为什么导出需要幂等? + /// 1. 防止重复请求:用户多次点击导出按钮 + /// 2. 限流保护:避免服务器被大量导出请求打垮 + /// + /// 实现原理: + /// 框架会根据请求的唯一标识(如请求头中的请求ID)判断是否重复请求。 + /// 如果是重复请求,直接返回之前的结果,不再执行方法体。 + /// + /// + /// 查询参数 + /// Excel 文件下载响应 + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwWeb input) + { + // ExportExcel 是基类方法,返回 IActionResult。 + // IActionResult 是 ASP.NET Core 的"动作结果"接口, + // 可以是 JsonResult、FileResult、StatusCodeResult 等。 + return ExportExcel(await _service.SelectHwWebList(input), "官网页面数据"); + } + + /// + /// 根据编码查询页面详情。 + /// + /// 【C# 语法知识点 - 路由参数约束】 + /// [HttpGet("{webCode:long}")] 中的 :long 是"路由约束"。 + /// + /// 含义:webCode 参数必须是 long 类型。 + /// 如果传入非数字,会返回 404 而不是 400。 + /// + /// 常见的路由约束: + /// - :int:整数 + /// - :long:长整数 + /// - :guid:GUID + /// - :regex(pattern):正则表达式 + /// + /// 对比 Java Spring Boot: + /// Java: @GetMapping("/{webCode}") + /// public Result getInfo(@PathVariable Long webCode) + /// + /// C# 的路由约束在路由模板中定义,更声明式。 + /// + /// + /// 页面编码(从路由中获取) + /// Ajax 响应结果 + [HttpGet("{webCode:long}")] + public async Task GetInfo(long webCode) + { + // Success 是基类方法,返回成功结果。 + // 这里把查询到的数据包装成 AjaxResult 返回。 + return Success(await _service.SelectHwWebByWebcode(webCode)); + } + + /// + /// 新增页面。 + /// + /// 【C# 语法知识点 - [FromBody] 特性】 + /// [FromBody] 表示从请求体(Request Body)绑定参数。 + /// 框架会自动把 JSON 请求体反序列化为 HwWeb 对象。 + /// + /// 对比 Java Spring Boot: + /// Java: public Result add(@RequestBody HwWeb input) + /// C#: public async Task<HwPortalAjaxResult> Add([FromBody] HwWeb input) + /// + /// 两者完全一样,都是 @RequestBody / [FromBody]。 + /// + /// + /// 页面数据(从请求体 JSON 反序列化) + /// Ajax 响应结果 + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwWeb input) + { + // ToAjax 是基类方法,根据影响行数返回成功或失败。 + // 如果 rows > 0 返回成功,否则返回失败。 + return ToAjax(await _service.InsertHwWeb(input)); + } + + /// + /// 更新页面。 + /// + /// 页面数据 + /// Ajax 响应结果 + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwWeb input) + { + return ToAjax(await _service.UpdateHwWeb(input)); + } + + /// + /// 批量删除页面。 + /// + /// 【批量删除的设计】 + /// 前端传来的删除请求通常是:DELETE portal/hwWeb/1,2,3,4,5 + /// 这里的 webIds 是 "1,2,3,4,5" 字符串,需要解析成数组。 + /// + /// ParseLongArray 是基类方法,把 "1,2,3" 解析成 [1, 2, 3]。 + /// + /// + /// 逗号分隔的 ID 字符串 + /// Ajax 响应结果 + [HttpDelete("{webIds}")] + [Idempotent] + public async Task Remove(string webIds) + { + // ParseLongArray 把 "1,2,3" 解析成 [1, 2, 3]。 + return ToAjax(await _service.DeleteHwWebByWebIds(ParseLongArray(webIds))); + } + + /// + /// 查询页面列表(不分页)。 + /// + /// 查询参数 + /// Ajax 响应结果 + [HttpGet("getHwWebList")] + public async Task GetHwWebList([FromQuery] HwWeb input) + { + return Success(await _service.SelectHwWebList(input)); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebDocumentController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebDocumentController.cs new file mode 100644 index 0000000..6d5cf4c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebDocumentController.cs @@ -0,0 +1,89 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/hwWebDocument")] +public class HwWebDocumentController : HwPortalControllerBase +{ + private readonly HwWebDocumentService _service; + + public HwWebDocumentController(HwWebDocumentService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task> List([FromQuery] HwWebDocument input) + { + List list = await _service.SelectHwWebDocumentList(input); + foreach (HwWebDocument doc in list) + { + bool hasSecret = doc.HasSecret; + doc.SecretKey = null; + if (hasSecret) + { + doc.DocumentAddress = null; + } + } + + return GetDataTable(list); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwWebDocument input) + { + return ExportExcel(await _service.SelectHwWebDocumentList(input), "资料文件数据"); + } + + [HttpGet("{documentId}")] + public async Task GetInfo(string documentId) + { + HwWebDocument doc = await _service.SelectHwWebDocumentByDocumentId(documentId); + if (doc != null) + { + bool hasSecret = doc.HasSecret; + doc.SecretKey = null; + if (hasSecret) + { + doc.DocumentAddress = null; + } + } + + return Success(doc); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwWebDocument input) + { + return ToAjax(await _service.InsertHwWebDocument(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwWebDocument input) + { + return ToAjax(await _service.UpdateHwWebDocument(input)); + } + + [HttpDelete("{documentIds}")] + [Idempotent] + public async Task Remove(string documentIds) + { + return ToAjax(await _service.DeleteHwWebDocumentByDocumentIds(documentIds.Split(',', StringSplitOptions.RemoveEmptyEntries))); + } + + [HttpPost("getSecureDocumentAddress")] + [Idempotent] + public async Task GetSecureDocumentAddress([FromBody] SecureDocumentRequest request) + { + try + { + return Success(await _service.VerifyAndGetDocumentAddress(request.DocumentId, request.ProvidedKey)); + } + catch (Exception ex) + { + return Error(ex.Message); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenu1Controller.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenu1Controller.cs new file mode 100644 index 0000000..3df22a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenu1Controller.cs @@ -0,0 +1,61 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/hwWebMenu1")] +public class HwWebMenu1Controller : HwPortalControllerBase +{ + private readonly HwWebMenu1Service _service; + + public HwWebMenu1Controller(HwWebMenu1Service service) + { + _service = service; + } + + [HttpGet("list")] + public async Task List([FromQuery] HwWebMenu1 input) + { + return Success(await _service.SelectHwWebMenuList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwWebMenu1 input) + { + return ExportExcel(await _service.SelectHwWebMenuList(input), "官网次级菜单数据"); + } + + [HttpGet("{webMenuId:long}")] + public async Task GetInfo(long webMenuId) + { + return Success(await _service.SelectHwWebMenuByWebMenuId(webMenuId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwWebMenu1 input) + { + return ToAjax(await _service.InsertHwWebMenu(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwWebMenu1 input) + { + return ToAjax(await _service.UpdateHwWebMenu(input)); + } + + [HttpDelete("{webMenuIds}")] + [Idempotent] + public async Task Remove(string webMenuIds) + { + return ToAjax(await _service.DeleteHwWebMenuByWebMenuIds(ParseLongArray(webMenuIds))); + } + + [HttpGet("selectMenuTree")] + public async Task SelectMenuTree([FromQuery] HwWebMenu1 input) + { + return Success(await _service.SelectMenuTree(input)); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenuController.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenuController.cs new file mode 100644 index 0000000..3c4ab36 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwWebMenuController.cs @@ -0,0 +1,61 @@ +namespace Admin.NET.Plugin.HwPortal; + +[AllowAnonymous] +[Route("portal/hwWebMenu")] +public class HwWebMenuController : HwPortalControllerBase +{ + private readonly HwWebMenuService _service; + + public HwWebMenuController(HwWebMenuService service) + { + _service = service; + } + + [HttpGet("list")] + public async Task List([FromQuery] HwWebMenu input) + { + return Success(await _service.SelectHwWebMenuList(input)); + } + + [HttpPost("export")] + [Idempotent] + public async Task Export([FromQuery] HwWebMenu input) + { + return ExportExcel(await _service.SelectHwWebMenuList(input), "官网菜单数据"); + } + + [HttpGet("{webMenuId:long}")] + public async Task GetInfo(long webMenuId) + { + return Success(await _service.SelectHwWebMenuByWebMenuId(webMenuId)); + } + + [HttpPost] + [Idempotent] + public async Task Add([FromBody] HwWebMenu input) + { + return ToAjax(await _service.InsertHwWebMenu(input)); + } + + [HttpPut] + [Idempotent] + public async Task Edit([FromBody] HwWebMenu input) + { + return ToAjax(await _service.UpdateHwWebMenu(input)); + } + + [HttpDelete("{webMenuIds}")] + [Idempotent] + public async Task Remove(string webMenuIds) + { + return ToAjax(await _service.DeleteHwWebMenuByWebMenuIds(ParseLongArray(webMenuIds))); + } + + [HttpGet("selectMenuTree")] + public async Task SelectMenuTree([FromQuery] HwWebMenu input) + { + return Success(await _service.SelectMenuTree(input)); + } + + private static long[] ParseLongArray(string value) => value.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(long.Parse).ToArray(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsCollectRequest.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsCollectRequest.cs new file mode 100644 index 0000000..75fa9a8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsCollectRequest.cs @@ -0,0 +1,213 @@ +// ============================================================================ +// 【文件说明】AnalyticsCollectRequest.cs - 分析事件收集请求 DTO +// ============================================================================ +// 这是前端上报访问事件的请求数据结构。 +// +// 【什么是 DTO?】 +// DTO = Data Transfer Object(数据传输对象) +// - 用于在 API 和前端之间传递数据 +// - 不包含业务逻辑 +// - 通常是简单的属性容器 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用 POJO 或 Record: +// public class AnalyticsCollectRequest { +// private String visitorId; +// private String sessionId; +// // getter/setter... +// } +// +// 或 Java 14+ Record: +// public record AnalyticsCollectRequest( +// String visitorId, +// String sessionId, +// ... +// ) {} +// +// C# 用普通类 + 自动属性,更简洁。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 分析事件收集请求。 +/// +/// 【业务场景】 +/// 前端在以下时机上报事件: +/// - 页面加载完成:page_view +/// - 离开页面:page_leave(带停留时长) +/// - 提交搜索:search_submit(带关键词) +/// - 点击下载:download_click +/// - 提交联系表单:contact_submit +/// +/// +/// 【C# 语法知识点 - 自动属性】 +/// public string VisitorId { get; set; } +/// +/// 这是 C# 3.0 引入的"自动属性"语法: +/// - 编译器自动生成私有字段 +/// - 不需要显式声明 getter/setter +/// - 等价于 Java 的 private field + public getter/setter +/// +/// 对比 Java: +/// Java: +/// private String visitorId; +/// public String getVisitorId() { return visitorId; } +/// public void setVisitorId(String visitorId) { this.visitorId = visitorId; } +/// +/// C# 一行代码完成同样功能。 +/// +/// +public class AnalyticsCollectRequest +{ + /// + /// 访客 ID。 + /// + /// 【业务说明】 + /// 由前端生成并存储在 Cookie 或 localStorage: + /// - 首次访问时生成 UUID + /// - 后续访问复用同一 ID + /// - 用于识别同一访客的多次访问 + /// + /// + public string VisitorId { get; set; } + + /// + /// 会话 ID。 + /// + /// 【业务说明】 + /// 每次访问生成新的会话 ID: + /// - 用于区分不同的访问会话 + /// - 计算跳出率(单页会话占比) + /// + /// + public string SessionId { get; set; } + + /// + /// 事件类型。 + /// + /// 【允许值】 + /// - page_view:页面浏览 + /// - page_leave:离开页面 + /// - search_submit:提交搜索 + /// - download_click:点击下载 + /// - contact_submit:提交联系表单 + /// + /// + public string EventType { get; set; } + + /// + /// 页面路径。 + /// + /// 【示例】 + /// /product/detail + /// /search?keyword=xxx + /// + /// + public string Path { get; set; } + + /// + /// 来源页面(Referrer)。 + /// + /// 【业务说明】 + /// 用户从哪个页面跳转过来: + /// - 搜索引擎:https://www.google.com/search?q=xxx + /// - 外部网站:https://partner.com/link + /// - 直接访问:为空 + /// + /// + public string Referrer { get; set; } + + /// + /// UTM 来源参数。 + /// + /// 【业务说明】 + /// 用于营销追踪: + /// - utm_source:流量来源(如 google, baidu) + /// - utm_medium:营销媒介(如 cpc, email) + /// - utm_campaign:营销活动名称 + /// + /// + public string UtmSource { get; set; } + + /// + /// UTM 媒介参数。 + /// + public string UtmMedium { get; set; } + + /// + /// UTM 活动参数。 + /// + public string UtmCampaign { get; set; } + + /// + /// 搜索关键词。 + /// + /// 【业务说明】 + /// 仅 search_submit 事件需要此字段。 + /// + /// + public string Keyword { get; set; } + + /// + /// User-Agent 字符串。 + /// + /// 【业务说明】 + /// 前端可以上报自己的 UA,如果不传则使用请求头中的 UA。 + /// + /// + public string Ua { get; set; } + + /// + /// 设备类型(前端上报)。 + /// + /// 【允许值】 + /// - Mobile:手机 + /// - Tablet:平板 + /// - Desktop:桌面 + /// + /// + public string Device { get; set; } + + /// + /// 浏览器名称(前端上报)。 + /// + public string Browser { get; set; } + + /// + /// 操作系统(前端上报)。 + /// + public string Os { get; set; } + + /// + /// 停留时长(毫秒)。 + /// + /// 【C# 语法知识点 - 可空值类型】 + /// long? 是可空的长整型: + /// - 可以是 null(表示未提供) + /// - 可以是 long 值 + /// + /// 对比 Java: + /// Java 用 Long 包装类: + /// private Long stayMs; // 可以为 null + /// + /// C# 的可空值类型是值类型,更高效。 + /// + /// + /// 【业务说明】 + /// 仅 page_leave 事件需要此字段。 + /// + /// + public long? StayMs { get; set; } + + /// + /// 事件时间(Unix 毫秒时间戳)。 + /// + /// 【业务说明】 + /// 前端上报事件发生的时间: + /// - 如果不传,使用服务器当前时间 + /// - 用于处理网络延迟导致的时间偏差 + /// + /// + public long? EventTime { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsDashboardDTO.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsDashboardDTO.cs new file mode 100644 index 0000000..354e885 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsDashboardDTO.cs @@ -0,0 +1,156 @@ +// ============================================================================ +// 【文件说明】AnalyticsDashboardDTO.cs - 分析仪表盘数据 DTO +// ============================================================================ +// 这是仪表盘接口返回的数据结构,包含网站访问统计的核心指标。 +// +// 【核心指标说明】 +// - PV(Page View):页面浏览量,用户每打开一个页面计为 1 次 +// - UV(Unique Visitor):独立访客数,同一访客多次访问只计 1 次 +// - IP UV:独立 IP 数,同一 IP 多次访问只计 1 次 +// - 跳出率:只浏览一页就离开的会话占比 +// - 平均停留时长:用户在网站的平均停留时间 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用 POJO: +// public class AnalyticsDashboardDTO { +// private String statDate; +// private Long pv; +// private Long uv; +// // getter/setter... +// } +// +// C# 用自动属性,代码更简洁。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 分析仪表盘数据。 +/// +/// 【业务场景】 +/// 管理后台的"数据分析"页面展示: +/// - 今日/指定日期的访问统计 +// - 热门页面排行 +// - 搜索关键词排行 +/// +/// +public class AnalyticsDashboardDTO +{ + /// + /// 统计日期(格式:yyyy-MM-dd)。 + /// + public string StatDate { get; set; } + + /// + /// 页面浏览量(PV)。 + /// + /// 【业务说明】 + /// 用户每打开一个页面计为 1 次 PV。 + /// 同一用户刷新页面会增加 PV。 + /// + /// + public long? Pv { get; set; } + + /// + /// 独立访客数(UV)。 + /// + /// 【业务说明】 + /// 按 visitorId 去重统计。 + /// 同一访客多次访问只计 1 次 UV。 + /// + /// + public long? Uv { get; set; } + + /// + /// 独立 IP 数。 + /// + /// 【业务说明】 + /// 按 IP 哈希去重统计。 + /// 同一 IP 多次访问只计 1 次。 + /// 注意:由于存储的是哈希值,无法反查原始 IP。 + /// + /// + public long? IpUv { get; set; } + + /// + /// 平均停留时长(毫秒)。 + /// + /// 【计算方式】 + /// 所有 page_leave 事件的停留时长平均值。 + /// + /// + public long? AvgStayMs { get; set; } + + /// + /// 跳出率(百分比)。 + /// + /// 【计算方式】 + /// 跳出率 = 单页会话数 / 总会话数 × 100 + /// + /// 【业务含义】 + /// - 跳出率高:用户只看了一页就离开,可能内容不相关或体验差 + /// - 跳出率低:用户浏览了多个页面,内容吸引人 + /// + /// + public double? BounceRate { get; set; } + + /// + /// 搜索次数。 + /// + /// 【业务说明】 + /// search_submit 事件的总次数。 + /// + /// + public long? SearchCount { get; set; } + + /// + /// 下载次数。 + /// + /// 【业务说明】 + /// download_click 事件的总次数。 + /// + /// + public long? DownloadCount { get; set; } + + /// + /// 入口页面排行。 + /// + /// 【业务说明】 + /// 用户进入网站的第一页统计: + /// - 哪些页面是用户进入网站的入口 + /// - 用于评估推广页面的效果 + /// + /// + /// 【C# 语法知识点 - 集合属性初始化】 + /// List<T> Property { get; set; } = new(); + /// + /// = new() 是属性初始化器: + /// - 创建对象时自动初始化集合 + /// - 避免 null 引用异常 + /// - new() 是 C# 9.0 的目标类型 new 表达式 + /// + /// 对比 Java: + /// Java 通常不初始化,或需要显式初始化: + /// private List<Item> items = new ArrayList<>(); + /// + /// + public List EntryPages { get; set; } = new(); + + /// + /// 热门页面排行。 + /// + /// 【业务说明】 + /// 浏览量最高的页面列表。 + /// + /// + public List HotPages { get; set; } = new(); + + /// + /// 热门搜索关键词排行。 + /// + /// 【业务说明】 + /// 搜索次数最多的关键词列表。 + /// + /// + public List HotKeywords { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsRankItemDTO.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsRankItemDTO.cs new file mode 100644 index 0000000..82fa3b7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/AnalyticsRankItemDTO.cs @@ -0,0 +1,79 @@ +// ============================================================================ +// 【文件说明】AnalyticsRankItemDTO.cs - 分析排行项 DTO +// ============================================================================ +// 这是排行榜中的单项数据结构,用于表示排行中的一个条目。 +// +// 【使用场景】 +// - 入口页面排行:Name = 页面路径,Value = 访问次数 +// - 热门页面排行:Name = 页面路径,Value = 浏览量 +// - 搜索关键词排行:Name = 关键词,Value = 搜索次数 +// +// 【设计模式 - 通用 DTO】 +// 这个 DTO 设计得很通用,可以用于多种排行场景: +// - 不限定具体的业务含义 +// - Name 和 Value 可以代表不同的事物 +// - 减少 DTO 类的数量 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常也用类似的通用类: +// public class RankItem { +// private String name; +// private Long value; +// // getter/setter... +// } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 分析排行项。 +/// +/// 【业务场景】 +/// 用于表示排行榜中的单个条目: +/// - Name:条目名称(如页面路径、关键词) +/// - Value:条目值(如访问次数、浏览量) +/// +/// +/// 【设计说明】 +/// 这是一个通用的排行项结构,可用于: +/// - 入口页面排行:Name = 页面路径,Value = 进入次数 +/// - 热门页面排行:Name = 页面路径,Value = PV +/// - 搜索关键词排行:Name = 关键词,Value = 搜索次数 +/// +/// +public class AnalyticsRankItemDTO +{ + /// + /// 条目名称。 + /// + /// 【业务含义】 + /// 根据排行类型不同,含义不同: + /// - 入口页面排行:页面路径(如 /product/detail) + /// - 热门页面排行:页面路径(如 /search) + /// - 搜索关键词排行:搜索关键词(如 "产品介绍") + /// + /// + public string Name { get; set; } + + /// + /// 条目值。 + /// + /// 【业务含义】 + /// 根据排行类型不同,含义不同: + /// - 入口页面排行:进入次数 + /// - 热门页面排行:页面浏览量(PV) + /// - 搜索关键词排行:搜索次数 + /// + /// + /// 【C# 语法知识点 - 可空值类型】 + /// long? 是可空的长整型: + /// - 可以是 null(表示无数据) + /// - 可以是 long 值 + /// + /// 为什么用可空类型? + /// - 数据库查询可能返回 null + /// - 前端需要区分"0 次"和"无数据" + /// + /// + public long? Value { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/HwProductInfoJoinDetailRow.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/HwProductInfoJoinDetailRow.cs new file mode 100644 index 0000000..e1ba0ae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/HwProductInfoJoinDetailRow.cs @@ -0,0 +1,137 @@ +// ============================================================================ +// 【文件说明】HwProductInfoJoinDetailRow.cs - 产品信息关联明细行 DTO +// ============================================================================ +// 这是一个"数据传输对象"(DTO),用于接收 SQL JOIN 查询的扁平结果集。 +// +// 【什么是 DTO?】 +// DTO = Data Transfer Object(数据传输对象) +// 用于在不同层之间传输数据,不包含业务逻辑。 +// +// 【为什么需要这个类?】 +// SQL JOIN 查询返回的是"扁平结构": +// 产品ID | 产品名称 | 明细ID | 明细标题 | ... +// 1 | 产品A | 101 | 明细1 | ... +// 1 | 产品A | 102 | 明细2 | ... +// +// 每行包含产品字段 + 明细字段,需要用这个类接收。 +// 然后在 Service 层通过 LINQ GroupBy 转换成树形结构。 +// +// 【命名约定】 +// Row 后缀表示"行",代表 SQL 结果集的一行。 +// Join 表示这是 JOIN 查询的结果。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品信息关联明细行 DTO。 +/// +/// 【C# 语法知识点 - internal sealed class】 +/// internal:只能在当前程序集(Assembly)内访问。 +/// sealed:密封类,不能被继承。 +/// +/// 为什么用 internal? +/// 这个类只在插件内部使用,不需要暴露给外部。 +/// +/// 为什么用 sealed? +/// 1. 这个类不需要被继承 +/// 2. 密封类可以有编译器优化(虚方法调用转直接调用) +/// +/// 对比 Java: +/// Java 用 package-private(不写访问修饰符)实现类似 internal 的效果。 +/// Java 用 final 关键字实现 sealed 的效果: +/// final class HwProductInfoJoinDetailRow extends HwProductInfo { ... } +/// +/// +/// 【继承设计】 +/// : HwProductInfo 表示继承自产品信息实体类。 +/// 这样这个类就包含了产品的所有字段,再加上明细字段。 +/// +/// 对比 Java: +/// Java 的继承写法一样: +/// class HwProductInfoJoinDetailRow extends HwProductInfo { ... } +/// +/// +/// 【字段命名约定】 +/// Sub 前缀表示"子表字段"(明细表字段): +/// - SubProductInfoDetailId:明细ID +/// - SubProductInfoDetailTitle:明细标题 +/// - ... +/// +/// 这样可以区分主表字段和子表字段,避免命名冲突。 +/// +/// +internal sealed class HwProductInfoJoinDetailRow : HwProductInfo +{ + /// + /// 明细ID(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_detail_id")] + public long? SubProductInfoDetailId { get; set; } + + /// + /// 明细父ID(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_parent_id")] + public long? SubParentId { get; set; } + + /// + /// 明细所属产品ID(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_id")] + public long? SubProductInfoId { get; set; } + + /// + /// 明细标题(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_detail_title")] + public string SubProductInfoDetailTitle { get; set; } + + /// + /// 明细描述(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_detail_desc")] + public string SubProductInfoDetailDesc { get; set; } + + /// + /// 明细排序号(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_detail_order")] + public long? SubProductInfoDetailOrder { get; set; } + + /// + /// 明细图片URL(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_product_info_detail_pic")] + public string SubProductInfoDetailPic { get; set; } + + /// + /// 明细祖级列表(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_ancestors")] + public string SubAncestors { get; set; } + + /// + /// 明细创建时间(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_create_time")] + public DateTime? SubCreateTime { get; set; } + + /// + /// 明细创建人(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_create_by")] + public string SubCreateBy { get; set; } + + /// + /// 明细更新时间(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_update_time")] + public DateTime? SubUpdateTime { get; set; } + + /// + /// 明细更新人(子表字段)。 + /// + [SugarColumn(ColumnName = "sub_update_by")] + public string SubUpdateBy { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs new file mode 100644 index 0000000..c0a9393 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs @@ -0,0 +1,86 @@ +// ============================================================================ +// 【文件说明】SearchPageDTO.cs - 搜索分页结果 DTO +// ============================================================================ +// 这是搜索接口返回的分页数据结构。 +// +// 【分页设计说明】 +// 分页是 Web 开发的常见需求: +// - 数据量大时,不可能一次返回所有数据 +// - 分页返回:每页 N 条,返回总数和当前页数据 +// +// 【与 Java Spring Boot 的对比】 +// Java 若依的分页结构: +// public class TableDataInfo { +// private long total; // 总记录数 +// private List rows; // 数据列表 +// private int code; // 状态码 +// private String msg; // 消息 +// } +// +// C# 这里更简洁,只包含核心字段: +// - Total:总记录数 +// - Rows:当前页数据 +// +// Admin.NET 框架会自动包装统一的返回格式,所以 DTO 不需要 code/msg 字段。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索分页结果。 +/// +/// 【业务场景】 +/// 前端搜索接口返回的数据结构: +/// - Total:总记录数(用于前端分页组件) +/// - Rows:当前页数据列表 +/// +/// +/// 【设计模式 - 分页 DTO】 +/// 这是标准的分页数据结构: +/// - 不包含页码、每页条数(这些由前端维护) +/// - 只返回总数和数据列表 +/// - 适用于任何分页场景 +/// +/// +public class SearchPageDTO +{ + /// + /// 总记录数。 + /// + /// 【业务说明】 + /// 用于前端计算总页数: + /// - 总页数 = Math.Ceiling(Total / PageSize) + /// - 前端分页组件需要此值显示页码 + /// + /// + /// 【C# 语法知识点 - long 类型】 + /// long 是 64 位整数: + /// - 范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 + /// - 对比 Java:Java 的 long 也是 64 位 + /// - 为什么用 long?数据量可能超过 int 的 21 亿上限 + /// + /// + public long Total { get; set; } + + /// + /// 当前页数据列表。 + /// + /// 【业务说明】 + /// 当前页的搜索结果列表。 + /// 每个元素是一个 SearchResultDTO 对象。 + /// + /// + /// 【C# 语法知识点 - 泛型集合】 + /// List<SearchResultDTO> 是泛型集合: + /// - List:动态数组,可以添加、删除元素 + /// - <SearchResultDTO>:泛型参数,指定元素类型 + /// + /// 对比 Java: + /// Java: List<SearchResultDTO> rows = new ArrayList<>(); + /// C#: List<SearchResultDTO> Rows { get; set; } = new(); + /// + /// C# 的 = new() 是目标类型 new 表达式,编译器自动推断类型。 + /// + /// + public List Rows { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchRawRecord.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchRawRecord.cs new file mode 100644 index 0000000..2381a92 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchRawRecord.cs @@ -0,0 +1,181 @@ +// ============================================================================ +// 【文件说明】SearchRawRecord.cs - 搜索原始记录 DTO +// ============================================================================ +// 这是旧版搜索服务返回的原始记录结构。 +// +// 【业务背景】 +// 旧版搜索直接查询业务表,返回的是原始数据: +// - 不经过索引表 +// - 字段直接映射数据库列 +// - 用于兼容旧版搜索功能 +// +// 【与新版搜索的区别】 +// 新版搜索使用 HwPortalSearchDoc 索引表: +// - 预先构建索引 +// - 查询更快 +// - 支持复杂搜索 +// +// 旧版搜索使用 SearchRawRecord: +// - 直接查询业务表 +// - LIKE 查询,性能较差 +// - 作为降级方案保留 +// +// 【与 Java 若依的对比】 +// Java 若依的搜索通常直接返回业务实体: +// public class SearchResult { +// private String sourceType; +// private String bizId; +// private String title; +// ... +// } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索原始记录。 +/// +/// 【业务场景】 +/// 旧版搜索服务(LegacyHwSearchQueryService)返回的数据结构。 +/// 直接从业务表查询,不经过索引表。 +/// +/// +/// 【设计说明】 +/// 这是一个扁平化的搜索结果: +/// - 包含所有可能的字段 +/// - 不同来源类型的记录可能有部分字段为空 +/// - 用于快速迁移旧版搜索功能 +/// +/// +public class SearchRawRecord +{ + /// + /// 来源类型。 + /// + /// 【允许值】 + /// - menu:菜单 + /// - web:页面 + /// - web1:页面1 + /// - document:文档 + /// - configType:配置类型 + /// + /// + public string SourceType { get; set; } + + /// + /// 业务 ID。 + /// + /// 【业务说明】 + /// 对应来源表的业务主键: + /// - menu:菜单 ID + /// - web:页面编码 + /// - document:文档 ID + /// + /// + public string BizId { get; set; } + + /// + /// 标题。 + /// + /// 【业务说明】 + /// 搜索结果展示的主要文本。 + /// + /// + public string Title { get; set; } + + /// + /// 内容。 + /// + /// 【业务说明】 + /// 搜索匹配的正文内容。 + /// 可能包含 HTML 标签,需要前端处理。 + /// + /// + public string Content { get; set; } + + /// + /// 页面编码。 + /// + /// 【业务说明】 + /// 用于前端路由跳转。 + /// + /// + public string WebCode { get; set; } + + /// + /// 类型 ID。 + /// + public string TypeId { get; set; } + + /// + /// 设备 ID。 + /// + public string DeviceId { get; set; } + + /// + /// 菜单 ID。 + /// + public string MenuId { get; set; } + + /// + /// 文档 ID。 + /// + public string DocumentId { get; set; } + + /// + /// 相关性得分。 + /// + /// 【业务说明】 + /// 用于搜索结果排序: + /// - 分数越高,排名越靠前 + /// - 旧版搜索可能使用固定值 + /// + /// + /// 【C# 语法知识点 - 可空值类型】 + /// int? 是可空的整型: + /// - 可以是 null(表示未计算得分) + /// - 可以是 int 值 + /// + /// + public int? Score { get; set; } + + /// + /// 更新时间。 + /// + /// 【业务说明】 + /// 记录的最后更新时间。 + /// 用于搜索结果展示"最近更新"。 + /// + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 路由路径。 + /// + /// 【业务说明】 + /// 前端跳转的路由路径。 + /// 例如:/product/detail + /// + /// + public string Route { get; set; } + + /// + /// 路由查询参数(JSON 格式)。 + /// + /// 【业务说明】 + /// 路由的查询参数,JSON 格式: + /// 例如:{"id": "123", "type": "product"} + /// + /// + public string RouteQueryJson { get; set; } + + /// + /// 编辑路由。 + /// + /// 【业务说明】 + /// 管理后台的编辑页面路由。 + /// 用于从搜索结果直接跳转到编辑页面。 + /// + /// + public string EditRoute { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs new file mode 100644 index 0000000..85f42dd --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs @@ -0,0 +1,156 @@ +// ============================================================================ +// 【文件说明】SearchResultDTO.cs - 搜索结果项 DTO +// ============================================================================ +// 这是搜索结果列表中的单项数据结构。 +// +// 【设计说明】 +// 这是面向前端的搜索结果格式: +// - 不直接返回数据库实体 +// - 只包含前端需要的字段 +// - 字段经过处理(如 Snippet 是截断后的摘要) +// +// 【与 SearchRawRecord 的区别】 +// - SearchRawRecord:旧版搜索的原始数据,字段多且可能为空 +// - SearchResultDTO:新版搜索的结果数据,字段精简且友好 +// +// 【与 Java Spring Boot 的对比】 +// Java 若依的搜索结果 VO: +// public class SearchResultVO { +// private String sourceType; +// private String title; +// private String snippet; +// // getter/setter... +// } +// +// C# 用自动属性,代码更简洁。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索结果项。 +/// +/// 【业务场景】 +/// 前端搜索结果列表中的单个条目: +/// - 展示标题、摘要 +/// - 点击跳转到详情页 +/// - 显示来源类型图标 +/// +/// +/// 【设计模式 - DTO vs Entity】 +/// 为什么不直接返回数据库实体? +/// 1. 安全性:实体可能包含敏感字段 +/// 2. 灵活性:DTO 可以组合多个实体的字段 +/// 3. 性能:DTO 只返回需要的字段,减少数据传输 +/// 4. 解耦:数据库结构变化不影响 API 契约 +/// +/// +public class SearchResultDTO +{ + /// + /// 来源类型。 + /// + /// 【允许值】 + /// - menu:菜单 + /// - web:页面 + /// - web1:页面1 + /// - document:文档 + /// - configType:配置类型 + /// + /// 【前端用途】 + /// 用于显示不同类型的结果图标。 + /// + /// + public string SourceType { get; set; } + + /// + /// 标题。 + /// + /// 【业务说明】 + /// 搜索结果展示的主要文本。 + /// 用于搜索结果列表的标题显示。 + /// + /// + public string Title { get; set; } + + /// + /// 内容摘要。 + /// + /// 【业务说明】 + /// 从 Content 中截取的摘要: + /// - 包含搜索关键词的上下文 + /// - 长度有限,适合列表展示 + /// - 可能包含高亮标记 + /// + /// + public string Snippet { get; set; } + + /// + /// 相关性得分。 + /// + /// 【业务说明】 + /// 用于搜索结果排序: + /// - 分数越高,排名越靠前 + /// - 前端可以显示"相关度"提示 + /// + /// + /// 【C# 语法知识点 - 可空值类型】 + /// int? 是可空的整型: + /// - 可以是 null(表示未计算得分) + /// - 可以是 int 值 + /// + /// + public int? Score { get; set; } + + /// + /// 路由路径。 + /// + /// 【业务说明】 + /// 前端跳转的路由路径。 + /// 例如:/product/detail + /// + /// + public string Route { get; set; } + + /// + /// 路由查询参数(字典格式)。 + /// + /// 【业务说明】 + /// 路由的查询参数,已解析为字典: + /// - Key:参数名 + /// - Value:参数值(可能是字符串、数字等) + /// + /// 例如:{"id": "123", "type": "product"} + /// + /// + /// 【C# 语法知识点 - Dictionary<K,V> 字典】 + /// Dictionary<string, object> 是键值对集合: + /// - Key:string 类型 + /// - Value:object 类型(可以存储任意类型) + /// + /// 对比 Java: + /// Java: Map<String, Object> routeQuery = new HashMap<>(); + /// + /// 为什么用 object 类型? + /// - 参数值可能是字符串、数字、布尔值等 + /// - object 是所有类型的基类,可以存储任意值 + /// + /// + /// 【C# 语法知识点 - 集合初始化器】 + /// = new() 是属性初始化器: + /// - 创建对象时自动初始化字典 + /// - 避免 null 引用异常 + /// + /// + public Dictionary RouteQuery { get; set; } = new(); + + /// + /// 编辑路由。 + /// + /// 【业务说明】 + /// 管理后台的编辑页面路由。 + /// 用于从搜索结果直接跳转到编辑页面。 + /// + /// + public string EditRoute { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SecureDocumentRequest.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SecureDocumentRequest.cs new file mode 100644 index 0000000..f65092d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SecureDocumentRequest.cs @@ -0,0 +1,84 @@ +// ============================================================================ +// 【文件说明】SecureDocumentRequest.cs - 安全文档访问请求 DTO +// ============================================================================ +// 这是访问受保护文档的请求数据结构。 +// +// 【业务背景】 +// 某些文档需要密码保护: +// - 机密文档:需要输入密码才能查看 +// - 内部文档:限制访问权限 +// - 临时分享:设置访问密码 +// +// 【安全设计】 +// - 不直接在 URL 中传递密码(会被日志记录) +// - 通过请求体传递密码 +// - 服务端验证密码后返回文档内容 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用 @RequestBody 接收: +// @PostMapping("/secure-document") +// public Result access(@RequestBody SecureDocumentRequest request) { +// ... +// } +// +// C# 在 Admin.NET 中直接用参数接收,框架自动绑定。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 安全文档访问请求。 +/// +/// 【业务场景】 +/// 用户访问受密码保护的文档时: +/// 1. 前端弹出密码输入框 +/// 2. 用户输入密码 +/// 3. 前端发送此请求 +/// 4. 服务端验证密码,返回文档内容 +/// +/// +/// 【安全说明】 +/// - 密码不存储在前端 +/// - 每次访问都需要验证 +/// - 密码错误返回 403 错误 +/// +/// +public class SecureDocumentRequest +{ + /// + /// 文档 ID。 + /// + /// 【业务说明】 + /// 要访问的文档的唯一标识。 + /// 用于查询文档信息和验证密码。 + /// + /// + public string DocumentId { get; set; } + + /// + /// 用户提供的密码。 + /// + /// 【业务说明】 + /// 用户输入的访问密码。 + /// 与文档存储的密码进行比对。 + /// + /// + /// 【安全最佳实践】 + /// - 密码不明文存储,存储哈希值 + /// - 传输使用 HTTPS 加密 + /// - 验证失败不返回具体错误原因 + /// + /// + /// 【C# 语法知识点 - 字符串属性】 + /// string 是 C# 的字符串类型: + /// - 引用类型,但不可变 + /// - 对比 Java:Java 的 String 也是不可变引用类型 + /// + /// 为什么用 string 而不是 SecureString? + /// - SecureString 是安全字符串,但使用复杂 + /// - 对于 Web API,HTTPS 已加密传输 + /// - string 更简单,适合大多数场景 + /// + /// + public string ProvidedKey { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/TreeSelect.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/TreeSelect.cs new file mode 100644 index 0000000..fbef16f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/TreeSelect.cs @@ -0,0 +1,146 @@ +// ============================================================================ +// 【文件说明】TreeSelect.cs - 树形选择器 DTO +// ============================================================================ +// 这是一个用于前端树形选择器的数据传输对象。 +// +// 【业务场景】 +// 前端需要展示树形选择器(如配置类型选择),数据格式要求: +// { +// "id": 1, +// "label": "产品配置", +// "children": [ +// { "id": 11, "label": "硬件产品", "children": [] }, +// { "id": 12, "label": "软件产品", "children": [] } +// ] +// } +// +// 这个类就是把数据库的树形实体转换成前端需要的格式。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 树形选择器 DTO。 +/// +/// 【用途说明】 +/// 用于前端树形选择器组件,如: +/// - Element UI 的 el-tree-select +/// - Ant Design 的 TreeSelect +/// +/// 标准的树形选择器数据格式: +/// - id:节点唯一标识 +/// - label:节点显示文本 +/// - children:子节点列表 +/// +/// +/// 【与 Java 若依的对比】 +/// 若依框架也有类似的 TreeSelect 类: +/// public class TreeSelect { +/// private Long id; +/// private String label; +/// private List<TreeSelect> children; +/// } +/// +/// 完全相同的设计,因为这是前端组件的标准数据格式。 +/// +/// +public class TreeSelect +{ + /// + /// 默认构造函数。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// public TreeSelect() { } + /// + /// 无参构造函数用于: + /// 1. 反序列化时创建对象(JSON 反序列化需要无参构造函数) + /// 2. 创建空对象后手动赋值 + /// + /// + public TreeSelect() + { + } + + /// + /// 从配置类型实体创建树形选择器节点。 + /// + /// 【C# 语法知识点 - 转换构造函数】 + /// public TreeSelect(HwPortalConfigType portalConfigType) + /// + /// 这是一个"转换构造函数",用于从一种类型转换成另一种类型。 + /// + /// 对比 Java: + /// Java 通常用静态工厂方法或工具类: + /// public static TreeSelect from(HwPortalConfigType entity) { + /// TreeSelect select = new TreeSelect(); + /// select.setId(entity.getConfigTypeId()); + /// select.setLabel(entity.getConfigTypeName()); + /// ... + /// return select; + /// } + /// + /// C# 的转换构造函数更直观,使用更方便: + /// TreeSelect select = new TreeSelect(configType); + /// + /// + /// 【递归构建子节点】 + /// Children = portalConfigType.Children.Select(u => new TreeSelect(u)).ToList() + /// + /// 这是一行递归代码: + /// 1. portalConfigType.Children 获取子节点列表 + /// 2. .Select(u => new TreeSelect(u)) 对每个子节点创建 TreeSelect + /// 3. 递归:创建子节点时,子节点又会创建孙节点... + /// + /// 对比 Java: + /// Java 需要手写递归: + /// private void buildChildren(HwPortalConfigType entity, TreeSelect select) { + /// if (entity.getChildren() != null && !entity.getChildren().isEmpty()) { + /// select.setChildren(entity.getChildren().stream() + /// .map(child -> { + /// TreeSelect childSelect = new TreeSelect(child); + /// buildChildren(child, childSelect); + /// return childSelect; + /// }) + /// .collect(Collectors.toList())); + /// } + /// } + /// + /// C# 的 LINQ + 转换构造函数让递归变得非常简洁。 + /// + /// + /// 配置类型实体 + public TreeSelect(HwPortalConfigType portalConfigType) + { + // Id 和 Label 是树形选择器的标准字段。 + Id = portalConfigType.ConfigTypeId; + Label = portalConfigType.ConfigTypeName; + + // 【递归构建子节点】 + // 对每个子节点递归调用构造函数,自动构建整个子树。 + Children = portalConfigType.Children.Select(u => new TreeSelect(u)).ToList(); + } + + /// + /// 节点唯一标识。 + /// + public long? Id { get; set; } + + /// + /// 节点显示文本。 + /// + public string Label { get; set; } + + /// + /// 子节点列表。 + /// + /// 【集合初始化】 + /// = new() 是 C# 9.0 的"目标类型 new"语法。 + /// 等价于 = new List<TreeSelect>()。 + /// + /// 为什么初始化为空列表? + /// 1. 避免 null 引用异常 + /// 2. 叶子节点不需要特殊处理(Children.Count = 0) + /// + /// + public List Children { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfo.cs new file mode 100644 index 0000000..4dbefe7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfo.cs @@ -0,0 +1,29 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_about_us_info")] +public class HwAboutUsInfo : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "about_us_info_id", IsPrimaryKey = true, IsIdentity = true)] + public long? AboutUsInfoId { get; set; } + + [SugarColumn(ColumnName = "about_us_info_type")] + public string AboutUsInfoType { get; set; } + + [SugarColumn(ColumnName = "about_us_info_etitle")] + public string AboutUsInfoEtitle { get; set; } + + [SugarColumn(ColumnName = "about_us_info_title")] + public string AboutUsInfoTitle { get; set; } + + [SugarColumn(ColumnName = "about_us_info_desc")] + public string AboutUsInfoDesc { get; set; } + + [SugarColumn(ColumnName = "about_us_info_order")] + public long? AboutUsInfoOrder { get; set; } + + [SugarColumn(ColumnName = "display_modal")] + public string DisplayModal { get; set; } + + [SugarColumn(ColumnName = "about_us_info_pic")] + public string AboutUsInfoPic { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfoDetail.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfoDetail.cs new file mode 100644 index 0000000..6759a74 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwAboutUsInfoDetail.cs @@ -0,0 +1,23 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_about_us_info_detail")] +public class HwAboutUsInfoDetail : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "us_info_detail_id", IsPrimaryKey = true, IsIdentity = true)] + public long? UsInfoDetailId { get; set; } + + [SugarColumn(ColumnName = "about_us_info_id")] + public long? AboutUsInfoId { get; set; } + + [SugarColumn(ColumnName = "us_info_detail_title")] + public string UsInfoDetailTitle { get; set; } + + [SugarColumn(ColumnName = "us_info_detail_desc")] + public string UsInfoDetailDesc { get; set; } + + [SugarColumn(ColumnName = "us_info_detail_order")] + public long? UsInfoDetailOrder { get; set; } + + [SugarColumn(ColumnName = "us_info_detail_pic")] + public string UsInfoDetailPic { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwContactUsInfo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwContactUsInfo.cs new file mode 100644 index 0000000..ad3706e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwContactUsInfo.cs @@ -0,0 +1,20 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_contact_us_info")] +public class HwContactUsInfo : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "contact_us_info_id", IsPrimaryKey = true, IsIdentity = true)] + public long? ContactUsInfoId { get; set; } + + [SugarColumn(ColumnName = "user_name")] + public string UserName { get; set; } + + [SugarColumn(ColumnName = "user_email")] + public string UserEmail { get; set; } + + [SugarColumn(ColumnName = "user_phone")] + public string UserPhone { get; set; } + + [SugarColumn(ColumnName = "user_ip")] + public string UserIp { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfig.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfig.cs new file mode 100644 index 0000000..ea04897 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfig.cs @@ -0,0 +1,172 @@ +// ============================================================================ +// 【文件说明】HwPortalConfig.cs - 门户配置实体类 +// ============================================================================ +// 这个实体类对应数据库表 hw_portal_config,用于存储门户的配置信息。 +// +// 【业务场景】 +// 门户配置是官网的核心配置数据,包括: +// - 首页轮播图配置 +// - 导航菜单配置 +// - 页面区块配置 +// - 按钮和路由配置 +// +// 这个表支持树形结构(parent_id + ancestors),可以形成层级配置。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户配置实体类。 +/// +/// 【业务说明】 +/// 门户配置用于管理官网的各种配置项,支持: +/// 1. 树形结构:parent_id + ancestors 实现层级配置 +/// 2. 分类管理:portal_config_type 区分不同类型的配置 +/// 3. 排序控制:portal_config_order 控制展示顺序 +/// 4. 路由配置:router_address 配置跳转地址 +/// +/// +[SugarTable("hw_portal_config")] +public class HwPortalConfig : HwPortalBaseEntity +{ + /// + /// 配置主键ID。 + /// + [SugarColumn(ColumnName = "portal_config_id", IsPrimaryKey = true, IsIdentity = true)] + public long? PortalConfigId { get; set; } + + /// + /// 配置类型。 + /// + /// 【业务说明】 + /// 用于区分不同类型的配置,如: + /// - "1": 首页配置 + /// - "2": 产品配置 + /// - "3": 案例配置 + /// + /// + [SugarColumn(ColumnName = "portal_config_type")] + public string PortalConfigType { get; set; } + + /// + /// 配置类型ID。 + /// + /// 【业务说明】 + /// 关联 hw_portal_config_type 表的主键。 + /// + /// + [SugarColumn(ColumnName = "portal_config_type_id")] + public long? PortalConfigTypeId { get; set; } + + /// + /// 配置标题。 + /// + [SugarColumn(ColumnName = "portal_config_title")] + public string PortalConfigTitle { get; set; } + + /// + /// 排序号。 + /// + [SugarColumn(ColumnName = "portal_config_order")] + public long? PortalConfigOrder { get; set; } + + /// + /// 配置描述。 + /// + [SugarColumn(ColumnName = "portal_config_desc")] + public string PortalConfigDesc { get; set; } + + /// + /// 按钮名称。 + /// + /// 【业务说明】 + /// 如果这个配置项需要展示按钮,这里配置按钮的文字。 + /// 例如:"了解更多"、"立即咨询"。 + /// + /// + [SugarColumn(ColumnName = "button_name")] + public string ButtonName { get; set; } + + /// + /// 路由地址。 + /// + /// 【业务说明】 + /// 点击配置项后跳转的路由地址。 + /// 例如:"/product/detail"、"http://external.com"。 + /// + /// + [SugarColumn(ColumnName = "router_address")] + public string RouterAddress { get; set; } + + /// + /// 配置图片。 + /// + /// 【业务说明】 + /// 配置项的图片URL,用于轮播图、缩略图等。 + /// + /// + [SugarColumn(ColumnName = "portal_config_pic")] + public string PortalConfigPic { get; set; } + + /// + /// 配置类型名称(冗余字段)。 + /// + [SugarColumn(ColumnName = "config_type_name")] + public string ConfigTypeName { get; set; } + + /// + /// 首页配置类型图片。 + /// + [SugarColumn(ColumnName = "home_config_type_pic")] + public string HomeConfigTypePic { get; set; } + + /// + /// 首页配置类型图标。 + /// + [SugarColumn(ColumnName = "config_type_icon")] + public string HomeConfigTypeIcon { get; set; } + + /// + /// 首页配置类型名称。 + /// + [SugarColumn(ColumnName = "home_config_type_name")] + public string HomeConfigTypeName { get; set; } + + /// + /// 首页配置类型分类。 + /// + [SugarColumn(ColumnName = "config_type_classfication")] + public string HomeConfigTypeClassfication { get; set; } + + /// + /// 父配置ID。 + /// + /// 【树形结构说明】 + /// 用于构建层级配置结构: + /// - parent_id 为 null 或 0:顶级配置 + /// - parent_id 有值:子配置 + /// + /// + [SugarColumn(ColumnName = "parent_id")] + public long? ParentId { get; set; } + + /// + /// 祖级列表。 + /// + /// 【树形结构说明】 + /// ancestors 存储从根节点到当前节点的完整路径,用逗号分隔。 + /// 例如:"0,100,101" 表示:根节点(0) -> 一级节点(100) -> 当前节点(101) + /// + /// 【为什么需要 ancestors?】 + /// 1. 快速查询所有子节点:WHERE ancestors LIKE '0,100,%' + /// 2. 快速查询所有父节点:按 ancestors 中的 ID 逐级查询 + /// 3. 避免递归查询:不需要递归就能获取层级关系 + /// + /// 对比 Java 若依: + /// 若依的菜单、部门等树形结构也使用 ancestors 字段。 + /// 这是若依框架的标准设计模式。 + /// + /// + [SugarColumn(ColumnName = "ancestors")] + public string Ancestors { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfigType.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfigType.cs new file mode 100644 index 0000000..8eb17a1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwPortalConfigType.cs @@ -0,0 +1,38 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_portal_config_type")] +public class HwPortalConfigType : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "config_type_id", IsPrimaryKey = true, IsIdentity = true)] + public long? ConfigTypeId { get; set; } + + [SugarColumn(ColumnName = "config_type_classfication")] + public string ConfigTypeClassfication { get; set; } + + [SugarColumn(ColumnName = "config_type_name")] + public string ConfigTypeName { get; set; } + + [SugarColumn(ColumnName = "home_config_type_name")] + public string HomeConfigTypeName { get; set; } + + [SugarColumn(ColumnName = "config_type_desc")] + public string ConfigTypeDesc { get; set; } + + [SugarColumn(ColumnName = "config_type_icon")] + public string ConfigTypeIcon { get; set; } + + [SugarColumn(ColumnName = "home_config_type_pic")] + public string HomeConfigTypePic { get; set; } + + [SugarColumn(ColumnName = "parent_id")] + public long? ParentId { get; set; } + + [SugarColumn(ColumnName = "ancestors")] + public string Ancestors { get; set; } + + [SugarColumn(IsIgnore = true)] + public List HwProductCaseInfoList { get; set; } = new(); + + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductCaseInfo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductCaseInfo.cs new file mode 100644 index 0000000..cf5e5b2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductCaseInfo.cs @@ -0,0 +1,29 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_product_case_info")] +public class HwProductCaseInfo : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "case_info_id", IsPrimaryKey = true, IsIdentity = true)] + public long? CaseInfoId { get; set; } + + [SugarColumn(ColumnName = "case_info_title")] + public string CaseInfoTitle { get; set; } + + [SugarColumn(ColumnName = "config_type_id")] + public string ConfigTypeId { get; set; } + + [SugarColumn(ColumnName = "typical_flag")] + public string TypicalFlag { get; set; } + + [SugarColumn(ColumnName = "case_info_desc")] + public string CaseInfoDesc { get; set; } + + [SugarColumn(ColumnName = "case_info_pic")] + public string CaseInfoPic { get; set; } + + [SugarColumn(ColumnName = "case_info_html")] + public string CaseInfoHtml { get; set; } + + [SugarColumn(IsIgnore = true)] + public string HomeTypicalFlag { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfo.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfo.cs new file mode 100644 index 0000000..0628b30 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfo.cs @@ -0,0 +1,174 @@ +// ============================================================================ +// 【文件说明】HwProductInfo.cs - 产品信息实体类 +// ============================================================================ +// 这个实体类对应数据库表 hw_product_info,用于存储产品信息。 +// +// 【业务场景】 +// 产品信息是官网的核心数据之一,用于展示公司的产品/服务。 +// 产品可以有层级关系(父子产品),支持多种配置模式。 +// +// 【数据库表结构】 +// hw_product_info 表字段: +// - product_info_id: 主键ID(自增) +// - config_type_id: 配置类型ID +// - tab_flag: 标签标记 +// - config_modal: 配置模式 +// - product_info_etitle: 英文标题 +// - product_info_ctitle: 中文标题 +// - product_info_order: 排序号 +// - parent_id: 父产品ID(支持层级) +// - config_type_name: 配置类型名称 +// + 继承自基类的公共字段 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品信息实体类。 +/// +/// 【业务说明】 +/// 产品信息用于官网展示,支持: +/// 1. 层级结构:parent_id 关联父产品,形成树形结构 +/// 2. 多种配置模式:config_modal 区分不同的展示方式 +/// 3. 排序:product_info_order 控制展示顺序 +/// +/// +[SugarTable("hw_product_info")] +public class HwProductInfo : HwPortalBaseEntity +{ + /// + /// 产品主键ID。 + /// + [SugarColumn(ColumnName = "product_info_id", IsPrimaryKey = true, IsIdentity = true)] + public long? ProductInfoId { get; set; } + + /// + /// 配置类型ID。 + /// + /// 【业务说明】 + /// 关联 hw_portal_config_type 表,用于分类产品。 + /// 例如:硬件产品、软件产品、服务等。 + /// + /// + [SugarColumn(ColumnName = "config_type_id")] + public string ConfigTypeId { get; set; } + + /// + /// 标签标记。 + /// + /// 【业务说明】 + /// 用于标记产品的特殊属性,如"热门"、"新品"、"推荐"等。 + /// + /// + [SugarColumn(ColumnName = "tab_flag")] + public string TabFlag { get; set; } + + /// + /// 配置模式。 + /// + /// 【业务说明】 + /// 不同的配置模式决定产品的展示方式: + /// - "1": 模式1(列表展示) + /// - "2": 模式2(卡片展示) + /// - "13": 树形展示(支持层级结构) + /// + /// 这个字段让前端可以根据模式选择不同的渲染组件。 + /// + /// + [SugarColumn(ColumnName = "config_modal")] + public string ConfigModal { get; set; } + + /// + /// 产品英文标题。 + /// + [SugarColumn(ColumnName = "product_info_etitle")] + public string ProductInfoEtitle { get; set; } + + /// + /// 产品中文标题。 + /// + [SugarColumn(ColumnName = "product_info_ctitle")] + public string ProductInfoCtitle { get; set; } + + /// + /// 排序号。 + /// + /// 【业务说明】 + /// 数值越小越靠前,用于控制产品在列表中的展示顺序。 + /// + /// + [SugarColumn(ColumnName = "product_info_order")] + public long? ProductInfoOrder { get; set; } + + /// + /// 父产品ID。 + /// + /// 【树形结构说明】 + /// - parent_id 为 null 或 0:顶级产品 + /// - parent_id 有值:子产品,关联父产品 + /// + /// 这样可以形成层级结构: + /// 产品A (parent_id = null) + /// ├── 产品A-1 (parent_id = A的ID) + /// └── 产品A-2 (parent_id = A的ID) + /// + /// 对比 Java 若依: + /// 若依的菜单、部门等也是类似的树形结构设计。 + /// + /// + [SugarColumn(ColumnName = "parent_id")] + public long? ParentId { get; set; } + + /// + /// 配置类型名称(冗余字段)。 + /// + /// 【冗余字段说明】 + /// 这个字段存储 config_type_name,虽然可以通过 config_type_id 关联查询得到, + /// 但直接存储可以减少 JOIN 查询,提高查询性能。 + /// + /// 这是"空间换时间"的设计权衡: + /// - 优点:查询快,不需要 JOIN + /// - 缺点:需要维护数据一致性 + /// + /// 对比 Java: + /// Java 项目中也常见这种冗余字段设计,特别是在高并发场景。 + /// + /// + [SugarColumn(ColumnName = "config_type_name")] + public string ConfigTypeName { get; set; } + + /// + /// 产品明细列表(导航属性)。 + /// + /// 【C# 语法知识点 - 导航属性】 + /// 导航属性是 ORM 中的概念,用于表示实体之间的关联关系。 + /// 这里的 HwProductInfoDetailList 表示"一个产品有多个明细"。 + /// + /// 【SugarColumn(IsIgnore = true) 说明】 + /// IsIgnore = true 表示这个属性不映射到数据库列。 + /// 它是一个"计算属性",由业务代码填充,而不是从数据库读取。 + /// + /// 对比 Java JPA: + /// JPA 的导航属性: + /// @OneToMany(mappedBy = "productInfo", cascade = CascadeType.ALL) + /// private List<HwProductInfoDetail> details = new ArrayList<>(); + /// + /// JPA 会自动加载关联数据(懒加载或急加载), + /// 但 SqlSugar 这里需要手动填充,更灵活但需要更多代码。 + /// + /// + [SugarColumn(IsIgnore = true)] + // 【C# 语法知识点 - 集合初始化器】 + // = new() 是 C# 9.0 的"目标类型 new"语法。 + // 编译器会根据类型推断自动创建 List<HwProductInfoDetail>()。 + // + // 对比 Java: + // Java 需要写完整: + // private List<HwProductInfoDetail> details = new ArrayList<>(); + // + // 为什么初始化为空列表而不是 null? + // 1. 避免 NullReferenceException + // 2. 可以直接遍历:foreach (var item in list) 不需要判空 + // 3. 可以直接添加:list.Add(item) 不需要先 new + public List HwProductInfoDetailList { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfoDetail.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfoDetail.cs new file mode 100644 index 0000000..287c50e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwProductInfoDetail.cs @@ -0,0 +1,41 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_product_info_detail")] +public class HwProductInfoDetail : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "product_info_detail_id", IsPrimaryKey = true, IsIdentity = true)] + public long? ProductInfoDetailId { get; set; } + + [SugarColumn(ColumnName = "parent_id")] + public long? ParentId { get; set; } + + [SugarColumn(ColumnName = "product_info_id")] + public long? ProductInfoId { get; set; } + + [SugarColumn(ColumnName = "config_modal")] + public string ConfigModal { get; set; } + + [SugarColumn(ColumnName = "config_model")] + public string ConfigModel { get; set; } + + [SugarColumn(ColumnName = "product_info_detail_title")] + public string ProductInfoDetailTitle { get; set; } + + [SugarColumn(ColumnName = "product_info_detail_desc")] + public string ProductInfoDetailDesc { get; set; } + + [SugarColumn(ColumnName = "product_info_detail_order")] + public long? ProductInfoDetailOrder { get; set; } + + [SugarColumn(ColumnName = "product_info_detail_pic")] + public string ProductInfoDetailPic { get; set; } + + [SugarColumn(ColumnName = "ancestors")] + public string Ancestors { get; set; } + + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); + + [SugarColumn(IsIgnore = true)] + public List HwProductInfoDetailList { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb.cs new file mode 100644 index 0000000..b94ee6a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb.cs @@ -0,0 +1,143 @@ +// ============================================================================ +// 【文件说明】HwWeb.cs - 官网页面实体类 +// ============================================================================ +// 这个实体类对应数据库表 hw_web,用于存储官网页面配置信息。 +// +// 【数据库表结构】 +// hw_web 表字段: +// - web_id: 主键ID(自增) +// - web_json: JSON格式的页面配置 +// - web_json_string: JSON字符串格式 +// - web_code: 页面编码(业务主键) +// - is_delete: 逻辑删除标记 +// - web_json_english: 英文版JSON配置 +// - create_by, create_time, update_by, update_time, remark: 继承自基类 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 官网页面实体类。 +/// +/// 【C# 语法知识点 - 类继承】 +/// public class HwWeb : HwPortalBaseEntity +/// HwWeb 继承自 HwPortalBaseEntity,自动获得 CreateBy、CreateTime 等公共字段。 +/// +/// 对比 Java: +/// Java 的继承语法完全一样: +/// public class HwWeb extends HwPortalBaseEntity { ... } +/// +/// +/// 【ORM 映射说明】 +/// [SugarTable("hw_web")] 告诉 SqlSugar ORM:这个类映射到数据库的 hw_web 表。 +/// +/// 对比 Java MyBatis/JPA: +/// JPA: @Table(name = "hw_web") +/// MyBatis: 在 XML 中配置 <table tableName="hw_web"> +/// +/// +[SugarTable("hw_web")] +// 【C# 语法知识点 - 特性(Attribute)】 +// [SugarTable("hw_web")] 是 SqlSugar ORM 的特性,用于指定数据库表名。 +// 如果不写,SqlSugar 会默认用类名(HwWeb)作为表名。 +// 但数据库表名通常是 snake_case(hw_web),所以需要显式指定。 +public class HwWeb : HwPortalBaseEntity +{ + /// + /// 页面主键ID。 + /// + /// 【C# 语法知识点 - 可空值类型 long?】 + /// long? 表示"可空的长整型",等价于 Nullable<long>。 + /// + /// 为什么用可空类型? + /// 1. 新增记录时,ID 还没生成(数据库自增),此时是 null + /// 2. 查询时,某些关联查询可能返回 null + /// 3. 和前端交互时,空值更明确 + /// + /// 对比 Java: + /// Java 的 Long 本身就是引用类型,可以为 null: + /// private Long webId; + /// + /// C# 的 long 是值类型,不能为 null,所以用 long? 来支持 null。 + /// + /// + [SugarColumn(ColumnName = "web_id", IsPrimaryKey = true, IsIdentity = true)] + // 【SugarColumn 特性参数说明】 + // ColumnName = "web_id":映射到数据库列 web_id + // IsPrimaryKey = true:标记为主键 + // IsIdentity = true:标记为自增列(数据库自动生成值) + // + // 对比 Java JPA: + // @Id + // @GeneratedValue(strategy = GenerationType.IDENTITY) + // @Column(name = "web_id") + // private Long webId; + public long? WebId { get; set; } + + /// + /// JSON格式的页面配置。 + /// + /// 【业务说明】 + /// 这个字段存储页面配置的 JSON 数据,前端解析后渲染页面。 + /// 例如:导航菜单、轮播图、内容区块等配置。 + /// + /// + [SugarColumn(ColumnName = "web_json")] + public string WebJson { get; set; } + + /// + /// JSON字符串格式。 + /// + [SugarColumn(ColumnName = "web_json_string")] + public string WebJsonString { get; set; } + + /// + /// 页面编码(业务主键)。 + /// + /// 【业务主键 vs 技术主键】 + /// - 技术主键:web_id,数据库自增,无业务含义 + /// - 业务主键:web_code,有业务含义,如 "home_page"、"about_us" + /// + /// 为什么需要两个主键? + /// 1. 技术主键保证数据库性能(自增整数比字符串索引快) + /// 2. 业务主键方便开发和运维(看到 web_code 就知道是哪个页面) + /// 3. 业务主键可以跨环境保持一致(开发/测试/生产) + /// + /// + [SugarColumn(ColumnName = "web_code")] + public long? WebCode { get; set; } + + /// + /// 逻辑删除标记。 + /// + /// 【逻辑删除 vs 物理删除】 + /// - 物理删除:DELETE FROM table WHERE id = ?,数据真的没了 + /// - 逻辑删除:UPDATE table SET is_delete = '1' WHERE id = ?,数据还在,只是标记为删除 + /// + /// 为什么用逻辑删除? + /// 1. 数据可恢复:误删后可以恢复 + /// 2. 审计需求:保留删除记录 + /// 3. 关联数据:其他表可能引用这条数据 + /// + /// 这里的值: + /// - "0":正常 + /// - "1":已删除 + /// + /// 对比 Java 若依: + /// 若依框架也使用逻辑删除,字段通常是 del_flag。 + /// + /// + [SugarColumn(ColumnName = "is_delete")] + public string IsDelete { get; set; } + + /// + /// 英文版JSON配置。 + /// + /// 【国际化支持】 + /// 这个字段存储英文版页面配置,用于多语言网站。 + /// 用户切换语言时,前端读取不同的 JSON 配置。 + /// + /// + [SugarColumn(ColumnName = "web_json_english")] + public string WebJsonEnglish { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb1.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb1.cs new file mode 100644 index 0000000..4fd6f2b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWeb1.cs @@ -0,0 +1,29 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web1")] +public class HwWeb1 : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "web_id", IsPrimaryKey = true, IsIdentity = true)] + public long? WebId { get; set; } + + [SugarColumn(ColumnName = "web_json")] + public string WebJson { get; set; } + + [SugarColumn(ColumnName = "web_json_string")] + public string WebJsonString { get; set; } + + [SugarColumn(ColumnName = "web_code")] + public long? WebCode { get; set; } + + [SugarColumn(ColumnName = "device_id")] + public long? DeviceId { get; set; } + + [SugarColumn(ColumnName = "typeId")] + public long? TypeId { get; set; } + + [SugarColumn(ColumnName = "is_delete")] + public string IsDelete { get; set; } + + [SugarColumn(ColumnName = "web_json_english")] + public string WebJsonEnglish { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebDocument.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebDocument.cs new file mode 100644 index 0000000..d8cd5e8 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebDocument.cs @@ -0,0 +1,32 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web_document")] +public class HwWebDocument : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "document_id", IsPrimaryKey = true)] + public string DocumentId { get; set; } + + [SugarColumn(ColumnName = "tenant_id")] + public long? TenantId { get; set; } + + [SugarColumn(ColumnName = "document_address")] + public string DocumentAddress { get; set; } + + [SugarColumn(ColumnName = "web_code")] + public string WebCode { get; set; } + + [SugarColumn(ColumnName = "secretKey")] + public string SecretKey { get; set; } + + [SugarColumn(ColumnName = "json")] + public string Json { get; set; } + + [SugarColumn(ColumnName = "type")] + public string Type { get; set; } + + [SugarColumn(ColumnName = "is_delete")] + public string IsDelete { get; set; } + + [SugarColumn(IsIgnore = true)] + public bool HasSecret => !string.IsNullOrWhiteSpace(SecretKey); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu.cs new file mode 100644 index 0000000..8e63bf0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu.cs @@ -0,0 +1,41 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web_menu")] +public class HwWebMenu : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "web_menu_id", IsPrimaryKey = true)] + public long? WebMenuId { get; set; } + + [SugarColumn(ColumnName = "parent")] + public long? Parent { get; set; } + + [SugarColumn(ColumnName = "ancestors")] + public string Ancestors { get; set; } + + [SugarColumn(ColumnName = "status")] + public string Status { get; set; } + + [SugarColumn(ColumnName = "web_menu_name")] + public string WebMenuName { get; set; } + + [SugarColumn(ColumnName = "tenant_id")] + public long? TenantId { get; set; } + + [SugarColumn(ColumnName = "web_menu__pic")] + public string WebMenuPic { get; set; } + + [SugarColumn(ColumnName = "web_menu_type")] + public long? WebMenuType { get; set; } + + [SugarColumn(ColumnName = "order")] + public long? Order { get; set; } + + [SugarColumn(ColumnName = "is_delete")] + public string IsDelete { get; set; } + + [SugarColumn(ColumnName = "web_menu_name_english")] + public string WebMenuNameEnglish { get; set; } + + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu1.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu1.cs new file mode 100644 index 0000000..3bbb602 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebMenu1.cs @@ -0,0 +1,41 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web_menu1")] +public class HwWebMenu1 : HwPortalBaseEntity +{ + [SugarColumn(ColumnName = "web_menu_id", IsPrimaryKey = true)] + public long? WebMenuId { get; set; } + + [SugarColumn(ColumnName = "parent")] + public long? Parent { get; set; } + + [SugarColumn(ColumnName = "ancestors")] + public string Ancestors { get; set; } + + [SugarColumn(ColumnName = "status")] + public string Status { get; set; } + + [SugarColumn(ColumnName = "web_menu_name")] + public string WebMenuName { get; set; } + + [SugarColumn(ColumnName = "tenant_id")] + public long? TenantId { get; set; } + + [SugarColumn(ColumnName = "web_menu__pic")] + public string WebMenuPic { get; set; } + + [SugarColumn(ColumnName = "web_menu_type")] + public long? WebMenuType { get; set; } + + [SugarColumn(ColumnName = "value")] + public string Value { get; set; } + + [SugarColumn(ColumnName = "is_delete")] + public string IsDelete { get; set; } + + [SugarColumn(ColumnName = "web_menu_name_english")] + public string WebMenuNameEnglish { get; set; } + + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitDaily.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitDaily.cs new file mode 100644 index 0000000..8067b69 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitDaily.cs @@ -0,0 +1,35 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web_visit_daily")] +public class HwWebVisitDaily +{ + [SugarColumn(ColumnName = "stat_date", IsPrimaryKey = true)] + public DateTime? StatDate { get; set; } + + [SugarColumn(ColumnName = "pv")] + public long? Pv { get; set; } + + [SugarColumn(ColumnName = "uv")] + public long? Uv { get; set; } + + [SugarColumn(ColumnName = "ip_uv")] + public long? IpUv { get; set; } + + [SugarColumn(ColumnName = "avg_stay_ms")] + public long? AvgStayMs { get; set; } + + [SugarColumn(ColumnName = "bounce_rate")] + public double? BounceRate { get; set; } + + [SugarColumn(ColumnName = "search_count")] + public long? SearchCount { get; set; } + + [SugarColumn(ColumnName = "download_count")] + public long? DownloadCount { get; set; } + + [SugarColumn(ColumnName = "created_at")] + public DateTime? CreatedAt { get; set; } + + [SugarColumn(ColumnName = "updated_at")] + public DateTime? UpdatedAt { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitEvent.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitEvent.cs new file mode 100644 index 0000000..04992e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Entity/HwWebVisitEvent.cs @@ -0,0 +1,59 @@ +namespace Admin.NET.Plugin.HwPortal; + +[SugarTable("hw_web_visit_event")] +public class HwWebVisitEvent +{ + [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)] + public long? Id { get; set; } + + [SugarColumn(ColumnName = "event_type")] + public string EventType { get; set; } + + [SugarColumn(ColumnName = "visitor_id")] + public string VisitorId { get; set; } + + [SugarColumn(ColumnName = "session_id")] + public string SessionId { get; set; } + + [SugarColumn(ColumnName = "path")] + public string Path { get; set; } + + [SugarColumn(ColumnName = "referrer")] + public string Referrer { get; set; } + + [SugarColumn(ColumnName = "utm_source")] + public string UtmSource { get; set; } + + [SugarColumn(ColumnName = "utm_medium")] + public string UtmMedium { get; set; } + + [SugarColumn(ColumnName = "utm_campaign")] + public string UtmCampaign { get; set; } + + [SugarColumn(ColumnName = "keyword")] + public string Keyword { get; set; } + + [SugarColumn(ColumnName = "ip_hash")] + public string IpHash { get; set; } + + [SugarColumn(ColumnName = "ua")] + public string Ua { get; set; } + + [SugarColumn(ColumnName = "device")] + public string Device { get; set; } + + [SugarColumn(ColumnName = "browser")] + public string Browser { get; set; } + + [SugarColumn(ColumnName = "os")] + public string Os { get; set; } + + [SugarColumn(ColumnName = "stay_ms")] + public long? StayMs { get; set; } + + [SugarColumn(ColumnName = "event_time")] + public DateTime? EventTime { get; set; } + + [SugarColumn(ColumnName = "created_at")] + public DateTime? CreatedAt { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/GlobalUsings.cs new file mode 100644 index 0000000..477ffb5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/GlobalUsings.cs @@ -0,0 +1,18 @@ +global using Admin.NET.Core; +global using Furion; +global using Furion.DependencyInjection; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Mvc; +global using SqlSugar; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.ComponentModel; +global using System.ComponentModel.DataAnnotations; +global using System.Globalization; +global using System.Reflection; +global using System.Security.Claims; +global using System.Text; +global using System.Text.Json; +global using System.Text.RegularExpressions; +global using System.Xml; +global using System.Xml.Linq; diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMapperRegistry.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMapperRegistry.cs new file mode 100644 index 0000000..ae3d1a9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMapperRegistry.cs @@ -0,0 +1,449 @@ +// ============================================================================ +// 【文件说明】HwPortalMapperRegistry.cs - MyBatis 风格 XML SQL 解析注册表 +// ============================================================================ +// 这个类是"类 MyBatis"架构的核心组件,负责解析 XML 中定义的 SQL 语句, +// 并将 MyBatis 风格的动态 SQL(如 )转换为可执行的 SQL。 +// +// 【架构定位】 +// 在 Java MyBatis 中: +// XML 文件由 SqlSessionFactoryBuilder 解析,生成 Configuration 对象, +// 包含所有 MappedStatement(映射语句)。 +// +// 在这个 C# 实现中: +// HwPortalMapperRegistry 扮演类似角色: +// - 从嵌入资源加载 XML +// - 解析并缓存 SQL 定义 +// - 提供 Prepare() 方法渲染最终 SQL +// +// 【核心功能】 +// 1. XML 加载:从程序集嵌入资源读取 Mapper XML 文件 +// 2. 延迟初始化:Lazy<T> 模式,第一次使用时才加载 +// 3. 动态 SQL:支持 <if>、<where>、<foreach>、<trim>、<choose> 等标签 +// 4. 参数处理:#{paramName} 占位符替换为 @p0、@p1 等数据库参数 +// +// 【与 Java MyBatis 的对比】 +// Java MyBatis 特性: +// - OGNL 表达式引擎(复杂但功能强大) +// - 完整的动态 SQL 标签支持 +// - 类型处理器(TypeHandler) +// - 结果映射(ResultMap) +// +// 这个 C# 实现: +// - 自定义轻量级表达式解析(EvaluateTest) +// - 支持常用标签的子集(覆盖 90% 业务场景) +// - 直接使用 SqlSugar 的参数和结果映射 +// - 代码量少,容易理解和维护 +// +// 【性能优化点】 +// 1. 正则表达式预编译:RegexOptions.Compiled 提高匹配速度 +// 2. 延迟加载:应用启动时不解析 XML,第一次请求时才加载 +// 3. 缓存策略:Dictionary 缓存解析后的 Mapper 定义 +// 4. 嵌入资源:XML 文件打包进 DLL,避免文件 IO +// +// 【适合场景】 +// - 从 Java 迁移过来的项目,有大量现成的 XML SQL +// - 需要动态 SQL 但不想引入完整 ORM +// - 复杂 SQL 用原生写法比 Lambda 更清晰 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +public sealed class HwPortalMapperRegistry +{ + // 用正则把 MyBatis 风格的 #{name} 占位符替换成数据库参数。 + private static readonly Regex ParameterRegex = new(@"#\{\s*([A-Za-z0-9_]+)\s*\}", RegexOptions.Compiled); + + // MyBatis 标签的一个常见能力是自动去掉开头多余的 and/or。 + // 这里手动模拟这个行为。 + private static readonly Regex LeadingWhereRegex = new(@"^\s*(and|or)\s+", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + // Lazy 表示“延迟初始化”: + // 第一次真正用到 _mapperCache.Value 时,才会执行 LoadMappers。 + // 这样可以减少应用启动时的无效开销。 + private readonly Lazy> _mapperCache; + + public HwPortalMapperRegistry() + { + _mapperCache = new Lazy>(LoadMappers, true); + } + + public HwPortalPreparedSql Prepare(string mapperName, string statementId, object parameter) + { + // TryGetValue 是 Dictionary 的安全取值方式: + // 找到返回 true,没找到返回 false,不会直接抛异常。 + if (!_mapperCache.Value.TryGetValue(mapperName, out HwPortalMapperDefinition mapper)) + { + throw Oops.Oh($"未找到 Mapper:{mapperName}"); + } + + if (!mapper.Statements.TryGetValue(statementId, out XElement statementElement)) + { + throw Oops.Oh($"未找到 SQL 语句:{mapperName}.{statementId}"); + } + + // context 是本次 SQL 渲染过程中的运行时上下文, + // 里面会保存参数对象、foreach 临时变量、最终生成的数据库参数集合。 + HwPortalRenderContext context = new(parameter); + string sql = RenderNodes(statementElement.Nodes(), mapper, context); + return new HwPortalPreparedSql(NormalizeSql(sql), context.Parameters); + } + + private static Dictionary LoadMappers() + { + // 这里不是从磁盘直接读文件,而是从程序集嵌入资源里读取 XML。 + // 对应 csproj 里配置的 。 + Assembly assembly = typeof(HwPortalMapperRegistry).Assembly; + string[] resources = assembly.GetManifestResourceNames() + .Where(name => name.Contains(".Sql.", StringComparison.OrdinalIgnoreCase) && name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + Dictionary cache = new(StringComparer.OrdinalIgnoreCase); + foreach (string resource in resources) + { + // using 声明会在当前作用域结束时自动释放资源。 + using Stream stream = assembly.GetManifestResourceStream(resource); + using XmlReader reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }); + XDocument document = XDocument.Load(reader); + XElement mapperElement = document.Root ?? throw Oops.Oh($"SQL 资源解析失败:{resource}"); + string namespaceValue = mapperElement.Attribute("namespace")?.Value ?? string.Empty; + string mapperName = namespaceValue.Split('.').Last(); + Dictionary sqlFragments = mapperElement.Elements("sql") + .Where(u => u.Attribute("id") != null) + .ToDictionary(u => u.Attribute("id")!.Value, u => u, StringComparer.OrdinalIgnoreCase); + Dictionary statements = mapperElement.Elements() + .Where(u => u.Name.LocalName is "select" or "insert" or "update" or "delete") + .Where(u => u.Attribute("id") != null) + .ToDictionary(u => u.Attribute("id")!.Value, u => u, StringComparer.OrdinalIgnoreCase); + cache[mapperName] = new HwPortalMapperDefinition(mapperName, sqlFragments, statements); + } + + return cache; + } + + private static string RenderNodes(IEnumerable nodes, HwPortalMapperDefinition mapper, HwPortalRenderContext context) + { + // StringBuilder 适合做频繁字符串拼接,比不断用 + 更节省内存。 + StringBuilder builder = new(); + foreach (XNode node in nodes) + { + if (node is XText textNode) + { + builder.Append(ReplaceParameters(textNode.Value, context)); + continue; + } + + if (node is not XElement element) + { + continue; + } + + switch (element.Name.LocalName) + { + case "include": + // :复用 SQL 片段。 + string refId = element.Attribute("refid")?.Value ?? string.Empty; + if (!mapper.SqlFragments.TryGetValue(refId, out XElement fragment)) + { + throw Oops.Oh($"未找到 SQL 片段:{mapper.MapperName}.{refId}"); + } + builder.Append(RenderNodes(fragment.Nodes(), mapper, context)); + break; + case "if": + // :只有条件成立时才拼接内部 SQL。 + if (EvaluateTest(element.Attribute("test")?.Value, context)) + { + builder.Append(RenderNodes(element.Nodes(), mapper, context)); + } + break; + case "where": + // :内部有内容才输出 WHERE,并自动清理开头多余的 and/or。 + string whereSql = NormalizeSql(RenderNodes(element.Nodes(), mapper, context)); + whereSql = LeadingWhereRegex.Replace(whereSql, string.Empty); + if (!string.IsNullOrWhiteSpace(whereSql)) + { + builder.Append(" WHERE ").Append(whereSql).Append(' '); + } + break; + case "trim": + // :常用于 insert / update 动态拼接字段时,去掉多余逗号。 + string trimBody = NormalizeSql(RenderNodes(element.Nodes(), mapper, context)); + string suffixOverrides = element.Attribute("suffixOverrides")?.Value ?? string.Empty; + string prefixOverrides = element.Attribute("prefixOverrides")?.Value ?? string.Empty; + trimBody = ApplyOverrides(trimBody, prefixOverrides, suffixOverrides); + if (!string.IsNullOrWhiteSpace(trimBody)) + { + string prefix = element.Attribute("prefix")?.Value ?? string.Empty; + string suffix = element.Attribute("suffix")?.Value ?? string.Empty; + builder.Append(prefix).Append(' ').Append(trimBody).Append(' ').Append(suffix).Append(' '); + } + break; + case "choose": + // 相当于 if / else if / else。 + XElement chosen = element.Elements("when").FirstOrDefault(u => EvaluateTest(u.Attribute("test")?.Value, context)) + ?? element.Element("otherwise"); + if (chosen != null) + { + builder.Append(RenderNodes(chosen.Nodes(), mapper, context)); + } + break; + case "foreach": + // :常用于 in (...) 批量参数拼接。 + builder.Append(RenderForeach(element, mapper, context)); + break; + case "when": + case "otherwise": + builder.Append(RenderNodes(element.Nodes(), mapper, context)); + break; + default: + builder.Append(RenderNodes(element.Nodes(), mapper, context)); + break; + } + } + + return builder.ToString(); + } + + private static string RenderForeach(XElement element, HwPortalMapperDefinition mapper, HwPortalRenderContext context) + { + string collectionName = element.Attribute("collection")?.Value ?? string.Empty; + string itemName = element.Attribute("item")?.Value ?? "item"; + string open = element.Attribute("open")?.Value ?? string.Empty; + string close = element.Attribute("close")?.Value ?? string.Empty; + string separator = element.Attribute("separator")?.Value ?? ","; + IEnumerable values = context.ResolveCollection(collectionName); + List fragments = new(); + foreach (object value in values) + { + // 这里模拟 MyBatis foreach 的 item 变量作用域: + // 每一轮循环都把当前值压入上下文,渲染后再移除。 + context.PushScoped(itemName, value); + string sql = NormalizeSql(RenderNodes(element.Nodes(), mapper, context)); + context.PopScoped(itemName); + if (!string.IsNullOrWhiteSpace(sql)) + { + fragments.Add(sql); + } + } + + if (fragments.Count == 0) + { + return string.Empty; + } + + return string.Concat(open, string.Join(separator, fragments), close); + } + + private static string ReplaceParameters(string text, HwPortalRenderContext context) + { + return ParameterRegex.Replace(text, match => + { + string propertyName = match.Groups[1].Value; + // AddParameter 会把真正的值存进参数集合,并返回类似 @p0、@p1 的参数名。 + // 这样做的目的是防 SQL 注入,而不是字符串直接拼接。 + string parameterName = context.AddParameter(propertyName, context.ResolveValue(propertyName)); + return parameterName; + }); + } + + private static bool EvaluateTest(string expression, HwPortalRenderContext context) + { + // 当前实现只覆盖 ruoyi-portal 里实际用到的 test 表达式子集, + // 例如:a != null、a != ''、a == ''、多个 and 条件。 + if (string.IsNullOrWhiteSpace(expression)) + { + return true; + } + + string[] conditions = expression.Split("and", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + foreach (string condition in conditions) + { + if (!EvaluateCondition(condition.Trim(), context)) + { + return false; + } + } + + return true; + } + + private static bool EvaluateCondition(string condition, HwPortalRenderContext context) + { + if (condition.Contains("!=", StringComparison.Ordinal)) + { + string[] parts = condition.Split("!=", 2, StringSplitOptions.TrimEntries); + object leftValue = context.ResolveValue(parts[0]); + object rightValue = ParseLiteral(parts[1], context); + return !EqualsNormalized(leftValue, rightValue); + } + + if (condition.Contains("==", StringComparison.Ordinal)) + { + string[] parts = condition.Split("==", 2, StringSplitOptions.TrimEntries); + object leftValue = context.ResolveValue(parts[0]); + object rightValue = ParseLiteral(parts[1], context); + return EqualsNormalized(leftValue, rightValue); + } + + return context.ResolveValue(condition) != null; + } + + private static object ParseLiteral(string token, HwPortalRenderContext context) + { + // 这里把 test 表达式右侧的字面量解析成真正的 C# 值: + // null -> null + // 'abc' -> 字符串 + // 123 -> long + if (string.Equals(token, "null", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (token.StartsWith('\'') && token.EndsWith('\'') && token.Length >= 2) + { + return token[1..^1]; + } + + if (long.TryParse(token, out long longValue)) + { + return longValue; + } + + return context.ResolveValue(token); + } + + private static bool EqualsNormalized(object leftValue, object rightValue) + { + string leftText = NormalizeCompareValue(leftValue); + string rightText = NormalizeCompareValue(rightValue); + return string.Equals(leftText, rightText, StringComparison.Ordinal); + } + + private static string NormalizeCompareValue(object value) + { + return value switch + { + null => "", + string stringValue => stringValue, + DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture), + _ => Convert.ToString(value, CultureInfo.InvariantCulture) + }; + } + + private static string ApplyOverrides(string sql, string prefixOverrides, string suffixOverrides) + { + string result = sql.Trim(); + if (!string.IsNullOrWhiteSpace(prefixOverrides)) + { + foreach (string candidate in prefixOverrides.Split('|', StringSplitOptions.RemoveEmptyEntries)) + { + if (result.StartsWith(candidate, StringComparison.OrdinalIgnoreCase)) + { + result = result[candidate.Length..].TrimStart(); + } + } + } + + if (!string.IsNullOrWhiteSpace(suffixOverrides)) + { + foreach (string candidate in suffixOverrides.Split('|', StringSplitOptions.RemoveEmptyEntries)) + { + if (result.EndsWith(candidate, StringComparison.OrdinalIgnoreCase)) + { + result = result[..^candidate.Length].TrimEnd(); + } + } + } + + return result; + } + + private static string NormalizeSql(string sql) + { + // 把连续空白折叠成单个空格,减少最终 SQL 的噪音。 + return Regex.Replace(sql, @"\s+", " ").Trim(); + } + + private sealed record HwPortalMapperDefinition(string MapperName, Dictionary SqlFragments, Dictionary Statements); + + public sealed record HwPortalPreparedSql(string Sql, SugarParameter[] Parameters); + + private sealed class HwPortalRenderContext + { + // _parameter 对应业务层传进来的查询对象/DTO/匿名对象。 + private readonly object _parameter; + + // _scopedValues 用来保存 foreach 里临时注入的变量,例如 item。 + private readonly Dictionary _scopedValues = new(StringComparer.OrdinalIgnoreCase); + + // _parameters 是最终交给数据库执行的参数集合。 + private readonly List _parameters = new(); + private int _parameterIndex; + + public HwPortalRenderContext(object parameter) + { + _parameter = parameter; + } + + public SugarParameter[] Parameters => _parameters.ToArray(); + + public object ResolveValue(string name) + { + // 先查临时作用域变量,再查主参数对象。 + if (_scopedValues.TryGetValue(name, out object scopedValue)) + { + return scopedValue; + } + + if (_parameter == null) + { + return null; + } + + if (_parameter is IDictionary dictionary) + { + // 支持 Dictionary / 匿名对象混用,是为了兼容不同调用方式。 + foreach (DictionaryEntry entry in dictionary) + { + if (string.Equals(Convert.ToString(entry.Key, CultureInfo.InvariantCulture), name, StringComparison.OrdinalIgnoreCase)) + { + return entry.Value; + } + } + } + + // 反射取属性: + // 这是 C# 运行时动态读取对象属性的常见方式。 + PropertyInfo property = _parameter.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); + return property?.GetValue(_parameter); + } + + public IEnumerable ResolveCollection(string name) + { + object value = ResolveValue(name); + if (value is string or null) + { + return Array.Empty(); + } + + return value as IEnumerable ?? Array.Empty(); + } + + public string AddParameter(string propertyName, object value) + { + // 数据库参数名并不需要和原属性名完全一致,唯一即可。 + string name = $"@p{_parameterIndex++}"; + _parameters.Add(new SugarParameter(name, value ?? DBNull.Value)); + return name; + } + + public void PushScoped(string name, object value) + { + _scopedValues[name] = value; + } + + public void PopScoped(string name) + { + _scopedValues.Remove(name); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMyBatisExecutor.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMyBatisExecutor.cs new file mode 100644 index 0000000..436ad5b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Infrastructure/HwPortalMyBatisExecutor.cs @@ -0,0 +1,367 @@ +// ============================================================================ +// 【文件说明】HwPortalMyBatisExecutor.cs - MyBatis 风格 SQL 执行器 +// ============================================================================ +// 这个类是"类 MyBatis"架构的核心执行器,负责把 XML 中定义的 SQL 转换为真实的数据库操作。 +// +// 【架构定位】 +// 在 Java MyBatis 中: +// SqlSession sqlSession = sqlSessionFactory.openSession(); +// UserMapper mapper = sqlSession.getMapper(UserMapper.class); +// User user = mapper.selectById(1L); +// +// 在这个 C# 实现中: +// HwPortalMyBatisExecutor executor = ...; // 由 DI 容器注入 +// User user = await executor.QuerySingleAsync("UserMapper", "selectById", new { id = 1L }); +// +// 两者区别: +// - Java MyBatis 用动态代理生成 Mapper 接口的实现 +// - 这里用执行器模式,直接通过字符串名称调用,更简单但不够类型安全 +// +// 【为什么需要这个类?】 +// 1. 兼容迁移:从 Java 迁移过来的项目有大量 XML SQL 定义 +// 2. 复杂 SQL:有些 SQL 用 Lambda/LINQ 很难表达,原生 SQL 更直接 +// 3. 动态条件:MyBatis 的 标签支持动态 SQL 组装 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// MyBatis 风格 SQL 执行器。 +/// +/// 【C# 语法知识点 - sealed 密封类】 +/// sealed 关键字表示"密封类",不能被继承。 +/// +/// 为什么要密封? +/// 1. 安全性:防止子类篡改核心逻辑 +/// 2. 性能:编译器可以对密封类做优化(如方法内联) +/// 3. 设计意图:这个类就是最终实现,不需要扩展 +/// +/// 对比 Java: +/// Java 用 final 关键字:public final class MyBatisExecutor { ... } +/// C# 用 sealed 关键字:public sealed class HwPortalMyBatisExecutor { ... } +/// +/// +/// 【依赖注入设计】 +/// 构造函数接收两个依赖: +/// - ISqlSugarClient:SqlSugar ORM 的数据库客户端 +/// - HwPortalMapperRegistry:XML Mapper 的注册表(负责解析 SQL) +/// +/// 这是"依赖倒置原则"的体现:高层模块(执行器)不依赖低层模块(具体数据库实现), +/// 而是依赖抽象(接口)。 +/// +/// +public sealed class HwPortalMyBatisExecutor +{ + // 【C# 语法知识点 - readonly 字段】 + // readonly 表示"只读字段",只能在构造函数中赋值,之后不能修改。 + // + // 为什么用 readonly? + // 1. 防止意外修改:数据库客户端不应该被替换 + // 2. 线程安全:readonly 字段天然线程安全 + // 3. 依赖注入的最佳实践:注入的依赖通常都应该是 readonly + // + // 对比 Java: + // Java 用 final 关键字:private final SqlSession sqlSession; + // C# 用 readonly 关键字:private readonly ISqlSugarClient _db; + // + // ISqlSugarClient 是 SqlSugar ORM 的数据库客户端抽象。 + private readonly ISqlSugarClient _db; + + // 【命名约定】 + // _registry 以下划线开头,表示私有字段。 + // 这是 C# 的常见命名规范(尤其是依赖注入的字段)。 + // + // Registry 是"注册表"模式: + // - 负责把 XML 中定义的 SQL 语句解析成可执行的 SQL + // - 缓存解析结果,避免每次执行都重新解析 XML + // - 处理参数替换(#{paramName} -> @paramName) + private readonly HwPortalMapperRegistry _registry; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// 构造函数是创建对象时自动调用的特殊方法,负责初始化对象状态。 + /// + /// 语法特点: + /// - 方法名必须与类名相同 + /// - 没有返回值(连 void 都不写) + /// - 可以重载(多个参数不同的构造函数) + /// + /// 对比 Java: + /// Java 的构造函数语法几乎一样: + /// public HwPortalMyBatisExecutor(ISqlSugarClient db, HwPortalMapperRegistry registry) { + /// this.db = db; + /// this.registry = registry; + /// } + /// + /// C# 的区别: + /// - 可以用 this 简化:this._db = db;(但 C# 更常用 _db = db; 省略 this) + /// - Java 习惯用 this.db = db; 区分成员变量和参数 + /// + /// + /// 【依赖注入容器的工作流程】 + /// 1. 应用启动时,Startup.cs 中注册服务: + /// services.AddScoped(); + /// services.AddSingleton(); + /// + /// 2. 当某个类(如 HwWebService)需要 HwPortalMyBatisExecutor 时: + /// public HwWebService(HwPortalMyBatisExecutor executor) { ... } + /// + /// 3. DI 容器检查 HwPortalMyBatisExecutor 的构造函数需要什么: + /// - 需要 ISqlSugarClient -> 容器创建/获取实例 + /// - 需要 HwPortalMapperRegistry -> 容器创建/获取实例(因为是 Singleton,用已有的) + /// + /// 4. 容器调用这个构造函数,传入所需的依赖 + /// + /// 5. 返回创建好的 HwPortalMyBatisExecutor 实例给请求者 + /// + /// 整个过程是"递归解析依赖":A 依赖 B,B 依赖 C,容器会自动按顺序创建 C->B->A。 + /// + /// + /// SqlSugar 数据库客户端(由 DI 容器提供) + /// Mapper 注册表(由 DI 容器提供) + public HwPortalMyBatisExecutor(ISqlSugarClient db, HwPortalMapperRegistry registry) + { + _db = db; + _registry = registry; + } + + /// + /// 查询列表(返回多条记录)。 + /// + /// 【C# 语法知识点 - 泛型方法 <T>】 + /// public Task<List<T>> QueryListAsync<T>(...) + /// + /// 这里的 <T> 是泛型参数,表示"这个方法可以处理任何类型"。 + /// + /// 调用示例: + /// List<HwProductInfo> products = await executor.QueryListAsync<HwProductInfo>( + /// "HwProductInfoMapper", + /// "selectList", + /// new { categoryId = 1 } + /// ); + /// + /// 类型推断简化: + /// var products = await executor.QueryListAsync<HwProductInfo>(...); + /// // 或者直接: + /// List<HwProductInfo> products = await executor.QueryListAsync(...); + /// // 编译器会根据赋值目标推断 T 是 HwProductInfo + /// + /// 对比 Java: + /// Java 的泛型方法写法: + /// public <T> List<T> queryList(String mapperName, String statementId, Object parameter) { ... } + /// + /// 关键差异: + /// - Java 的泛型有"类型擦除",运行时 List<T> 只是 List,T 变成了 Object + /// - C# 的泛型是"真实泛型",运行时 T 就是具体的类型,性能更好 + /// + /// + /// 【C# 语法知识点 - 默认参数值】 + /// object parameter = null 表示参数有默认值 null。 + /// + /// 调用时可以省略: + /// QueryListAsync<HwProductInfo>("Mapper", "selectAll"); // parameter 自动为 null + /// + /// 对比 Java: + /// Java 不支持默认参数值,需要方法重载: + /// public <T> List<T> queryList(String mapperName, String statementId) { + /// return queryList(mapperName, statementId, null); + /// } + /// public <T> List<T> queryList(String mapperName, String statementId, Object parameter) { ... } + /// + /// C# 的默认参数更简洁,减少代码重复。 + /// + /// + /// 【C# 语法知识点 - Task<T> 异步返回类型】 + /// Task<List<T>> 表示"一个异步操作,最终会返回 List<T>"。 + /// + /// 这是 .NET 异步编程的基础: + /// - Task:表示一个异步操作(不返回值的异步方法用 Task) + /// - Task<T>:表示返回 T 类型的异步操作 + /// + /// 对比 Java: + /// Java 的异步返回类型: + /// - CompletableFuture<T>(Java 8+) + /// - ListenableFuture<T>(Guava) + /// - Future<T>(基础版,功能弱) + /// + /// C# 的 Task 比 Java 的 Future 功能更强大,语言级别支持 await/async。 + /// + /// + /// 返回的数据类型(如 HwProductInfo) + /// Mapper 名称(对应 XML 文件名,如 "HwProductInfoMapper") + /// SQL 语句 ID(对应 XML 中的 id,如 "selectList") + /// 查询参数对象(可选,null 表示无参数) + /// 数据列表的异步任务 + public Task> QueryListAsync(string mapperName, string statementId, object parameter = null) + { + // 泛型方法: + // T 表示“希望数据库结果被映射成什么类型”。 + // 例如 QueryListAsync(...) 最终会返回 List。 + HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); + return _db.Ado.SqlQueryAsync(preparedSql.Sql, preparedSql.Parameters); + } + + /// + /// 查询单条记录。 + /// + /// 【方法实现分析】 + /// 这个方法的实现是"先查列表再取第一条": + /// 1. 调用 QueryListAsync 获取列表 + /// 2. 用 FirstOrDefault() 取第一条(或 null) + /// + /// 优点: + /// - 代码复用:复用 QueryListAsync 的逻辑 + /// - 简单易懂:初学者容易理解 + /// + /// 缺点: + /// - 性能略差:数据库可能返回多条,浪费带宽 + /// - 语义不清:LIMIT 1 应该在 SQL 层做,但这里依赖 XML 定义 + /// + /// 【优化建议】 + /// 生产环境应该在 XML 的 SQL 里加 LIMIT 1(MySQL)或 TOP 1(SQL Server), + /// 确保数据库只返回一条记录。 + /// + /// + /// 【C# 语法知识点 - FirstOrDefault()】 + /// FirstOrDefault() 是 LINQ 方法,返回集合的第一个元素,如果没有则返回默认值。 + /// + /// 对于引用类型(如 HwProductInfo),默认值是 null。 + /// 对于值类型(如 int),默认值是 0。 + /// + /// 对比 Java: + /// Java Stream:list.stream().findFirst().orElse(null) + /// + /// C# 的 FirstOrDefault() 更简洁。 + /// + /// + /// 【C# 语法知识点 - async/await 详解】 + /// async 标记方法为异步,await 等待异步操作完成。 + /// + /// 执行流程: + /// 1. 调用 QueryListAsync,它返回一个 Task(尚未完成的任务) + /// 2. await 表示"暂停这个方法,等待任务完成后再继续" + /// 3. 等待期间,当前线程可以处理其他请求(不阻塞) + /// 4. 任务完成后,方法继续执行,返回结果 + /// + /// 关键点: + /// - await 只能在 async 方法中使用 + /// - await 不会阻塞线程,只是让出控制权 + /// - 编译器会把 async/await 转换成状态机代码(类似回调,但更优雅) + /// + /// + /// 返回的数据类型 + /// Mapper 名称 + /// SQL 语句 ID + /// 查询参数(可选) + /// 单条记录或 null 的异步任务 + public async Task QuerySingleAsync(string mapperName, string statementId, object parameter = null) + { + // 当前实现为了简单,先查列表再取第一条。 + // 对初学者来说,这种写法比直接封装多种执行路径更好理解。 + List items = await QueryListAsync(mapperName, statementId, parameter); + return items.FirstOrDefault(); + } + + /// + /// 执行非查询 SQL(增删改)。 + /// + /// 【业务场景】 + /// 用于执行 INSERT、UPDATE、DELETE 等不返回结果集的 SQL。 + /// 返回"影响行数": + /// - 插入:返回插入的行数(通常是 1) + /// - 更新:返回实际修改的行数 + /// - 删除:返回实际删除的行数 + /// + /// 示例: + /// int rows = await executor.ExecuteAsync("UserMapper", "updateStatus", new { userId = 1, status = "active" }); + /// if (rows > 0) { /* 更新成功 */ } + /// + /// + /// 【C# 语法知识点 - async/await 返回值】 + /// return await _db.Ado.ExecuteCommandAsync(...) + /// + /// 这里的 await 会: + /// 1. 等待数据库操作完成 + /// 2. 解包 Task<int> 得到 int 结果 + /// 3. 返回这个 int + /// + /// 如果去掉 await 直接返回 Task: + /// return _db.Ado.ExecuteCommandAsync(...); // 编译错误,类型不匹配 + /// + /// 必须 await 才能拿到实际值。 + /// + /// + /// Mapper 名称 + /// SQL 语句 ID + /// 执行参数(可选) + /// 影响行数的异步任务 + public async Task ExecuteAsync(string mapperName, string statementId, object parameter = null) + { + HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); + return await _db.Ado.ExecuteCommandAsync(preparedSql.Sql, preparedSql.Parameters); + } + + /// + /// 插入记录并返回自增主键。 + /// + /// 【业务场景】 + /// 新增记录时,数据库会自动生成主键(自增 ID)。 + /// 这个方法执行 INSERT 后,立即查询并返回生成的主键值。 + /// + /// 典型用法: + /// var newUser = new User { Name = "张三" }; // Id 还未知 + /// long newId = await executor.InsertReturnIdentityAsync("UserMapper", "insert", newUser); + /// newUser.Id = newId; // 回填主键 + /// + /// 对比 Java MyBatis: + /// Java 配置 useGeneratedKeys="true" keyProperty="id" 后, + /// MyBatis 会自动把生成的主键设置到实体的 id 属性,不需要手动查询。 + /// + /// 这里采用"执行 INSERT + 查询 LAST_INSERT_ID()"的方式, + /// 虽然多了一次查询,但更通用(不依赖具体 ORM 的主键回填机制)。 + /// + /// + /// 【MySQL LAST_INSERT_ID() 说明】 + /// LAST_INSERT_ID() 是 MySQL 的函数,返回当前连接最近生成的自增主键。 + /// + /// 关键点: + /// 1. 是"当前连接"级别的,不受其他连接影响(线程安全) + /// 2. 只对 AUTO_INCREMENT 列有效 + /// 3. 一次插入多行时,返回第一行的 ID + /// + /// 其他数据库的等效写法: + /// - SQL Server: SELECT SCOPE_IDENTITY() + /// - PostgreSQL: RETURNING id 或 SELECT currval('seq_name') + /// - Oracle: RETURNING id INTO ...(需要存储过程) + /// + /// + /// 【C# 语法知识点 - 为什么这里不用默认参数?】 + /// 注意这个方法没有 parameter = null,而是要求必须传 parameter。 + /// + /// 原因: + /// 1. 插入操作必须有数据,不可能无参数插入 + /// 2. 强制调用方提供参数,避免空引用错误 + /// 3. 语义清晰:插入什么必须明确指定 + /// + /// 设计原则: + /// - 可选的用默认参数(如查询条件) + /// - 强制的不用默认参数(如插入数据) + /// + /// + /// Mapper 名称 + /// SQL 语句 ID + /// 插入数据对象(必须) + /// 自增主键值的异步任务 + public async Task InsertReturnIdentityAsync(string mapperName, string statementId, object parameter) + { + HwPortalPreparedSql preparedSql = _registry.Prepare(mapperName, statementId, parameter); + await _db.Ado.ExecuteCommandAsync(preparedSql.Sql, preparedSql.Parameters); + + // LAST_INSERT_ID() 是 MySQL 风格的“最近一次自增主键”读取方式。 + // 这里为了保持和源 Java/MyBatis 行为一致,先这样处理。 + List ids = await _db.Ado.SqlQueryAsync("SELECT LAST_INSERT_ID();"); + return ids.FirstOrDefault(); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Entity/HwPortalSearchDoc.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Entity/HwPortalSearchDoc.cs new file mode 100644 index 0000000..3cbe052 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Entity/HwPortalSearchDoc.cs @@ -0,0 +1,234 @@ +// ============================================================================ +// 【文件说明】HwPortalSearchDoc.cs - 搜索索引实体类 +// ============================================================================ +// 这是一个 Entity Framework Core (EF Core) 的实体类,用于存储搜索索引数据。 +// +// 【什么是搜索索引?】 +// 搜索索引是"读模型"(Read Model),用于优化搜索查询性能: +// 1. 把分散在多个业务表的数据聚合到一个表 +// 2. 预处理搜索内容(如拼接标题、正文) +// 3. 建立索引加速查询 +// +// 【与 SqlSugar 实体的区别】 +// - SqlSugar 实体(如 HwWeb):映射到业务表,用于 CRUD 操作 +// - EF Core 实体(如 HwPortalSearchDoc):映射到索引表,用于搜索查询 +// +// 为什么搜索用 EF Core 而不是 SqlSugar? +// 1. EF Core 的 LINQ 查询更强大 +// 2. EF Core 对复杂查询的支持更好 +// 3. 项目技术栈混合使用,各取所长 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Data JPA: +// @Entity +// @Table(name = "hw_portal_search_doc") +// @Index(name = "uk_doc_id", columnList = "doc_id", unique = true) +// public class HwPortalSearchDoc { ... } +// +// EF Core: +// [Table("hw_portal_search_doc")] +// [Index(nameof(DocId), IsUnique = true)] +// public class HwPortalSearchDoc { ... } +// +// 两者概念相同,只是注解/特性的写法不同。 +// ============================================================================ + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// hw-portal 搜索索引实体。 +/// +/// 【设计模式 - CQRS(命令查询职责分离)】 +/// 这个实体体现了 CQRS 的"读模型"概念: +/// - 命令模型(HwWeb 等):负责增删改,保证数据一致性 +/// - 查询模型(HwPortalSearchDoc):负责查询,优化读取性能 +/// +/// 好处: +/// 1. 读写分离,各自优化 +/// 2. 搜索失败不影响主业务 +/// 3. 可以针对搜索场景做特殊处理 +/// +/// +[Table("hw_portal_search_doc")] +// 【EF Core 索引特性】 +// [Index(nameof(DocId), IsUnique = true)] 创建唯一索引。 +// nameof(DocId) 获取属性名称字符串,避免硬编码。 +// +// 对比 Java JPA: +// Java: @Index(name = "uk_doc_id", columnList = "doc_id", unique = true) +// C#: [Index(nameof(DocId), IsUnique = true)] +// +// C# 的 nameof 更安全,重构时编译器会检查。 +[Index(nameof(DocId), IsUnique = true)] +[Index(nameof(SourceType))] +[Index(nameof(UpdatedAt))] +public class HwPortalSearchDoc +{ + /// + /// 主键ID。 + /// + /// 【C# 语法知识点 - [Key] 特性】 + /// [Key] 标记主键字段。 + /// + /// 对比 Java JPA: + /// Java: @Id + /// @GeneratedValue(strategy = GenerationType.IDENTITY) + /// private Long id; + /// + /// EF Core 会自动识别名为 Id 或 {类名}Id 的属性为主键, + /// 但显式标注 [Key] 更清晰。 + /// + /// + [Key] + public long Id { get; set; } + + /// + /// 搜索文档唯一键。 + /// + /// 【业务说明】 + /// DocId 是文档的唯一标识,格式通常是:{SourceType}_{BizId} + /// 例如:hw_web_100, hw_product_200 + /// + /// 为什么需要 DocId? + /// 1. 全局唯一:不同来源的文档可以区分 + /// 2. 幂等更新:相同 DocId 的文档会被更新而不是重复插入 + /// + /// + /// 【C# 语法知识点 - [Required] 和 [MaxLength] 特性】 + /// [Required]:必填字段,不能为 null + /// [MaxLength(128)]:最大长度 128 字符 + /// + /// 对比 Java JPA: + /// Java: @Column(name = "doc_id", nullable = false, length = 128) + /// @NotNull + /// private String docId; + /// + /// + [Required] + [MaxLength(128)] + public string DocId { get; set; } = string.Empty; + + /// + /// 来源类型(如 hw_web, hw_product 等)。 + /// + [Required] + [MaxLength(32)] + public string SourceType { get; set; } = string.Empty; + + /// + /// 业务主键(如 web_id, product_info_id 等)。 + /// + [MaxLength(64)] + public string BizId { get; set; } + + /// + /// 搜索标题。 + /// + [MaxLength(500)] + public string Title { get; set; } + + /// + /// 搜索正文内容。 + /// + /// 【EF Core 列类型映射】 + /// Content 字段在 DbContext.OnModelCreating 中被映射为 longtext: + /// entity.Property(x => x.Content).HasColumnType("longtext"); + /// + /// 这是因为搜索内容可能很长,需要大文本类型。 + /// + /// + public string Content { get; set; } + + /// + /// 页面编码(用于跳转到详情页)。 + /// + [MaxLength(64)] + public string WebCode { get; set; } + + /// + /// 类型ID。 + /// + [MaxLength(64)] + public string TypeId { get; set; } + + /// + /// 设备ID。 + /// + [MaxLength(64)] + public string DeviceId { get; set; } + + /// + /// 菜单ID。 + /// + [MaxLength(64)] + public string MenuId { get; set; } + + /// + /// 文档ID。 + /// + [MaxLength(64)] + public string DocumentId { get; set; } + + /// + /// 基础分值(用于排序)。 + /// + public int BaseScore { get; set; } + + /// + /// 前台路由(用户点击搜索结果跳转的页面)。 + /// + [MaxLength(255)] + public string Route { get; set; } + + /// + /// 前台路由参数(JSON 格式)。 + /// + /// 【JSON 列类型】 + /// 在 DbContext 中映射为 json 类型: + /// entity.Property(x => x.RouteQueryJson).HasColumnType("json"); + /// + /// MySQL 5.7+ 支持 JSON 类型,可以直接存储 JSON 数据。 + /// + /// + public string RouteQueryJson { get; set; } + + /// + /// 编辑路由(后台编辑页面的路由)。 + /// + [MaxLength(255)] + public string EditRoute { get; set; } + + /// + /// 逻辑删除标记。 + /// + /// 【逻辑删除】 + /// "0":正常 + /// "1":已删除 + /// + /// 搜索索引也需要逻辑删除,因为: + /// 1. 删除业务数据时,索引也要标记删除 + /// 2. 可以保留历史记录用于审计 + /// + /// + [MaxLength(1)] + public string IsDelete { get; set; } = "0"; + + /// + /// 业务更新时间(来源数据的更新时间)。 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 索引创建时间。 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 索引更新时间。 + /// + public DateTime ModifiedAt { get; set; } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/HwPortalSearchDbContext.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/HwPortalSearchDbContext.cs new file mode 100644 index 0000000..5e21663 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/HwPortalSearchDbContext.cs @@ -0,0 +1,185 @@ +// ============================================================================ +// 【文件说明】HwPortalSearchDbContext.cs - 搜索子系统 EF Core 数据库上下文 +// ============================================================================ +// 这是 Entity Framework Core 的数据库上下文,专门用于搜索索引数据。 +// +// 【什么是 DbContext?】 +// DbContext 是 EF Core 的核心类,负责: +// 1. 管理数据库连接 +// 2. 跟踪实体状态(新增、修改、删除) +// 3. 执行 LINQ 查询并转换为 SQL +// 4. 保存变更到数据库 +// +// 【与 SqlSugar 的区别】 +// 项目中同时使用了两个 ORM: +// - SqlSugar:用于业务表的 CRUD 操作(如 HwWeb, HwProductInfo) +// - EF Core:用于搜索索引表(HwPortalSearchDoc) +// +// 为什么搜索用 EF Core? +// 1. EF Core 的 LINQ 查询更强大,支持复杂查询 +// 2. EF Core 的迁移工具更成熟 +// 3. 搜索索引是独立的读模型,用不同的 ORM 隔离 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Data JPA: +// @Entity +// @Table(name = "hw_portal_search_doc") +// public class HwPortalSearchDoc { ... } +// +// public interface SearchDocRepository extends JpaRepository { +// // JPA 自动实现 CRUD +// } +// +// EF Core: +// public class HwPortalSearchDbContext : DbContext { +// public DbSet SearchDocs { get; } +// } +// +// 两者概念相似: +// - JPA 的 Repository ≈ EF Core 的 DbSet +// - JPA 的 EntityManager ≈ EF Core 的 DbContext +// ============================================================================ + +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索子系统专用 DbContext。 +/// +/// 【设计原则 - 单一职责】 +/// 这个 DbContext 只管理搜索相关的实体,不包含业务实体。 +/// +/// 好处: +/// 1. 关注点分离:搜索逻辑和业务逻辑解耦 +/// 2. 性能优化:可以单独配置搜索数据库连接 +/// 3. 独立部署:搜索可以迁移到独立的数据库服务器 +/// +/// +/// 【C# 语法知识点 - 继承 DbContext】 +/// public class HwPortalSearchDbContext : DbContext +/// +/// DbContext 是 EF Core 的基类,继承后: +/// 1. 可以定义 DbSet<T> 属性来映射表 +/// 2. 可以重写 OnModelCreating 配置映射 +/// 3. 可以重写 OnConfiguring 配置连接 +/// +/// 对比 Java JPA: +/// Java 不需要显式继承 DbContext,而是用 @Entity 和 Repository。 +/// +/// +public class HwPortalSearchDbContext : DbContext +{ + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数注入】 + /// public HwPortalSearchDbContext(DbContextOptions<HwPortalSearchDbContext> options) : base(options) + /// + /// DbContextOptions<T> 包含数据库连接配置: + /// - 连接字符串 + /// - 数据库提供者(MySQL, PostgreSQL, SQLite 等) + /// - 其他配置选项 + /// + /// : base(options) 把配置传给父类 DbContext。 + /// + /// 对比 Java Spring Boot: + /// Java 通常用 @Autowired 注入 DataSource 或 EntityManagerFactory: + /// @Autowired + /// public SearchRepository(EntityManager em) { + /// this.em = em; + /// } + /// + /// C# 的构造函数注入是 DI 的标准方式。 + /// + /// + /// 数据库上下文配置选项 + public HwPortalSearchDbContext(DbContextOptions options) : base(options) + { + } + + /// + /// 搜索文档表映射。 + /// + /// 【C# 语法知识点 - DbSet<T> 属性】 + /// public DbSet<HwPortalSearchDoc> SearchDocs => Set<HwPortalSearchDoc>(); + /// + /// DbSet<T> 是 EF Core 的"表映射": + /// - 每个 DbSet<T> 对应数据库中的一张表 + /// - 可以用 LINQ 查询:_db.SearchDocs.Where(x => x.Title.Contains("关键词")) + /// - 可以增删改:_db.SearchDocs.Add(doc), _db.SearchDocs.Remove(doc) + /// + /// => Set<HwPortalSearchDoc>() 是表达式体属性: + /// - 等价于 { return Set<HwPortalSearchDoc>(); } + /// - Set<T>() 是 DbContext 的方法,返回 DbSet<T> + /// + /// 对比 Java JPA: + /// Java 的 Repository 接口类似: + /// public interface SearchDocRepository extends JpaRepository { + /// // JPA 自动实现 CRUD + /// } + /// + /// C# 的 DbSet 更接近底层,但 LINQ 查询更强大。 + /// + /// + public DbSet SearchDocs => Set(); + + /// + /// 配置实体映射。 + /// + /// 【C# 语法知识点 - OnModelCreating 方法】 + /// protected override void OnModelCreating(ModelBuilder modelBuilder) + /// + /// OnModelCreating 是 DbContext 的虚方法,用于: + /// 1. 配置实体到表的映射 + /// 2. 配置列类型、索引、约束 + /// 3. 配置关系(一对多、多对多) + /// + /// ModelBuilder 是"流畅 API"(Fluent API): + /// - 用链式方法调用配置映射 + /// - 比 Data Annotations(特性)更灵活 + /// + /// 对比 Java JPA: + /// Java 可以用 @Column, @Table 等注解配置: + /// @Column(name = "content", columnDefinition = "LONGTEXT") + /// private String content; + /// + /// 或者用 JPA 的 @Entity + @Table 注解。 + /// + /// EF Core 的 Fluent API 更强大,适合复杂配置。 + /// + /// + /// 模型构建器 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 调用父类方法(如果有基础配置)。 + base.OnModelCreating(modelBuilder); + + // 【配置 HwPortalSearchDoc 实体】 + // modelBuilder.Entity<HwPortalSearchDoc>(entity => { ... }) + // 对特定实体进行详细配置。 + modelBuilder.Entity(entity => + { + // 【列类型配置】 + // entity.Property(x => x.Content).HasColumnType("longtext") + // + // 为什么显式指定列类型? + // 1. 不同数据库提供者的默认映射不同 + // - MySQL: string → VARCHAR(255) + // - PostgreSQL: string → TEXT + // 2. 搜索内容可能很长,需要 longtext(MySQL 的 4GB 文本类型) + // 3. 显式声明避免迁移时类型变化 + // + // 对比 Java JPA: + // Java: @Column(name = "content", columnDefinition = "LONGTEXT") + entity.Property(x => x.Content).HasColumnType("longtext"); + + // 【JSON 列类型】 + // MySQL 5.7+ 支持 JSON 类型: + // - 自动验证 JSON 格式 + // - 支持 JSON 函数查询(如 JSON_EXTRACT) + // - 更高效的存储 + entity.Property(x => x.RouteQueryJson).HasColumnType("json"); + }); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Option/HwPortalSearchOptions.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Option/HwPortalSearchOptions.cs new file mode 100644 index 0000000..f8d3807 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Option/HwPortalSearchOptions.cs @@ -0,0 +1,188 @@ +// ============================================================================ +// 【文件说明】HwPortalSearchOptions.cs - 搜索子系统配置选项 +// ============================================================================ +// 这个类定义了搜索子系统的所有配置项,对应配置文件中的 HwPortalSearch 节点。 +// +// 【配置绑定机制】 +// 在 Startup.cs 中通过 services.AddConfigurableOptions() 注册。 +// 框架会自动从配置文件(如 Search.json)读取配置并绑定到这个类。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @ConfigurationProperties(prefix = "hwportal.search") +// @Configuration +// public class HwPortalSearchProperties { +// private String engine = "es"; +// private Boolean enableLegacyFallback = true; +// // ... getter/setter +// } +// +// ASP.NET Core + Furion: +// public sealed class HwPortalSearchOptions : IConfigurableOptions +// +// 两者概念相同:都是把配置文件的值映射到类的属性。 +// ============================================================================ + +using Microsoft.Extensions.Configuration; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// hw-portal 搜索子系统配置。 +/// +/// 【C# 语法知识点 - sealed 密封类】 +/// sealed 表示"密封类",不能被继承。 +/// 配置类通常不需要继承,密封可以防止意外扩展。 +/// +/// 对比 Java: +/// Java 通常用 final 关键字:public final class HwPortalSearchOptions { ... } +/// +/// +/// 【C# 语法知识点 - IConfigurableOptions<T> 接口】 +/// IConfigurableOptions<T> 是 Furion 框架的配置接口。 +/// 实现这个接口后,框架会: +/// 1. 自动从配置文件读取配置 +/// 2. 绑定到类的属性 +/// 3. 调用 PostConfigure 方法进行后处理 +/// +/// 对比 Java Spring Boot: +/// Java 用 @ConfigurationProperties 注解实现类似功能。 +/// +/// +public sealed class HwPortalSearchOptions : IConfigurableOptions +{ + /// + /// 搜索引擎标识。 + /// + /// 【业务说明】 + /// 支持多种搜索引擎: + /// - "es":Elasticsearch(推荐,性能最好) + /// - "indexed":MySQL 索引表(当前实现) + /// - "mysql_fulltext":MySQL 全文索引 + /// - "mysql/legacy":原 MySQL 兜底 SQL(兼容旧版) + /// + /// + /// 【C# 语法知识点 - 属性默认值】 + /// = "es" 是属性的默认值。 + /// 如果配置文件没有设置这个字段,就使用默认值 "es"。 + /// + /// 对比 Java: + /// Java 需要在字段声明或构造函数中设置默认值: + /// private String engine = "es"; + /// + /// + public string Engine { get; set; } = "es"; + + /// + /// 新搜索失败或索引尚未建立时,是否回退旧 SQL 搜索。 + /// + /// 【降级策略】 + /// 这是"优雅降级"的设计: + /// 1. 优先使用新搜索引擎(性能更好) + /// 2. 如果失败,自动回退到旧 SQL(保证可用性) + /// + /// 对比 Java 若依: + /// 若依的搜索也有类似的降级机制,但通常在代码中硬编码。 + /// 这里通过配置控制,更灵活。 + /// + /// + public bool EnableLegacyFallback { get; set; } = true; + + /// + /// 搜索专用连接串。 + /// + /// 【数据库隔离策略】 + /// 搜索功能可以: + /// 1. 使用独立的数据库(推荐,隔离搜索负载) + /// 2. 复用主库连接(简单,但不适合高并发搜索场景) + /// + /// 这个配置项设置独立的搜索数据库连接字符串。 + /// 如果为空,会根据 UseMainDbConnectionWhenEmpty 决定是否回退。 + /// + /// + public string ConnectionString { get; set; } + + /// + /// 连接串为空时,是否复用主业务库连接。 + /// + public bool UseMainDbConnectionWhenEmpty { get; set; } = true; + + /// + /// 全量重建时的批处理大小。 + /// + /// 【批量处理说明】 + /// 搜索索引重建时,不是一次性加载所有数据, + /// 而是分批处理,避免内存溢出和数据库压力过大。 + /// + /// 默认 200 条/批,可根据服务器内存调整。 + /// + /// + public int BatchSize { get; set; } = 200; + + /// + /// 单次搜索最多返回多少条候选记录。 + /// + /// 【性能保护】 + /// 限制返回条数,避免: + /// 1. 用户搜索太宽泛的关键词(如"a")返回过多结果 + /// 2. 前端渲染大量数据卡顿 + /// 3. 网络传输过大的响应 + /// + /// + public int TakeLimit { get; set; } = 500; + + /// + /// 是否在运行时自动初始化搜索表结构。 + /// + /// 【自动建表】 + /// 如果开启,应用启动时会检查并创建搜索表。 + /// + /// 生产环境建议关闭,由 DBA 手动管理表结构。 + /// 开发/测试环境可以开启,方便快速启动。 + /// + /// + public bool AutoInitSchema { get; set; } = true; + + /// + /// 配置后处理方法。 + /// + /// 【C# 语法知识点 - PostConfigure 模式】 + /// PostConfigure 是 IConfigurableOptions 接口的方法。 + /// 框架在绑定配置后会调用这个方法,用于: + /// 1. 校验配置值是否合法 + /// 2. 设置默认值 + /// 3. 修正不合理的配置 + /// + /// 对比 Java Spring Boot: + /// Java 通常用 @PostConstruct 或 @Validated 注解实现类似功能: + /// @PostConstruct + /// public void init() { + /// if (batchSize < 1 || batchSize > 1000) { + /// batchSize = 200; + /// } + /// } + /// + /// + /// 【Math.Clamp 方法】 + /// Math.Clamp(value, min, max) 把值限制在 [min, max] 范围内: + /// - 如果 value < min,返回 min + /// - 如果 value > max,返回 max + /// - 否则返回 value + /// + /// 这是 .NET Core 2.0 引入的便捷方法,比手写 if-else 更简洁。 + /// + /// + /// 配置选项实例 + /// 配置对象 + public void PostConfigure(HwPortalSearchOptions options, IConfiguration configuration) + { + // 限制 BatchSize 在 1-1000 范围内,防止配置错误。 + options.BatchSize = Math.Clamp(options.BatchSize, 1, 1000); + + // 限制 TakeLimit 在 20-1000 范围内。 + options.TakeLimit = Math.Clamp(options.TakeLimit, 20, 1000); + + // 如果 Engine 为空或空白,使用默认值 "es"。 + options.Engine = string.IsNullOrWhiteSpace(options.Engine) ? "es" : options.Engine.Trim(); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchIndexService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchIndexService.cs new file mode 100644 index 0000000..586a55a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchIndexService.cs @@ -0,0 +1,346 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal; + +public sealed class HwSearchIndexService : IHwSearchIndexService, ITransient +{ + // 这里保留 DbContext,是因为当前搜索索引表走的是独立读模型方案。 + // 业务表继续由 SqlSugar + MyBatisExecutor 负责,搜索索引表则由 EF 负责统一 upsert。 + private readonly HwPortalSearchDbContext _db; + private readonly PortalSearchDocConverter _converter; + private readonly IHwSearchSchemaService _schemaService; + private readonly PortalSearchRouteResolver _routeResolver; + + public HwSearchIndexService(HwPortalSearchDbContext db, PortalSearchDocConverter converter, IHwSearchSchemaService schemaService, PortalSearchRouteResolver routeResolver) + { + _db = db; + _converter = converter; + _schemaService = schemaService; + _routeResolver = routeResolver; + } + + public async Task UpsertMenuAsync(HwWebMenu menu, CancellationToken cancellationToken = default) + { + if (menu?.WebMenuId == null) + { + return; + } + + // 搜索索引表是读模型,不参与主业务事务。 + // 这里先确保表结构,再写入索引,避免第一次触发搜索重建时因为表不存在直接中断主链路。 + await _schemaService.EnsureCreatedAsync(cancellationToken); + await UpsertAsync(new HwPortalSearchDoc + { + DocId = BuildMenuDocId(menu.WebMenuId.Value), + SourceType = PortalSearchDocConverter.SourceMenu, + BizId = menu.WebMenuId.Value.ToString(), + + // 菜单索引标题只保留中文主标题。 + // 当前 Java 源实现并不会把英文名塞进 title,因此这里不能为了“看起来更丰富”擅自扩字段口径。 + Title = menu.WebMenuName, + Content = NormalizeSearchText(menu.WebMenuName, menu.Ancestors), + MenuId = menu.WebMenuId.Value.ToString(), + + // route / routeQueryJson 是搜索结果最终给前端跳转用的协议字段。 + // 把它们提前固化进索引文档,可以让搜索服务层只关注打分和摘要,不再关心复杂跳转规则。 + Route = "/test", + RouteQueryJson = SerializeRouteQuery(new Dictionary { ["id"] = menu.WebMenuId.Value.ToString() }), + UpdatedAt = menu.UpdateTime ?? menu.CreateTime ?? HwPortalContextHelper.Now() + }, cancellationToken); + } + + public async Task UpsertWebAsync(HwWeb web, CancellationToken cancellationToken = default) + { + if (web?.WebCode == null) + { + return; + } + + await _schemaService.EnsureCreatedAsync(cancellationToken); + + string webCode = web.WebCode.Value.ToString(); + await UpsertAsync(new HwPortalSearchDoc + { + DocId = BuildWebDocId(web.WebCode.Value), + SourceType = PortalSearchDocConverter.SourceWeb, + BizId = web.WebId?.ToString(), + Title = $"页面#{webCode}", + + // web/web1 搜索正文必须先走 JSON 文本抽取。 + // 如果直接存整段 JSON,后续 mysql like / 全文索引都会把样式字段、URL、图标键一起纳入命中,搜索会非常脏。 + Content = _converter.ExtractSearchableText(web.WebJsonString), + WebCode = webCode, + + // 这里不单独存 BaseScore,是因为当前服务层最后仍会根据来源类型和标题/正文命中再次打分。 + // 这能保证索引链路与 legacy SQL 链路最终排序尽量保持一致。 + Route = BuildWebRoute(webCode), + RouteQueryJson = BuildWebRouteQueryJson(webCode), + UpdatedAt = web.UpdateTime ?? web.CreateTime ?? HwPortalContextHelper.Now() + }, cancellationToken); + } + + public async Task UpsertWeb1Async(HwWeb1 web1, CancellationToken cancellationToken = default) + { + if (web1?.WebCode == null || web1.TypeId == null || web1.DeviceId == null) + { + return; + } + + await _schemaService.EnsureCreatedAsync(cancellationToken); + + string webCode = web1.WebCode.Value.ToString(); + string typeId = web1.TypeId.Value.ToString(); + string deviceId = web1.DeviceId.Value.ToString(); + await UpsertAsync(new HwPortalSearchDoc + { + DocId = BuildWeb1DocId(web1.WebCode.Value, web1.TypeId.Value, web1.DeviceId.Value), + SourceType = PortalSearchDocConverter.SourceWeb1, + BizId = web1.WebId?.ToString(), + Title = $"详情#{webCode}-{typeId}-{deviceId}", + Content = _converter.ExtractSearchableText(web1.WebJsonString), + WebCode = webCode, + TypeId = typeId, + DeviceId = deviceId, + + // web1 是详情页模型,所以 routeQuery 里必须同时保留 webCode/typeId/deviceId 三元组。 + // 少任何一个维度,前端都无法唯一定位到正确详情页。 + Route = "/productCenter/detail", + RouteQueryJson = SerializeRouteQuery(new Dictionary + { + ["webCode"] = webCode, + ["typeId"] = typeId, + ["deviceId"] = deviceId + }), + UpdatedAt = web1.UpdateTime ?? web1.CreateTime ?? HwPortalContextHelper.Now() + }, cancellationToken); + } + + public async Task UpsertDocumentAsync(HwWebDocument document, CancellationToken cancellationToken = default) + { + if (document == null || string.IsNullOrWhiteSpace(document.DocumentId)) + { + return; + } + + await _schemaService.EnsureCreatedAsync(cancellationToken); + + string title = string.IsNullOrWhiteSpace(document.Json) ? document.DocumentId : document.Json; + await UpsertAsync(new HwPortalSearchDoc + { + DocId = BuildDocumentDocId(document.DocumentId), + SourceType = PortalSearchDocConverter.SourceDocument, + BizId = document.DocumentId, + + // 文档这里刻意保留原 JSON/原文内容,而不是做 JSON 抽取。 + // 原因是最新源模块的旧 SQL 搜索就是直接对 json 字段做 like,迁移时要保持这个命中口径。 + Title = title, + Content = document.Json ?? string.Empty, + WebCode = document.WebCode, + TypeId = document.Type, + DocumentId = document.DocumentId, + + // 文档搜索前台统一落到 serviceSupport 页面,再由 documentId 做二次定位。 + // 这就是为什么这里 route 固定、但 routeQueryJson 仍然要保留 documentId。 + Route = "/serviceSupport", + RouteQueryJson = SerializeRouteQuery(new Dictionary { ["documentId"] = document.DocumentId }), + UpdatedAt = document.UpdateTime ?? document.CreateTime ?? HwPortalContextHelper.Now() + }, cancellationToken); + } + + public async Task UpsertConfigTypeAsync(HwPortalConfigType configType, CancellationToken cancellationToken = default) + { + if (configType?.ConfigTypeId == null) + { + return; + } + + await _schemaService.EnsureCreatedAsync(cancellationToken); + + string configTypeId = configType.ConfigTypeId.Value.ToString(); + string routeWebCode = await _routeResolver.ResolveConfigTypeWebCode(configType); + await UpsertAsync(new HwPortalSearchDoc + { + DocId = BuildConfigTypeDocId(configType.ConfigTypeId.Value), + SourceType = PortalSearchDocConverter.SourceConfigType, + BizId = configTypeId, + Title = configType.ConfigTypeName, + Content = NormalizeSearchText(configType.ConfigTypeName, configType.HomeConfigTypeName, configType.ConfigTypeDesc), + + // 这里同时保留 TypeId 和解析后的 WebCode: + // TypeId 用于保底还原业务来源, + // WebCode 用于前端真正跳转。 + // 两者不能互相覆盖,否则后续查问题时会丢失来源语义。 + WebCode = routeWebCode, + TypeId = configTypeId, + Route = "/productCenter", + + // 编辑端入口这里固定写 productCenter/edit,是为了保持和源模块“配置分类统一从产品中心编辑页维护”的口径一致。 + RouteQueryJson = BuildConfigTypeRouteQueryJson(routeWebCode, configTypeId), + EditRoute = "/productCenter/edit", + UpdatedAt = configType.UpdateTime ?? configType.CreateTime ?? HwPortalContextHelper.Now() + }, cancellationToken); + } + + public Task DeleteMenuAsync(long menuId, CancellationToken cancellationToken = default) + { + return DeleteByDocIdAsync(BuildMenuDocId(menuId), cancellationToken); + } + + public Task DeleteWebAsync(long webCode, CancellationToken cancellationToken = default) + { + return DeleteByDocIdAsync(BuildWebDocId(webCode), cancellationToken); + } + + public Task DeleteWeb1Async(long webCode, long typeId, long deviceId, CancellationToken cancellationToken = default) + { + return DeleteByDocIdAsync(BuildWeb1DocId(webCode, typeId, deviceId), cancellationToken); + } + + public Task DeleteDocumentAsync(string documentId, CancellationToken cancellationToken = default) + { + return DeleteByDocIdAsync(BuildDocumentDocId(documentId), cancellationToken); + } + + public Task DeleteConfigTypeAsync(long configTypeId, CancellationToken cancellationToken = default) + { + return DeleteByDocIdAsync(BuildConfigTypeDocId(configTypeId), cancellationToken); + } + + public async Task DeleteByDocIdAsync(string docId, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(docId)) + { + return; + } + + await _schemaService.EnsureCreatedAsync(cancellationToken); + + HwPortalSearchDoc existing = await _db.SearchDocs.SingleOrDefaultAsync(x => x.DocId == docId, cancellationToken); + if (existing == null) + { + return; + } + + // Why: + // 删除业务数据时保留索引行而不是物理删掉,后续同 docId 再创建时可以直接复用同一条索引文档。 + existing.IsDelete = "1"; + existing.ModifiedAt = HwPortalContextHelper.Now(); + await _db.SaveChangesAsync(cancellationToken); + } + + private async Task UpsertAsync(HwPortalSearchDoc input, CancellationToken cancellationToken) + { + // SingleOrDefaultAsync 对应 Java 里“按唯一键查一条,查不到返回 null”。 + // 这里按 DocId 查,而不是按数据库自增 Id 查,是因为 DocId 才是业务稳定键。 + HwPortalSearchDoc existing = await _db.SearchDocs.SingleOrDefaultAsync(x => x.DocId == input.DocId, cancellationToken); + DateTime now = HwPortalContextHelper.Now(); + if (existing == null) + { + // 新文档直接创建一条索引记录。 + // 这里强制落 IsDelete=0,是为了兼容之前可能被逻辑删过、但现在又重新出现的业务对象。 + input.IsDelete = "0"; + input.CreatedAt = now; + input.ModifiedAt = now; + input.UpdatedAt ??= now; + _db.SearchDocs.Add(input); + } + else + { + // 已存在时做覆盖更新,而不是新插一条。 + // 原因是 doc_id 被设计成搜索文档自然键,后续 rebuild / 静默修复都依赖它具备幂等性。 + + // 下面这一组字段赋值可以理解成“把最新业务快照完整覆盖回索引表”。 + // 这里不做脏字段比较,是因为索引表重建更看重稳定和简单,而不是极致写入性能。 + existing.SourceType = input.SourceType; + existing.BizId = input.BizId; + existing.Title = input.Title; + existing.Content = input.Content; + existing.WebCode = input.WebCode; + existing.TypeId = input.TypeId; + existing.DeviceId = input.DeviceId; + existing.MenuId = input.MenuId; + existing.DocumentId = input.DocumentId; + existing.BaseScore = input.BaseScore; + existing.Route = input.Route; + existing.RouteQueryJson = input.RouteQueryJson; + existing.EditRoute = input.EditRoute; + existing.IsDelete = "0"; + existing.UpdatedAt = input.UpdatedAt ?? existing.UpdatedAt ?? now; + existing.ModifiedAt = now; + } + + // SaveChangesAsync 就是把当前 DbContext 跟踪到的变更真正落库。 + // 和 Java JPA 类似,在调用它之前,对象只是“内存态已修改”,还没有写进数据库。 + await _db.SaveChangesAsync(cancellationToken); + } + + private static string NormalizeSearchText(params string[] values) + { + // 统一把多段文本拼接成一段可搜索正文,并压缩连续空白。 + // 这样能减少不同来源文本格式差异对全文索引和摘要截取的影响。 + string combined = string.Join(' ', values.Where(item => !string.IsNullOrWhiteSpace(item))); + return Regex.Replace(combined, @"\s+", " ").Trim(); + } + + private static string SerializeRouteQuery(Dictionary routeQuery) + { + return JsonSerializer.Serialize(routeQuery ?? new Dictionary()); + } + + private static string BuildMenuDocId(long menuId) => $"menu:{menuId}"; + + private static string BuildWebDocId(long webCode) => $"web:{webCode}"; + + private static string BuildWeb1DocId(long webCode, long typeId, long deviceId) => $"web1:{webCode}:{typeId}:{deviceId}"; + + private static string BuildDocumentDocId(string documentId) => $"doc:{documentId}"; + + private static string BuildConfigTypeDocId(long configTypeId) => $"configType:{configTypeId}"; + + private static string BuildWebRoute(string webCode) + { + if (string.Equals(webCode, "-1", StringComparison.Ordinal)) + { + // -1 在门户里约定表示首页。 + return "/index"; + } + + if (string.Equals(webCode, "7", StringComparison.Ordinal)) + { + // 7 在门户里约定表示产品中心。 + return "/productCenter"; + } + + // 其余页面目前统一走 /test,再由 query 参数决定真正内容。 + // 这属于历史前端路由设计,后端不能自作主张改掉。 + return "/test"; + } + + private static string BuildWebRouteQueryJson(string webCode) + { + if (string.Equals(webCode, "-1", StringComparison.Ordinal) || string.Equals(webCode, "7", StringComparison.Ordinal)) + { + return SerializeRouteQuery(new Dictionary()); + } + + return SerializeRouteQuery(new Dictionary { ["id"] = webCode }); + } + + private static string BuildConfigTypeRouteQueryJson(string routeWebCode, string configTypeId) + { + string normalized = !string.IsNullOrWhiteSpace(routeWebCode) ? routeWebCode : configTypeId; + if (string.IsNullOrWhiteSpace(normalized)) + { + return SerializeRouteQuery(new Dictionary()); + } + + // 这里同时写 id / configTypeId 两个键,是为了兼容前台旧逻辑。 + // 旧前台某些页面读 id,某些页面读 configTypeId;只保留一个键会导致部分跳转失效。 + return SerializeRouteQuery(new Dictionary + { + ["id"] = normalized, + ["configTypeId"] = normalized + }); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchQueryService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchQueryService.cs new file mode 100644 index 0000000..77f0c24 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchQueryService.cs @@ -0,0 +1,188 @@ +using System.Data; +using System.Data.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Admin.NET.Plugin.HwPortal; + +public sealed class HwSearchQueryService : ITransient +{ + private readonly HwPortalSearchDbContext _db; + private readonly HwPortalSearchOptions _options; + private readonly IHwSearchSchemaService _schemaService; + + public HwSearchQueryService(HwPortalSearchDbContext db, IOptions options, IHwSearchSchemaService schemaService) + { + _db = db; + _options = options.Value; + _schemaService = schemaService; + } + + public async Task> SearchAsync(string keyword, int take, CancellationToken cancellationToken = default) + { + await _schemaService.EnsureCreatedAsync(cancellationToken); + + int normalizedTake = Math.Clamp(take, 1, _options.TakeLimit); + List fullTextResult = await SearchWithFullTextAsync(keyword, normalizedTake, cancellationToken); + if (fullTextResult.Count > 0) + { + return fullTextResult; + } + + return await SearchWithLikeAsync(keyword, normalizedTake, cancellationToken); + } + + public async Task HasIndexedDocumentAsync(CancellationToken cancellationToken = default) + { + await _schemaService.EnsureCreatedAsync(cancellationToken); + return await _db.SearchDocs.AsNoTracking().AnyAsync(x => x.IsDelete == "0", cancellationToken); + } + + private async Task> SearchWithFullTextAsync(string keyword, int take, CancellationToken cancellationToken) + { + DbConnection connection = _db.Database.GetDbConnection(); + bool shouldClose = connection.State != ConnectionState.Open; + if (shouldClose) + { + await connection.OpenAsync(cancellationToken); + } + + try + { + await using DbCommand command = connection.CreateCommand(); + command.CommandText = + """ + SELECT + s.source_type AS SourceType, + s.biz_id AS BizId, + s.title AS Title, + s.content AS Content, + s.web_code AS WebCode, + s.type_id AS TypeId, + s.device_id AS DeviceId, + s.menu_id AS MenuId, + s.document_id AS DocumentId, + s.base_score AS Score, + s.updated_at AS UpdatedAt, + s.route AS Route, + s.route_query_json AS RouteQueryJson, + s.edit_route AS EditRoute + FROM hw_portal_search_doc s + WHERE s.is_delete = '0' + AND MATCH(s.title, s.content) AGAINST (@keyword IN NATURAL LANGUAGE MODE) + ORDER BY + MATCH(s.title, s.content) AGAINST (@keyword IN NATURAL LANGUAGE MODE) DESC, + CASE WHEN s.title IS NOT NULL AND s.title LIKE CONCAT('%', @likeKeyword, '%') THEN 1 ELSE 0 END DESC, + CASE WHEN s.content IS NOT NULL AND s.content LIKE CONCAT('%', @likeKeyword, '%') THEN 1 ELSE 0 END DESC, + s.base_score DESC, + s.updated_at DESC + LIMIT @take; + """; + AddParameter(command, "@keyword", keyword); + AddParameter(command, "@likeKeyword", keyword); + AddParameter(command, "@take", take); + + List rows = new(); + await using DbDataReader reader = await command.ExecuteReaderAsync(cancellationToken); + while (await reader.ReadAsync(cancellationToken)) + { + rows.Add(new SearchRawRecord + { + SourceType = GetString(reader, "SourceType"), + BizId = GetString(reader, "BizId"), + Title = GetString(reader, "Title"), + Content = GetString(reader, "Content"), + WebCode = GetString(reader, "WebCode"), + TypeId = GetString(reader, "TypeId"), + DeviceId = GetString(reader, "DeviceId"), + MenuId = GetString(reader, "MenuId"), + DocumentId = GetString(reader, "DocumentId"), + Score = GetNullableInt(reader, "Score"), + UpdatedAt = GetNullableDateTime(reader, "UpdatedAt"), + Route = GetString(reader, "Route"), + RouteQueryJson = GetString(reader, "RouteQueryJson"), + EditRoute = GetString(reader, "EditRoute") + }); + } + + return rows; + } + finally + { + if (shouldClose) + { + await connection.CloseAsync(); + } + } + } + + private async Task> SearchWithLikeAsync(string keyword, int take, CancellationToken cancellationToken) + { + string likeKeyword = $"%{keyword}%"; + + // Why: + // FULLTEXT 对中文和短词的命中并不稳定,因此这里保留 EF LINQ 版兜底, + // 即使全文索引未命中,也能保证门户搜索功能可用。 + List docs = await _db.SearchDocs + .AsNoTracking() + .Where(x => x.IsDelete == "0") + .Where(x => + (x.Title != null && EF.Functions.Like(x.Title, likeKeyword)) || + (x.Content != null && EF.Functions.Like(x.Content, likeKeyword))) + .OrderByDescending(x => x.Title != null && EF.Functions.Like(x.Title, likeKeyword)) + .ThenByDescending(x => x.Content != null && EF.Functions.Like(x.Content, likeKeyword)) + .ThenByDescending(x => x.BaseScore) + .ThenByDescending(x => x.UpdatedAt) + .Take(take) + .ToListAsync(cancellationToken); + + return docs.Select(ToRawRecord).ToList(); + } + + private static SearchRawRecord ToRawRecord(HwPortalSearchDoc doc) + { + return new SearchRawRecord + { + SourceType = doc.SourceType, + BizId = doc.BizId, + Title = doc.Title, + Content = doc.Content, + WebCode = doc.WebCode, + TypeId = doc.TypeId, + DeviceId = doc.DeviceId, + MenuId = doc.MenuId, + DocumentId = doc.DocumentId, + Score = doc.BaseScore, + UpdatedAt = doc.UpdatedAt, + Route = doc.Route, + RouteQueryJson = doc.RouteQueryJson, + EditRoute = doc.EditRoute + }; + } + + private static void AddParameter(DbCommand command, string name, object value) + { + DbParameter parameter = command.CreateParameter(); + parameter.ParameterName = name; + parameter.Value = value ?? DBNull.Value; + command.Parameters.Add(parameter); + } + + private static string GetString(DbDataReader reader, string name) + { + object value = reader[name]; + return value == DBNull.Value ? null : Convert.ToString(value); + } + + private static int? GetNullableInt(DbDataReader reader, string name) + { + object value = reader[name]; + return value == DBNull.Value ? null : Convert.ToInt32(value); + } + + private static DateTime? GetNullableDateTime(DbDataReader reader, string name) + { + object value = reader[name]; + return value == DBNull.Value ? null : Convert.ToDateTime(value); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchRebuildService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchRebuildService.cs new file mode 100644 index 0000000..2293588 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchRebuildService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal; + +public sealed class HwSearchRebuildService : IHwSearchRebuildService, ITransient +{ + private readonly HwPortalSearchDbContext _db; + private readonly IHwSearchSchemaService _schemaService; + private readonly IHwSearchIndexService _indexService; + private readonly HwWebMenuService _menuService; + private readonly HwWebService _webService; + private readonly HwWeb1Service _web1Service; + private readonly HwWebDocumentService _documentService; + private readonly HwPortalConfigTypeService _configTypeService; + + public HwSearchRebuildService( + HwPortalSearchDbContext db, + IHwSearchSchemaService schemaService, + IHwSearchIndexService indexService, + HwWebMenuService menuService, + HwWebService webService, + HwWeb1Service web1Service, + HwWebDocumentService documentService, + HwPortalConfigTypeService configTypeService) + { + _db = db; + _schemaService = schemaService; + _indexService = indexService; + _menuService = menuService; + _webService = webService; + _web1Service = web1Service; + _documentService = documentService; + _configTypeService = configTypeService; + } + + public async Task RebuildAllAsync() + { + await _schemaService.EnsureCreatedAsync(); + await _db.Database.ExecuteSqlRawAsync("DELETE FROM `hw_portal_search_doc`;"); + + foreach (HwWebMenu item in await _menuService.SelectHwWebMenuList(new HwWebMenu())) + { + await _indexService.UpsertMenuAsync(item); + } + + foreach (HwWeb item in await _webService.SelectHwWebList(new HwWeb())) + { + await _indexService.UpsertWebAsync(item); + } + + foreach (HwWeb1 item in await _web1Service.SelectHwWebList(new HwWeb1())) + { + await _indexService.UpsertWeb1Async(item); + } + + foreach (HwWebDocument item in await _documentService.SelectHwWebDocumentList(new HwWebDocument())) + { + await _indexService.UpsertDocumentAsync(item); + } + + foreach (HwPortalConfigType item in await _configTypeService.SelectHwPortalConfigTypeList(new HwPortalConfigType())) + { + await _indexService.UpsertConfigTypeAsync(item); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchSchemaService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchSchemaService.cs new file mode 100644 index 0000000..5823800 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/HwSearchSchemaService.cs @@ -0,0 +1,415 @@ +// ============================================================================ +// 【文件说明】HwSearchSchemaService.cs - 搜索表结构初始化服务 +// ============================================================================ +// 这个服务负责自动创建搜索索引表和索引。 +// +// 【为什么需要自动建表?】 +// 1. 开发环境:快速启动,不需要手动执行 SQL 脚本 +// 2. 测试环境:每次测试前可以重建干净的表 +// 3. CI/CD:自动化部署时自动创建表结构 +// +// 【设计模式 - 单例初始化】 +// 这个服务使用"延迟初始化 + 双重检查锁"模式: +// 1. 第一次调用时才创建表 +// 2. 使用锁保证线程安全 +// 3. 使用 volatile 标记避免指令重排 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用 Flyway 或 Liquibase 做数据库迁移: +// @Bean +// public Flyway flyway() { +// return Flyway.configure() +// .locations("db/migration") +// .dataSource(dataSource) +// .load(); +// } +// +// C# 这里用代码直接建表,更简单但不适合复杂迁移场景。 +// ============================================================================ + +using System.Data; +using System.Data.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索表结构初始化服务。 +/// +/// 【服务职责】 +/// 1. 检查搜索表是否存在 +/// 2. 创建搜索表(如果不存在) +/// 3. 创建索引(唯一索引、普通索引、全文索引) +/// +/// +/// 【C# 语法知识点 - sealed 密封类 + ITransient】 +/// sealed + ITransient 的组合表示: +/// - 这是一个瞬态服务(每次请求创建新实例) +/// - 不能被继承(不需要扩展) +/// +/// 对比 Java: +/// Java 通常用 @Service + @Scope("prototype") 实现类似效果。 +/// +/// +public sealed class HwSearchSchemaService : IHwSearchSchemaService, ITransient +{ + /// + /// 表名常量。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是编译期常量,值在编译时就确定了。 + /// 编译器会把常量值直接嵌入调用处。 + /// + /// + private const string TableName = "hw_portal_search_doc"; + + /// + /// 初始化锁。 + /// + /// 【C# 语法知识点 - SemaphoreSlim 信号量】 + /// SemaphoreSlim 是轻量级信号量,用于异步场景的并发控制。 + /// + /// SemaphoreSlim(1, 1) 表示: + /// - 初始计数:1(允许 1 个线程进入) + /// - 最大计数:1(最多允许 1 个线程同时进入) + /// + /// 这是一个"互斥锁"(Mutex),保证同一时间只有一个线程能执行初始化。 + /// + /// 对比 Java: + /// Java 用 ReentrantLock 或 synchronized: + /// private static final ReentrantLock lock = new ReentrantLock(); + /// lock.lock(); + /// try { ... } finally { lock.unlock(); } + /// + /// C# 的 SemaphoreSlim 支持 async/await,更适合异步编程。 + /// + /// + /// 【为什么用 static readonly?】 + /// - static:所有实例共享同一个锁 + /// - readonly:只能在声明时或构造函数中赋值 + /// + /// 这样保证整个应用只有一个锁,所有线程都竞争同一个锁。 + /// + /// + private static readonly SemaphoreSlim InitLock = new(1, 1); + + /// + /// 初始化完成标记。 + /// + /// 【C# 语法知识点 - volatile 关键字】 + /// volatile 表示"易变字段",告诉编译器: + /// 1. 不要优化这个字段的访问(如缓存到寄存器) + /// 2. 每次读写都直接操作内存 + /// 3. 避免指令重排 + /// + /// 为什么需要 volatile? + /// 在多线程环境下,一个线程修改了 _initialized,其他线程要能立即看到。 + /// + /// 对比 Java: + /// Java 也有 volatile 关键字,含义完全一样: + /// private static volatile boolean initialized = false; + /// + /// + private static volatile bool _initialized; + + /// + /// 数据库上下文。 + /// + private readonly HwPortalSearchDbContext _db; + + /// + /// 搜索配置选项。 + /// + /// 【C# 语法知识点 - IOptions<T> 模式】 + /// IOptions<T> 是 ASP.NET Core 的配置模式: + /// 1. 在 Startup 中注册配置 + /// 2. 通过依赖注入获取 IOptions<T> + /// 3. .Value 获取配置实例 + /// + /// 对比 Java Spring Boot: + /// Java 通常直接注入配置类: + /// @Autowired + /// private HwPortalSearchProperties properties; + /// + /// C# 的 IOptions 模式更灵活,支持配置热更新。 + /// + /// + private readonly HwPortalSearchOptions _options; + + /// + /// 构造函数(依赖注入)。 + /// + /// 数据库上下文 + /// 配置选项 + public HwSearchSchemaService(HwPortalSearchDbContext db, IOptions options) + { + _db = db; + _options = options.Value; + } + + /// + /// 确保搜索表已创建。 + /// + /// 【双重检查锁模式】 + /// 这是经典的线程安全单例初始化模式: + /// + /// 1. 第一次检查(无锁):if (!_options.AutoInitSchema || _initialized) + /// - 快速路径:如果已初始化,直接返回,不获取锁 + /// - 性能优化:避免每次都获取锁 + /// + /// 2. 获取锁:await InitLock.WaitAsync(cancellationToken) + /// - 保证同一时间只有一个线程能执行初始化 + /// + /// 3. 第二次检查(有锁):if (_initialized) + /// - 防止重复初始化 + /// - 场景:线程 A 和 B 同时通过第一次检查,A 先获取锁并初始化完成, + /// B 获取锁后要再次检查是否已初始化 + /// + /// 对比 Java: + /// Java 的双重检查锁写法类似: + /// if (!initialized) { + /// synchronized (lock) { + /// if (!initialized) { + /// // 初始化 + /// initialized = true; + /// } + /// } + /// } + /// + /// C# 的异步版本需要用 SemaphoreSlim 替代 lock。 + /// + /// + /// 取消令牌 + public async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) + { + // 【第一次检查 - 快速路径】 + // 如果配置关闭了自动初始化,或者已经初始化完成,直接返回。 + if (!_options.AutoInitSchema || _initialized) + { + return; + } + + // 【获取锁】 + // WaitAsync 会阻塞当前线程,直到获取锁。 + // cancellationToken 支持取消等待。 + await InitLock.WaitAsync(cancellationToken); + try + { + // 【第二次检查 - 防止重复初始化】 + if (_initialized) + { + return; + } + + // 【执行建表 SQL】 + // ExecuteSqlRawAsync 执行原生 SQL 语句。 + // """...""" 是 C# 11 的"原始字符串字面量"语法: + // - 三个双引号开始和结束 + // - 内部可以包含换行、引号等特殊字符 + // - 不需要转义 + // + // 对比 Java: + // Java 需要用字符串拼接或文本块(Java 15+): + // String sql = """ + // CREATE TABLE IF NOT EXISTS ... + // """; + // + // C# 的原始字符串更灵活,可以控制缩进。 + await _db.Database.ExecuteSqlRawAsync( + """ + CREATE TABLE IF NOT EXISTS `hw_portal_search_doc` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `doc_id` VARCHAR(128) NOT NULL COMMENT '搜索文档唯一键', + `source_type` VARCHAR(32) NOT NULL COMMENT '来源类型', + `biz_id` VARCHAR(64) NULL COMMENT '业务主键', + `title` VARCHAR(500) NULL COMMENT '搜索标题', + `content` LONGTEXT NULL COMMENT '搜索正文', + `web_code` VARCHAR(64) NULL COMMENT '页面编码', + `type_id` VARCHAR(64) NULL COMMENT '类型ID', + `device_id` VARCHAR(64) NULL COMMENT '设备ID', + `menu_id` VARCHAR(64) NULL COMMENT '菜单ID', + `document_id` VARCHAR(64) NULL COMMENT '文档ID', + `base_score` INT NOT NULL DEFAULT 0 COMMENT '基础分', + `route` VARCHAR(255) NULL COMMENT '前台路由', + `route_query_json` JSON NULL COMMENT '前台路由参数', + `edit_route` VARCHAR(255) NULL COMMENT '编辑路由', + `is_delete` CHAR(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', + `updated_at` DATETIME NULL COMMENT '业务更新时间', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '索引创建时间', + `modified_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '索引更新时间', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='hw-portal 搜索索引表'; + """, + cancellationToken); + + // 【创建索引】 + // 索引可以加速查询,但需要单独创建。 + // EnsureIndexAsync 方法会检查索引是否存在,不存在才创建。 + await EnsureIndexAsync("uk_hw_portal_search_doc_doc_id", + "ALTER TABLE `hw_portal_search_doc` ADD UNIQUE KEY `uk_hw_portal_search_doc_doc_id` (`doc_id`);", + cancellationToken); + await EnsureIndexAsync("idx_hw_portal_search_doc_source_type", + "ALTER TABLE `hw_portal_search_doc` ADD KEY `idx_hw_portal_search_doc_source_type` (`source_type`);", + cancellationToken); + await EnsureIndexAsync("idx_hw_portal_search_doc_updated_at", + "ALTER TABLE `hw_portal_search_doc` ADD KEY `idx_hw_portal_search_doc_updated_at` (`updated_at`);", + cancellationToken); + + // 【全文索引】 + // FULLTEXT 索引用于全文搜索,支持 LIKE '%keyword%' 查询优化。 + // 只有 MyISAM 和 InnoDB(MySQL 5.6+)引擎支持全文索引。 + await EnsureIndexAsync("ft_hw_portal_search_doc_title_content", + "ALTER TABLE `hw_portal_search_doc` ADD FULLTEXT KEY `ft_hw_portal_search_doc_title_content` (`title`, `content`);", + cancellationToken); + + // 【标记初始化完成】 + // 必须在所有操作完成后才设置,否则其他线程可能提前返回。 + _initialized = true; + } + finally + { + // 【释放锁】 + // finally 保证无论成功还是异常,锁都会被释放。 + // 如果不释放,后续所有请求都会被阻塞。 + InitLock.Release(); + } + } + + /// + /// 确保索引存在(不存在则创建)。 + /// + /// 索引名称 + /// 创建索引的 DDL 语句 + /// 取消令牌 + private async Task EnsureIndexAsync(string indexName, string ddl, CancellationToken cancellationToken) + { + // 先检查索引是否存在。 + long count = await GetIndexCountAsync(indexName, cancellationToken); + if (count > 0) + { + // 索引已存在,跳过创建。 + return; + } + + // 索引不存在,执行创建语句。 + await _db.Database.ExecuteSqlRawAsync(ddl, cancellationToken); + } + + /// + /// 查询索引是否存在。 + /// + /// 【查询方式】 + /// 通过查询 information_schema.statistics 表判断索引是否存在: + /// - information_schema 是 MySQL 的元数据库 + /// - statistics 表存储了所有表和索引的信息 + /// + /// 对比 Java JDBC: + /// Java 通常用 DatabaseMetaData 获取索引信息: + /// DatabaseMetaData metaData = connection.getMetaData(); + /// ResultSet indexes = metaData.getIndexInfo(null, null, "hw_portal_search_doc", false, false); + /// + /// + /// 索引名称 + /// 取消令牌 + /// 索引数量(0 表示不存在) + private async Task GetIndexCountAsync(string indexName, CancellationToken cancellationToken) + { + // 【获取底层 ADO.NET 连接】 + // _db.Database.GetDbConnection() 获取 EF Core 包装的底层 DbConnection。 + // 有时需要直接使用 ADO.NET API,如执行原生 SQL、获取元数据等。 + DbConnection connection = _db.Database.GetDbConnection(); + + // 【连接状态检查】 + // 如果连接未打开,需要手动打开。 + // EF Core 通常会自动管理连接,但这里我们需要确保连接可用。 + bool shouldClose = connection.State != ConnectionState.Open; + if (shouldClose) + { + await connection.OpenAsync(cancellationToken); + } + + try + { + // 【创建命令】 + // await using 是 C# 8 的"异步 using"语法。 + // 会自动调用 DisposeAsync,适合实现了 IAsyncDisposable 的对象。 + // + // 对比 Java: + // Java 用 try-with-resources: + // try (PreparedStatement stmt = connection.prepareStatement(sql)) { + // ... + // } + await using DbCommand command = connection.CreateCommand(); + command.CommandText = + """ + SELECT COUNT(1) + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = @tableName + AND index_name = @indexName; + """; + + // 【添加参数】 + // 使用参数化查询,防止 SQL 注入。 + AddParameter(command, "@tableName", TableName); + AddParameter(command, "@indexName", indexName); + + // 【执行查询】 + // ExecuteScalarAsync 返回结果集的第一行第一列。 + // 这里返回的是 COUNT(1) 的结果,即索引数量。 + object result = await command.ExecuteScalarAsync(cancellationToken); + + // 【结果转换】 + // result 可能是 null 或 DBNull(理论上不应该,但防御性编程)。 + // Convert.ToInt64 可以处理多种数值类型(int, long, decimal 等)。 + return result == null || result == DBNull.Value ? 0 : Convert.ToInt64(result); + } + finally + { + // 【关闭连接】 + // 如果是我们打开的连接,用完后要关闭。 + // 如果连接本来就是打开的,不要关闭(可能是事务中的连接)。 + if (shouldClose) + { + await connection.CloseAsync(); + } + } + } + + /// + /// 添加命令参数。 + /// + /// 【C# 语法知识点 - static 静态方法】 + /// private static void AddParameter(...) + /// + /// 静态方法不依赖实例状态,可以直接调用。 + /// 这个方法是工具方法,不需要访问实例成员,所以声明为 static。 + /// + /// 对比 Java: + /// Java 的静态方法语法完全一样。 + /// + /// + /// 数据库命令 + /// 参数名 + /// 参数值 + private static void AddParameter(DbCommand command, string name, object value) + { + // 【创建参数】 + // CreateParameter() 创建一个参数对象。 + // 不同数据库提供者的参数类型不同,但都继承自 DbParameter。 + DbParameter parameter = command.CreateParameter(); + parameter.ParameterName = name; + + // 【空值处理】 + // value ?? DBNull.Value 表示: + // - 如果 value 不为 null,使用 value + // - 如果 value 为 null,使用 DBNull.Value + // + // 数据库不能直接存储 C# 的 null,需要用 DBNull.Value 表示数据库的 NULL。 + parameter.Value = value ?? DBNull.Value; + + // 【添加参数到命令】 + command.Parameters.Add(parameter); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchIndexService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchIndexService.cs new file mode 100644 index 0000000..d2d1e4e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchIndexService.cs @@ -0,0 +1,152 @@ +// ============================================================================ +// 【文件说明】IHwSearchIndexService.cs - 搜索索引服务接口 +// ============================================================================ +// 这个接口定义了搜索索引的增删改操作契约。 +// +// 【CQRS 模式说明】 +// 这个接口体现了 CQRS(命令查询职责分离)的"命令"部分: +// - 命令:增删改操作 +// - 查询:搜索操作(在 HwSearchQueryService 中) +// +// 【Upsert 操作说明】 +// Upsert = Update + Insert,是一种"合并"操作: +// - 如果记录存在,更新 +// - 如果记录不存在,插入 +// +// 这是索引同步的常用模式,避免先查询再决定 insert/update。 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用 Repository 模式: +// public interface SearchIndexRepository { +// void upsertMenu(HwWebMenu menu); +// void deleteMenu(Long menuId); +// } +// +// C# 这里用 Service 接口,语义更接近业务操作。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索索引服务接口。 +/// +/// 【服务职责】 +/// 负责搜索索引的增删改操作: +/// 1. Upsert:插入或更新索引 +/// 2. Delete:删除索引 +/// +/// 注意:这个接口只负责"写"操作,"读"操作在 HwSearchQueryService 中。 +/// +/// +/// 【接口方法命名约定】 +/// - UpsertXxxAsync:插入或更新某个类型的索引 +/// - DeleteXxxAsync:删除某个类型的索引 +/// - Async 后缀:表示异步方法 +/// +/// 对比 Java: +/// Java 通常不用 Async 后缀,而是返回 CompletableFuture: +/// CompletableFuture upsertMenu(HwWebMenu menu); +/// +/// +public interface IHwSearchIndexService +{ + /// + /// 插入或更新菜单索引。 + /// + /// 菜单实体 + /// 取消令牌 + Task UpsertMenuAsync(HwWebMenu menu, CancellationToken cancellationToken = default); + + /// + /// 插入或更新页面索引。 + /// + /// 页面实体 + /// 取消令牌 + Task UpsertWebAsync(HwWeb web, CancellationToken cancellationToken = default); + + /// + /// 插入或更新页面1索引。 + /// + /// 页面1实体 + /// 取消令牌 + Task UpsertWeb1Async(HwWeb1 web1, CancellationToken cancellationToken = default); + + /// + /// 插入或更新文档索引。 + /// + /// 文档实体 + /// 取消令牌 + Task UpsertDocumentAsync(HwWebDocument document, CancellationToken cancellationToken = default); + + /// + /// 插入或更新配置类型索引。 + /// + /// 配置类型实体 + /// 取消令牌 + Task UpsertConfigTypeAsync(HwPortalConfigType configType, CancellationToken cancellationToken = default); + + /// + /// 删除菜单索引。 + /// + /// 菜单ID + /// 取消令牌 + Task DeleteMenuAsync(long menuId, CancellationToken cancellationToken = default); + + /// + /// 删除页面索引。 + /// + /// 页面编码 + /// 取消令牌 + Task DeleteWebAsync(long webCode, CancellationToken cancellationToken = default); + + /// + /// 删除页面1索引。 + /// + /// 【复合主键删除】 + /// Web1 的索引由多个字段组成复合键: + /// - webCode:页面编码 + /// - typeId:类型ID + /// - deviceId:设备ID + /// + /// 删除时需要提供所有键字段。 + /// + /// + /// 页面编码 + /// 类型ID + /// 设备ID + /// 取消令牌 + Task DeleteWeb1Async(long webCode, long typeId, long deviceId, CancellationToken cancellationToken = default); + + /// + /// 删除文档索引。 + /// + /// 文档ID + /// 取消令牌 + Task DeleteDocumentAsync(string documentId, CancellationToken cancellationToken = default); + + /// + /// 删除配置类型索引。 + /// + /// 配置类型ID + /// 取消令牌 + Task DeleteConfigTypeAsync(long configTypeId, CancellationToken cancellationToken = default); + + /// + /// 根据文档ID删除索引。 + /// + /// 【通用删除方法】 + /// DocId 是索引的全局唯一标识,格式:{SourceType}_{BizId} + /// + /// 这个方法提供了更通用的删除方式: + /// - 不需要知道具体类型 + /// - 只需要 DocId + /// + /// 使用场景: + /// 1. 批量删除:遍历 DocId 列表逐个删除 + /// 2. 清理无效索引:根据外部数据源清理 + /// + /// + /// 文档唯一ID + /// 取消令牌 + Task DeleteByDocIdAsync(string docId, CancellationToken cancellationToken = default); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchSchemaService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchSchemaService.cs new file mode 100644 index 0000000..71518b1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/SearchEf/Service/IHwSearchSchemaService.cs @@ -0,0 +1,93 @@ +// ============================================================================ +// 【文件说明】IHwSearchSchemaService.cs - 搜索表结构服务接口 +// ============================================================================ +// 这个接口定义了搜索表结构初始化的契约。 +// +// 【为什么需要接口?】 +// 1. 依赖倒置原则(DIP):高层模块依赖抽象,不依赖具体实现 +// 2. 便于测试:可以 Mock 这个接口进行单元测试 +// 3. 便于扩展:未来可以有不同的实现(如支持 PostgreSQL) +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// public interface IHwSearchSchemaService { +// CompletableFuture ensureCreatedAsync(); +// } +// +// C# ASP.NET Core: +// public interface IHwSearchSchemaService { +// Task EnsureCreatedAsync(CancellationToken cancellationToken = default); +// } +// +// 两者概念完全一样,只是异步返回类型不同: +// - Java 用 CompletableFuture +// - C# 用 Task +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索表结构服务接口。 +/// +/// 【C# 语法知识点 - 接口定义】 +/// public interface IHwSearchSchemaService +/// +/// 接口定义了一组方法签名,不包含实现: +/// - 只声明方法,不实现方法 +/// - 实现类必须提供所有方法的具体实现 +/// +/// 对比 Java: +/// Java 的接口语法完全一样: +/// public interface IHwSearchSchemaService { +/// void ensureCreatedAsync(); +/// } +/// +/// 【命名约定】 +/// C# 接口命名约定:I + PascalCase +/// - IHwSearchSchemaService:I 前缀表示 Interface +/// - HwSearchSchemaService:实现类不加 I 前缀 +/// +/// Java 接口命名约定:不加 I 前缀 +/// - HwSearchSchemaService:接口 +/// - HwSearchSchemaServiceImpl:实现类 +/// +/// +public interface IHwSearchSchemaService +{ + /// + /// 确保搜索表已创建。 + /// + /// 【方法语义】 + /// "Ensure" 表示"确保",是一种幂等操作: + /// - 如果表已存在,不做任何操作 + /// - 如果表不存在,创建表 + /// + /// 对比 "Create": + /// - Create:每次都创建,重复调用会报错 + /// - EnsureCreated:幂等,可以安全地多次调用 + /// + /// + /// 【C# 语法知识点 - CancellationToken 参数】 + /// CancellationToken cancellationToken = default + /// + /// 这是 C# 异步编程的标准模式: + /// - CancellationToken:取消令牌,用于取消长时间运行的操作 + /// - = default:默认值,调用时可以不传 + /// + /// 使用场景: + /// 1. 用户取消请求:浏览器关闭或刷新 + /// 2. 超时取消:操作超过指定时间 + /// 3. 应用关闭:服务器需要优雅关闭 + /// + /// 对比 Java: + /// Java 没有内置的取消机制,通常用自定义标志位: + /// private volatile boolean cancelled = false; + /// public void cancel() { cancelled = true; } + /// + /// C# 的 CancellationToken 是框架级支持,更优雅。 + /// + /// + /// 取消令牌 + /// 异步任务 + Task EnsureCreatedAsync(CancellationToken cancellationToken = default); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Analytics/HwAnalyticsService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Analytics/HwAnalyticsService.cs new file mode 100644 index 0000000..46e8103 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Analytics/HwAnalyticsService.cs @@ -0,0 +1,738 @@ +// ============================================================================ +// 【文件说明】HwAnalyticsService.cs - 网站访问统计分析服务 +// ============================================================================ +// 这个服务负责收集和分析网站访问数据,类似于 Google Analytics 的功能。 +// +// 【核心功能】 +// 1. 收集访问事件:页面浏览、搜索、下载等 +// 2. 解析 User-Agent:获取浏览器、操作系统、设备类型 +// 3. 生成日报统计:PV、UV、跳出率等 +// 4. 提供仪表盘数据:热门页面、搜索关键词排行 +// +// 【数据流程】 +// 1. 前端上报事件 → Collect 方法接收 +// 2. 解析并归一化数据 → 写入明细表 +// 3. 聚合计算日报 → 写入日报表 +// 4. 查询仪表盘 → 返回统计数据 +// +// 【隐私保护】 +// - IP 地址不直接存储,而是存储哈希值 +// - 访客 ID 由前端生成,不包含个人信息 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用定时任务聚合统计: +// @Scheduled(cron = "0 5 0 * * ?") // 每天凌晨 0:05 执行 +// public void aggregateDailyStats() { ... } +// +// C# 这里在写入事件时实时聚合,保证数据即时可用。 +// ============================================================================ + +using UAParser; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 网站访问统计分析服务。 +/// +/// 【服务职责】 +/// 1. 收集访问事件(页面浏览、搜索、下载等) +/// 2. 解析 User-Agent 获取设备信息 +/// 3. 聚合生成日报统计 +/// 4. 提供仪表盘查询接口 +/// +/// +/// 【C# 语法知识点 - ITransient 瞬态服务】 +/// ITransient 表示每次请求都创建新实例。 +/// 分析服务通常是无状态的,适合瞬态模式。 +/// +/// +public class HwAnalyticsService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + private const string Mapper = "HwAnalyticsMapper"; + private const string InsertVisitEventSql = + """ + INSERT INTO hw_web_visit_event ( + event_type, visitor_id, session_id, path, referrer, utm_source, utm_medium, utm_campaign, + keyword, ip_hash, ua, device, browser, os, stay_ms, event_time, created_at + ) VALUES ( + @EventType, @VisitorId, @SessionId, @Path, @Referrer, @UtmSource, @UtmMedium, @UtmCampaign, + @Keyword, @IpHash, @Ua, @Device, @Browser, @Os, @StayMs, @EventTime, NOW() + ) + """; + private const string CountEventByTypeSql = + """ + SELECT COUNT(1) + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = @eventType + """; + private const string CountDistinctVisitorSql = + """ + SELECT COUNT(DISTINCT visitor_id) + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + AND visitor_id IS NOT NULL + AND visitor_id != '' + """; + private const string CountDistinctIpSql = + """ + SELECT COUNT(DISTINCT ip_hash) + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + AND ip_hash IS NOT NULL + AND ip_hash != '' + """; + private const string AvgStayMsSql = + """ + SELECT COALESCE(ROUND(AVG(stay_ms)), 0) + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_leave' + AND stay_ms IS NOT NULL + AND stay_ms >= 0 + """; + private const string CountDistinctSessionsSql = + """ + SELECT COUNT(DISTINCT session_id) + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + AND session_id IS NOT NULL + AND session_id != '' + """; + private const string CountSinglePageSessionsSql = + """ + SELECT COUNT(1) + FROM ( + SELECT session_id + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + AND session_id IS NOT NULL + AND session_id != '' + GROUP BY session_id + HAVING COUNT(1) = 1 + ) t + """; + private const string SelectTopEntryPagesSql = + """ + SELECT IFNULL(e.path, '/') AS name, COUNT(1) AS value + FROM hw_web_visit_event e + INNER JOIN ( + SELECT session_id, MIN(event_time) AS min_event_time + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + AND session_id IS NOT NULL + AND session_id != '' + GROUP BY session_id + ) s ON s.session_id = e.session_id AND s.min_event_time = e.event_time + WHERE DATE(e.event_time) = @statDate + AND e.event_type = 'page_view' + GROUP BY e.path + ORDER BY value DESC + LIMIT @limit + """; + private const string SelectTopHotPagesSql = + """ + SELECT IFNULL(path, '/') AS name, COUNT(1) AS value + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'page_view' + GROUP BY path + ORDER BY value DESC + LIMIT @limit + """; + private const string SelectTopKeywordsSql = + """ + SELECT keyword AS name, COUNT(1) AS value + FROM hw_web_visit_event + WHERE DATE(event_time) = @statDate + AND event_type = 'search_submit' + AND keyword IS NOT NULL + AND keyword != '' + GROUP BY keyword + ORDER BY value DESC + LIMIT @limit + """; + private const string UpsertDailySql = + """ + INSERT INTO hw_web_visit_daily ( + stat_date, pv, uv, ip_uv, avg_stay_ms, bounce_rate, search_count, download_count, created_at, updated_at + ) VALUES ( + @StatDate, @Pv, @Uv, @IpUv, @AvgStayMs, @BounceRate, @SearchCount, @DownloadCount, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + pv = VALUES(pv), + uv = VALUES(uv), + ip_uv = VALUES(ip_uv), + avg_stay_ms = VALUES(avg_stay_ms), + bounce_rate = VALUES(bounce_rate), + search_count = VALUES(search_count), + download_count = VALUES(download_count), + updated_at = NOW() + """; + private const string SelectDailyByDateSql = + """ + SELECT stat_date AS StatDate, pv AS Pv, uv AS Uv, ip_uv AS IpUv, avg_stay_ms AS AvgStayMs, + bounce_rate AS BounceRate, search_count AS SearchCount, download_count AS DownloadCount, + created_at AS CreatedAt, updated_at AS UpdatedAt + FROM hw_web_visit_daily + WHERE stat_date = @statDate + """; + + /// + /// 允许的事件类型集合。 + /// + /// 【C# 语法知识点 - HashSet<T> 哈希集合】 + /// HashSet 适合做"是否存在"判断,查找效率通常比 List 更高。 + /// + /// 为什么用 HashSet? + /// - Contains 方法是 O(1) 时间复杂度 + /// - List 的 Contains 是 O(n) 时间复杂度 + /// + /// StringComparer.OrdinalIgnoreCase 参数: + /// - 忽略大小写比较 + /// - "page_view" 和 "PAGE_VIEW" 被视为相同 + /// + /// + /// 【业务说明】 + /// 只允许预定义的事件类型,避免脏数据进入统计表: + /// - page_view:页面浏览 + /// - page_leave:离开页面 + /// - search_submit:提交搜索 + /// - download_click:点击下载 + /// - contact_submit:提交联系表单 + /// + /// + private static readonly HashSet AllowedEvents = new(StringComparer.OrdinalIgnoreCase) + { + "page_view", "page_leave", "search_submit", "download_click", "contact_submit" + }; + + /// + /// MyBatis 执行器(依赖注入)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + private readonly ISqlSugarClient _db; + + /// + /// 日志记录器(依赖注入)。 + /// + /// 【C# 语法知识点 - ILogger<T> 泛型日志接口】 + /// ILogger<HwAnalyticsService> 是带类别的日志器: + /// - 日志会自动包含类名前缀 + /// - 便于筛选特定类的日志 + /// + /// 对比 Java: + /// Java: private static final Logger logger = LoggerFactory.getLogger(HwAnalyticsService.class); + /// C#: private readonly ILogger<HwAnalyticsService> _logger; + /// + /// C# 通过 DI 注入,更易于测试和配置。 + /// + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// 日志记录器 + public HwAnalyticsService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, ILogger logger) + { + _executor = executor; + _db = db; + _logger = logger; + } + + /// + /// 收集访问事件。 + /// + /// 【处理流程】 + /// 1. 校验事件类型是否合法 + /// 2. 归一化字段值(去除空白、截断长度) + /// 3. 解析 User-Agent 获取设备信息 + /// 4. 对 IP 地址进行哈希处理 + /// 5. 写入明细事件表 + /// 6. 刷新日报统计 + /// + /// + /// 事件请求 + /// 请求 IP + /// 请求 User-Agent + public async Task Collect(AnalyticsCollectRequest request, string requestIp, string requestUserAgent) + { + if (request == null) + { + // 【Furion 异常处理】 + // Oh.Oh("消息") 是 Furion 框架的异常抛出方式: + // - 自动包装成友好的错误响应 + // - 支持多语言 + // - 自动记录日志 + // + // 对比 Java: + // Java: throw new BusinessException("请求体不能为空"); + throw Oops.Oh("请求体不能为空"); + } + + // 先校验事件类型是否合法,避免脏数据进入统计表。 + string eventType = NormalizeText(request.EventType, 32); + if (!AllowedEvents.Contains(eventType)) + { + throw Oops.Oh("不支持的事件类型"); + } + + // 【对象初始化器】 + // 对象初始化时统一做字段归一化和长度裁剪。 + // new HwWebVisitEvent { ... } 是对象初始化器语法。 + HwWebVisitEvent entity = new() + { + EventType = eventType, + VisitorId = RequireText(request.VisitorId, 64, "visitorId不能为空"), + SessionId = RequireText(request.SessionId, 64, "sessionId不能为空"), + Path = RequireText(request.Path, 500, "path不能为空"), + Referrer = NormalizeText(request.Referrer, 500), + UtmSource = NormalizeText(request.UtmSource, 128), + UtmMedium = NormalizeText(request.UtmMedium, 128), + UtmCampaign = NormalizeText(request.UtmCampaign, 128), + Keyword = NormalizeText(request.Keyword, 128), + StayMs = NormalizeStayMs(request.StayMs), + EventTime = ResolveEventTime(request.EventTime) + }; + + // Why:优先用前端上报的 ua;如果前端没传,就退回请求头里的 User-Agent。 + string ua = NormalizeText(string.IsNullOrWhiteSpace(request.Ua) ? requestUserAgent : request.Ua, 500); + + // 【User-Agent 解析】 + // UAParser 是一个现成库,用来从 User-Agent 字符串里拆出浏览器、系统等信息。 + // Parser.GetDefault() 获取默认解析器。 + // Parse(ua) 返回 ClientInfo 对象,包含: + // - UA:浏览器信息 + // - OS:操作系统信息 + // - Device:设备信息 + ClientInfo client = Parser.GetDefault().Parse(ua ?? string.Empty); + entity.Ua = ua; + + // 【null 合并运算符 ??】 + // 如果前端传了 Browser,用前端的值;否则用解析器的值。 + entity.Browser = NormalizeText(string.IsNullOrWhiteSpace(request.Browser) ? client.UA.Family : request.Browser, 64); + entity.Os = NormalizeText(string.IsNullOrWhiteSpace(request.Os) ? client.OS.Family : request.Os, 64); + entity.Device = NormalizeText(string.IsNullOrWhiteSpace(request.Device) ? DetectDevice(ua) : request.Device, 64); + entity.IpHash = BuildIpHash(requestIp); + + // 先写明细事件,再刷新日报统计。 + // 回滚到 XML 方案时可直接恢复: + // await _executor.InsertReturnIdentityAsync(Mapper, "insertVisitEvent", entity); + await _db.Ado.ExecuteCommandAsync(InsertVisitEventSql, BuildParameters(entity)); + await RefreshDailyStat(entity.EventTime?.Date ?? DateTime.Now.Date); + } + + /// + /// 获取仪表盘统计数据。 + /// + /// 【返回数据】 + /// - PV:页面浏览量 + /// - UV:独立访客数 + /// - IP UV:独立 IP 数 + /// - 跳出率:只浏览一页的会话占比 + /// - 热门页面排行 + /// - 搜索关键词排行 + /// + /// + /// 统计日期 + /// 排行条数限制 + /// 仪表盘数据 + public async Task GetDashboard(DateTime? statDate, int? rankLimit) + { + // 【DateTime.Date 属性】 + // .Date 会把时间截断到"当天 00:00:00"。 + // 例如:2024-01-15 14:30:00 → 2024-01-15 00:00:00 + DateTime targetDate = (statDate ?? DateTime.Now).Date; + int topN = NormalizeRankLimit(rankLimit); + + // 先刷新统计,确保数据最新。 + await RefreshDailyStat(targetDate); + + // 【查询日报数据】 + // QuerySingleAsync<T> 查询单条记录。 + // 如果查询结果为空,返回 null。 + // 回滚到 XML 方案时可直接恢复: + // HwWebVisitDaily daily = await _executor.QuerySingleAsync(Mapper, "selectDailyByDate", new { statDate = targetDate }); + HwWebVisitDaily daily = await QuerySingleOrDefaultAsync(SelectDailyByDateSql, new { statDate = targetDate }); + + // 【null 合并运算符 ??】 + // daily?.Pv ?? 0 表示: + // - 如果 daily 为 null,返回 0 + // - 如果 daily.Pv 为 null,返回 0 + // - 否则返回 daily.Pv + AnalyticsDashboardDTO dashboard = new() + { + StatDate = targetDate.ToString("yyyy-MM-dd"), + Pv = daily?.Pv ?? 0, + Uv = daily?.Uv ?? 0, + IpUv = daily?.IpUv ?? 0, + AvgStayMs = daily?.AvgStayMs ?? 0, + BounceRate = daily?.BounceRate ?? 0, + SearchCount = daily?.SearchCount ?? 0, + DownloadCount = daily?.DownloadCount ?? 0, + // 回滚到 XML 方案时可直接恢复: + // EntryPages = await _executor.QueryListAsync(Mapper, "selectTopEntryPages", new { statDate = targetDate, limit = topN }), + EntryPages = await QueryListAsync(SelectTopEntryPagesSql, new { statDate = targetDate, limit = topN }), + // 回滚到 XML 方案时可直接恢复: + // HotPages = await _executor.QueryListAsync(Mapper, "selectTopHotPages", new { statDate = targetDate, limit = topN }), + HotPages = await QueryListAsync(SelectTopHotPagesSql, new { statDate = targetDate, limit = topN }), + // 回滚到 XML 方案时可直接恢复: + // HotKeywords = await _executor.QueryListAsync(Mapper, "selectTopKeywords", new { statDate = targetDate, limit = topN }) + HotKeywords = await QueryListAsync(SelectTopKeywordsSql, new { statDate = targetDate, limit = topN }) + }; + return dashboard; + } + + /// + /// 刷新日报统计。 + /// + /// 【聚合逻辑】 + /// 从明细表聚合以下指标: + /// - PV:页面浏览次数 + /// - UV:独立访客数(按 visitorId 去重) + /// - IP UV:独立 IP 数(按 ipHash 去重) + /// - 平均停留时长 + /// - 搜索次数 + /// - 下载次数 + /// - 跳出率 + /// + /// + /// 统计日期 + public async Task RefreshDailyStat(DateTime? statDate) + { + // 日报统计的核心做法: + // 先从明细表聚合出 pv / uv / ip_uv / 搜索次数等指标,再 upsert 到日报表。 + DateTime targetDate = (statDate ?? DateTime.Now).Date; + + // 【查询聚合值】 + long pv = await QueryLong("countEventByType", new { statDate = targetDate, eventType = "page_view" }); + long uv = await QueryLong("countDistinctVisitor", new { statDate = targetDate }); + long ipUv = await QueryLong("countDistinctIp", new { statDate = targetDate }); + long avgStayMs = await QueryLong("avgStayMs", new { statDate = targetDate }); + long searchCount = await QueryLong("countEventByType", new { statDate = targetDate, eventType = "search_submit" }); + long downloadCount = await QueryLong("countEventByType", new { statDate = targetDate, eventType = "download_click" }); + long sessionCount = await QueryLong("countDistinctSessions", new { statDate = targetDate }); + long singlePageSessions = await QueryLong("countSinglePageSessions", new { statDate = targetDate }); + + // 【跳出率计算】 + double bounceRate = 0D; + if (sessionCount > 0) + { + // Why:跳出率 = 单页 session 数 / 总 session 数。 + // Math.Round 四舍五入: + // - 第一个参数:要舍入的值 + // - 第二个参数:保留小数位数 + // - 第三个参数:舍入方式(AwayFromZero 表示四舍五入) + bounceRate = Math.Round(singlePageSessions * 100.0D / sessionCount, 2, MidpointRounding.AwayFromZero); + } + + HwWebVisitDaily daily = new() + { + StatDate = targetDate, + Pv = pv, + Uv = uv, + IpUv = ipUv, + AvgStayMs = avgStayMs, + BounceRate = bounceRate, + SearchCount = searchCount, + DownloadCount = downloadCount + }; + + // 【Upsert 操作】 + // upsert = update + insert + // 如果记录存在,更新;不存在,插入。 + // 回滚到 XML 方案时可直接恢复: + // await _executor.ExecuteAsync(Mapper, "upsertDaily", daily); + await _db.Ado.ExecuteCommandAsync(UpsertDailySql, BuildParameters(daily)); + } + + /// + /// 查询单个长整数值。 + /// + /// SQL 语句 ID + /// 参数 + /// 查询结果(默认 0) + private async Task QueryLong(string statementId, object parameter) + { + // 回滚到 XML 方案时可直接恢复: + // long? value = await _executor.QuerySingleAsync(Mapper, statementId, parameter); + string sql = statementId switch + { + "countEventByType" => CountEventByTypeSql, + "countDistinctVisitor" => CountDistinctVisitorSql, + "countDistinctIp" => CountDistinctIpSql, + "avgStayMs" => AvgStayMsSql, + "countDistinctSessions" => CountDistinctSessionsSql, + "countSinglePageSessions" => CountSinglePageSessionsSql, + _ => throw Oops.Oh($"不支持的统计语句:{statementId}") + }; + List rows = await _db.Ado.SqlQueryAsync(sql, BuildParameters(parameter)); + return rows.FirstOrDefault() ?? 0L; + } + + private async Task QuerySingleOrDefaultAsync(string sql, object parameter) + { + List rows = await _db.Ado.SqlQueryAsync(sql, BuildParameters(parameter)); + return rows.FirstOrDefault(); + } + + private Task> QueryListAsync(string sql, object parameter) + { + return _db.Ado.SqlQueryAsync(sql, BuildParameters(parameter)); + } + + private static SugarParameter[] BuildParameters(object parameter) + { + if (parameter == null) + { + return Array.Empty(); + } + + if (parameter is SugarParameter[] sugarParameters) + { + return sugarParameters; + } + + if (parameter is IEnumerable sugarParameterEnumerable) + { + return sugarParameterEnumerable.ToArray(); + } + + List parameters = new(); + foreach (PropertyInfo property in parameter.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + object value = property.GetValue(parameter); + parameters.Add(new SugarParameter($"@{property.Name}", value ?? DBNull.Value)); + } + + return parameters.ToArray(); + } + + /// + /// 校验并获取必填文本。 + /// + /// 【处理逻辑】 + /// 1. 调用 NormalizeText 归一化 + /// 2. 如果结果为空,抛出异常 + /// + /// + /// 输入文本 + /// 最大长度 + /// 空值错误消息 + /// 归一化后的文本 + private static string RequireText(string text, int maxLen, string emptyMessage) + { + string normalized = NormalizeText(text, maxLen); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw Oops.Oh(emptyMessage); + } + + return normalized; + } + + /// + /// 归一化文本(去除空白、截断长度)。 + /// + /// 【C# 语法知识点 - 字符串截取】 + /// normalized[..maxLen] 是范围操作符: + // - 从索引 0 开始 + // - 到索引 maxLen 结束(不含) + // - 等价于 normalized.Substring(0, maxLen) + /// + /// + /// 输入文本 + /// 最大长度 + /// 归一化后的文本 + private static string NormalizeText(string text, int maxLen) + { + // Trim() 用于去掉首尾空白。 + string normalized = text?.Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + return null; + } + + // 【范围操作符】 + // normalized[..maxLen] 表示从开始到 maxLen 位置。 + // 如果长度超过 maxLen,截断;否则返回原字符串。 + return normalized.Length > maxLen ? normalized[..maxLen] : normalized; + } + + /// + /// 归一化停留时长。 + /// + /// 【处理逻辑】 + /// - 负值转为 0 + /// - 超过 7 天的值限制为 7 天(防止异常数据) + /// + /// + /// 停留时长(毫秒) + /// 归一化后的时长 + private static long? NormalizeStayMs(long? stayMs) + { + // 【可空值类型】 + // long? 是可空的长整型,可以是 null 或 long 值。 + // .HasValue 判断是否有值。 + // .Value 获取值(如果有值)。 + if (!stayMs.HasValue) + { + return null; + } + + if (stayMs.Value < 0) + { + return 0L; + } + + // 【限制最大值】 + // 7L * 24 * 3600 * 1000 = 7 天的毫秒数 + // Math.Min 取较小值,防止异常数据。 + return Math.Min(stayMs.Value, 7L * 24 * 3600 * 1000); + } + + /// + /// 解析事件时间。 + /// + /// 【Unix 时间戳转换】 + /// 前端传的是 Unix 毫秒时间戳,这里转换成本地 DateTime。 + /// + /// DateTimeOffset.FromUnixTimeMilliseconds: + /// - 把 Unix 毫秒时间戳转换为 DateTimeOffset + /// - .LocalDateTime 获取本地时间 + /// + /// + /// Unix 毫秒时间戳 + /// 本地时间 + private DateTime ResolveEventTime(long? timestamp) + { + if (!timestamp.HasValue || timestamp.Value <= 0) + { + return DateTime.Now; + } + + try + { + // 前端传的是 Unix 毫秒时间戳,这里转换成本地 DateTime。 + return DateTimeOffset.FromUnixTimeMilliseconds(timestamp.Value).LocalDateTime; + } + catch (Exception ex) + { + // 【结构化日志】 + // _logger.LogWarning(ex, "消息 {属性名}", 属性值) + // 属性名会被自动替换为属性值,便于日志查询。 + // + // 对比 Java: + // Java: logger.warn("invalid eventTime: {}", timestamp, ex); + _logger.LogWarning(ex, "invalid eventTime: {Timestamp}", timestamp); + return DateTime.Now; + } + } + + /// + /// 检测设备类型。 + /// + /// 【检测逻辑】 + /// - 包含 ipad/tablet:平板 + /// - 包含 mobile/android/iphone:手机 + /// - 其他:桌面 + /// + /// + /// User-Agent 字符串 + /// 设备类型 + private static string DetectDevice(string ua) + { + // ToLowerInvariant() 转换为小写(不依赖区域设置)。 + string text = (ua ?? string.Empty).ToLowerInvariant(); + if (text.Contains("ipad", StringComparison.Ordinal) || text.Contains("tablet", StringComparison.Ordinal)) + { + return "Tablet"; + } + + if (text.Contains("mobile", StringComparison.Ordinal) || text.Contains("android", StringComparison.Ordinal) || text.Contains("iphone", StringComparison.Ordinal)) + { + return "Mobile"; + } + + return "Desktop"; + } + + /// + /// 构建 IP 哈希值。 + /// + /// 【隐私保护】 + /// Why:出于隐私和合规考虑,不直接存明文 IP,而是落哈希值。 + /// + /// 哈希算法:SHA256 + /// 加盐值:hw-portal-analytics(防止彩虹表攻击) + /// + /// + /// 【C# 语法知识点 - using 语句】 + /// using 语句确保资源正确释放: + /// - SHA256 实现了 IDisposable 接口 + /// - 离开 using 块时自动调用 Dispose() + /// + /// 对比 Java: + /// Java 用 try-with-resources: + /// try (MessageDigest md = MessageDigest.getInstance("SHA-256")) { + /// ... + /// } + /// + /// + /// IP 地址 + /// 哈希值(十六进制字符串) + private static string BuildIpHash(string ip) + { + string safeIp = string.IsNullOrWhiteSpace(ip) ? "unknown" : ip; + try + { + // 【SHA256 哈希】 + using System.Security.Cryptography.SHA256 sha256 = System.Security.Cryptography.SHA256.Create(); + + // ComputeHash 计算字节数组的哈希值。 + // Encoding.UTF8.GetBytes 把字符串转换为字节数组。 + byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes($"{safeIp}|hw-portal-analytics")); + + // Convert.ToHexString 把字节数组转换为十六进制字符串。 + // ToLowerInvariant() 转换为小写。 + return Convert.ToHexString(hash).ToLowerInvariant(); + } + catch + { + // 【降级处理】 + // 如果 SHA256 失败,使用 GetHashCode 作为降级方案。 + // GetHashCode 返回 int,ToString("x") 转换为十六进制。 + return safeIp.GetHashCode().ToString("x", CultureInfo.InvariantCulture); + } + } + + /// + /// 归一化排行条数限制。 + /// + /// 【处理逻辑】 + /// - 默认 10 条 + /// - 最大 50 条 + /// + /// + /// 排行条数 + /// 归一化后的条数 + private static int NormalizeRankLimit(int? rankLimit) + { + if (!rankLimit.HasValue || rankLimit.Value <= 0) + { + return 10; + } + + return Math.Min(rankLimit.Value, 50); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoDetailService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoDetailService.cs new file mode 100644 index 0000000..e4c7f24 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoDetailService.cs @@ -0,0 +1,509 @@ +// ============================================================================ +// 【文件说明】HwAboutUsInfoDetailService.cs - 关于我们明细信息服务类 +// ============================================================================ +// 这个服务类负责处理"关于我们"模块的明细数据业务逻辑,包括: +// - 明细信息的 CRUD 操作 +// - 与主信息的关联查询 +// +// 【分层架构说明】 +// Controller(控制器层)-> Service(服务层)-> Repository(数据访问层) +// +// 控制器只负责:接收请求、调用服务、返回响应 +// 服务层负责:业务逻辑、数据组装、事务控制 +// 数据访问层负责:数据库 CRUD 操作 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwAboutUsInfoDetailServiceImpl implements HwAboutUsInfoDetailService { ... } +// +// ASP.NET Core + Furion: +// public class HwAboutUsInfoDetailService : ITransient { ... } +// +// ITransient 是 Furion 的"生命周期接口",表示"瞬态服务": +// - 每次请求都创建新实例 +// - 类似 Java Spring 的 @Scope("prototype") +// +// Furion 支持三种生命周期: +// - ITransient:瞬态,每次请求新实例 +// - IScoped:作用域,同一请求内共享实例 +// - ISingleton:单例,全局共享一个实例 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 关于我们明细信息服务类。 +/// +/// 【C# 语法知识点 - 接口实现】 +/// public class HwAboutUsInfoDetailService : ITransient +/// +/// ITransient 是 Furion 框架的"标记接口"(Marker Interface)。 +/// 它没有任何方法,只是用来标记服务的生命周期。 +/// +/// 对比 Java Spring: +/// Java Spring 用 @Service + @Scope 注解来定义服务: +/// @Service +/// @Scope("prototype") // 等价于 ITransient +/// public class HwAboutUsInfoDetailService { ... } +/// +/// C# Furion 用接口来标记,更符合"接口隔离原则"。 +/// +/// +/// 【SqlSugar ORM 数据访问】 +/// 这个服务使用 ISqlSugarClient 进行数据库操作。 +/// SqlSugar 是 .NET 生态中流行的 ORM 框架,类似于 Java 的 MyBatis-Plus。 +/// +/// 对比 Java MyBatis: +// Java MyBatis: +// @Mapper +// public interface HwAboutUsInfoDetailMapper { +// HwAboutUsInfoDetail selectById(@Param("id") Long id); +// } +// +// C# SqlSugar: +// _db.Queryable().Where(...).FirstAsync(); +// +// SqlSugar 的优势: +// - 强类型,编译期检查 +// - Lambda 表达式,IDE 智能提示 +// - 链式调用,代码更流畅 +// - 性能优秀,接近原生 SQL +/// +/// +public class HwAboutUsInfoDetailService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了。 + /// + /// 对比 Java: + /// Java: private static final String MAPPER = "HwAboutUsInfoDetailMapper"; + /// C#: private const string Mapper = "HwAboutUsInfoDetailMapper"; + /// + /// C# 的命名约定: + /// - const 通常用 PascalCase(首字母大写) + /// - Java 的 static final 通常用 UPPER_SNAKE_CASE + /// + /// + /// 【为什么保留这个常量?】 + /// 虽然当前使用 SqlSugar,但保留了 XML Mapper 的回滚能力。 + /// 这个常量用于指定 XML 文件中 mapper 的 namespace。 + /// + /// + private const string Mapper = "HwAboutUsInfoDetailMapper"; + + /// + /// MyBatis 执行器。 + /// + /// 【依赖注入】 + /// 通过构造函数注入,和控制器注入服务的方式一样。 + /// + /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 + /// 当前代码中已注释掉使用,但保留以备回滚。 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + /// 【ISqlSugarClient 接口】 + /// 这是 SqlSugar 的核心接口,提供数据库访问能力。 + /// + /// 对比 Java: + /// Java MyBatis: SqlSession 或 Mapper 接口 + /// C# SqlSugar: ISqlSugarClient + /// + /// 通过依赖注入获取,由框架在启动时配置并注册到 DI 容器。 + /// + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// public HwAboutUsInfoDetailService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + /// + /// 对比 Java: + /// Java: + /// @Autowired + /// public HwAboutUsInfoDetailService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) { + /// this.executor = executor; + /// this.db = db; + /// } + /// + /// C#: + /// public HwAboutUsInfoDetailService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + /// { + /// _executor = executor; + /// _db = db; + /// } + /// + /// C# 的改进: + /// 1. 不需要 @Autowired 注解,框架自动识别构造函数 + /// 2. 使用 _executor 命名约定表示私有字段 + /// 3. 可以使用主构造函数(C# 12+)进一步简化 + /// + /// + /// MyBatis 执行器(保留用于回滚) + /// SqlSugar 数据访问对象 + public HwAboutUsInfoDetailService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据明细ID查询关于我们明细信息。 + /// + /// 【方法命名约定】 + /// Select + 实体名 + By + 主键名:根据主键查询单条记录 + /// + /// 对比 Java 若依: + /// 若依通常使用:selectXxxById、getXxxById + /// 这里使用:SelectXxxByXxxId,更符合 C# 的 PascalCase 命名规范 + /// + /// + /// 【async/await 异步编程】 + /// public async Task Select... + /// + /// 对比 Java: + /// Java: + /// public CompletableFuture select...(...) { ... } + /// 或 + /// public HwAboutUsInfoDetail select...(...) { ... } // 同步 + /// + /// C#: + /// public async Task Select...(...) { ... } + /// + /// async/await 的优势: + /// - 代码看起来像同步,但底层是异步 + /// - 避免回调地狱 + /// - 更好的性能(不阻塞线程) + /// + /// + /// 明细ID + /// 明细实体,不存在则返回 null + public async Task SelectHwAboutUsInfoDetailByUsInfoDetailId(long usInfoDetailId) + { + // 【回滚注释说明】 + // 这行注释说明如何回滚到 XML Mapper 方案: + // 只需取消注释下面这行,注释掉 SqlSugar 代码即可 + // 这种设计保留了"后悔药",便于调试和问题排查 + // + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwAboutUsInfoDetailByUsInfoDetailId", new { usInfoDetailId }); + + // 【SqlSugar 查询语法】 + // _db.Queryable():创建一个针对 HwAboutUsInfoDetail 表的查询 + // .Where(item => item.UsInfoDetailId == usInfoDetailId):添加 WHERE 条件 + // .FirstAsync():执行查询,返回第一条记录,如果没有则返回 null + // + // 生成的 SQL 类似于: + // SELECT * FROM hw_about_us_info_detail WHERE UsInfoDetailId = @usInfoDetailId LIMIT 1 + // + // 对比 Java MyBatis: + // Java: mapper.selectOne(new QueryWrapper().eq("UsInfoDetailId", usInfoDetailId)); + // C#: _db.Queryable().Where(item => item.UsInfoDetailId == usInfoDetailId).FirstAsync(); + // + // C# 的优势:Lambda 表达式是强类型的,拼写错误在编译期就能发现 + return await _db.Queryable() + .Where(item => item.UsInfoDetailId == usInfoDetailId) + .FirstAsync(); + } + + /// + /// 查询关于我们明细列表。 + /// + /// 【动态查询条件构建】 + /// 这个方法展示了如何根据输入参数动态构建查询条件。 + /// 只有参数有值时,才添加对应的 WHERE 条件。 + /// + /// + /// 【空合并运算符 ??】 + /// input ?? new HwAboutUsInfoDetail() + /// + /// 如果 input 为 null,则创建一个新的默认对象。 + /// 这样可以避免后面的空引用异常。 + /// + /// 对比 Java: + /// Java: input != null ? input : new HwAboutUsInfoDetail(); + /// C#: input ?? new HwAboutUsInfoDetail(); + /// + /// C# 更简洁! + /// + /// + /// 查询条件 + /// 明细列表 + public async Task> SelectHwAboutUsInfoDetailList(HwAboutUsInfoDetail input) + { + // 【防御性编程】 + // 确保 query 不为 null,避免后续空引用异常 + HwAboutUsInfoDetail query = input ?? new HwAboutUsInfoDetail(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwAboutUsInfoDetailList", query); + + // 【SqlSugar WhereIF 动态条件】 + // .WhereIF(condition, expression):只有当 condition 为 true 时,才添加 WHERE 条件 + // + // 对比 Java MyBatis 的 XML: + // + // AND AboutUsInfoId = #{aboutUsInfoId} + // + // + // C# SqlSugar 的 WhereIF 更直观,而且类型安全 + // + // 【HasValue 可空类型检查】 + // query.AboutUsInfoId.HasValue 检查可空 long? 是否有值 + // + // 对比 Java: + // Java: query.getAboutUsInfoId() != null + // C#: query.AboutUsInfoId.HasValue + // + // 【string.IsNullOrWhiteSpace】 + // 检查字符串是否为 null、空字符串 "" 或仅包含空白字符 + // + // 对比 Java: + // Java: StringUtils.isBlank(str)(需要 Apache Commons Lang) + // C#: string.IsNullOrWhiteSpace(str)(内置) + // + // 【Contains 模糊查询】 + // item.UsInfoDetailTitle.Contains(query.UsInfoDetailTitle) + // 生成 SQL:WHERE UsInfoDetailTitle LIKE '%xxx%' + // + // 【ToListAsync】 + // 执行查询并返回列表,异步版本 + return await _db.Queryable() + .WhereIF(query.AboutUsInfoId.HasValue, item => item.AboutUsInfoId == query.AboutUsInfoId) + .WhereIF(!string.IsNullOrWhiteSpace(query.UsInfoDetailTitle), item => item.UsInfoDetailTitle.Contains(query.UsInfoDetailTitle)) + .WhereIF(!string.IsNullOrWhiteSpace(query.UsInfoDetailDesc), item => item.UsInfoDetailDesc.Contains(query.UsInfoDetailDesc)) + .WhereIF(query.UsInfoDetailOrder.HasValue, item => item.UsInfoDetailOrder == query.UsInfoDetailOrder) + .WhereIF(!string.IsNullOrWhiteSpace(query.UsInfoDetailPic), item => item.UsInfoDetailPic == query.UsInfoDetailPic) + .ToListAsync(); + } + + /// + /// 新增关于我们明细信息。 + /// + /// 【插入操作返回值】 + /// 返回 int 表示影响行数(1 表示成功,0 表示失败)。 + /// 这是为了兼容原有 XML Mapper 的返回类型。 + /// + /// + /// 【时间戳处理】 + /// HwPortalContextHelper.Now() 获取当前时间。 + /// 使用助手类的好处: + /// 1. 可以统一控制时间来源(如测试时 mock) + /// 2. 可以统一时区处理 + /// 3. 便于后续扩展(如使用 UTC 时间) + /// + /// + /// 明细数据 + /// 影响行数(1成功,0失败) + public async Task InsertHwAboutUsInfoDetail(HwAboutUsInfoDetail input) + { + // 【审计字段自动填充】 + // CreateTime 由代码自动设置,不需要前端传递 + // 这是常见的"审计字段"处理模式 + input.CreateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwAboutUsInfoDetail", input); + + // 【SqlSugar 插入语法】 + // _db.Insertable(input):创建一个插入操作 + // .ExecuteReturnEntityAsync():执行插入并返回插入后的实体(包含自增主键) + // + // 对比 Java MyBatis: + // Java: + // mapper.insert(input); + // Long identity = input.getUsInfoDetailId(); // 需要配置 useGeneratedKeys + // + // C#: + // var entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + // input.UsInfoDetailId = entity.UsInfoDetailId; + // + // C# 的优势:ExecuteReturnEntityAsync 直接返回插入后的完整实体 + HwAboutUsInfoDetail entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + + // 【回填主键】 + // 将生成的自增主键回填到输入对象 + // 这样调用方可以获取到新插入记录的 ID + input.UsInfoDetailId = entity.UsInfoDetailId; + + // 【返回影响行数】 + // 为了兼容原有接口,返回 1 或 0 + // 而不是返回主键值 + return input.UsInfoDetailId > 0 ? 1 : 0; + } + + /// + /// 更新关于我们明细信息。 + /// + /// 【更新策略】 + /// 这里使用"先查询后更新"的策略: + /// 1. 先根据 ID 查询现有记录 + /// 2. 如果记录不存在,返回 0 + /// 3. 只更新输入对象中不为 null 的字段 + /// 4. 执行更新 + /// + /// 这种策略的好处: + /// - 避免全字段更新(减少并发冲突) + /// - 可以处理部分字段更新的场景 + /// - 保持原有数据不变 + /// + /// + /// 【对比 Java 若依】 + /// 若依通常使用 MyBatis 的 标签实现动态更新: + /// + /// UPDATE table + /// + /// field1 = #{field1}, + /// field2 = #{field2}, + /// + /// WHERE id = #{id} + /// + /// + /// C# 这里手动控制,更灵活但需要更多代码。 + /// 也可以用 SqlSugar 的 .UpdateColumns() 实现类似效果。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwAboutUsInfoDetail(HwAboutUsInfoDetail input) + { + // 【审计字段更新】 + // 自动设置更新时间 + input.UpdateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwAboutUsInfoDetail", input); + + // 【查询现有记录】 + // 先查出数据库中的当前记录 + // 如果记录不存在,返回 0 表示更新失败 + HwAboutUsInfoDetail current = await SelectHwAboutUsInfoDetailByUsInfoDetailId(input.UsInfoDetailId ?? 0); + if (current == null) + { + return 0; + } + + // 【字段级更新】 + // 只更新输入对象中明确有值的字段 + // null 值表示"不更新此字段" + // + // 这种设计支持部分更新(PATCH 语义) + // 对比全量更新(PUT 语义),更灵活 + + // 更新关联主表ID + if (input.AboutUsInfoId.HasValue) + { + current.AboutUsInfoId = input.AboutUsInfoId; + } + + // 更新标题(null 检查避免覆盖为 null) + if (input.UsInfoDetailTitle != null) + { + current.UsInfoDetailTitle = input.UsInfoDetailTitle; + } + + // 更新描述 + if (input.UsInfoDetailDesc != null) + { + current.UsInfoDetailDesc = input.UsInfoDetailDesc; + } + + // 更新排序号 + if (input.UsInfoDetailOrder.HasValue) + { + current.UsInfoDetailOrder = input.UsInfoDetailOrder; + } + + // 更新图片 + if (input.UsInfoDetailPic != null) + { + current.UsInfoDetailPic = input.UsInfoDetailPic; + } + + // 更新创建时间(通常不应该更新,但保留兼容) + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + // 更新创建人 + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + // 【必须更新的字段】 + // UpdateTime 每次更新都必须更新 + current.UpdateTime = input.UpdateTime; + + // 更新更新人 + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + // 【执行更新】 + // _db.Updateable(current):创建更新操作 + // .ExecuteCommandAsync():执行更新,返回影响行数 + // + // 生成的 SQL 类似于: + // UPDATE hw_about_us_info_detail + // SET AboutUsInfoId = @AboutUsInfoId, UsInfoDetailTitle = @UsInfoDetailTitle, ... + // WHERE UsInfoDetailId = @UsInfoDetailId + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除关于我们明细信息。 + /// + /// 【批量删除】 + /// 根据 ID 数组批量删除记录。 + /// + /// + /// 【IN 语句】 + // .In(usInfoDetailIds) 生成 SQL:WHERE UsInfoDetailId IN (1, 2, 3) + // + // 对比 Java MyBatis: + // Java XML: + // + // #{id} + // + // + // C# SqlSugar: + // .In(usInfoDetailIds) + // + // C# 更简洁! + /// + /// + /// 明细ID数组 + /// 影响行数 + public async Task DeleteHwAboutUsInfoDetailByUsInfoDetailIds(long[] usInfoDetailIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwAboutUsInfoDetailByUsInfoDetailIds", new { array = usInfoDetailIds }); + + // 【SqlSugar 删除语法】 + // _db.Deleteable():创建删除操作 + // .In(usInfoDetailIds):添加 IN 条件 + // .ExecuteCommandAsync():执行删除,返回影响行数 + // + // 生成的 SQL: + // DELETE FROM hw_about_us_info_detail WHERE UsInfoDetailId IN (@p1, @p2, @p3) + // + // 【注意】这里是物理删除,不是软删除 + // 如果需要软删除,应该使用 Updateable 更新 IsDelete 字段 + return await _db.Deleteable() + .In(usInfoDetailIds) + .ExecuteCommandAsync(); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoService.cs new file mode 100644 index 0000000..f77e53c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwAboutUsInfoService.cs @@ -0,0 +1,262 @@ +// ============================================================================ +// 【文件说明】HwAboutUsInfoService.cs - 关于我们信息服务类 +// ============================================================================ +// 这个服务类负责处理"关于我们"模块的主信息业务逻辑,包括: +// - 关于我们信息的 CRUD 操作 +// - 中英文标题、描述等多字段管理 +// +// 【业务背景】 +// "关于我们"是企业官网的常见模块,用于展示公司介绍、发展历程、团队信息等内容。 +// 这个服务管理主信息表,明细信息由 HwAboutUsInfoDetailService 管理。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwAboutUsInfoServiceImpl implements HwAboutUsInfoService { ... } +// +// ASP.NET Core + Furion: +// public class HwAboutUsInfoService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 关于我们信息服务类。 +/// +/// 【服务职责】 +/// 1. 管理"关于我们"主信息的增删改查 +/// 2. 处理多语言字段(中英文标题和描述) +/// 3. 处理空字符串规范化(与 XML 方案保持一致) +/// +/// +/// 【空字符串处理策略】 +/// 原 XML 方案中,MyBatis 的 <if test="... != null and ... != ''"> +/// 会跳过空字符串字段不入库。 +/// 为了保持数据一致性,这里使用 NormalizeEmptyToNull 方法 +/// 将空字符串统一转为 null。 +/// +/// +public class HwAboutUsInfoService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwAboutUsInfoMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + public HwAboutUsInfoService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据主键查询关于我们信息。 + /// + /// 关于我们信息ID + /// 关于我们实体 + public async Task SelectHwAboutUsInfoByAboutUsInfoId(long aboutUsInfoId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwAboutUsInfoByAboutUsInfoId", new { aboutUsInfoId }); + return await _db.Queryable() + .Where(item => item.AboutUsInfoId == aboutUsInfoId) + .FirstAsync(); + } + + /// + /// 查询关于我们信息列表。 + /// + /// 【动态查询条件】 + /// 支持按类型、标题、描述、排序号、图片等条件筛选。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 查询条件 + /// 关于我们信息列表 + public async Task> SelectHwAboutUsInfoList(HwAboutUsInfo input) + { + HwAboutUsInfo query = input ?? new HwAboutUsInfo(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwAboutUsInfoList", query); + return await _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(query.AboutUsInfoType), item => item.AboutUsInfoType == query.AboutUsInfoType) + .WhereIF(!string.IsNullOrWhiteSpace(query.AboutUsInfoTitle), item => item.AboutUsInfoTitle.Contains(query.AboutUsInfoTitle)) + .WhereIF(!string.IsNullOrWhiteSpace(query.AboutUsInfoDesc), item => item.AboutUsInfoDesc.Contains(query.AboutUsInfoDesc)) + .WhereIF(query.AboutUsInfoOrder.HasValue, item => item.AboutUsInfoOrder == query.AboutUsInfoOrder) + .WhereIF(!string.IsNullOrWhiteSpace(query.AboutUsInfoPic), item => item.AboutUsInfoPic == query.AboutUsInfoPic) + .ToListAsync(); + } + + /// + /// 新增关于我们信息。 + /// + /// 【空字符串规范化】 + /// Why:这里先把"原 XML 会跳过的空串字段"归一成 null, + /// 避免改造后写出不同的数据口径。 + /// + /// 原 XML 中 MyBatis 的条件: + /// <if test="aboutUsInfoEtitle != null and aboutUsInfoEtitle != ''"> + /// about_us_info_etitle = #{aboutUsInfoEtitle}, + /// </if> + /// + /// 当传入空字符串时,XML 不会更新该字段。 + /// 为了保持行为一致,这里将空字符串转为 null。 + /// + /// + /// 关于我们信息 + /// 影响行数 + public async Task InsertHwAboutUsInfo(HwAboutUsInfo input) + { + // 【数据规范化】 + // 将空字符串转为 null,与 XML 方案保持一致 + input.AboutUsInfoEtitle = NormalizeEmptyToNull(input.AboutUsInfoEtitle); + input.AboutUsInfoTitle = NormalizeEmptyToNull(input.AboutUsInfoTitle); + input.AboutUsInfoDesc = NormalizeEmptyToNull(input.AboutUsInfoDesc); + input.DisplayModal = NormalizeEmptyToNull(input.DisplayModal); + + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwAboutUsInfo", input); + HwAboutUsInfo entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.AboutUsInfoId = entity.AboutUsInfoId; + return input.AboutUsInfoId > 0 ? 1 : 0; + } + + /// + /// 更新关于我们信息。 + /// + /// 【字段级更新策略】 + /// 1. 先查询现有记录 + /// 2. 对非空字段进行更新 + /// 3. 空字符串也视为有效值(使用 string.IsNullOrWhiteSpace 判断) + /// + /// 【注意】 + /// 这里对字符串字段使用 string.IsNullOrWhiteSpace 判断, + /// 而不是简单的 != null,这是为了兼容前端可能传入空字符串的情况。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwAboutUsInfo(HwAboutUsInfo input) + { + input.UpdateTime = HwPortalContextHelper.Now(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwAboutUsInfo", input); + HwAboutUsInfo current = await SelectHwAboutUsInfoByAboutUsInfoId(input.AboutUsInfoId ?? 0); + if (current == null) + { + return 0; + } + + // 【类型字段】 + // 使用 != null 判断,允许更新为空字符串 + if (input.AboutUsInfoType != null) + { + current.AboutUsInfoType = input.AboutUsInfoType; + } + + // 【英文字段】 + // 使用 string.IsNullOrWhiteSpace 判断,空字符串不更新 + if (!string.IsNullOrWhiteSpace(input.AboutUsInfoEtitle)) + { + current.AboutUsInfoEtitle = input.AboutUsInfoEtitle; + } + + // 【中文字段】 + if (!string.IsNullOrWhiteSpace(input.AboutUsInfoTitle)) + { + current.AboutUsInfoTitle = input.AboutUsInfoTitle; + } + + // 【描述字段】 + if (!string.IsNullOrWhiteSpace(input.AboutUsInfoDesc)) + { + current.AboutUsInfoDesc = input.AboutUsInfoDesc; + } + + // 【排序号】 + if (input.AboutUsInfoOrder.HasValue) + { + current.AboutUsInfoOrder = input.AboutUsInfoOrder; + } + + // 【显示模式】 + if (!string.IsNullOrWhiteSpace(input.DisplayModal)) + { + current.DisplayModal = input.DisplayModal; + } + + // 【图片字段】 + // 图片字段使用 != null 判断,允许更新为空 + if (input.AboutUsInfoPic != null) + { + current.AboutUsInfoPic = input.AboutUsInfoPic; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + current.UpdateTime = input.UpdateTime; + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除关于我们信息。 + /// + /// 关于我们信息ID数组 + /// 影响行数 + public async Task DeleteHwAboutUsInfoByAboutUsInfoIds(long[] aboutUsInfoIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwAboutUsInfoByAboutUsInfoIds", new { array = aboutUsInfoIds }); + return await _db.Deleteable() + .In(aboutUsInfoIds) + .ExecuteCommandAsync(); + } + + /// + /// 将空字符串规范化转为 null。 + /// + /// 【辅助方法】 + /// 用于保持与 XML 方案的数据一致性。 + /// 原 XML 中 <if> 标签会跳过空字符串, + /// 这里主动将空字符串转为 null,达到同样效果。 + /// + /// + /// 输入字符串 + /// null 或原值 + private static string NormalizeEmptyToNull(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwContactUsInfoService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwContactUsInfoService.cs new file mode 100644 index 0000000..a97fd46 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwContactUsInfoService.cs @@ -0,0 +1,240 @@ +// ============================================================================ +// 【文件说明】HwContactUsInfoService.cs - 联系我们信息服务类 +// ============================================================================ +// 这个服务类负责处理"联系我们"模块的业务逻辑,包括: +// - 用户留言/咨询信息的 CRUD 操作 +// - 用户信息(姓名、邮箱、电话、IP)管理 +// +// 【业务背景】 +// "联系我们"是企业官网的常见模块,用于收集用户的咨询、反馈、合作意向等。 +// 这个服务管理用户提交的联系信息。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwContactUsInfoServiceImpl implements HwContactUsInfoService { ... } +// +// ASP.NET Core + Furion: +// public class HwContactUsInfoService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 联系我们信息服务类。 +/// +/// 【服务职责】 +/// 1. 管理用户联系信息的增删改查 +/// 2. 处理用户信息字段(姓名、邮箱、电话、IP) +/// 3. 数据规范化(空字符串处理) +/// +/// +/// 【用户姓名字段特殊处理】 +/// Why:用户姓名在原 XML 中空串不会入库,这里同步保持, +/// 避免后台筛选出现空值脏数据。 +/// +/// +public class HwContactUsInfoService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwContactUsInfoMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + public HwContactUsInfoService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据ID查询联系信息。 + /// + /// 联系信息ID + /// 联系信息实体 + public async Task SelectHwContactUsInfoByContactUsInfoId(long contactUsInfoId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwContactUsInfoByContactUsInfoId", new { contactUsInfoId }); + return await _db.Queryable() + .Where(item => item.ContactUsInfoId == contactUsInfoId) + .FirstAsync(); + } + + /// + /// 查询联系信息列表。 + /// + /// 【动态查询条件】 + /// 支持按用户名、邮箱、电话、IP等条件模糊查询。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 查询条件 + /// 联系信息列表 + public async Task> SelectHwContactUsInfoList(HwContactUsInfo input) + { + HwContactUsInfo query = input ?? new HwContactUsInfo(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwContactUsInfoList", query); + return await _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(query.UserName), item => item.UserName.Contains(query.UserName)) + .WhereIF(!string.IsNullOrWhiteSpace(query.UserEmail), item => item.UserEmail.Contains(query.UserEmail)) + .WhereIF(!string.IsNullOrWhiteSpace(query.UserPhone), item => item.UserPhone.Contains(query.UserPhone)) + .WhereIF(!string.IsNullOrWhiteSpace(query.UserIp), item => item.UserIp.Contains(query.UserIp)) + .ToListAsync(); + } + + /// + /// 新增联系信息。 + /// + /// 【用户姓名字段特殊处理】 + /// Why:用户姓名在原 XML 中空串不会入库,这里同步保持, + /// 避免后台筛选出现空值脏数据。 + /// + /// 原 XML 中 MyBatis 的条件: + /// <if test="userName != null and userName != ''"> + /// user_name = #{userName}, + /// </if> + /// + /// 当传入空字符串时,XML 不会插入该字段。 + /// 为了保持行为一致,这里将空字符串转为 null。 + /// + /// + /// 联系信息 + /// 影响行数 + public async Task InsertHwContactUsInfo(HwContactUsInfo input) + { + // 【数据规范化】 + // 将用户姓名空字符串转为 null,与 XML 方案保持一致 + // Why:用户姓名在原 XML 中空串不会入库,这里同步保持,避免后台筛选出现空值脏数据。 + input.UserName = NormalizeEmptyToNull(input.UserName); + + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwContactUsInfo", input); + HwContactUsInfo entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.ContactUsInfoId = entity.ContactUsInfoId; + return input.ContactUsInfoId > 0 ? 1 : 0; + } + + /// + /// 更新联系信息。 + /// + /// 【字段级更新策略】 + /// 1. 先查询现有记录 + /// 2. 对非空字段进行更新 + /// 3. 空字符串也视为有效值 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwContactUsInfo(HwContactUsInfo input) + { + input.UpdateTime = HwPortalContextHelper.Now(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwContactUsInfo", input); + HwContactUsInfo current = await SelectHwContactUsInfoByContactUsInfoId(input.ContactUsInfoId ?? 0); + if (current == null) + { + return 0; + } + + // 【用户姓名字段】 + // 使用 string.IsNullOrWhiteSpace 判断,空字符串不更新 + if (!string.IsNullOrWhiteSpace(input.UserName)) + { + current.UserName = input.UserName; + } + + // 【邮箱字段】 + // 使用 != null 判断,允许更新为空字符串 + if (input.UserEmail != null) + { + current.UserEmail = input.UserEmail; + } + + // 【电话字段】 + if (input.UserPhone != null) + { + current.UserPhone = input.UserPhone; + } + + // 【IP字段】 + if (input.UserIp != null) + { + current.UserIp = input.UserIp; + } + + // 【备注字段】 + if (input.Remark != null) + { + current.Remark = input.Remark; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + current.UpdateTime = input.UpdateTime; + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除联系信息。 + /// + /// 联系信息ID数组 + /// 影响行数 + public async Task DeleteHwContactUsInfoByContactUsInfoIds(long[] contactUsInfoIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwContactUsInfoByContactUsInfoIds", new { array = contactUsInfoIds }); + return await _db.Deleteable() + .In(contactUsInfoIds) + .ExecuteCommandAsync(); + } + + /// + /// 将空字符串规范化转为 null。 + /// + /// 【辅助方法】 + /// 用于保持与 XML 方案的数据一致性。 + /// 原 XML 中 <if> 标签会跳过空字符串, + /// 这里主动将空字符串转为 null,达到同样效果。 + /// + /// + /// 输入字符串 + /// null 或原值 + private static string NormalizeEmptyToNull(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigService.cs new file mode 100644 index 0000000..7dc134a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigService.cs @@ -0,0 +1,347 @@ +// ============================================================================ +// 【文件说明】HwPortalConfigService.cs - 门户配置服务类 +// ============================================================================ +// 这个服务类负责处理门户配置的业务逻辑,包括: +// - 门户配置的 CRUD 操作 +// - 关联查询(配置 + 配置类型) +// - 空字符串规范化处理 +// +// 【业务背景】 +// 门户配置模块用于管理网站首页的各种配置项,如轮播图、推荐内容等。 +// 支持关联查询配置类型信息。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwPortalConfigServiceImpl implements HwPortalConfigService { ... } +// +// ASP.NET Core + Furion: +// public class HwPortalConfigService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户配置服务类。 +/// +/// 【服务职责】 +/// 1. 管理门户配置的增删改查 +/// 2. 支持关联查询配置类型信息 +/// 3. 处理空字符串规范化 +/// +/// +/// 【特殊类型处理】 +/// 当 PortalConfigType 为 "2" 时,使用 JOIN 查询关联 HwPortalConfigType 表, +/// 返回包含配置类型名称等附加信息的结果。 +/// +/// +public class HwPortalConfigService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwPortalConfigMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + public HwPortalConfigService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据配置ID查询门户配置。 + /// + /// 配置ID + /// 门户配置实体 + public async Task SelectHwPortalConfigByPortalConfigId(long portalConfigId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwPortalConfigByPortalConfigId", new { portalConfigId }); + return await _db.Queryable() + .Where(item => item.PortalConfigId == portalConfigId) + .FirstAsync(); + } + + /// + /// 查询门户配置列表。 + /// + /// 【特殊类型处理】 + /// 当 PortalConfigType 为 "2"(HwPortalConstants.PortalConfigTypeTwo)时, + /// 使用 BuildPortalConfigJoinQuery 方法进行 JOIN 查询, + /// 返回包含配置类型名称等附加信息的结果。 + /// + /// + /// 【动态查询条件】 + /// 支持按配置类型、类型ID、标题、排序号、描述、按钮名、路由地址、图片等条件筛选。 + /// + /// + /// 查询条件 + /// 门户配置列表 + public async Task> SelectHwPortalConfigList(HwPortalConfig input) + { + HwPortalConfig query = input ?? new HwPortalConfig(); + + // 【特殊类型处理】 + // 当配置类型为 "2" 时,使用 JOIN 查询关联配置类型表 + if (string.Equals(HwPortalConstants.PortalConfigTypeTwo, query.PortalConfigType, StringComparison.Ordinal)) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwPortalConfigList2", query); + return await BuildPortalConfigJoinQuery(query).ToListAsync(); + } + + // 【普通查询】 + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwPortalConfigList", query); + return await _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigType), item => item.PortalConfigType == query.PortalConfigType) + .WhereIF(query.PortalConfigTypeId.HasValue, item => item.PortalConfigTypeId == query.PortalConfigTypeId) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigTitle), item => item.PortalConfigTitle.Contains(query.PortalConfigTitle)) + .WhereIF(query.PortalConfigOrder.HasValue, item => item.PortalConfigOrder == query.PortalConfigOrder) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigDesc), item => item.PortalConfigDesc == query.PortalConfigDesc) + .WhereIF(!string.IsNullOrWhiteSpace(query.ButtonName), item => item.ButtonName.Contains(query.ButtonName)) + .WhereIF(!string.IsNullOrWhiteSpace(query.RouterAddress), item => item.RouterAddress == query.RouterAddress) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigPic), item => item.PortalConfigPic == query.PortalConfigPic) + .ToListAsync(); + } + + /// + /// 新增门户配置。 + /// + /// 【空字符串规范化】 + /// Why:这里只把原 XML 会忽略的空串字段转成 null, + /// 其它字段继续保留调用方原值,避免语义漂移。 + /// + /// + /// 门户配置数据 + /// 影响行数 + public async Task InsertHwPortalConfig(HwPortalConfig input) + { + // 【数据规范化】 + // 将空字符串转为 null,与 XML 方案保持一致 + input.PortalConfigType = NormalizeEmptyToNull(input.PortalConfigType); + input.PortalConfigTitle = NormalizeEmptyToNull(input.PortalConfigTitle); + input.PortalConfigPic = NormalizeEmptyToNull(input.PortalConfigPic); + + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwPortalConfig", input); + HwPortalConfig entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.PortalConfigId = entity.PortalConfigId; + return input.PortalConfigId > 0 ? 1 : 0; + } + + /// + /// 更新门户配置。 + /// + /// 【字段级更新策略】 + /// 只更新输入对象中不为 null 或不为空的字段。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwPortalConfig(HwPortalConfig input) + { + // 【审计字段】 + input.UpdateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwPortalConfig", input); + HwPortalConfig current = await SelectHwPortalConfigByPortalConfigId(input.PortalConfigId ?? 0); + if (current == null) + { + return 0; + } + + // 【配置类型】 + if (!string.IsNullOrWhiteSpace(input.PortalConfigType)) + { + current.PortalConfigType = input.PortalConfigType; + } + + // 【类型ID】 + if (input.PortalConfigTypeId.HasValue) + { + current.PortalConfigTypeId = input.PortalConfigTypeId; + } + + // 【标题】 + if (!string.IsNullOrWhiteSpace(input.PortalConfigTitle)) + { + current.PortalConfigTitle = input.PortalConfigTitle; + } + + // 【排序号】 + if (input.PortalConfigOrder.HasValue) + { + current.PortalConfigOrder = input.PortalConfigOrder; + } + + // 【描述】 + if (input.PortalConfigDesc != null) + { + current.PortalConfigDesc = input.PortalConfigDesc; + } + + // 【按钮名】 + if (input.ButtonName != null) + { + current.ButtonName = input.ButtonName; + } + + // 【路由地址】 + if (input.RouterAddress != null) + { + current.RouterAddress = input.RouterAddress; + } + + // 【图片】 + if (!string.IsNullOrWhiteSpace(input.PortalConfigPic)) + { + current.PortalConfigPic = input.PortalConfigPic; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + current.UpdateTime = input.UpdateTime; + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除门户配置。 + /// + /// 配置ID数组 + /// 影响行数 + public async Task DeleteHwPortalConfigByPortalConfigIds(long[] portalConfigIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwPortalConfigByPortalConfigIds", new { array = portalConfigIds }); + return await _db.Deleteable() + .In(portalConfigIds) + .ExecuteCommandAsync(); + } + + /// + /// 查询门户配置关联列表(JOIN查询)。 + /// + /// 【关联查询】 + /// 使用 BuildPortalConfigJoinQuery 方法进行 JOIN 查询, + /// 返回包含配置类型信息的完整结果。 + /// + /// + /// 查询条件 + /// 门户配置列表(含类型信息) + public async Task> SelectHwPortalConfigJoinList(HwPortalConfig input) + { + HwPortalConfig query = input ?? new HwPortalConfig(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwPortalConfigJoinList", query); + return await BuildPortalConfigJoinQuery(query).ToListAsync(); + } + + /// + /// 构建门户配置 JOIN 查询。 + /// + /// 【SqlSugar JOIN 查询】 + /// 使用 SqlSugar 的 Queryable 进行左连接(Left Join)查询, + /// 关联 HwPortalConfigType 表获取配置类型信息。 + /// + /// + /// 【字段投影】 + /// Why:这里统一把关联侧字段一起投影出来,避免 provider 对条件投影翻译不稳定。 + /// 对不需要这些字段的调用方来说,多出冗余属性不会改变行为,但能明显降低切换风险。 + /// + /// + /// 查询条件 + /// JOIN 查询对象 + private ISugarQueryable BuildPortalConfigJoinQuery(HwPortalConfig query) + { + return _db.Queryable((config, configType) => + new JoinQueryInfos(JoinType.Left, config.PortalConfigTypeId == configType.ConfigTypeId)) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigType), (config, _) => config.PortalConfigType == query.PortalConfigType) + .WhereIF(query.PortalConfigTypeId.HasValue, (config, _) => config.PortalConfigTypeId == query.PortalConfigTypeId) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigTitle), (config, _) => config.PortalConfigTitle.Contains(query.PortalConfigTitle)) + .WhereIF(query.PortalConfigOrder.HasValue, (config, _) => config.PortalConfigOrder == query.PortalConfigOrder) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigDesc), (config, _) => config.PortalConfigDesc == query.PortalConfigDesc) + .WhereIF(!string.IsNullOrWhiteSpace(query.ButtonName), (config, _) => config.ButtonName.Contains(query.ButtonName)) + .WhereIF(!string.IsNullOrWhiteSpace(query.RouterAddress), (config, _) => config.RouterAddress == query.RouterAddress) + .WhereIF(!string.IsNullOrWhiteSpace(query.PortalConfigPic), (config, _) => config.PortalConfigPic == query.PortalConfigPic) + .Select((config, configType) => new HwPortalConfig + { + // 【主表字段】 + PortalConfigId = config.PortalConfigId, + PortalConfigType = config.PortalConfigType, + PortalConfigTypeId = config.PortalConfigTypeId, + PortalConfigTitle = config.PortalConfigTitle, + PortalConfigOrder = config.PortalConfigOrder, + PortalConfigDesc = config.PortalConfigDesc, + ButtonName = config.ButtonName, + RouterAddress = config.RouterAddress, + PortalConfigPic = config.PortalConfigPic, + CreateTime = config.CreateTime, + CreateBy = config.CreateBy, + UpdateTime = config.UpdateTime, + UpdateBy = config.UpdateBy, + + // 【关联表字段】 + ConfigTypeName = configType.ConfigTypeName, + + // 【冗余字段】 + // Why:这里统一把关联侧字段一起投影出来,避免 provider 对条件投影翻译不稳定。 + // 对不需要这些字段的调用方来说,多出冗余属性不会改变行为,但能明显降低切换风险。 + HomeConfigTypePic = configType.HomeConfigTypePic, + HomeConfigTypeIcon = configType.ConfigTypeIcon, + HomeConfigTypeName = configType.HomeConfigTypeName, + HomeConfigTypeClassfication = configType.ConfigTypeClassfication, + ParentId = configType.ParentId, + Ancestors = configType.Ancestors + }); + } + + /// + /// 将空字符串规范化转为 null。 + /// + /// 【辅助方法】 + /// 用于保持与 XML 方案的数据一致性。 + /// 原 XML 中 <if> 标签会跳过空字符串, + /// 这里主动将空字符串转为 null,达到同样效果。 + /// + /// + /// 输入字符串 + /// null 或原值 + private static string NormalizeEmptyToNull(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs new file mode 100644 index 0000000..05514f6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs @@ -0,0 +1,800 @@ +// ============================================================================ +// 【文件说明】HwPortalConfigTypeService.cs - 门户配置类型服务类 +// ============================================================================ +// 这个服务类负责处理门户配置类型的业务逻辑,包括: +// - 配置类型的 CRUD 操作 +// - 树形结构(父子关系)管理 +// - 祖先路径(Ancestors)自动计算 +// - 搜索索引同步 +// +// 【业务背景】 +// 门户配置类型用于管理网站首页的各种配置分类,如轮播图类型、推荐内容类型等。 +// 支持多级树形结构,例如:首页配置 -> 轮播图 -> 轮播图项 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwPortalConfigTypeServiceImpl implements HwPortalConfigTypeService { ... } +// +// ASP.NET Core + Furion: +// public class HwPortalConfigTypeService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户配置类型服务类。 +/// +/// 【服务职责】 +/// 1. 管理配置类型的增删改查 +/// 2. 自动计算和维护树形结构的祖先路径(Ancestors) +/// 3. 配置类型变更时同步更新搜索索引 +/// 4. 提供树形结构查询和转换 +/// +/// +/// 【树形结构说明】 +/// - ParentId:父节点ID,0表示顶级节点 +/// - Ancestors:祖先路径,格式为"0,1,2"表示从根到父节点的路径 +/// +/// 示例: +/// 首页配置(ID=1, ParentId=0, Ancestors="0") +/// └── 轮播图(ID=2, ParentId=1, Ancestors="0,1") +/// └── 轮播图项(ID=3, ParentId=2, Ancestors="0,1,2") +/// +/// +/// 【依赖服务说明】 +/// - IHwSearchIndexService:搜索索引服务,用于在数据变更时更新搜索索引 +/// - ILogger:日志记录器,用于记录错误信息 +/// +/// +public class HwPortalConfigTypeService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了。 + /// + /// 对比 Java: + /// Java: private static final String MAPPER = "HwPortalConfigTypeMapper"; + /// C#: private const string Mapper = "HwPortalConfigTypeMapper"; + /// + /// C# 的命名约定: + /// - const 通常用 PascalCase(首字母大写) + /// - Java 的 static final 通常用 UPPER_SNAKE_CASE + /// + /// + private const string Mapper = "HwPortalConfigTypeMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + /// 【依赖注入】 + /// 通过构造函数注入,和控制器注入服务的方式一样。 + /// + /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 + /// 当前代码中已注释掉使用,但保留以备回滚。 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + /// 【ISqlSugarClient 接口】 + /// 这是 SqlSugar 的核心接口,提供数据库访问能力。 + /// + /// 对比 Java: + /// Java MyBatis: SqlSession 或 Mapper 接口 + /// C# SqlSugar: ISqlSugarClient + /// + /// 通过依赖注入获取,由框架在启动时配置并注册到 DI 容器。 + /// + /// + private readonly ISqlSugarClient _db; + + /// + /// 搜索索引服务。 + /// + /// 【接口注入】 + /// IHwSearchIndexService 是自定义接口,用于在数据变更时同步更新搜索索引。 + /// 这是"关注点分离"的设计:数据操作和搜索索引更新解耦。 + /// + /// 对比 Java: + /// Java Spring 通常用 @Autowired 注入 Service。 + /// C# 通过构造函数注入,更明确依赖关系。 + /// + /// + private readonly IHwSearchIndexService _searchIndexService; + + /// + /// 日志记录器。 + /// + /// 【泛型日志接口】 + /// ILogger<HwPortalConfigTypeService> 是 Microsoft.Extensions.Logging 提供的日志接口。 + /// 泛型参数 T 用于标识日志的类别,通常传入当前类。 + /// + /// 对比 Java SLF4J: + /// Java: private static final Logger log = LoggerFactory.getLogger(HwPortalConfigTypeService.class); + /// C#: private readonly ILogger<HwPortalConfigTypeService> _logger; + /// + /// C# 的 ILogger 是接口,通过 DI 注入,更便于单元测试。 + /// + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// public HwPortalConfigTypeService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, ...) + /// + /// 对比 Java: + /// Java: + /// @Autowired + /// public HwPortalConfigTypeService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, ...) { + /// this.executor = executor; + /// this.db = db; + /// ... + /// } + /// + /// C#: + /// public HwPortalConfigTypeService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, ...) + /// { + /// _executor = executor; + /// _db = db; + /// ... + /// } + /// + /// C# 的改进: + /// 1. 不需要 @Autowired 注解,框架自动识别构造函数 + /// 2. 使用 _executor 命名约定表示私有字段 + /// 3. 可以使用主构造函数(C# 12+)进一步简化 + /// + /// + /// MyBatis 执行器(保留用于回滚) + /// SqlSugar 数据访问对象 + /// 搜索索引服务 + /// 日志记录器 + public HwPortalConfigTypeService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchIndexService searchIndexService, ILogger logger) + { + _executor = executor; + _db = db; + _searchIndexService = searchIndexService; + _logger = logger; + } + + /// + /// 根据类型ID查询配置类型信息。 + /// + /// 【方法命名约定】 + /// Select + 实体名 + By + 主键名:根据主键查询单条记录 + /// + /// 对比 Java 若依: + /// 若依通常使用:selectXxxById、getXxxById + /// 这里使用:SelectXxxByConfigTypeId,更符合 C# 的 PascalCase 命名规范 + /// + /// + /// 配置类型ID + /// 配置类型实体 + public async Task SelectHwPortalConfigTypeByConfigTypeId(long configTypeId) + { + // 【回滚注释说明】 + // 这行注释说明如何回滚到 XML Mapper 方案: + // 只需取消注释下面这行,注释掉 SqlSugar 代码即可 + // 这种设计保留了"后悔药",便于调试和问题排查 + // + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwPortalConfigTypeByConfigTypeId", new { configTypeId }); + + // 【SqlSugar 查询语法】 + // _db.Queryable():创建一个针对 HwPortalConfigType 表的查询 + // .Where(item => item.ConfigTypeId == configTypeId):添加 WHERE 条件 + // .FirstAsync():执行查询,返回第一条记录,如果没有则返回 null + // + // 生成的 SQL 类似于: + // SELECT * FROM hw_portal_config_type WHERE ConfigTypeId = @configTypeId LIMIT 1 + // + // 对比 Java MyBatis: + // Java: mapper.selectOne(new QueryWrapper().eq("ConfigTypeId", configTypeId)); + // C#: _db.Queryable().Where(item => item.ConfigTypeId == configTypeId).FirstAsync(); + // + // C# 的优势:Lambda 表达式是强类型的,拼写错误在编译期就能发现 + return await _db.Queryable() + .Where(item => item.ConfigTypeId == configTypeId) + .FirstAsync(); + } + + /// + /// 查询配置类型列表。 + /// + /// 【动态查询条件】 + /// 支持按分类、类型名、首页类型名、描述、图标、图片、父ID、祖先路径等条件筛选。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 查询条件 + /// 配置类型列表 + public async Task> SelectHwPortalConfigTypeList(HwPortalConfigType input) + { + // 【防御性编程】 + // 确保 query 不为 null,避免后续空引用异常 + HwPortalConfigType query = input ?? new HwPortalConfigType(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwPortalConfigTypeList", query); + + // 【SqlSugar WhereIF 动态条件】 + // .WhereIF(condition, expression):只有当 condition 为 true 时,才添加 WHERE 条件 + // + // 对比 Java MyBatis 的 XML: + // + // AND ConfigTypeClassfication = #{configTypeClassfication} + // + // + // C# SqlSugar 的 WhereIF 更直观,而且类型安全 + // + // 【string.IsNullOrWhiteSpace】 + // 检查字符串是否为 null、空字符串 "" 或仅包含空白字符 + // + // 对比 Java: + // Java: StringUtils.isBlank(str)(需要 Apache Commons Lang) + // C#: string.IsNullOrWhiteSpace(str)(内置) + // + // 【Contains 模糊查询】 + // item.ConfigTypeName.Contains(query.ConfigTypeName) + // 生成 SQL:WHERE ConfigTypeName LIKE '%xxx%' + // + // 【HasValue 可空类型检查】 + // query.ParentId.HasValue 检查可空 long? 是否有值 + // + // 对比 Java: + // Java: query.getParentId() != null + // C#: query.ParentId.HasValue + // + // 【ToListAsync】 + // 执行查询并返回列表,异步版本 + return await _db.Queryable() + .WhereIF(!string.IsNullOrWhiteSpace(query.ConfigTypeClassfication), item => item.ConfigTypeClassfication == query.ConfigTypeClassfication) + .WhereIF(!string.IsNullOrWhiteSpace(query.ConfigTypeName), item => item.ConfigTypeName.Contains(query.ConfigTypeName)) + .WhereIF(!string.IsNullOrWhiteSpace(query.HomeConfigTypeName), item => item.HomeConfigTypeName.Contains(query.HomeConfigTypeName)) + .WhereIF(!string.IsNullOrWhiteSpace(query.ConfigTypeDesc), item => item.ConfigTypeDesc == query.ConfigTypeDesc) + .WhereIF(!string.IsNullOrWhiteSpace(query.ConfigTypeIcon), item => item.ConfigTypeIcon == query.ConfigTypeIcon) + .WhereIF(!string.IsNullOrWhiteSpace(query.HomeConfigTypePic), item => item.HomeConfigTypePic == query.HomeConfigTypePic) + .WhereIF(query.ParentId.HasValue, item => item.ParentId == query.ParentId) + .WhereIF(!string.IsNullOrWhiteSpace(query.Ancestors), item => item.Ancestors == query.Ancestors) + .ToListAsync(); + } + + /// + /// 查询配置类型列表(带分类过滤和树形结构)。 + /// + /// 【业务逻辑】 + /// 1. 如果指定了分类(ConfigTypeClassfication),查询该分类下的所有节点 + /// 2. 找出该分类下的顶级节点(ParentId 为 null 或 0) + /// 3. 递归收集所有子孙节点 + /// 4. 构建树形结构返回 + /// + /// 这种设计支持"按分类查看树形结构"的业务需求。 + /// + /// + /// 查询条件 + /// 树形配置类型列表 + public async Task> SelectConfigTypeList(HwPortalConfigType input) + { + HwPortalConfigType query = input ?? new HwPortalConfigType(); + + // 【分类过滤逻辑】 + // 如果指定了分类,需要特殊处理: + // 1. 查询所有记录 + // 2. 找出该分类下的顶级节点 + // 3. 递归收集所有子孙节点 + // 4. 构建树形结构 + if (!string.IsNullOrWhiteSpace(query.ConfigTypeClassfication)) + { + // 查询所有配置类型 + List allList = await SelectHwPortalConfigTypeList(new HwPortalConfigType()); + + // 【LINQ Where 过滤】 + // 找出该分类下的顶级节点(ParentId 为 null 或 0) + // string.Equals 用于区分大小写的精确比较 + List topLevelNodes = allList + .Where(item => string.Equals(query.ConfigTypeClassfication, item.ConfigTypeClassfication, StringComparison.Ordinal) + && (!item.ParentId.HasValue || item.ParentId == 0)) + .ToList(); + + // 【递归收集子孙节点】 + // 使用 List 收集所有相关节点 + List completeList = new(); + foreach (HwPortalConfigType topNode in topLevelNodes) + { + completeList.Add(topNode); + // 递归添加所有子孙节点 + AddAllDescendants(allList, topNode, completeList); + } + + // 构建树形结构并返回 + return BuildPortalConfigTypeTree(completeList); + } + + // 【普通查询】 + // 如果没有指定分类,直接查询并构建树形结构 + List list = await SelectHwPortalConfigTypeList(query); + return BuildPortalConfigTypeTree(list); + } + + /// + /// 新增配置类型。 + /// + /// 【树形结构处理】 + /// 1. 如果没有指定父ID,设置为顶级节点(ParentId=0, Ancestors="0") + /// 2. 如果指定了父ID,查询父节点的祖先路径,拼接成新的祖先路径 + /// + /// 示例: + /// 父节点:Ancestors="0,1",当前 ParentId=2 + /// 新节点:Ancestors="0,1,2" + /// + /// + /// 【搜索索引同步】 + /// 新增成功后,同步更新搜索索引,确保新数据可被搜索到。 + /// + /// + /// 配置类型数据 + /// 影响行数 + public async Task InsertHwPortalConfigType(HwPortalConfigType input) + { + // 【树形结构处理】 + // 如果没有父ID,设置为顶级节点 + if (!input.ParentId.HasValue) + { + input.ParentId = 0; + input.Ancestors = "0"; + } + else + { + // 查询父节点的祖先路径,拼接成新的祖先路径 + // 【?. 空条件运算符】 + // info?.Ancestors 表示:如果 info 为 null,返回 null;否则返回 Ancestors + // 对比 Java:info != null ? info.getAncestors() : null + HwPortalConfigType info = await SelectHwPortalConfigTypeByConfigTypeId(input.ParentId.Value); + input.Ancestors = $"{info?.Ancestors},{input.ParentId}"; + } + + // 【数据规范化】 + // 将空字符串转为 null,与 XML 方案保持一致 + input.ConfigTypeClassfication = NormalizeEmptyToNull(input.ConfigTypeClassfication); + input.ConfigTypeName = NormalizeEmptyToNull(input.ConfigTypeName); + + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + input.CreateBy = HwPortalContextHelper.CurrentUserName(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwPortalConfigType", input); + + // 【SqlSugar 插入语法】 + // _db.Insertable(input):创建一个插入操作 + // .ExecuteReturnEntityAsync():执行插入并返回插入后的实体(包含自增主键) + HwPortalConfigType entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.ConfigTypeId = entity.ConfigTypeId; + + // 【搜索索引同步】 + // 如果插入成功,更新搜索索引 + if (input.ConfigTypeId > 0) + { + // 【静默更新】 + // 使用 try-catch 包裹,确保索引更新失败不影响主业务 + await UpsertSearchIndexQuietly(input, "hw_portal_config_type"); + } + + // 返回影响行数(1成功,0失败) + return input.ConfigTypeId > 0 ? 1 : 0; + } + + /// + /// 更新配置类型。 + /// + /// 【字段级更新策略】 + /// 1. 先查询现有记录 + /// 2. 对非空字段进行更新 + /// 3. 空字符串也视为有效值 + /// + /// + /// 【搜索索引同步】 + /// 更新成功后,同步更新搜索索引。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwPortalConfigType(HwPortalConfigType input) + { + // 【审计字段】 + input.UpdateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "updateHwPortalConfigType", input); + + // 查询现有记录 + HwPortalConfigType current = await SelectHwPortalConfigTypeByConfigTypeId(input.ConfigTypeId ?? 0); + if (current == null) + { + return 0; + } + + // 【分类字段】 + if (!string.IsNullOrWhiteSpace(input.ConfigTypeClassfication)) + { + current.ConfigTypeClassfication = input.ConfigTypeClassfication; + } + + // 【类型名字段】 + if (!string.IsNullOrWhiteSpace(input.ConfigTypeName)) + { + current.ConfigTypeName = input.ConfigTypeName; + } + + // 【首页类型名字段】 + if (input.HomeConfigTypeName != null) + { + current.HomeConfigTypeName = input.HomeConfigTypeName; + } + + // 【描述字段】 + if (input.ConfigTypeDesc != null) + { + current.ConfigTypeDesc = input.ConfigTypeDesc; + } + + // 【图标字段】 + if (input.ConfigTypeIcon != null) + { + current.ConfigTypeIcon = input.ConfigTypeIcon; + } + + // 【图片字段】 + if (input.HomeConfigTypePic != null) + { + current.HomeConfigTypePic = input.HomeConfigTypePic; + } + + // 【父ID字段】 + if (input.ParentId.HasValue) + { + current.ParentId = input.ParentId; + } + + // 【祖先路径字段】 + if (input.Ancestors != null) + { + current.Ancestors = input.Ancestors; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + current.UpdateTime = input.UpdateTime; + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + // 【执行更新】 + int rows = await _db.Updateable(current).ExecuteCommandAsync(); + + // 【搜索索引同步】 + if (rows > 0 && input.ConfigTypeId.HasValue) + { + // 查询最新数据,确保索引数据完整 + HwPortalConfigType latest = await SelectHwPortalConfigTypeByConfigTypeId(input.ConfigTypeId.Value); + await UpsertSearchIndexQuietly(latest, "hw_portal_config_type"); + } + + return rows; + } + + /// + /// 批量删除配置类型。 + /// + /// 【物理删除】 + /// 这里是物理删除,不是软删除。 + /// 注意:删除父节点时,需要考虑子节点的处理。 + /// + /// + /// 【搜索索引同步】 + /// 删除成功后,同步删除搜索索引。 + /// + /// + /// 配置类型ID数组 + /// 影响行数 + public async Task DeleteHwPortalConfigTypeByConfigTypeIds(long[] configTypeIds) + { + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwPortalConfigTypeByConfigTypeIds", new { array = configTypeIds }); + + // 【SqlSugar 删除语法】 + // _db.Deleteable():创建删除操作 + // .In(configTypeIds):添加 IN 条件 + // .ExecuteCommandAsync():执行删除,返回影响行数 + // + // 生成的 SQL: + // DELETE FROM hw_portal_config_type WHERE ConfigTypeId IN (@p1, @p2, @p3) + int rows = await _db.Deleteable() + .In(configTypeIds) + .ExecuteCommandAsync(); + + // 【搜索索引同步】 + if (rows > 0) + { + await DeleteSearchIndexQuietly(configTypeIds, "hw_portal_config_type"); + } + + return rows; + } + + /// + /// 查询配置类型树选择列表。 + /// + /// 【树选择组件】 + /// 返回 TreeSelect 列表,用于前端下拉树组件。 + /// TreeSelect 是一种通用的树形选择数据结构。 + /// + /// + /// 查询条件 + /// 树选择列表 + public async Task> SelectPortalConfigTypeTreeList(HwPortalConfigType input) + { + // 回滚到 XML 方案时可直接恢复: + // List list = await _executor.QueryListAsync(Mapper, "selectHwPortalConfigTypeList", input ?? new HwPortalConfigType()); + + // 查询配置类型列表 + List list = await SelectHwPortalConfigTypeList(input); + + // 构建树形结构并转换为 TreeSelect + return BuildPortalConfigTypeTreeSelect(list); + } + + /// + /// 构建配置类型树选择列表。 + /// + /// 【方法链式调用】 + /// BuildPortalConfigTypeTree(list).Select(...).ToList() + /// + // 1. BuildPortalConfigTypeTree:构建树形结构 + // 2. Select:将每个节点转换为 TreeSelect + // 3. ToList:转换为列表 + /// + /// 对比 Java Stream: + /// Java: buildPortalConfigTypeTree(list).stream().map(u -> new TreeSelect(u)).collect(Collectors.toList()); + /// C#: BuildPortalConfigTypeTree(list).Select(u => new TreeSelect(u)).ToList(); + /// + /// C# 的 LINQ 更简洁! + /// + /// + /// 配置类型列表 + /// 树选择列表 + public List BuildPortalConfigTypeTreeSelect(List portalConfigTypes) + { + return BuildPortalConfigTypeTree(portalConfigTypes).Select(u => new TreeSelect(u)).ToList(); + } + + /// + /// 构建配置类型树形结构。 + /// + /// 【树形结构构建算法】 + /// 这是一个经典的"扁平列表转树形结构"算法: + /// + /// 输入:扁平的节点列表,每个节点有 parentId 指向父节点 + /// 输出:树形结构,顶级节点包含 children 子节点 + /// + /// 算法步骤: + /// 1. 收集所有节点ID到 tempList + /// 2. 遍历节点列表,找出所有顶级节点(parentId 不在 tempList 中) + /// 3. 对每个顶级节点,调用 RecursionFn 递归构建子树 + /// 4. 返回树形结构列表 + /// + /// 对比 Java 若依: + /// 若依的 TreeBuildUtils.buildTree() 做同样的事情。 + /// 这是若依框架的标准树构建算法。 + /// + /// + /// 扁平的配置类型列表 + /// 树形结构的配置类型列表 + public List BuildPortalConfigTypeTree(List portalConfigTypes) + { + // 结果列表 + List returnList = new(); + + // 【收集所有节点ID】 + // .Select(item => item.ConfigTypeId) 提取所有 ID + // .ToList() 转换为列表 + List tempList = portalConfigTypes.Select(item => item.ConfigTypeId).ToList(); + + // 【遍历查找顶级节点】 + foreach (HwPortalConfigType item in portalConfigTypes) + { + // 【顶级节点判定】 + // 如果 parentId 不在 tempList 中,说明是顶级节点 + // 包括:parentId 为 null、0、或指向不存在的节点 + if (!tempList.Contains(item.ParentId)) + { + // 递归构建子树 + RecursionFn(portalConfigTypes, item); + returnList.Add(item); + } + } + + // 如果没找到顶级节点,返回原列表(兜底处理) + return returnList.Count == 0 ? portalConfigTypes : returnList; + } + + /// + /// 递归添加所有子孙节点。 + /// + /// 【递归算法】 + /// 递归遍历所有子节点,并将它们添加到结果列表中。 + /// 用于在按分类查询时,收集某个顶级节点下的所有节点。 + /// + /// + /// 所有节点列表 + /// 父节点 + /// 结果列表 + private static void AddAllDescendants(List allList, HwPortalConfigType parentNode, List resultList) + { + // 遍历所有节点,找出父节点的直接子节点 + foreach (HwPortalConfigType item in allList) + { + // 如果 item 的 parentId 等于 parentNode 的 id,说明是子节点 + if (item.ParentId.HasValue && item.ParentId == parentNode.ConfigTypeId) + { + // 添加到结果列表 + resultList.Add(item); + // 递归添加子节点的子节点 + AddAllDescendants(allList, item, resultList); + } + } + } + + /// + /// 递归构建子树。 + /// + /// 【递归算法说明】 + /// 递归是一种"自己调用自己"的编程技巧。 + /// + /// 这里的递归逻辑: + /// 1. 找出当前节点的所有直接子节点 + /// 2. 把子节点挂到当前节点的 Children 属性 + /// 3. 对每个子节点,递归执行步骤 1-2 + /// + /// 终止条件:节点没有子节点(HasChild 返回 false) + /// + /// + /// 所有节点列表 + /// 当前节点 + private static void RecursionFn(List list, HwPortalConfigType current) + { + // 找出当前节点的所有直接子节点 + List childList = GetChildList(list, current); + + // 把子节点挂到当前节点 + current.Children = childList; + + // 【递归调用】 + // 对每个有子节点的子节点,继续递归构建子树 + // .Where(child => HasChild(list, child)) 筛选有子节点的节点 + foreach (HwPortalConfigType child in childList.Where(child => HasChild(list, child))) + { + RecursionFn(list, child); + } + } + + /// + /// 获取当前节点的直接子节点列表。 + /// + /// 所有节点列表 + /// 当前节点 + /// 子节点列表 + private static List GetChildList(List list, HwPortalConfigType current) + { + // 【LINQ Where 过滤】 + // item.ParentId.Value == current.ConfigTypeId.Value + // 条件:item 的 parentId 等于 current 的 id + // + // .HasValue 检查可空类型是否有值(不为 null) + // .Value 获取可空类型的实际值 + // + // 对比 Java: + // Java: list.stream().filter(item -> item.getParentId() != null && current.getConfigTypeId() != null && item.getParentId().equals(current.getConfigTypeId())).collect(Collectors.toList()); + // + // C# 的可空类型处理更优雅 + return list.Where(item => item.ParentId.HasValue && current.ConfigTypeId.HasValue && item.ParentId.Value == current.ConfigTypeId.Value).ToList(); + } + + /// + /// 判断当前节点是否有子节点。 + /// + /// 所有节点列表 + /// 当前节点 + /// 是否有子节点 + private static bool HasChild(List list, HwPortalConfigType current) + { + return GetChildList(list, current).Count > 0; + } + + /// + /// 将空字符串规范化转为 null。 + /// + /// 【辅助方法】 + /// 用于保持与 XML 方案的数据一致性。 + /// 原 XML 中 <if> 标签会跳过空字符串, + /// 这里主动将空字符串转为 null,达到同样效果。 + /// + /// + /// 输入字符串 + /// null 或原值 + private static string NormalizeEmptyToNull(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + /// + /// 静默更新搜索索引。 + /// + /// 【"静默"的含义】 + /// "静默"(Quietly)表示:索引更新失败不抛异常,只记录日志。 + /// + /// 为什么静默? + /// 1. 搜索索引是附属能力:主业务(增删改)成功就够了 + /// 2. 避免事务回滚:如果索引更新失败导致事务回滚,用户体验不好 + /// 3. 可以后续修复:索引问题可以后台修复,不影响用户操作 + /// + /// 这是"最终一致性"的设计思想: + /// 主业务强一致,附属能力最终一致。 + /// + /// + /// 配置类型数据 + /// 数据来源 + private async Task UpsertSearchIndexQuietly(HwPortalConfigType input, string source) + { + try + { + // 尝试更新搜索索引 + await _searchIndexService.UpsertConfigTypeAsync(input); + } + catch (Exception ex) + { + // 记录错误日志,但不抛出异常 + _logger.LogError(ex, "sync portal search index failed after {Source} changed", source); + } + } + + /// + /// 静默删除搜索索引。 + /// + /// 配置类型ID集合 + /// 数据来源 + private async Task DeleteSearchIndexQuietly(IEnumerable configTypeIds, string source) + { + try + { + // 【Distinct 去重】 + // .Distinct() 去除重复的 ID + // 对比 Java:stream().distinct() + foreach (long configTypeId in configTypeIds.Distinct()) + { + await _searchIndexService.DeleteConfigTypeAsync(configTypeId); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "delete portal search index failed after {Source} changed", source); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductCaseInfoService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductCaseInfoService.cs new file mode 100644 index 0000000..23408a0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductCaseInfoService.cs @@ -0,0 +1,355 @@ +// ============================================================================ +// 【文件说明】HwProductCaseInfoService.cs - 产品案例信息服务类 +// ============================================================================ +// 这个服务类负责处理产品案例信息的业务逻辑,包括: +// - 产品案例的 CRUD 操作 +// - 典型案例查询 +// - 关联查询(案例 + 其他信息) +// +// 【业务背景】 +// 产品案例模块用于管理企业的产品案例展示,支持标记典型案例。 +// 典型案例会在网站首页等重点位置展示。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwProductCaseInfoServiceImpl implements HwProductCaseInfoService { ... } +// +// ASP.NET Core + Furion: +// public class HwProductCaseInfoService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品案例信息服务类。 +/// +/// 【服务职责】 +/// 1. 管理产品案例的增删改查 +/// 2. 提供典型案例查询功能 +/// 3. 支持关联查询获取完整案例信息 +/// +/// +/// 【典型案例标记】 +/// - HomeTypicalFlag:是否在首页展示("0"否,"1"是) +/// - TypicalFlag:是否典型案例("0"否,"1"是) +/// +/// 典型案例优先级: +/// 1. 优先返回标记为 TypicalFlagYes 的案例 +/// 2. 如果没有典型案例,返回第一个案例 +/// 3. 如果列表为空,返回空对象 +/// +/// +/// 【当前实现说明】 +/// 这个服务目前仍使用 XML Mapper 方式访问数据。 +/// 后续可以考虑迁移到 SqlSugar,以获得更好的类型安全和性能。 +/// +/// +public class HwProductCaseInfoService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了。 + /// + /// 对比 Java: + /// Java: private static final String MAPPER = "HwProductCaseInfoMapper"; + /// C#: private const string Mapper = "HwProductCaseInfoMapper"; + /// + /// C# 的命名约定: + /// - const 通常用 PascalCase(首字母大写) + /// - Java 的 static final 通常用 UPPER_SNAKE_CASE + /// + /// + private const string Mapper = "HwProductCaseInfoMapper"; + + /// + /// MyBatis 执行器。 + /// + /// 【依赖注入】 + /// 通过构造函数注入,和控制器注入服务的方式一样。 + /// + /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 + /// 它负责解析 XML 中的 SQL 语句,执行参数绑定,并将结果映射到实体对象。 + /// + /// 对比 Java MyBatis: + /// Java MyBatis 使用 Mapper 接口 + XML 配置: + /// @Mapper + /// public interface HwProductCaseInfoMapper { + /// @Select("SELECT * FROM hw_product_case_info WHERE case_info_id = #{caseInfoId}") + /// HwProductCaseInfo selectById(Long caseInfoId); + /// } + /// + /// C# 这里使用执行器模式: + /// _executor.QuerySingleAsync<HwProductCaseInfo>(Mapper, "selectHwProductCaseInfoByCaseInfoId", new { caseInfoId }); + /// + /// 两种方式的对比: + /// - Java MyBatis:编译期检查,类型安全 + /// - C# 执行器模式:更灵活,可以动态选择 SQL,但缺少编译期检查 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// public HwProductCaseInfoService(HwPortalMyBatisExecutor executor) + /// + /// 对比 Java: + /// Java: + /// @Autowired + /// public HwProductCaseInfoService(HwPortalMyBatisExecutor executor) { + /// this.executor = executor; + /// } + /// + /// C#: + /// public HwProductCaseInfoService(HwPortalMyBatisExecutor executor) + /// { + /// _executor = executor; + /// } + /// + /// C# 的改进: + /// 1. 不需要 @Autowired 注解,框架自动识别构造函数 + /// 2. 使用 _executor 命名约定表示私有字段 + /// 3. 可以使用主构造函数(C# 12+)进一步简化 + /// + /// + /// MyBatis 执行器 + public HwProductCaseInfoService(HwPortalMyBatisExecutor executor) + { + _executor = executor; + } + + /// + /// 根据案例ID查询产品案例信息。 + /// + /// 【方法命名约定】 + /// Select + 实体名 + By + 主键名:根据主键查询单条记录 + /// + /// 对比 Java 若依: + /// 若依通常使用:selectXxxById、getXxxById + /// 这里使用:SelectXxxByCaseInfoId,更符合 C# 的 PascalCase 命名规范 + /// + /// + /// 【XML Mapper 调用】 + /// _executor.QuerySingleAsync<T>(mapperName, statementId, parameters) + /// - mapperName:XML 文件中 mapper 的 namespace + /// - statementId:SQL 语句的 id + /// - parameters:参数对象(匿名对象或实体) + /// + /// + /// 案例ID + /// 产品案例实体 + public Task SelectHwProductCaseInfoByCaseInfoId(long caseInfoId) + { + // 【匿名对象传参】 + // new { caseInfoId } 是 C# 的"匿名对象"语法。 + // 编译器会自动创建一个包含 caseInfoId 属性的临时类。 + // + // 对比 Java: + // Java 没有匿名对象语法,通常用: + // - Map<String, Object> params = new HashMap<>(); + // - params.put("caseInfoId", caseInfoId); + // 或者: + // - @Param("caseInfoId") Long caseInfoId(MyBatis 注解) + // + // C# 的匿名对象更简洁,编译器自动推断类型。 + return _executor.QuerySingleAsync(Mapper, "selectHwProductCaseInfoByCaseInfoId", new { caseInfoId }); + } + + /// + /// 查询产品案例列表。 + /// + /// 【动态查询条件】 + /// 查询条件通过 input 对象传递,XML 中使用 <if> 标签动态构建 WHERE 子句。 + /// + /// 对比 Java MyBatis XML: + /// <select id="selectHwProductCaseInfoList" resultType="HwProductCaseInfo"> + /// SELECT * FROM hw_product_case_info + /// <where> + /// <if test="caseName != null and caseName != ''"> + /// AND case_name LIKE CONCAT('%', #{caseName}, '%') + /// </if> + /// </where> + /// </select> + /// + /// + /// 查询条件 + /// 产品案例列表 + public Task> SelectHwProductCaseInfoList(HwProductCaseInfo input) + { + // 【空合并运算符 ??】 + // input ?? new HwProductCaseInfo() 含义: + // 如果 input 不为 null,就用 input; + // 否则 new 一个默认的 HwProductCaseInfo 对象。 + // + // 对比 Java: + // Java 需要手写: + // if (input == null) input = new HwProductCaseInfo(); + // 或者用 Optional: + // input = Optional.ofNullable(input).orElse(new HwProductCaseInfo()); + // + // C# 的空合并运算符更简洁! + return _executor.QueryListAsync(Mapper, "selectHwProductCaseInfoList", input ?? new HwProductCaseInfo()); + } + + /// + /// 新增产品案例信息。 + /// + /// 【审计字段自动填充】 + /// CreateTime 由代码自动设置,不需要前端传递。 + /// 这是常见的"审计字段"处理模式。 + /// + /// + /// 【自增主键回填】 + /// InsertReturnIdentityAsync 返回自增主键值, + /// 然后回填到 input 对象,方便调用方获取新记录的 ID。 + /// + /// + /// 产品案例数据 + /// 影响行数(1成功,0失败) + public async Task InsertHwProductCaseInfo(HwProductCaseInfo input) + { + // 【静态工具类调用】 + // HwPortalContextHelper.Now() 获取当前时间。 + // 这是"上下文助手"模式:封装了请求上下文相关的操作。 + // + // 对比 Java Spring: + // Java 通常用 new Date() 或 LocalDateTime.now()。 + // 这里封装一层的好处:可以统一控制时间来源(如测试时 mock)。 + input.CreateTime = HwPortalContextHelper.Now(); + + // 【执行插入并获取自增主键】 + // InsertReturnIdentityAsync 返回插入记录的自增主键值。 + // 对比 Java MyBatis: + // Java 需要配置 useGeneratedKeys="true" keyProperty="caseInfoId", + // 然后通过 input.getCaseInfoId() 获取主键。 + long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwProductCaseInfo", input); + + // 【回填自增主键】 + // 把自增主键回填到实体对象,方便调用方使用。 + input.CaseInfoId = identity; + + // 返回 1 表示成功,0 表示失败。 + return identity > 0 ? 1 : 0; + } + + /// + /// 更新产品案例信息。 + /// + /// 【审计字段更新】 + /// UpdateTime 由代码自动设置,标记记录更新时间。 + /// + /// + /// 产品案例数据 + /// 影响行数 + public Task UpdateHwProductCaseInfo(HwProductCaseInfo input) + { + // 【审计字段】 + input.UpdateTime = HwPortalContextHelper.Now(); + + // 【执行更新】 + // ExecuteAsync 执行 UPDATE 语句,返回影响行数。 + return _executor.ExecuteAsync(Mapper, "updateHwProductCaseInfo", input); + } + + /// + /// 批量删除产品案例信息。 + /// + /// 【批量删除】 + /// 根据 ID 数组批量删除记录。 + /// XML 中使用 <foreach> 标签生成 IN 语句。 + /// + /// 对比 Java MyBatis XML: + /// <delete id="deleteHwProductCaseInfoByCaseInfoIds"> + /// DELETE FROM hw_product_case_info + /// WHERE case_info_id IN + /// <foreach collection="array" item="id" open="(" separator="," close=")"> + /// #{id} + /// </foreach> + /// </delete> + /// + /// + /// 案例ID数组 + /// 影响行数 + public Task DeleteHwProductCaseInfoByCaseInfoIds(long[] caseInfoIds) + { + // 【匿名对象传参】 + // new { array = caseInfoIds } 将数组包装为匿名对象的属性。 + // XML 中通过 #{array} 引用整个数组,<foreach> 遍历它。 + return _executor.ExecuteAsync(Mapper, "deleteHwProductCaseInfoByCaseInfoIds", new { array = caseInfoIds }); + } + + /// + /// 获取首页典型案例信息。 + /// + /// 【业务逻辑】 + /// 1. 设置查询条件:HomeTypicalFlag = "1"(首页展示) + /// 2. 查询符合条件的案例列表 + /// 3. 优先返回标记为 TypicalFlagYes 的案例 + /// 4. 如果没有典型案例,返回第一个案例 + /// 5. 如果列表为空,返回空对象(不是 null) + /// + /// 【设计模式:空对象模式】 + /// 返回 new HwProductCaseInfo() 而不是 null, + /// 避免调用方出现 NullReferenceException。 + /// 这是一种防御性编程实践。 + /// + /// + /// 查询条件 + /// 典型案例实体 + public async Task GetTypicalHomeCaseInfo(HwProductCaseInfo input) + { + // 【空合并赋值运算符 ??=】 + // input ??= new HwProductCaseInfo() 等价于: + // if (input == null) input = new HwProductCaseInfo(); + // + // 这是 C# 8.0 引入的语法糖,让代码更简洁。 + input ??= new HwProductCaseInfo(); + + // 【设置查询条件】 + // 只查询首页展示的案例(HomeTypicalFlag = "1") + input.HomeTypicalFlag = HwPortalConstants.HomeTypicalFlagYes; + + // 【执行查询】 + List list = await SelectHwProductCaseInfoList(input); + + // 【优先返回典型案例】 + // .FirstOrDefault(predicate) 返回第一个满足条件的元素,如果没有则返回 null。 + // string.Equals(a, b, StringComparison.Ordinal) 区分大小写的精确比较。 + // + // 对比 Java: + // Java: list.stream().filter(u -> HwPortalConstants.TypicalFlagYes.equals(u.getTypicalFlag())).findFirst().orElse(null); + // C#: list.FirstOrDefault(u => string.Equals(u.TypicalFlag, HwPortalConstants.TypicalFlagYes, StringComparison.Ordinal)); + HwProductCaseInfo typical = list.FirstOrDefault(u => string.Equals(u.TypicalFlag, HwPortalConstants.TypicalFlagYes, StringComparison.Ordinal)); + + // 【兜底策略】 + // 如果没有典型案例,返回第一个案例。 + // 如果列表为空,返回空对象。 + // ?? 是空合并运算符:如果左边为 null,返回右边。 + return typical ?? list.FirstOrDefault() ?? new HwProductCaseInfo(); + } + + /// + /// 查询产品案例关联列表(JOIN查询)。 + /// + /// 【关联查询】 + /// 这个方法执行 JOIN 查询,返回包含关联信息的完整案例数据。 + /// 关联查询通常在 XML 中定义,使用 <resultMap> 映射结果。 + /// + /// 对比 Java MyBatis: + /// Java MyBatis 使用 <resultMap> + <association> 或 <collection> 处理关联。 + /// C# 这里的实现类似,也是通过 XML 配置映射。 + /// + /// + /// 查询条件 + /// 产品案例列表(含关联信息) + public Task> SelectHwProductCaseInfoJoinList(HwProductCaseInfo input) + { + // 【空合并运算符 ??】 + // 确保 input 不为 null,避免 XML 中出现问题。 + return _executor.QueryListAsync(Mapper, "selectHwProductCaseInfoJoinList", input ?? new HwProductCaseInfo()); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoDetailService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoDetailService.cs new file mode 100644 index 0000000..dd920e1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoDetailService.cs @@ -0,0 +1,284 @@ +// ============================================================================ +// 【文件说明】HwProductInfoDetailService.cs - 产品信息明细服务类 +// ============================================================================ +// 这个服务类负责处理产品信息明细的业务逻辑,包括: +// - 产品明细的 CRUD 操作 +// - 树形结构(父子关系)管理 +// - 祖先路径(Ancestors)自动计算 +// +// 【业务背景】 +// 产品信息明细用于管理产品的详细配置项,支持多级树形结构。 +// 例如:产品 -> 版本 -> 配置项 -> 子配置项 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwProductInfoDetailServiceImpl implements HwProductInfoDetailService { ... } +// +// ASP.NET Core + Furion: +// public class HwProductInfoDetailService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品信息明细服务类。 +/// +/// 【服务职责】 +/// 1. 管理产品明细的增删改查 +/// 2. 自动计算和维护树形结构的祖先路径(Ancestors) +/// 3. 处理空字符串规范化 +/// +/// +/// 【树形结构说明】 +/// - ParentId:父节点ID,0表示顶级节点 +/// - Ancestors:祖先路径,格式为"0,1,2"表示从根到父节点的路径 +/// +/// 示例: +/// 产品A(ID=1, ParentId=0, Ancestors="0") +/// └── 版本B(ID=2, ParentId=1, Ancestors="0,1") +/// └── 配置C(ID=3, ParentId=2, Ancestors="0,1,2") +/// +/// +public class HwProductInfoDetailService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwProductInfoDetailMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + public HwProductInfoDetailService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据明细ID查询产品明细信息。 + /// + /// 明细ID + /// 产品明细实体 + public async Task SelectHwProductInfoDetailByProductInfoDetailId(long productInfoDetailId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwProductInfoDetailByProductInfoDetailId", new { productInfoDetailId }); + return await _db.Queryable() + .Where(item => item.ProductInfoDetailId == productInfoDetailId) + .FirstAsync(); + } + + /// + /// 查询产品明细列表。 + /// + /// 【动态查询条件】 + /// 支持按父ID、产品ID、配置模式、标题、描述、排序号、图片、祖先路径等条件筛选。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 查询条件 + /// 产品明细列表 + public async Task> SelectHwProductInfoDetailList(HwProductInfoDetail input) + { + HwProductInfoDetail query = input ?? new HwProductInfoDetail(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwProductInfoDetailList", query); + return await _db.Queryable() + .WhereIF(query.ParentId.HasValue, item => item.ParentId == query.ParentId) + .WhereIF(query.ProductInfoId.HasValue, item => item.ProductInfoId == query.ProductInfoId) + .WhereIF(!string.IsNullOrWhiteSpace(query.ConfigModal), item => item.ConfigModal == query.ConfigModal) + .WhereIF(!string.IsNullOrWhiteSpace(query.ProductInfoDetailTitle), item => item.ProductInfoDetailTitle.Contains(query.ProductInfoDetailTitle)) + .WhereIF(!string.IsNullOrWhiteSpace(query.ProductInfoDetailDesc), item => item.ProductInfoDetailDesc.Contains(query.ProductInfoDetailDesc)) + .WhereIF(query.ProductInfoDetailOrder.HasValue, item => item.ProductInfoDetailOrder == query.ProductInfoDetailOrder) + .WhereIF(!string.IsNullOrWhiteSpace(query.ProductInfoDetailPic), item => item.ProductInfoDetailPic == query.ProductInfoDetailPic) + .WhereIF(!string.IsNullOrWhiteSpace(query.Ancestors), item => item.Ancestors == query.Ancestors) + .ToListAsync(); + } + + /// + /// 新增产品明细信息。 + /// + /// 【树形结构处理】 + /// 1. 如果没有指定父ID或父ID为0,设置为顶级节点(ParentId=0, Ancestors="0") + /// 2. 如果指定了父ID,查询父节点的祖先路径,拼接成新的祖先路径 + /// + /// 示例: + /// 父节点:Ancestors="0,1",当前 ParentId=2 + /// 新节点:Ancestors="0,1,2" + /// + /// + /// 【空字符串规范化】 + /// 将标题、描述、祖先路径的空字符串转为 null,与 XML 方案保持一致。 + /// + /// + /// 产品明细数据 + /// 影响行数 + public async Task InsertHwProductInfoDetail(HwProductInfoDetail input) + { + // 【树形结构处理】 + // 如果没有父ID或父ID为0,设置为顶级节点 + if (!input.ParentId.HasValue || input.ParentId == 0) + { + input.ParentId = 0; + input.Ancestors = "0"; + } + else + { + // 查询父节点的祖先路径,拼接成新的祖先路径 + HwProductInfoDetail info = await SelectHwProductInfoDetailByProductInfoDetailId(input.ParentId.Value); + input.Ancestors = $"{info?.Ancestors},{input.ParentId}"; + } + + // 【数据规范化】 + // 将空字符串转为 null,与 XML 方案保持一致 + input.ProductInfoDetailTitle = NormalizeEmptyToNull(input.ProductInfoDetailTitle); + input.ProductInfoDetailDesc = NormalizeEmptyToNull(input.ProductInfoDetailDesc); + input.Ancestors = NormalizeEmptyToNull(input.Ancestors); + + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + input.CreateBy = HwPortalContextHelper.CurrentUserName(); + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwProductInfoDetail", input); + HwProductInfoDetail entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.ProductInfoDetailId = entity.ProductInfoDetailId; + return input.ProductInfoDetailId > 0 ? 1 : 0; + } + + /// + /// 更新产品明细信息。 + /// + /// 【字段级更新策略】 + /// 1. 先查询现有记录 + /// 2. 对非空字段进行更新 + /// 3. 空字符串也视为有效值 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwProductInfoDetail(HwProductInfoDetail input) + { + // 【审计字段】 + input.UpdateTime = HwPortalContextHelper.Now(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwProductInfoDetail", input); + HwProductInfoDetail current = await SelectHwProductInfoDetailByProductInfoDetailId(input.ProductInfoDetailId ?? 0); + if (current == null) + { + return 0; + } + + // 【父ID】 + if (input.ParentId.HasValue) + { + current.ParentId = input.ParentId; + } + + // 【产品ID】 + if (input.ProductInfoId.HasValue) + { + current.ProductInfoId = input.ProductInfoId; + } + + // 【配置模式】 + if (input.ConfigModal != null) + { + current.ConfigModal = input.ConfigModal; + } + + // 【标题】 + if (!string.IsNullOrWhiteSpace(input.ProductInfoDetailTitle)) + { + current.ProductInfoDetailTitle = input.ProductInfoDetailTitle; + } + + // 【描述】 + if (!string.IsNullOrWhiteSpace(input.ProductInfoDetailDesc)) + { + current.ProductInfoDetailDesc = input.ProductInfoDetailDesc; + } + + // 【排序号】 + if (input.ProductInfoDetailOrder.HasValue) + { + current.ProductInfoDetailOrder = input.ProductInfoDetailOrder; + } + + // 【图片】 + if (input.ProductInfoDetailPic != null) + { + current.ProductInfoDetailPic = input.ProductInfoDetailPic; + } + + // 【祖先路径】 + if (!string.IsNullOrWhiteSpace(input.Ancestors)) + { + current.Ancestors = input.Ancestors; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + if (input.CreateBy != null) + { + current.CreateBy = input.CreateBy; + } + + current.UpdateTime = input.UpdateTime; + if (input.UpdateBy != null) + { + current.UpdateBy = input.UpdateBy; + } + + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除产品明细信息。 + /// + /// 明细ID数组 + /// 影响行数 + public async Task DeleteHwProductInfoDetailByProductInfoDetailIds(long[] productInfoDetailIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwProductInfoDetailByProductInfoDetailIds", new { array = productInfoDetailIds }); + return await _db.Deleteable() + .In(productInfoDetailIds) + .ExecuteCommandAsync(); + } + + /// + /// 将空字符串规范化转为 null。 + /// + /// 【辅助方法】 + /// 用于保持与 XML 方案的数据一致性。 + /// 原 XML 中 <if> 标签会跳过空字符串, + /// 这里主动将空字符串转为 null,达到同样效果。 + /// + /// + /// 输入字符串 + /// null 或原值 + private static string NormalizeEmptyToNull(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoService.cs new file mode 100644 index 0000000..9a242a2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwProductInfoService.cs @@ -0,0 +1,395 @@ +// ============================================================================ +// 【文件说明】HwProductInfoService.cs - 产品信息业务服务类 +// ============================================================================ +// 这个服务类负责处理产品信息的业务逻辑,包括: +// - 产品信息的 CRUD 操作 +// - 产品与明细的关联查询 +// - 产品明细树形结构的构建 +// +// 【核心知识点】 +// 1. LINQ GroupBy 分组操作 +// 2. 递归树构建算法 +// 3. 扁平数据转树形结构 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 产品信息业务服务类。 +/// +/// 【服务职责】 +/// 产品信息服务负责: +/// 1. 基础 CRUD:增删改查 +/// 2. 关联查询:产品 + 明细的 JOIN 查询 +/// 3. 数据转换:扁平结果集转树形结构 +/// +/// +public class HwProductInfoService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + private const string Mapper = "HwProductInfoMapper"; + + /// + /// MyBatis 执行器。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + public HwProductInfoService(HwPortalMyBatisExecutor executor) + { + _executor = executor; + } + + /// + /// 根据产品ID查询产品信息。 + /// + /// 产品ID + /// 产品实体 + public Task SelectHwProductInfoByProductInfoId(long productInfoId) + { + // 【匿名对象传参】 + // new { productInfoId } 创建一个包含 productInfoId 属性的匿名对象。 + // 编译器自动推断类型,比 Java 的 HashMap 更简洁。 + return _executor.QuerySingleAsync(Mapper, "selectHwProductInfoByProductInfoId", new { productInfoId }); + } + + /// + /// 查询产品列表。 + /// + /// 查询条件 + /// 产品列表 + public Task> SelectHwProductInfoList(HwProductInfo input) + { + // 【空合并运算符 ??】 + // input ?? new HwProductInfo():如果 input 为 null,创建默认对象。 + return _executor.QueryListAsync(Mapper, "selectHwProductInfoList", input ?? new HwProductInfo()); + } + + /// + /// 新增产品信息。 + /// + /// 产品数据 + /// 影响行数 + public async Task InsertHwProductInfo(HwProductInfo input) + { + // 【静态工具类调用】 + // HwPortalContextHelper.Now() 获取当前时间。 + // 这是"上下文助手"模式:封装了请求上下文相关的操作。 + // + // 对比 Java Spring: + // Java 通常用 new Date() 或 LocalDateTime.now()。 + // 这里封装一层的好处:可以统一控制时间来源(如测试时 mock)。 + input.CreateTime = HwPortalContextHelper.Now(); + + long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwProductInfo", input); + + // 回填自增主键。 + input.ProductInfoId = identity; + + return identity > 0 ? 1 : 0; + } + + /// + /// 更新产品信息。 + /// + /// 产品数据 + /// 影响行数 + public Task UpdateHwProductInfo(HwProductInfo input) + { + input.UpdateTime = HwPortalContextHelper.Now(); + return _executor.ExecuteAsync(Mapper, "updateHwProductInfo", input); + } + + /// + /// 批量删除产品信息。 + /// + /// 产品ID数组 + /// 影响行数 + public Task DeleteHwProductInfoByProductInfoIds(long[] productInfoIds) + { + return _executor.ExecuteAsync(Mapper, "deleteHwProductInfoByProductInfoIds", new { array = productInfoIds }); + } + + /// + /// 查询产品信息及其明细列表(关联查询)。 + /// + /// 【核心算法:扁平数据转树形结构】 + /// 这个方法展示了如何把"扁平的 JOIN 结果"转换成"树形的对象结构"。 + /// + /// 步骤: + /// 1. 执行 SQL JOIN 查询,得到扁平结果集 + /// 2. 按 ProductInfoId 分组 + /// 3. 每组的第一行作为产品主数据 + /// 4. 每组的所有行作为明细列表 + /// 5. 如果配置模式是树形(13),递归构建明细树 + /// + /// + /// 查询条件 + /// 产品列表(包含明细) + public async Task> SelectHwProductInfoJoinDetailList(HwProductInfo input) + { + // 【第一步:查询扁平结果集】 + // SQL JOIN 查询返回的是扁平结构: + // 产品1 | 明细1 + // 产品1 | 明细2 + // 产品1 | 明细3 + // 产品2 | 明细4 + // 产品2 | 明细5 + // + // 每行包含产品字段 + 明细字段。 + List rows = await _executor.QueryListAsync(Mapper, "selectHwProductInfoJoinDetailList", input ?? new HwProductInfo()); + + // 【第二步:LINQ GroupBy 分组转换】 + // rows.GroupBy(row => row.ProductInfoId) + // 按 ProductInfoId 分组,相同 ProductInfoId 的行归为一组。 + // + // .Select(group => { ... }) + // 对每个分组进行转换,生成一个 HwProductInfo 对象。 + // + // 对比 Java Stream: + // Java 写法: + // rows.stream() + // .collect(Collectors.groupingBy(HwProductInfoJoinDetailRow::getProductInfoId)) + // .entrySet().stream() + // .map(entry -> { + // HwProductInfoJoinDetailRow first = entry.getValue().get(0); + // HwProductInfo product = new HwProductInfo(); + // product.setProductInfoId(first.getProductInfoId()); + // // ... 更多字段 + // product.setDetailList(entry.getValue().stream() + // .map(row -> { + // HwProductInfoDetail detail = new HwProductInfoDetail(); + // // ... 字段映射 + // return detail; + // }) + // .collect(Collectors.toList())); + // return product; + // }) + // .collect(Collectors.toList()); + // + // C# 的 LINQ 更简洁,特别是对象初始化器语法。 + List products = rows + .GroupBy(row => row.ProductInfoId) + .Select(group => + { + // group.First() 获取分组中的第一行。 + // 第一行的产品字段就是该产品的数据。 + HwProductInfoJoinDetailRow first = group.First(); + + // 【对象初始化器】 + // new HwProductInfo { ... } 直接在创建时赋值。 + HwProductInfo product = new() + { + ProductInfoId = first.ProductInfoId, + ConfigTypeId = first.ConfigTypeId, + TabFlag = first.TabFlag, + ConfigModal = first.ConfigModal, + ProductInfoEtitle = first.ProductInfoEtitle, + ProductInfoCtitle = first.ProductInfoCtitle, + ProductInfoOrder = first.ProductInfoOrder, + CreateTime = first.CreateTime, + CreateBy = first.CreateBy, + UpdateTime = first.UpdateTime, + UpdateBy = first.UpdateBy, + ParentId = first.ParentId, + ConfigTypeName = first.ConfigTypeName, + + // 【嵌套 LINQ:构建明细列表】 + // group.Where(...).Select(...).ToList() + // 从当前分组中筛选出明细行,转换为明细对象列表。 + // + // .Where(item => item.SubProductInfoDetailId.HasValue) + // 过滤掉没有明细的行(SubProductInfoDetailId 为 null)。 + HwProductInfoDetailList = group + .Where(item => item.SubProductInfoDetailId.HasValue) + .Select(item => new HwProductInfoDetail + { + ProductInfoDetailId = item.SubProductInfoDetailId, + ParentId = item.SubParentId, + ProductInfoId = item.SubProductInfoId, + ProductInfoDetailTitle = item.SubProductInfoDetailTitle, + ProductInfoDetailDesc = item.SubProductInfoDetailDesc, + ProductInfoDetailOrder = item.SubProductInfoDetailOrder, + ProductInfoDetailPic = item.SubProductInfoDetailPic, + Ancestors = item.SubAncestors, + CreateTime = item.SubCreateTime, + CreateBy = item.SubCreateBy, + UpdateTime = item.SubUpdateTime, + UpdateBy = item.SubUpdateBy, + ConfigModel = item.ConfigModel + }) + .ToList() + }; + return product; + }) + .ToList(); + + // 【第三步:判断是否需要构建树形结构】 + // string.Equals(a, b, StringComparison.Ordinal) + // Ordinal 表示"按字节比较",最快但区分大小写。 + // + // 对比 Java: + // Java: a.equals(b) 或 a.equalsIgnoreCase(b) + // C#: string.Equals(a, b) 或 a == b(等价于 Equals) + if (string.Equals(input?.ConfigModal, HwPortalConstants.ProductInfoConfigModalTree, StringComparison.Ordinal)) + { + // 配置模式 13 要求把明细转换成树形结构。 + // .Where(u => u.HwProductInfoDetailList.Count > 0) 筛选有明细的产品。 + foreach (HwProductInfo productInfo in products.Where(u => u.HwProductInfoDetailList.Count > 0)) + { + productInfo.HwProductInfoDetailList = BuildProductInfoDetailTree(productInfo.HwProductInfoDetailList); + } + } + + // 【额外判断:明细内部的配置模式】 + // 除了主查询条件,还需要检查每个明细的 ConfigModel。 + // 这是源代码的业务规则,保持原行为。 + foreach (HwProductInfo productInfo in products) + { + foreach (HwProductInfoDetail detail in productInfo.HwProductInfoDetailList) + { + if (string.Equals(detail.ConfigModel, HwPortalConstants.ProductInfoConfigModalTree, StringComparison.Ordinal)) + { + productInfo.HwProductInfoDetailList = BuildProductInfoDetailTree(productInfo.HwProductInfoDetailList); + break; // 找到一个就跳出,避免重复构建。 + } + } + } + + return products; + } + + /// + /// 查询产品信息关联列表。 + /// + /// 查询条件 + /// 产品列表 + public Task> SelectHwProductInfoJoinList(HwProductInfo input) + { + return _executor.QueryListAsync(Mapper, "selectHwProductInfoJoinList", input ?? new HwProductInfo()); + } + + /// + /// 构建产品明细树形结构。 + /// + /// 【树形结构构建算法】 + /// 这是一个经典的"扁平列表转树形结构"算法: + /// + /// 输入:扁平的节点列表,每个节点有 parentId 指向父节点 + /// 输出:树形结构,顶级节点包含 children 子节点 + /// + /// 算法步骤: + /// 1. 找出所有顶级节点(parentId 为空/0/不在列表中) + /// 2. 对每个顶级节点,递归查找子节点 + /// 3. 子节点再递归查找孙节点,直到叶子节点 + /// + /// 对比 Java 若依: + /// 若依的 TreeBuildUtils.buildTree() 做同样的事情。 + /// 这是若依框架的标准树构建算法。 + /// + /// + /// 扁平的明细列表 + /// 树形结构的明细列表 + public List BuildProductInfoDetailTree(List productInfoDetails) + { + // 【收集所有节点 ID】 + // 用于判断某个 parentId 是否在列表中。 + // + // .Select(item => item.ProductInfoDetailId) 提取所有 ID。 + // .ToList() 转换为列表。 + List returnList = new(); + List tempList = productInfoDetails.Select(item => item.ProductInfoDetailId).ToList(); + + // 【遍历查找顶级节点】 + foreach (HwProductInfoDetail detail in productInfoDetails) + { + // 【顶级节点判定】 + // 满足以下任一条件即为顶级节点: + // 1. !detail.ParentId.HasValue:parentId 为 null + // 2. detail.ParentId == 0:parentId 为 0 + // 3. !tempList.Contains(detail.ParentId):parentId 不在列表中(父节点不存在) + if (!detail.ParentId.HasValue || detail.ParentId == 0 || !tempList.Contains(detail.ParentId)) + { + // 找到顶级节点后,递归构建其子树。 + RecursionFn(productInfoDetails, detail); + returnList.Add(detail); + } + } + + // 如果没找到顶级节点,返回原列表(兜底处理)。 + return returnList.Count == 0 ? productInfoDetails : returnList; + } + + /// + /// 递归构建子树。 + /// + /// 【递归算法说明】 + /// 递归是一种"自己调用自己"的编程技巧。 + /// + /// 这里的递归逻辑: + /// 1. 找出当前节点的所有直接子节点 + /// 2. 把子节点挂到当前节点的 Children 属性 + /// 3. 对每个子节点,递归执行步骤 1-2 + /// + /// 终止条件:节点没有子节点(HasChild 返回 false) + /// + /// + /// 所有节点列表 + /// 当前节点 + private static void RecursionFn(List list, HwProductInfoDetail current) + { + // 找出当前节点的所有直接子节点。 + List childList = GetChildList(list, current); + + // 把子节点挂到当前节点。 + // Children 和 HwProductInfoDetailList 都设置,兼容不同的访问方式。 + current.Children = childList; + current.HwProductInfoDetailList = childList; + + // 【递归调用】 + // 对每个有子节点的子节点,继续递归构建子树。 + // .Where(child => HasChild(list, child)) 筛选有子节点的节点。 + foreach (HwProductInfoDetail child in childList.Where(child => HasChild(list, child))) + { + RecursionFn(list, child); + } + } + + /// + /// 获取当前节点的直接子节点列表。 + /// + /// 所有节点列表 + /// 当前节点 + /// 子节点列表 + private static List GetChildList(List list, HwProductInfoDetail current) + { + // 【LINQ Where 过滤】 + // item.ParentId.Value == current.ProductInfoDetailId.Value + // 条件:item 的 parentId 等于 current 的 id。 + // + // .HasValue 检查可空类型是否有值(不为 null)。 + // .Value 获取可空类型的实际值。 + // + // 对比 Java: + // Java: list.stream().filter(item -> item.getParentId() != null && current.getProductInfoDetailId() != null && item.getParentId().equals(current.getProductInfoDetailId())).collect(Collectors.toList()); + // + // C# 的可空类型处理更优雅。 + return list.Where(item => item.ParentId.HasValue && current.ProductInfoDetailId.HasValue && item.ParentId.Value == current.ProductInfoDetailId.Value).ToList(); + } + + /// + /// 判断当前节点是否有子节点。 + /// + /// 所有节点列表 + /// 当前节点 + /// 是否有子节点 + private static bool HasChild(List list, HwProductInfoDetail current) + { + return GetChildList(list, current).Count > 0; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs new file mode 100644 index 0000000..d77685b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs @@ -0,0 +1,292 @@ +// ============================================================================ +// 【文件说明】HwWeb1Service.cs - 网页内容服务类 +// ============================================================================ +// 这个服务类负责处理网页内容的业务逻辑,包括: +// - 网页内容的 CRUD 操作 +// - 多维度查询(网站代码、设备ID、类型ID) +// - 搜索索引重建 +// +// 【业务背景】 +// 网页内容模块用于管理网站的页面内容,支持按网站代码、设备类型、页面类型等多维度管理。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwWeb1ServiceImpl implements HwWeb1Service { ... } +// +// ASP.NET Core + Furion: +// public class HwWeb1Service : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 网页内容服务类。 +/// +/// 【服务职责】 +/// 1. 管理网页内容的增删改查 +/// 2. 支持多维度查询(网站代码、设备ID、类型ID) +/// 3. 网页内容变更时自动重建搜索索引 +/// +/// +/// 【更新策略】 +/// 更新操作采用"先删除后插入"的策略: +/// 先删除符合条件的旧记录,然后插入新记录。 +/// 这种策略简化了更新逻辑,但会改变记录的ID。 +/// +/// +public class HwWeb1Service : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwWebMapper1"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 搜索索引重建服务。 + /// + private readonly IHwSearchRebuildService _searchRebuildService; + + /// + /// 日志记录器。 + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + /// 搜索索引重建服务 + /// 日志记录器 + public HwWeb1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) + { + _executor = executor; + _db = db; + _searchRebuildService = searchRebuildService; + _logger = logger; + } + + /// + /// 根据网站代码查询网页内容。 + /// + /// 【软删除过滤】 + /// 查询时自动过滤已删除的记录(IsDelete == "0")。 + /// + /// + /// 网站代码 + /// 网页内容实体 + public async Task SelectHwWebByWebcode(long webCode) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebByWebcode", new { webCode }); + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.WebCode == webCode) + .FirstAsync(); + } + + /// + /// 查询单个网页内容(多维度匹配)。 + /// + /// 【多维度匹配】 + /// 按网站代码、设备ID、类型ID三个维度精确匹配。 + /// + /// + /// 查询条件 + /// 网页内容实体 + public async Task SelectHwWebOne(HwWeb1 input) + { + HwWeb1 query = input ?? new HwWeb1(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebOne", query); + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.WebCode == query.WebCode && item.DeviceId == query.DeviceId && item.TypeId == query.TypeId) + .FirstAsync(); + } + + /// + /// 查询网页内容列表。 + /// + /// 【动态查询条件】 + /// 支持按网页ID、JSON内容、网站代码、设备ID、类型ID、英文JSON等条件筛选。 + /// + /// + /// 查询条件 + /// 网页内容列表 + public async Task> SelectHwWebList(HwWeb1 input) + { + HwWeb1 query = input ?? new HwWeb1(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwWebList", query); + return await _db.Queryable() + .Where(item => item.IsDelete == "0") + .WhereIF(query.WebId.HasValue, item => item.WebId == query.WebId) + .WhereIF(query.WebJson != null && query.WebJson != string.Empty, item => item.WebJson == query.WebJson) + .WhereIF(query.WebJsonString != null && query.WebJsonString != string.Empty, item => item.WebJsonString == query.WebJsonString) + .WhereIF(query.WebCode.HasValue, item => item.WebCode == query.WebCode) + .WhereIF(query.DeviceId.HasValue, item => item.DeviceId == query.DeviceId) + .WhereIF(query.TypeId.HasValue, item => item.TypeId == query.TypeId) + .WhereIF(query.WebJsonEnglish != null, item => item.WebJsonEnglish == query.WebJsonEnglish) + .ToListAsync(); + } + + /// + /// 新增网页内容。 + /// + /// 【搜索索引重建】 + /// 网页内容新增后,自动触发搜索索引重建。 + /// + /// + /// 网页内容数据 + /// 影响行数 + public async Task InsertHwWeb(HwWeb1 input) + { + // 【软删除标记初始化】 + input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); + HwWeb1 entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.WebId = entity.WebId; + + // 【搜索索引重建】 + if (input.WebId > 0) + { + await RebuildSearchIndexQuietly("hw_web1"); + } + + return input.WebId > 0 ? 1 : 0; + } + + /// + /// 更新网页内容。 + /// + /// 【更新策略】 + /// 采用"先删除后插入"的策略: + /// 1. 先查询并删除符合条件的旧记录(软删除) + /// 2. 设置新记录的 IsDelete = "0" + /// 3. 插入新记录 + /// + /// 这种策略简化了更新逻辑,但会改变记录的ID。 + /// + /// + /// 【搜索索引重建】 + /// 网页内容更新后,自动触发搜索索引重建。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwWeb(HwWeb1 input) + { + // 【查询旧记录】 + HwWeb1 query = new() + { + WebCode = input.WebCode, + TypeId = input.TypeId, + DeviceId = input.DeviceId + }; + List exists = await SelectHwWebList(query); + + // 【删除旧记录】 + if (exists.Count > 0) + { + await DeleteHwWebByWebIds(exists.Where(u => u.WebId.HasValue).Select(u => u.WebId!.Value).ToArray(), false); + } + + // 【设置新记录】 + input.IsDelete = "0"; + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); + HwWeb1 entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.WebId = entity.WebId; + + // 【搜索索引重建】 + if (input.WebId > 0) + { + await RebuildSearchIndexQuietly("hw_web1"); + } + + return input.WebId > 0 ? 1 : 0; + } + + /// + /// 批量删除网页内容(软删除)。 + /// + /// 网页ID数组 + /// 影响行数 + public Task DeleteHwWebByWebIds(long[] webIds) + { + return DeleteHwWebByWebIds(webIds, true); + } + + /// + /// 批量删除网页内容(软删除)。 + /// + /// 【软删除实现】 + /// 将 IsDelete 字段更新为"1",而不是物理删除。 + /// + /// + /// 【搜索索引重建】 + /// 网页内容删除后,自动触发搜索索引重建。 + /// + /// + /// 网页ID数组 + /// 是否重建搜索索引 + /// 影响行数 + private async Task DeleteHwWebByWebIds(long[] webIds, bool rebuild) + { + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebByWebIds", new { array = webIds }); + + // 【查询待删除的网页】 + List pages = await _db.Queryable() + .Where(item => item.WebId.HasValue && webIds.Contains(item.WebId.Value)) + .ToListAsync(); + + // 【软删除标记】 + foreach (HwWeb1 page in pages) + { + page.IsDelete = "1"; + } + + // 【批量更新】 + int rows = pages.Count == 0 ? 0 : await _db.Updateable(pages) + .UpdateColumns(item => new { item.IsDelete }) + .ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0 && rebuild) + { + await RebuildSearchIndexQuietly("hw_web1"); + } + + return rows; + } + + /// + /// 静默重建搜索索引。 + /// + /// 数据来源 + private async Task RebuildSearchIndexQuietly(string source) + { + try + { + await _searchRebuildService.RebuildAllAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs new file mode 100644 index 0000000..33b0e17 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs @@ -0,0 +1,363 @@ +// ============================================================================ +// 【文件说明】HwWebDocumentService.cs - 网页文档服务类 +// ============================================================================ +// 这个服务类负责处理网页文档的业务逻辑,包括: +// - 文档信息的 CRUD 操作 +// - 文档密钥验证 +// - 搜索索引重建 +// +// 【业务背景】 +// 网页文档模块用于管理网站的文档资源,支持密钥保护功能。 +// 当文档被修改时,会自动触发搜索索引重建。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwWebDocumentServiceImpl implements HwWebDocumentService { ... } +// +// ASP.NET Core + Furion: +// public class HwWebDocumentService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 网页文档服务类。 +/// +/// 【服务职责】 +/// 1. 管理网页文档的增删改查 +/// 2. 提供文档密钥验证功能 +/// 3. 文档变更时自动重建搜索索引 +/// +/// +/// 【软删除策略】 +/// 使用 IsDelete 字段标记删除状态("0"正常,"1"已删除), +/// 而不是物理删除,便于数据恢复和审计。 +/// +/// +public class HwWebDocumentService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwWebDocumentMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 搜索索引重建服务。 + /// + private readonly IHwSearchRebuildService _searchRebuildService; + + /// + /// 日志记录器。 + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + /// 搜索索引重建服务 + /// 日志记录器 + public HwWebDocumentService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) + { + _executor = executor; + _db = db; + _searchRebuildService = searchRebuildService; + _logger = logger; + } + + /// + /// 根据文档ID查询文档信息。 + /// + /// 【软删除过滤】 + /// 查询时自动过滤已删除的记录(IsDelete == "0")。 + /// + /// + /// 文档ID + /// 文档实体 + public async Task SelectHwWebDocumentByDocumentId(string documentId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebDocumentByDocumentId", new { documentId }); + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.DocumentId == documentId) + .FirstAsync(); + } + + /// + /// 查询文档列表。 + /// + /// 【动态查询条件】 + /// 支持按文档ID、租户ID、文档地址、网站代码、密钥、JSON、类型等条件筛选。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 查询条件 + /// 文档列表 + public async Task> SelectHwWebDocumentList(HwWebDocument input) + { + HwWebDocument query = input ?? new HwWebDocument(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwWebDocumentList", query); + return await _db.Queryable() + .Where(item => item.IsDelete == "0") + .WhereIF(!string.IsNullOrWhiteSpace(query.DocumentId), item => item.DocumentId == query.DocumentId) + .WhereIF(query.TenantId.HasValue, item => item.TenantId == query.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(query.DocumentAddress), item => item.DocumentAddress == query.DocumentAddress) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebCode), item => item.WebCode == query.WebCode) + .WhereIF(!string.IsNullOrWhiteSpace(query.SecretKey), item => item.SecretKey == query.SecretKey) + .WhereIF(query.Json != null && query.Json != string.Empty, item => item.Json == query.Json) + .WhereIF(query.Type != null && query.Type != string.Empty, item => item.Type == query.Type) + .ToListAsync(); + } + + /// + /// 新增文档信息。 + /// + /// 【搜索索引重建】 + /// 文档新增后,自动触发搜索索引重建,确保新文档可被搜索到。 + /// + /// + /// 文档数据 + /// 影响行数 + public async Task InsertHwWebDocument(HwWebDocument input) + { + // 【审计字段】 + input.CreateTime = HwPortalContextHelper.Now(); + + // 【软删除标记初始化】 + // 如果未指定删除标记,默认为"0"(未删除) + input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; + + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "insertHwWebDocument", input); + int rows = await _db.Insertable(input).ExecuteCommandAsync(); + + // 【搜索索引重建】 + // 文档变更后,异步重建搜索索引 + if (rows > 0) + { + await RebuildSearchIndexQuietly(); + } + + return rows; + } + + /// + /// 更新文档信息。 + /// + /// 【密钥字段特殊处理】 + /// Why:这里必须保留"传空串等于清空口令"的兼容语义,否则旧前端无法撤销文档密钥。 + /// + /// 如果传入的 SecretKey 为 null,自动设为空字符串。 + /// 如果传入的 SecretKey 为空字符串,在更新时转为 null。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwWebDocument(HwWebDocument input) + { + // 【密钥字段默认值】 + // 如果密钥为 null,设为空字符串,避免空引用异常 + if (input.SecretKey == null) + { + input.SecretKey = string.Empty; + } + + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "updateHwWebDocument", input); + HwWebDocument current = await SelectHwWebDocumentByDocumentId(input.DocumentId); + if (current == null) + { + return 0; + } + + // 【文档ID】 + if (input.DocumentId != null) + { + current.DocumentId = input.DocumentId; + } + + // 【租户ID】 + if (input.TenantId.HasValue) + { + current.TenantId = input.TenantId; + } + + // 【文档地址】 + if (input.DocumentAddress != null) + { + current.DocumentAddress = input.DocumentAddress; + } + + // 【审计字段】 + if (input.CreateTime.HasValue) + { + current.CreateTime = input.CreateTime; + } + + // 【网站代码】 + if (input.WebCode != null) + { + current.WebCode = input.WebCode; + } + + // 【密钥字段特殊处理】 + // Why:这里必须保留"传空串等于清空口令"的兼容语义,否则旧前端无法撤销文档密钥。 + // 如果传入空字符串,转为 null 存储 + if (input.SecretKey != null) + { + current.SecretKey = string.IsNullOrEmpty(input.SecretKey) ? null : input.SecretKey; + } + + // 【JSON数据】 + if (input.Json != null) + { + current.Json = input.Json; + } + + // 【类型】 + if (input.Type != null) + { + current.Type = input.Type; + } + + int rows = await _db.Updateable(current).ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0) + { + await RebuildSearchIndexQuietly(); + } + + return rows; + } + + /// + /// 批量删除文档信息(软删除)。 + /// + /// 【软删除实现】 + /// 不是物理删除,而是将 IsDelete 字段更新为"1"。 + /// 这样可以保留数据用于审计,也支持恢复操作。 + /// + /// + /// 文档ID数组 + /// 影响行数 + public async Task DeleteHwWebDocumentByDocumentIds(string[] documentIds) + { + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebDocumentByDocumentIds", new { array = documentIds }); + + // 【查询待删除的文档】 + List documents = await _db.Queryable() + .Where(item => documentIds.Contains(item.DocumentId)) + .ToListAsync(); + + // 【软删除标记】 + foreach (HwWebDocument document in documents) + { + document.IsDelete = "1"; + } + + // 【批量更新】 + // 只更新 IsDelete 字段,使用 UpdateColumns 指定更新字段 + int rows = documents.Count == 0 ? 0 : await _db.Updateable(documents) + .UpdateColumns(item => new { item.IsDelete }) + .ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0) + { + await RebuildSearchIndexQuietly(); + } + + return rows; + } + + /// + /// 验证文档密钥并获取文档地址。 + /// + /// 【密钥验证逻辑】 + /// 1. 如果文档没有设置密钥(SecretKey 为空),直接返回地址 + /// 2. 如果提供了正确的密钥,返回地址 + /// 3. 如果密钥错误或为空,抛出异常 + /// + /// + /// 【Oops.Oh 异常处理】 + /// Furion 框架提供的友好异常抛出方式, + /// 会自动包装为统一的 API 响应格式。 + /// + /// + /// 文档ID + /// 提供的密钥 + /// 文档地址 + public async Task VerifyAndGetDocumentAddress(string documentId, string providedKey) + { + // 【查询文档】 + HwWebDocument document = await SelectHwWebDocumentByDocumentId(documentId); + if (document == null) + { + throw Oops.Oh("文件不存在"); + } + + // 【无密钥保护】 + // 如果文档没有设置密钥,直接返回地址 + if (string.IsNullOrWhiteSpace(document.SecretKey)) + { + return document.DocumentAddress; + } + + // 【密钥验证】 + // 去除前后空格后比较 + string trimmedProvided = providedKey?.Trim(); + if (string.IsNullOrWhiteSpace(trimmedProvided)) + { + throw Oops.Oh("密钥不能为空"); + } + + // 【密钥匹配】 + // StringComparison.Ordinal 表示区分大小写的二进制比较 + if (string.Equals(document.SecretKey.Trim(), trimmedProvided, StringComparison.Ordinal)) + { + return document.DocumentAddress; + } + + // 【密钥错误】 + throw Oops.Oh("密钥错误"); + } + + /// + /// 静默重建搜索索引。 + /// + /// 【异步重建】 + /// 文档变更后,异步触发搜索索引重建。 + /// 使用 try-catch 包裹,确保索引重建失败不影响主业务流程。 + /// + /// + /// 【日志记录】 + /// 如果重建失败,记录错误日志,便于排查问题。 + /// + /// + private async Task RebuildSearchIndexQuietly() + { + try + { + await _searchRebuildService.RebuildAllAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "rebuild portal search index failed after hw_web_document changed"); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenu1Service.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenu1Service.cs new file mode 100644 index 0000000..c389c01 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenu1Service.cs @@ -0,0 +1,526 @@ +// ============================================================================ +// 【文件说明】HwWebMenu1Service.cs - 网站菜单服务类(变体版本) +// ============================================================================ +// 这个服务类负责处理网站菜单的业务逻辑(变体版本),包括: +// - 菜单的 CRUD 操作 +// - 树形菜单结构构建 +// - 软删除支持 +// +// 【业务背景】 +// 网站菜单模块用于管理网站的前端导航菜单,支持多级树形结构。 +// 这个服务是 HwWebMenuService 的变体版本,可能用于不同的菜单场景。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwWebMenu1ServiceImpl implements HwWebMenu1Service { ... } +// +// ASP.NET Core + Furion: +// public class HwWebMenu1Service : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 网站菜单服务类(变体版本)。 +/// +/// 【服务职责】 +/// 1. 管理网站菜单的增删改查 +/// 2. 构建树形菜单结构 +/// 3. 支持软删除(IsDelete 字段标记) +/// +/// +/// 【树形结构说明】 +/// - Parent:父菜单ID,null或0表示顶级菜单 +/// - Ancestors:祖先路径,格式为"0,1,2" +/// - 支持多级嵌套菜单结构 +/// +/// +/// 【与 HwWebMenuService 的区别】 +/// 这个服务是菜单服务的变体版本,可能用于: +/// - 不同的菜单类型(如前台菜单 vs 后台菜单) +/// - 不同的租户或站点 +/// - 不同的业务场景 +/// +/// +public class HwWebMenu1Service : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了。 + /// + /// 对比 Java: + /// Java: private static final String MAPPER = "HwWebMenuMapper1"; + /// C#: private const string Mapper = "HwWebMenuMapper1"; + /// + /// C# 的命名约定: + /// - const 通常用 PascalCase(首字母大写) + /// - Java 的 static final 通常用 UPPER_SNAKE_CASE + /// + /// + private const string Mapper = "HwWebMenuMapper1"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + /// 【依赖注入】 + /// 通过构造函数注入,和控制器注入服务的方式一样。 + /// + /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 + /// 当前代码中已注释掉使用,但保留以备回滚。 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + /// 【ISqlSugarClient 接口】 + /// 这是 SqlSugar 的核心接口,提供数据库访问能力。 + /// + /// 对比 Java: + /// Java MyBatis: SqlSession 或 Mapper 接口 + /// C# SqlSugar: ISqlSugarClient + /// + /// 通过依赖注入获取,由框架在启动时配置并注册到 DI 容器。 + /// + /// + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// 【C# 语法知识点 - 构造函数】 + /// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + /// + /// 对比 Java: + /// Java: + /// @Autowired + /// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db) { + /// this.executor = executor; + /// this.db = db; + /// } + /// + /// C#: + /// public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + /// { + /// _executor = executor; + /// _db = db; + /// } + /// + /// C# 的改进: + /// 1. 不需要 @Autowired 注解,框架自动识别构造函数 + /// 2. 使用 _executor 命名约定表示私有字段 + /// 3. 可以使用主构造函数(C# 12+)进一步简化 + /// + /// + /// MyBatis 执行器(保留用于回滚) + /// SqlSugar 数据访问对象 + public HwWebMenu1Service(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据菜单ID查询菜单信息。 + /// + /// 【方法命名约定】 + /// Select + 实体名 + By + 主键名:根据主键查询单条记录 + /// + /// 对比 Java 若依: + /// 若依通常使用:selectXxxById、getXxxById + /// 这里使用:SelectXxxByWebMenuId,更符合 C# 的 PascalCase 命名规范 + /// + /// + /// 【软删除过滤】 + /// 查询时自动过滤已删除的记录(IsDelete == "0")。 + /// 这是软删除的标准做法。 + /// + /// + /// 菜单ID + /// 菜单实体 + public async Task SelectHwWebMenuByWebMenuId(long webMenuId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebMenuByWebMenuId", new { webMenuId }); + + // 【SqlSugar 查询语法】 + // _db.Queryable():创建一个针对 HwWebMenu1 表的查询 + // .Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId):添加 WHERE 条件 + // .FirstAsync():执行查询,返回第一条记录,如果没有则返回 null + // + // 生成的 SQL 类似于: + // SELECT * FROM hw_web_menu1 WHERE IsDelete = '0' AND WebMenuId = @webMenuId LIMIT 1 + // + // 对比 Java MyBatis: + // Java: mapper.selectOne(new QueryWrapper().eq("IsDelete", "0").eq("WebMenuId", webMenuId)); + // C#: _db.Queryable().Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId).FirstAsync(); + // + // C# 的优势:Lambda 表达式是强类型的,拼写错误在编译期就能发现 + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId) + .FirstAsync(); + } + + /// + /// 查询菜单列表。 + /// + /// 【动态查询条件】 + /// 支持按父ID、祖先路径、状态、菜单名、租户ID、图片、类型、值、英文名等条件筛选。 + /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 + /// + /// + /// 【软删除过滤】 + /// 默认只查询未删除的记录(IsDelete == "0")。 + /// + /// + /// 查询条件 + /// 菜单列表 + public async Task> SelectHwWebMenuList(HwWebMenu1 input) + { + // 【防御性编程】 + // 确保 query 不为 null,避免后续空引用异常 + HwWebMenu1 query = input ?? new HwWebMenu1(); + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwWebMenuList", query); + + // 【SqlSugar WhereIF 动态条件】 + // .WhereIF(condition, expression):只有当 condition 为 true 时,才添加 WHERE 条件 + // + // 对比 Java MyBatis 的 XML: + // + // AND Parent = #{parent} + // + // + // C# SqlSugar 的 WhereIF 更直观,而且类型安全 + // + // 【string.IsNullOrWhiteSpace】 + // 检查字符串是否为 null、空字符串 "" 或仅包含空白字符 + // + // 对比 Java: + // Java: StringUtils.isBlank(str)(需要 Apache Commons Lang) + // C#: string.IsNullOrWhiteSpace(str)(内置) + // + // 【Contains 模糊查询】 + // item.WebMenuName.Contains(query.WebMenuName) + // 生成 SQL:WHERE WebMenuName LIKE '%xxx%' + // + // 【HasValue 可空类型检查】 + // query.Parent.HasValue 检查可空 long? 是否有值 + // + // 对比 Java: + // Java: query.getParent() != null + // C#: query.Parent.HasValue + // + // 【ToListAsync】 + // 执行查询并返回列表,异步版本 + return await _db.Queryable() + .Where(item => item.IsDelete == "0") + .WhereIF(query.Parent.HasValue, item => item.Parent == query.Parent) + .WhereIF(!string.IsNullOrWhiteSpace(query.Ancestors), item => item.Ancestors == query.Ancestors) + .WhereIF(!string.IsNullOrWhiteSpace(query.Status), item => item.Status == query.Status) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuName), item => item.WebMenuName.Contains(query.WebMenuName)) + .WhereIF(query.TenantId.HasValue, item => item.TenantId == query.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuPic), item => item.WebMenuPic == query.WebMenuPic) + .WhereIF(query.WebMenuType.HasValue, item => item.WebMenuType == query.WebMenuType) + .WhereIF(!string.IsNullOrWhiteSpace(query.Value), item => item.Value.Contains(query.Value)) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuNameEnglish), item => item.WebMenuNameEnglish.Contains(query.WebMenuNameEnglish)) + .ToListAsync(); + } + + /// + /// 查询菜单树。 + /// + /// 【树形结构构建】 + /// 先查询菜单列表,然后调用 BuildWebMenuTree 构建树形结构。 + /// + /// + /// 查询条件 + /// 树形菜单列表 + public async Task> SelectMenuTree(HwWebMenu1 input) + { + // 查询菜单列表 + List menus = await SelectHwWebMenuList(input); + + // 构建并返回树形结构 + return BuildWebMenuTree(menus); + } + + /// + /// 新增菜单。 + /// + /// 【软删除标记初始化】 + /// 如果未指定删除标记,默认为"0"(未删除)。 + /// 这是软删除的标准做法。 + /// + /// + /// 菜单数据 + /// 影响行数 + public async Task InsertHwWebMenu(HwWebMenu1 input) + { + // 【软删除标记初始化】 + // string.IsNullOrWhiteSpace 检查是否为 null、空字符串或仅包含空白字符 + // ?? 是空合并运算符:如果左边为 null,返回右边 + input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; + + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "insertHwWebMenu", input); + + // 【SqlSugar 插入语法】 + // _db.Insertable(input):创建一个插入操作 + // .ExecuteCommandAsync():执行插入,返回影响行数 + // + // 生成的 SQL 类似于: + // INSERT INTO hw_web_menu1 (Parent, Ancestors, Status, WebMenuName, ...) VALUES (@Parent, @Ancestors, ...) + return await _db.Insertable(input).ExecuteCommandAsync(); + } + + /// + /// 更新菜单信息。 + /// + /// 【字段级更新策略】 + /// 1. 先查询现有记录 + /// 2. 对非空字段进行更新 + /// 3. null 值表示"不更新此字段" + /// + /// 这种设计支持部分更新(PATCH 语义)。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwWebMenu(HwWebMenu1 input) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "updateHwWebMenu", input); + + // 查询现有记录 + HwWebMenu1 current = await SelectHwWebMenuByWebMenuId(input.WebMenuId ?? 0); + if (current == null) + { + return 0; + } + + // 【父菜单ID】 + // HasValue 检查可空类型是否有值 + if (input.Parent.HasValue) + { + current.Parent = input.Parent; + } + + // 【祖先路径】 + // != null 检查引用类型是否为 null + if (input.Ancestors != null) + { + current.Ancestors = input.Ancestors; + } + + // 【状态】 + if (input.Status != null) + { + current.Status = input.Status; + } + + // 【菜单名】 + if (input.WebMenuName != null) + { + current.WebMenuName = input.WebMenuName; + } + + // 【租户ID】 + if (input.TenantId.HasValue) + { + current.TenantId = input.TenantId; + } + + // 【菜单图片】 + if (input.WebMenuPic != null) + { + current.WebMenuPic = input.WebMenuPic; + } + + // 【菜单类型】 + if (input.WebMenuType.HasValue) + { + current.WebMenuType = input.WebMenuType; + } + + // 【值】 + if (input.Value != null) + { + current.Value = input.Value; + } + + // 【英文名】 + if (input.WebMenuNameEnglish != null) + { + current.WebMenuNameEnglish = input.WebMenuNameEnglish; + } + + // 【执行更新】 + // _db.Updateable(current):创建更新操作 + // .ExecuteCommandAsync():执行更新,返回影响行数 + return await _db.Updateable(current).ExecuteCommandAsync(); + } + + /// + /// 批量删除菜单(软删除)。 + /// + /// 【软删除实现】 + /// 不是物理删除,而是将 IsDelete 字段更新为"1"。 + /// 这样可以保留数据用于审计,也支持恢复操作。 + /// + /// 对比 Java MyBatis: + /// Java 通常也是 UPDATE table SET is_delete = '1' WHERE id IN (...) + /// + /// + /// 菜单ID数组 + /// 影响行数 + public async Task DeleteHwWebMenuByWebMenuIds(long[] webMenuIds) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.ExecuteAsync(Mapper, "deleteHwWebMenuByWebMenuIds", new { array = webMenuIds }); + + // 【查询待删除的菜单】 + // .Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value)) + // 条件:WebMenuId 有值,且在传入的 ID 数组中 + List menus = await _db.Queryable() + .Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value)) + .ToListAsync(); + + // 【软删除标记】 + // 将 IsDelete 字段设置为 "1",表示已删除 + foreach (HwWebMenu1 menu in menus) + { + menu.IsDelete = "1"; + } + + // 【批量更新】 + // 只更新 IsDelete 字段,使用 UpdateColumns 指定更新字段 + // 如果 menus 为空,返回 0;否则执行批量更新 + return menus.Count == 0 ? 0 : await _db.Updateable(menus) + .UpdateColumns(item => new { item.IsDelete }) + .ExecuteCommandAsync(); + } + + /// + /// 构建菜单树形结构。 + /// + /// 【树形结构构建算法】 + /// 这是一个经典的"扁平列表转树形结构"算法: + /// + /// 输入:扁平的节点列表,每个节点有 parentId 指向父节点 + /// 输出:树形结构,顶级节点包含 children 子节点 + /// + /// 算法步骤: + /// 1. 收集所有节点ID到 tempList + /// 2. 遍历节点列表,找出所有顶级节点(parentId 为 null、0 或不在列表中) + /// 3. 对每个顶级节点,调用 RecursionFn 递归构建子树 + /// 4. 返回树形结构列表 + /// + /// 对比 Java 若依: + /// 若依的 TreeBuildUtils.buildTree() 做同样的事情。 + /// 这是若依框架的标准树构建算法。 + /// + /// + /// 扁平菜单列表 + /// 树形菜单列表 + public List BuildWebMenuTree(List menus) + { + // 结果列表 + List returnList = new(); + + // 【收集所有节点ID】 + // .Select(item => item.WebMenuId) 提取所有 ID + // .ToList() 转换为列表 + List tempList = menus.Select(item => item.WebMenuId).ToList(); + + // 【遍历查找顶级节点】 + foreach (HwWebMenu1 menu in menus) + { + // 【顶级节点判定】 + // 满足以下任一条件即为顶级节点: + // 1. !menu.Parent.HasValue:parent 为 null + // 2. menu.Parent == 0:parent 为 0 + // 3. !tempList.Contains(menu.Parent):parent 不在列表中(父节点不存在) + if (!menu.Parent.HasValue || menu.Parent == 0 || !tempList.Contains(menu.Parent)) + { + // 递归构建子树 + RecursionFn(menus, menu); + returnList.Add(menu); + } + } + + // 如果没找到顶级节点,返回原列表(兜底处理) + return returnList.Count == 0 ? menus : returnList; + } + + /// + /// 递归构建子树。 + /// + /// 【递归算法说明】 + /// 递归是一种"自己调用自己"的编程技巧。 + /// + /// 这里的递归逻辑: + /// 1. 找出当前节点的所有直接子节点 + /// 2. 把子节点挂到当前节点的 Children 属性 + /// 3. 对每个子节点,递归执行步骤 1-2 + /// + /// 终止条件:节点没有子节点(HasChild 返回 false) + /// + /// + /// 所有节点列表 + /// 当前节点 + private static void RecursionFn(List list, HwWebMenu1 current) + { + // 找出当前节点的所有直接子节点 + List childList = GetChildList(list, current); + + // 把子节点挂到当前节点 + current.Children = childList; + + // 【递归调用】 + // 对每个有子节点的子节点,继续递归构建子树 + // .Where(child => HasChild(list, child)) 筛选有子节点的节点 + foreach (HwWebMenu1 child in childList.Where(child => HasChild(list, child))) + { + RecursionFn(list, child); + } + } + + /// + /// 获取当前节点的直接子节点列表。 + /// + /// 所有节点列表 + /// 当前节点 + /// 子节点列表 + private static List GetChildList(List list, HwWebMenu1 current) + { + // 【LINQ Where 过滤】 + // item.Parent.Value == current.WebMenuId.Value + // 条件:item 的 parent 等于 current 的 id + // + // .HasValue 检查可空类型是否有值(不为 null) + // .Value 获取可空类型的实际值 + // + // 对比 Java: + // Java: list.stream().filter(item -> item.getParent() != null && current.getWebMenuId() != null && item.getParent().equals(current.getWebMenuId())).collect(Collectors.toList()); + // + // C# 的可空类型处理更优雅 + return list.Where(item => item.Parent.HasValue && current.WebMenuId.HasValue && item.Parent.Value == current.WebMenuId.Value).ToList(); + } + + /// + /// 判断当前节点是否有子节点。 + /// + /// 所有节点列表 + /// 当前节点 + /// 是否有子节点 + private static bool HasChild(List list, HwWebMenu1 current) + { + return GetChildList(list, current).Count > 0; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs new file mode 100644 index 0000000..2b5d0a9 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs @@ -0,0 +1,388 @@ +// ============================================================================ +// 【文件说明】HwWebMenuService.cs - 网站菜单服务类 +// ============================================================================ +// 这个服务类负责处理网站菜单的业务逻辑,包括: +// - 菜单的 CRUD 操作 +// - 树形菜单结构构建 +// - 搜索索引重建 +// +// 【业务背景】 +// 网站菜单模块用于管理网站的前端导航菜单,支持多级树形结构。 +// 菜单变更时会自动触发搜索索引重建。 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwWebMenuServiceImpl implements HwWebMenuService { ... } +// +// ASP.NET Core + Furion: +// public class HwWebMenuService : ITransient { ... } +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 网站菜单服务类。 +/// +/// 【服务职责】 +/// 1. 管理网站菜单的增删改查 +/// 2. 构建树形菜单结构 +/// 3. 菜单变更时自动重建搜索索引 +/// +/// +/// 【树形结构说明】 +/// - Parent:父菜单ID,null或0表示顶级菜单 +/// - Ancestors:祖先路径,格式为"0,1,2" +/// - Order:排序号,用于控制菜单显示顺序 +/// +/// +public class HwWebMenuService : ITransient +{ + /// + /// MyBatis 映射器名称(保留用于回滚)。 + /// + private const string Mapper = "HwWebMenuMapper"; + + /// + /// MyBatis 执行器(保留用于回滚)。 + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 搜索索引重建服务。 + /// + private readonly IHwSearchRebuildService _searchRebuildService; + + /// + /// 日志记录器。 + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// SqlSugar 数据访问对象 + /// 搜索索引重建服务 + /// 日志记录器 + public HwWebMenuService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) + { + _executor = executor; + _db = db; + _searchRebuildService = searchRebuildService; + _logger = logger; + } + + /// + /// 根据菜单ID查询菜单信息。 + /// + /// 【软删除过滤】 + /// 查询时自动过滤已删除的记录(IsDelete == "0")。 + /// + /// + /// 菜单ID + /// 菜单实体 + public async Task SelectHwWebMenuByWebMenuId(long webMenuId) + { + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebMenuByWebMenuId", new { webMenuId }); + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.WebMenuId == webMenuId) + .FirstAsync(); + } + + /// + /// 查询菜单列表。 + /// + /// 【动态查询条件】 + /// 支持按父ID、祖先路径、状态、菜单名、租户ID、图片、类型、排序号、英文名等条件筛选。 + /// + /// + /// 【排序】 + /// 按 Parent、Order、WebMenuId 排序,确保菜单按层级和顺序显示。 + /// + /// + /// 查询条件 + /// 菜单列表 + public async Task> SelectHwWebMenuList(HwWebMenu input) + { + HwWebMenu query = input ?? new HwWebMenu(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwWebMenuList", query); + return await _db.Queryable() + .Where(item => item.IsDelete == "0") + .WhereIF(query.Parent.HasValue, item => item.Parent == query.Parent) + .WhereIF(!string.IsNullOrWhiteSpace(query.Ancestors), item => item.Ancestors == query.Ancestors) + .WhereIF(!string.IsNullOrWhiteSpace(query.Status), item => item.Status == query.Status) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuName), item => item.WebMenuName.Contains(query.WebMenuName)) + .WhereIF(query.TenantId.HasValue, item => item.TenantId == query.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuPic), item => item.WebMenuPic == query.WebMenuPic) + .WhereIF(query.WebMenuType.HasValue, item => item.WebMenuType == query.WebMenuType) + .WhereIF(query.Order.HasValue, item => item.Order == query.Order) + .WhereIF(!string.IsNullOrWhiteSpace(query.WebMenuNameEnglish), item => item.WebMenuNameEnglish == query.WebMenuNameEnglish) + .OrderBy(item => item.Parent) + .OrderBy(item => item.Order) + .OrderBy(item => item.WebMenuId) + .ToListAsync(); + } + + /// + /// 查询菜单树。 + /// + /// 【树形结构构建】 + /// 先查询菜单列表,然后调用 BuildWebMenuTree 构建树形结构。 + /// + /// + /// 查询条件 + /// 树形菜单列表 + public async Task> SelectMenuTree(HwWebMenu input) + { + List menus = await SelectHwWebMenuList(input); + return BuildWebMenuTree(menus); + } + + /// + /// 新增菜单。 + /// + /// 【搜索索引重建】 + /// 菜单新增后,自动触发搜索索引重建。 + /// + /// + /// 菜单数据 + /// 影响行数 + public async Task InsertHwWebMenu(HwWebMenu input) + { + // 【软删除标记初始化】 + input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; + + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "insertHwWebMenu", input); + int rows = await _db.Insertable(input).ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0) + { + await RebuildSearchIndexQuietly("hw_web_menu"); + } + + return rows; + } + + /// + /// 更新菜单信息。 + /// + /// 【字段级更新策略】 + /// 只更新输入对象中不为 null 的字段。 + /// + /// + /// 【搜索索引重建】 + /// 菜单更新后,自动触发搜索索引重建。 + /// + /// + /// 更新的数据 + /// 影响行数 + public async Task UpdateHwWebMenu(HwWebMenu input) + { + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "updateHwWebMenu", input); + HwWebMenu current = await SelectHwWebMenuByWebMenuId(input.WebMenuId ?? 0); + if (current == null) + { + return 0; + } + + // 【父菜单ID】 + if (input.Parent.HasValue) + { + current.Parent = input.Parent; + } + + // 【祖先路径】 + if (input.Ancestors != null) + { + current.Ancestors = input.Ancestors; + } + + // 【状态】 + if (input.Status != null) + { + current.Status = input.Status; + } + + // 【菜单名】 + if (input.WebMenuName != null) + { + current.WebMenuName = input.WebMenuName; + } + + // 【租户ID】 + if (input.TenantId.HasValue) + { + current.TenantId = input.TenantId; + } + + // 【菜单图片】 + if (input.WebMenuPic != null) + { + current.WebMenuPic = input.WebMenuPic; + } + + // 【菜单类型】 + if (input.WebMenuType.HasValue) + { + current.WebMenuType = input.WebMenuType; + } + + // 【排序号】 + if (input.Order.HasValue) + { + current.Order = input.Order; + } + + // 【英文名】 + if (input.WebMenuNameEnglish != null) + { + current.WebMenuNameEnglish = input.WebMenuNameEnglish; + } + + int rows = await _db.Updateable(current).ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0) + { + await RebuildSearchIndexQuietly("hw_web_menu"); + } + + return rows; + } + + /// + /// 批量删除菜单(软删除)。 + /// + /// 【软删除实现】 + /// 将 IsDelete 字段更新为"1",而不是物理删除。 + /// + /// + /// 【搜索索引重建】 + /// 菜单删除后,自动触发搜索索引重建。 + /// + /// + /// 菜单ID数组 + /// 影响行数 + public async Task DeleteHwWebMenuByWebMenuIds(long[] webMenuIds) + { + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebMenuByWebMenuIds", new { array = webMenuIds }); + + // 【查询待删除的菜单】 + List menus = await _db.Queryable() + .Where(item => item.WebMenuId.HasValue && webMenuIds.Contains(item.WebMenuId.Value)) + .ToListAsync(); + + // 【软删除标记】 + foreach (HwWebMenu menu in menus) + { + menu.IsDelete = "1"; + } + + // 【批量更新】 + int rows = menus.Count == 0 ? 0 : await _db.Updateable(menus) + .UpdateColumns(item => new { item.IsDelete }) + .ExecuteCommandAsync(); + + // 【搜索索引重建】 + if (rows > 0) + { + await RebuildSearchIndexQuietly("hw_web_menu"); + } + + return rows; + } + + /// + /// 构建菜单树形结构。 + /// + /// 【树形结构构建算法】 + /// 1. 收集所有节点ID + /// 2. 遍历找出顶级节点(Parent为null/0或不在列表中) + /// 3. 对每个顶级节点递归构建子树 + /// 4. 返回树形结构列表 + /// + /// + /// 扁平菜单列表 + /// 树形菜单列表 + public List BuildWebMenuTree(List menus) + { + List returnList = new(); + List tempList = menus.Select(item => item.WebMenuId).ToList(); + foreach (HwWebMenu menu in menus) + { + // 【顶级节点判定】 + if (!menu.Parent.HasValue || menu.Parent == 0 || !tempList.Contains(menu.Parent)) + { + RecursionFn(menus, menu); + returnList.Add(menu); + } + } + + return returnList.Count == 0 ? menus : returnList; + } + + /// + /// 递归构建子树。 + /// + /// 所有节点列表 + /// 当前节点 + private static void RecursionFn(List list, HwWebMenu current) + { + List childList = GetChildList(list, current); + current.Children = childList; + foreach (HwWebMenu child in childList.Where(child => HasChild(list, child))) + { + RecursionFn(list, child); + } + } + + /// + /// 获取子节点列表。 + /// + /// 所有节点列表 + /// 当前节点 + /// 子节点列表 + private static List GetChildList(List list, HwWebMenu current) + { + return list.Where(item => item.Parent.HasValue && current.WebMenuId.HasValue && item.Parent.Value == current.WebMenuId.Value).ToList(); + } + + /// + /// 判断是否有子节点。 + /// + /// 所有节点列表 + /// 当前节点 + /// 是否有子节点 + private static bool HasChild(List list, HwWebMenu current) + { + return GetChildList(list, current).Count > 0; + } + + /// + /// 静默重建搜索索引。 + /// + /// 数据来源 + private async Task RebuildSearchIndexQuietly(string source) + { + try + { + await _searchRebuildService.RebuildAllAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs new file mode 100644 index 0000000..996a2b5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs @@ -0,0 +1,397 @@ +// ============================================================================ +// 【文件说明】HwWebService.cs - 官网页面业务服务类 +// ============================================================================ +// 这个服务类负责处理官网页面的业务逻辑。 +// +// 【分层架构说明】 +// Controller(控制器层)-> Service(服务层)-> Repository(数据访问层) +// +// 控制器只负责:接收请求、调用服务、返回响应 +// 服务层负责:业务逻辑、数据组装、事务控制 +// 数据访问层负责:数据库 CRUD 操作 +// +// 【与 Java Spring Boot 的对比】 +// Java Spring Boot: +// @Service +// public class HwWebServiceImpl implements HwWebService { ... } +// +// ASP.NET Core + Furion: +// public class HwWebService : ITransient { ... } +// +// ITransient 是 Furion 的"生命周期接口",表示"瞬态服务": +// - 每次请求都创建新实例 +// - 类似 Java Spring 的 @Scope("prototype") +// +// Furion 支持三种生命周期: +// - ITransient:瞬态,每次请求新实例 +// - IScoped:作用域,同一请求内共享实例 +// - ISingleton:单例,全局共享一个实例 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 官网页面业务服务类。 +/// +/// 【C# 语法知识点 - 接口实现】 +/// public class HwWebService : ITransient +/// +/// ITransient 是 Furion 框架的"标记接口"(Marker Interface)。 +/// 它没有任何方法,只是用来标记服务的生命周期。 +/// +/// 对比 Java Spring: +/// Java Spring 用 @Service + @Scope 注解来定义服务: +/// @Service +/// @Scope("prototype") // 等价于 ITransient +/// public class HwWebService { ... } +/// +/// C# Furion 用接口来标记,更符合"接口隔离原则"。 +/// +/// +/// 【MyBatis 风格的数据访问】 +/// 这个服务使用 HwPortalMyBatisExecutor 执行 SQL。 +// 这是一种"类 MyBatis"的设计: +/// - SQL 写在 XML 文件中(Sql/HwWebMapper.xml) +/// - 通过执行器调用 XML 中定义的 SQL +/// - 参数通过匿名对象传递 +/// +/// 对比 Java MyBatis: +/// Java MyBatis: +/// @Mapper +/// public interface HwWebMapper { +/// HwWeb selectHwWebByWebcode(@Param("webCode") Long webCode); +/// } +/// +/// C# 这里用执行器模式,更灵活但需要手动调用。 +/// +/// +public class HwWebService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是"编译期常量",值在编译时就确定了。 + /// + /// 对比 Java: + /// Java: private static final String MAPPER = "HwWebMapper"; + /// C#: private const string Mapper = "HwWebMapper"; + /// + /// C# 的命名约定: + /// - const 通常用 PascalCase(首字母大写) + /// - Java 的 static final 通常用 UPPER_SNAKE_CASE + /// + /// + private const string Mapper = "HwWebMapper"; + + /// + /// MyBatis 执行器。 + /// + /// 【依赖注入】 + /// 通过构造函数注入,和控制器注入服务的方式一样。 + /// + /// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + + /// + /// SqlSugar 数据访问对象。 + /// + private readonly ISqlSugarClient _db; + + /// + /// 搜索索引重建服务。 + /// + private readonly IHwSearchRebuildService _searchRebuildService; + + /// + /// 日志记录器。 + /// + /// 【C# 语法知识点 - 泛型日志接口】 + /// ILogger<HwWebService> 是 Microsoft.Extensions.Logging 提供的日志接口。 + /// 泛型参数 T 用于标识日志的类别,通常传入当前类。 + /// + /// 使用方式: + /// _logger.LogInformation("信息日志"); + /// _logger.LogWarning("警告日志"); + /// _logger.LogError("错误日志"); + /// _logger.LogError(ex, "错误日志,带异常信息"); + /// + /// 对比 Java SLF4J: + /// Java: private static final Logger log = LoggerFactory.getLogger(HwWebService.class); + /// C#: private readonly ILogger<HwWebService> _logger; + /// + /// 两者的用法类似,但 C# 的 ILogger 是接口,通过 DI 注入。 + /// + /// + private readonly ILogger _logger; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + /// 搜索索引重建服务 + /// 日志记录器 + public HwWebService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) + { + _executor = executor; + _db = db; + _searchRebuildService = searchRebuildService; + _logger = logger; + } + + /// + /// 根据页面编码查询页面信息。 + /// + /// 页面编码 + /// 页面实体 + public async Task SelectHwWebByWebcode(long webCode) + { + // 【匿名对象传参】 + // new { webCode } 是 C# 的"匿名对象"语法。 + // 编译器会自动创建一个包含 webCode 属性的临时类。 + // + // 对比 Java: + // Java 没有匿名对象语法,通常用: + // - Map<String, Object> params = new HashMap<>(); + // - params.put("webCode", webCode); + // 或者: + // - @Param("webCode") Long webCode(MyBatis 注解) + // + // C# 的匿名对象更简洁,编译器自动推断类型。 + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QuerySingleAsync(Mapper, "selectHwWebByWebcode", new { webCode }); + return await _db.Queryable() + .Where(item => item.IsDelete == "0" && item.WebCode == webCode) + .FirstAsync(); + } + + /// + /// 查询页面列表。 + /// + /// 查询条件 + /// 页面列表 + public async Task> SelectHwWebList(HwWeb input) + { + // 【空合并运算符 ??】 + // input ?? new HwWeb() 含义: + // 如果 input 不为 null,就用 input; + // 否则 new 一个默认的 HwWeb 对象。 + // + // 对比 Java: + // Java 需要手写: + // if (input == null) input = new HwWeb(); + // 或者用 Optional: + // input = Optional.ofNullable(input).orElse(new HwWeb()); + HwWeb query = input ?? new HwWeb(); + // 回滚到 XML 方案时可直接恢复: + // return await _executor.QueryListAsync(Mapper, "selectHwWebList", query); + return await _db.Queryable() + .Where(item => item.IsDelete == "0") + .WhereIF(query.WebId.HasValue, item => item.WebId == query.WebId) + .WhereIF(query.WebJson != null && query.WebJson != string.Empty, item => item.WebJson == query.WebJson) + .WhereIF(query.WebJsonString != null && query.WebJsonString != string.Empty, item => item.WebJsonString == query.WebJsonString) + .WhereIF(query.WebCode.HasValue, item => item.WebCode == query.WebCode) + .WhereIF(query.WebJsonEnglish != null, item => item.WebJsonEnglish == query.WebJsonEnglish) + .ToListAsync(); + } + + /// + /// 新增页面。 + /// + /// 页面数据 + /// 影响行数(1=成功,0=失败) + public async Task InsertHwWeb(HwWeb input) + { + // 【async/await 异步编程】 + // await 会等待异步操作完成,然后继续执行后续代码。 + // + // InsertReturnIdentityAsync 返回自增主键值。 + // 对比 Java MyBatis: + // Java MyBatis 需要配置 useGeneratedKeys="true" keyProperty="webId" + // 然后通过 input.getWebId() 获取主键。 + input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); + HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + + // 把自增主键回填到实体对象。 + // 这样调用方可以获取到新插入记录的 ID。 + input.WebId = entity.WebId; + + if (input.WebId > 0) + { + // 【静默重建搜索索引】 + // 新增成功后,触发搜索索引重建。 + // "静默"表示:索引重建失败不影响主业务,只记录日志。 + await RebuildSearchIndexQuietly("hw_web"); + } + + // 返回 1 表示成功,0 表示失败。 + return input.WebId > 0 ? 1 : 0; + } + + /// + /// 更新页面。 + /// + /// 【版本化更新策略】 + /// 这里的更新不是传统 SQL UPDATE,而是: + /// 1. 按 webCode 找旧记录 + /// 2. 逻辑删旧记录 + /// 3. 插入新记录 + /// + /// 为什么这样做? + /// 1. 保留历史版本:旧记录还在,可以回滚 + /// 2. 避免缓存失效:旧 ID 的缓存自然过期 + /// 3. 保持引用稳定:其他表引用的是 webCode,不是 webId + /// + /// 这是一种"追加写入"的设计模式,适合需要审计追踪的场景。 + /// + /// + /// 页面数据 + /// 影响行数 + public async Task UpdateHwWeb(HwWeb input) + { + // 【对象初始化器】 + // HwWeb query = new() { WebCode = input.WebCode }; + // 等价于: + // HwWeb query = new HwWeb(); + // query.WebCode = input.WebCode; + // + // new() 是 C# 9.0 的"目标类型 new"语法,编译器自动推断类型。 + HwWeb query = new() + { + WebCode = input.WebCode + }; + + // 查询现有记录。 + List exists = await SelectHwWebList(query); + + if (exists.Count > 0) + { + // 【LINQ 链式操作】 + // exists.Where(...).Select(...).ToArray() + // + // Where:筛选符合条件的元素 + // Select:转换元素(这里是提取 WebId 值) + // ToArray:转换为数组 + // + // 对比 Java Stream: + // exists.stream() + // .filter(u -> u.getWebId() != null) + // .map(u -> u.getWebId()) + // .toArray(Long[]::new); + // + // C# 的 LINQ 和 Java Stream 非常相似。 + // + // 【! 非空断言】 + // u.WebId!.Value 中的 ! 是"非空断言"。 + // 告诉编译器:我确定这里不为 null,不要警告我。 + // 因为 Where 条件已经过滤了 HasValue 为 false 的项。 + await DeleteHwWebByWebIds(exists.Where(u => u.WebId.HasValue).Select(u => u.WebId!.Value).ToArray(), false); + } + + // 确保新记录是有效状态(is_delete = "0")。 + input.IsDelete = "0"; + + // 回滚到 XML 方案时可直接恢复: + // long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); + HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync(); + input.WebId = entity.WebId; + + if (input.WebId > 0) + { + await RebuildSearchIndexQuietly("hw_web"); + } + + return input.WebId > 0 ? 1 : 0; + } + + /// + /// 批量删除页面(公开方法,默认重建索引)。 + /// + /// 页面ID数组 + /// 影响行数 + public Task DeleteHwWebByWebIds(long[] webIds) + { + // 这里故意再包一层公开方法,把"是否重建索引"的内部开关藏起来。 + // 对外调用统一默认 true,只有内部特殊场景才传 false。 + return DeleteHwWebByWebIds(webIds, true); + } + + /// + /// 批量删除页面(私有方法,可控制是否重建索引)。 + /// + /// 页面ID数组 + /// 是否重建搜索索引 + /// 影响行数 + private async Task DeleteHwWebByWebIds(long[] webIds, bool rebuild) + { + // 执行删除 SQL。 + // 回滚到 XML 方案时可直接恢复: + // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebByWebIds", new { array = webIds }); + List pages = await _db.Queryable() + .Where(item => item.WebId.HasValue && webIds.Contains(item.WebId.Value)) + .ToListAsync(); + foreach (HwWeb page in pages) + { + page.IsDelete = "1"; + } + + int rows = pages.Count == 0 ? 0 : await _db.Updateable(pages) + .UpdateColumns(item => new { item.IsDelete }) + .ExecuteCommandAsync(); + + if (rows > 0 && rebuild) + { + // rebuild=false 的场景只出现在"更新前先删旧版本"这条内部链路。 + // 那里最后会统一在新记录插入成功后重建一次索引,所以这里不能重复触发。 + await RebuildSearchIndexQuietly("hw_web"); + } + + return rows; + } + + /// + /// 静默重建搜索索引。 + /// + /// 【"静默"的含义】 + /// "静默"(Quietly)表示:索引重建失败不抛异常,只记录日志。 + /// + /// 为什么静默? + /// 1. 搜索索引是附属能力:主业务(增删改)成功就够了 + /// 2. 避免事务回滚:如果索引重建失败导致事务回滚,用户体验不好 + /// 3. 可以后续修复:索引问题可以后台修复,不影响用户操作 + /// + /// 这是"最终一致性"的设计思想: + /// 主业务强一致,附属能力最终一致。 + /// + /// + /// 触发重建的数据源(用于日志记录) + private async Task RebuildSearchIndexQuietly(string source) + { + try + { + // 尝试重建所有搜索索引。 + await _searchRebuildService.RebuildAllAsync(); + } + catch (Exception ex) + { + // 【异常处理】 + // 这里用 ILogger 记录错误日志,而不是 throw。 + // + // _logger.LogError(ex, "message", args...) 参数说明: + // - ex:异常对象,会自动提取堆栈信息 + // - "message":日志消息模板 + // - args:模板参数(类似 String.Format) + // + // 对比 Java SLF4J: + // Java: log.error("rebuild portal search index failed after {} changed", source, ex); + // C#: _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); + // + // C# 的消息模板用 {Source} 占位符,Java 用 {}。 + _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs new file mode 100644 index 0000000..84ee83c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs @@ -0,0 +1,91 @@ +// ============================================================================ +// 【文件说明】HwNoopSearchRebuildService.cs - 空搜索重建服务 +// ============================================================================ +// 这是一个"空对象"(Null Object)模式的实现,什么都不做。 +// +// 【什么是空对象模式?】 +// 空对象模式是一种设计模式: +// - 提供一个"什么都不做"的实现 +// - 避免返回 null,减少空指针异常 +// - 提供默认行为,简化调用方代码 +// +// 【使用场景】 +// 1. 测试环境:不需要真正重建索引 +// 2. 配置禁用:搜索功能关闭时,使用空实现 +// 3. 开发阶段:索引功能还未完成,先用空实现占位 +// +// 【与 Java Spring Boot 的对比】 +// Java 可以用 @Profile 或 @ConditionalOnProperty 实现: +// @Profile("test") +// @Service +// public class NoopSearchRebuildService implements IHwSearchRebuildService { +// @Override +// public void rebuildAllAsync() { } +// } +// +// C# 通过 DI 注册不同的实现,效果类似。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 空搜索重建服务。 +/// +/// 【设计模式 - 空对象模式】 +/// 这个类实现了 IHwSearchRebuildService 接口,但方法体为空。 +/// +/// 为什么需要空实现? +/// 1. 避免空指针:调用方不需要判断 null +/// 2. 默认行为:未配置搜索功能时,使用空实现 +/// 3. 测试方便:测试环境不需要真正重建索引 +/// +/// 对比传统方式: +/// 传统方式需要判断 null: +/// if (_rebuildService != null) { +/// await _rebuildService.RebuildAllAsync(); +/// } +/// +/// 空对象模式可以直接调用: +/// await _rebuildService.RebuildAllAsync(); // 如果是空实现,什么都不做 +/// +/// +/// 【C# 语法知识点 - sealed 密封类】 +/// sealed 表示不能被继承。空对象类通常不需要继承。 +/// +/// +/// 【C# 语法知识点 - ITransient 瞬态服务】 +/// ITransient 表示每次请求都创建新实例。 +/// 空对象类没有状态,也可以用 ISingleton(单例)节省内存。 +/// +/// +public sealed class HwNoopSearchRebuildService : IHwSearchRebuildService, ITransient +{ + /// + /// 重建所有搜索索引(空实现)。 + /// + /// 【C# 语法知识点 - Task.CompletedTask】 + /// Task.CompletedTask 是一个已完成的 Task 实例。 + /// + /// 为什么返回 CompletedTask? + /// 1. 方法签名要求返回 Task + /// 2. 空实现不需要异步操作 + /// 3. 返回已完成的 Task,调用方 await 会立即返回 + /// + /// 对比 Java: + /// Java 返回 CompletableFuture.completedFuture(null): + /// return CompletableFuture.completedFuture(null); + /// + /// C# 的 Task.CompletedTask 更简洁。 + /// + /// 【性能说明】 + /// Task.CompletedTask 是单例,不会创建新对象。 + /// 比 Task.Run(() => { }) 更高效。 + /// + /// + /// 已完成的任务 + public Task RebuildAllAsync() + { + // 直接返回已完成的 Task,不执行任何操作。 + return Task.CompletedTask; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs new file mode 100644 index 0000000..cbaceb3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs @@ -0,0 +1,456 @@ +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace Admin.NET.Plugin.HwPortal; + +public class HwSearchService : ITransient +{ + // 搜索高亮时,关键词里如果包含正则特殊字符,需要先转义。 + private static readonly Regex EscapePattern = new(@"([\\.*+\[\](){}^$?|])", RegexOptions.Compiled); + + // 这里同时注入“索引主链路查询服务”和“legacy SQL 查询服务”。 + // 这么做是因为当前门户搜索不是单引擎,而是“主链路 + 回退链路”的双通道设计。 + private readonly HwSearchQueryService _queryService; + private readonly LegacyHwSearchQueryService _legacyQueryService; + private readonly PortalSearchDocConverter _converter; + private readonly HwPortalSearchOptions _options; + private readonly ILogger _logger; + + public HwSearchService( + HwSearchQueryService queryService, + LegacyHwSearchQueryService legacyQueryService, + PortalSearchDocConverter converter, + IOptions options, + ILogger logger) + { + _queryService = queryService; + _legacyQueryService = legacyQueryService; + _converter = converter; + _options = options.Value; + _logger = logger; + } + + public Task Search(string keyword, int? pageNum, int? pageSize) + { + // false 代表展示端搜索,不需要返回编辑端路由。 + return DoSearch(keyword, pageNum, pageSize, false); + } + + public Task SearchForEdit(string keyword, int? pageNum, int? pageSize) + { + // true 代表编辑端搜索,需要额外产出 editRoute 给后台编辑器跳转。 + return DoSearch(keyword, pageNum, pageSize, true); + } + + private async Task DoSearch(string keyword, int? pageNum, int? pageSize, bool editMode) + { + // 先把输入参数统一规范化,避免后面的业务逻辑到处判空。 + string normalizedKeyword = ValidateKeyword(keyword); + int normalizedPageNum = NormalizePageNum(pageNum); + int normalizedPageSize = NormalizePageSize(pageSize); + + // 这里先拿“候选记录全集”,再在内存里做二次打分和分页。 + // 这么做不是为了偷懒,而是为了严格贴住 Java 源实现: + // 源模块也是先查候选,再在服务层做过滤、高亮、打分、分页。 + List rawRecords = await LoadSearchRecordsAsync(normalizedKeyword); + if (rawRecords.Count == 0) + { + return new SearchPageDTO(); + } + + // LINQ 链式处理: + // Select = 投影 + // Where = 过滤 + // OrderByDescending = 倒序排序 + // ToList = 立即执行并转成 List + List all = rawRecords + .Select(raw => ToResult(raw, normalizedKeyword, editMode)) + .Where(item => item != null) + + // 这里排序不能偷懒省掉。 + // 因为 legacy SQL 虽然已经按 score/update_time 做了一轮排序,但服务层还会再追加“标题命中 +20、正文命中 +10”的二次打分。 + // 如果这里不重新按 Score 排,最终顺序就会和 Java 源实现不一致。 + .OrderByDescending(item => item.Score) + .ToList(); + + SearchPageDTO page = new() + { + Total = all.Count + }; + + int from = Math.Max(0, (normalizedPageNum - 1) * normalizedPageSize); + if (from >= all.Count) + { + return page; + } + + page.Rows = all.Skip(from).Take(normalizedPageSize).ToList(); + return page; + } + + private SearchResultDTO ToResult(SearchRawRecord raw, string keyword, bool editMode) + { + string sourceType = raw.SourceType; + string content = raw.Content ?? string.Empty; + if (string.Equals(sourceType, PortalSearchDocConverter.SourceWeb, StringComparison.Ordinal) || string.Equals(sourceType, PortalSearchDocConverter.SourceWeb1, StringComparison.Ordinal)) + { + // Why:web / web1 的正文是 JSON,需要先抽取可搜索文本,不能直接拿原 JSON 生搜。 + // 这里再次抽取而不是完全依赖索引层,是因为 legacy SQL 回退场景下查出来的仍然可能是原始 JSON。 + content = _converter.ExtractSearchableText(content); + if (!ContainsIgnoreCase(raw.Title, keyword) && !ContainsIgnoreCase(content, keyword)) + { + return null; + } + } + else if (!ContainsIgnoreCase(raw.Title, keyword) && !ContainsIgnoreCase(content, keyword)) + { + // 非 web/web1 来源不需要做 JSON 文本抽取,直接看标题和正文即可。 + // 如果两者都不命中,就说明这条候选记录只是 SQL 初筛命中,但不满足最终展示条件。 + return null; + } + + SearchResultDTO dto = new() + { + // 对象初始化器: + // 是 C# 中创建对象并同时赋值的常见语法。 + SourceType = sourceType, + Title = raw.Title, + Snippet = BuildSnippet(raw.Title, content, keyword), + Score = CalculateScore(raw, keyword), + Route = string.IsNullOrWhiteSpace(raw.Route) ? BuildRoute(sourceType, raw.WebCode) : raw.Route, + RouteQuery = BuildRouteQuery(raw) + }; + if (editMode) + { + // 只有编辑端才需要这个字段。 + // 展示端不返回 editRoute,可以避免前台无意中依赖后台路由协议。 + dto.EditRoute = string.IsNullOrWhiteSpace(raw.EditRoute) ? BuildEditRoute(raw) : raw.EditRoute; + } + + return dto; + } + + private static int CalculateScore(SearchRawRecord raw, string keyword) + { + // Why:保持原 Java 逻辑,标题命中比正文命中权重更高。 + int score = raw.Score ?? 0; + if (ContainsIgnoreCase(raw.Title, keyword)) + { + // 标题命中给更高权重,是因为用户通常更信任标题语义。 + // 如果正文和标题都命中,标题相关结果应该优先展示。 + score += 20; + } + + if (ContainsIgnoreCase(raw.Content, keyword)) + { + score += 10; + } + + return score; + } + + private static string BuildRoute(string sourceType, string webCode) + { + if (string.Equals(sourceType, PortalSearchDocConverter.SourceMenu, StringComparison.Ordinal)) + { + return "/test"; + } + + if (string.Equals(sourceType, PortalSearchDocConverter.SourceWeb, StringComparison.Ordinal)) + { + if (string.Equals(webCode, "-1", StringComparison.Ordinal)) + { + return "/index"; + } + + if (string.Equals(webCode, "7", StringComparison.Ordinal)) + { + return "/productCenter"; + } + + return "/test"; + } + + if (string.Equals(sourceType, PortalSearchDocConverter.SourceWeb1, StringComparison.Ordinal)) + { + return "/productCenter/detail"; + } + + if (string.Equals(sourceType, PortalSearchDocConverter.SourceDocument, StringComparison.Ordinal)) + { + return "/serviceSupport"; + } + + if (string.Equals(sourceType, PortalSearchDocConverter.SourceConfigType, StringComparison.Ordinal)) + { + return "/productCenter"; + } + + // 未知来源统一给首页兜底,避免前端因为 route 为 null 直接报错。 + return "/index"; + } + + private static Dictionary BuildRouteQuery(SearchRawRecord raw) + { + if (!string.IsNullOrWhiteSpace(raw.RouteQueryJson)) + { + Dictionary parsed = ParseRouteQueryJson(raw.RouteQueryJson); + if (parsed.Count > 0) + { + // 索引链路如果已经带了 routeQueryJson,优先相信索引层产出的结果。 + // 这样可以避免搜索服务和索引服务各自维护一套路由规则,后续改动时更容易失配。 + return parsed; + } + } + + Dictionary query = new(); + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceMenu, StringComparison.Ordinal)) + { + query["id"] = raw.MenuId; + return query; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceWeb, StringComparison.Ordinal) + && !string.Equals(raw.WebCode, "-1", StringComparison.Ordinal) + && !string.Equals(raw.WebCode, "7", StringComparison.Ordinal)) + { + query["id"] = raw.WebCode; + return query; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceWeb1, StringComparison.Ordinal)) + { + query["webCode"] = raw.WebCode; + query["typeId"] = raw.TypeId; + query["deviceId"] = raw.DeviceId; + return query; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceDocument, StringComparison.Ordinal)) + { + query["documentId"] = raw.DocumentId; + query["webCode"] = raw.WebCode; + query["typeId"] = raw.TypeId; + return query; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceConfigType, StringComparison.Ordinal)) + { + // 配置分类不能直接把数据库主键原样吐给前端。 + // 当前前端真实消费的是页面入口编码,所以这里优先取 webCode,其次才退回 typeId 做兜底。 + string routeWebCode = !string.IsNullOrWhiteSpace(raw.WebCode) ? raw.WebCode : raw.TypeId; + query["id"] = routeWebCode; + query["configTypeId"] = routeWebCode; + } + + return query; + } + + private async Task> LoadSearchRecordsAsync(string keyword) + { + if (!UseIndexedEngine()) + { + // 明确走 legacy 模式时,直接退回旧 SQL。 + // 这里不能再“顺手尝试一下索引表”,否则配置就失去可预测性了。 + return await _legacyQueryService.SearchAsync(keyword); + } + + try + { + // 主链路优先走索引表查询。 + // 索引表的好处是结构统一、可扩展性更好,也更方便后续替换成真正 ES。 + List records = await _queryService.SearchAsync(keyword, _options.TakeLimit); + if (records.Count > 0) + { + return records; + } + + if (_options.EnableLegacyFallback && !await _queryService.HasIndexedDocumentAsync()) + { + // 索引表为空时自动回退旧 SQL,是为了兼容“索引还没建好/被清空”的冷启动场景。 + // 只有在确认没有任何有效索引文档时才触发,避免把正常空结果误判成需要降级。 + return await _legacyQueryService.SearchAsync(keyword); + } + + return records; + } + catch (Exception ex) when (_options.EnableLegacyFallback) + { + // 这里 catch 后降级,而不是把异常抛给前端。 + // 业务目标是“搜索尽量可用”,所以主链路失败时优先保服务,再考虑排查索引问题。 + _logger.LogWarning(ex, "portal search indexed query failed, fallback to legacy mapper"); + return await _legacyQueryService.SearchAsync(keyword); + } + } + + private bool UseIndexedEngine() + { + // 这里把 mysql / legacy 都视为“明确关闭索引表主链路”的配置。 + // 这样老系统切换配置时,只需要改一个字符串,不用再理解内部是否是 EF 或全文索引实现。 + return !string.Equals(_options.Engine, "mysql", StringComparison.OrdinalIgnoreCase) + && !string.Equals(_options.Engine, "legacy", StringComparison.OrdinalIgnoreCase); + } + + private static string BuildEditRoute(SearchRawRecord raw) + { + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceMenu, StringComparison.Ordinal)) + { + return $"/editor?type=1&id={raw.MenuId}"; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceWeb, StringComparison.Ordinal)) + { + if (string.Equals(raw.WebCode, "7", StringComparison.Ordinal)) + { + return "/productCenter/edit"; + } + + if (string.Equals(raw.WebCode, "-1", StringComparison.Ordinal)) + { + return "/editor?type=3&id=-1"; + } + + return $"/editor?type=1&id={raw.WebCode}"; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceWeb1, StringComparison.Ordinal)) + { + return $"/editor?type=2&id={raw.WebCode},{raw.TypeId},{raw.DeviceId}"; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceDocument, StringComparison.Ordinal)) + { + if (!string.IsNullOrWhiteSpace(raw.WebCode) && !string.IsNullOrWhiteSpace(raw.TypeId) && !string.IsNullOrWhiteSpace(raw.DeviceId)) + { + // 文档如果同时绑定了详情页三元组,就尽量把上下文一起带给编辑器。 + // 这样后台编辑时可以直接还原到“哪个详情页下的哪个文档”。 + return $"/editor?type=2&id={raw.WebCode},{raw.TypeId},{raw.DeviceId}&documentId={raw.DocumentId}"; + } + + return $"/editor?type=2&documentId={raw.DocumentId}"; + } + + if (string.Equals(raw.SourceType, PortalSearchDocConverter.SourceConfigType, StringComparison.Ordinal)) + { + return "/productCenter/edit"; + } + + return "/editor"; + } + + private static string BuildSnippet(string title, string content, string keyword) + { + // 优先用标题做摘要;标题不命中再从正文里截取关键片段。 + if (!string.IsNullOrWhiteSpace(title) && ContainsIgnoreCase(title, keyword)) + { + return Highlight(title, keyword); + } + + if (string.IsNullOrWhiteSpace(content)) + { + return string.Empty; + } + + string normalized = Regex.Replace(content, @"\s+", " ").Trim(); + int index = normalized.IndexOf(keyword, StringComparison.OrdinalIgnoreCase); + if (index < 0) + { + // 没命中时直接截前 120 个字符,和原 Java 行为保持一致。 + // 这里不要擅自改成长摘要或整段正文,否则前端列表会明显变长。 + return normalized[..Math.Min(120, normalized.Length)]; + } + + int start = Math.Max(0, index - 60); + int end = Math.Min(normalized.Length, index + keyword.Length + 60); + + // 摘要窗口固定为“命中点前后各约 60 个字符”。 + // 这是原模块的可见行为之一,前端样式和用户感知都已经围绕这个长度形成预期,不宜随意改大改小。 + string snippet = normalized[start..end]; + if (start > 0) + { + snippet = "..." + snippet; + } + + if (end < normalized.Length) + { + snippet += "..."; + } + + return Highlight(snippet, keyword); + } + + private static string Highlight(string text, string keyword) + { + // 这里返回带 标签的 HTML,前端可以直接做高亮展示。 + if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(keyword)) + { + return text ?? string.Empty; + } + + // 先对关键词做正则转义,再参与 Regex.Replace。 + // 否则用户搜 “C++” “(test)” 这类带特殊字符的词时,高亮逻辑会直接跑偏。 + string escaped = EscapePattern.Replace(keyword, "\\$1"); + Match match = Regex.Match(text, $"(?i){escaped}"); + if (!match.Success) + { + return text; + } + + return Regex.Replace(text, $"(?i){escaped}", "$0"); + } + + private static bool ContainsIgnoreCase(string text, string keyword) + { + return !string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(keyword) && text.Contains(keyword, StringComparison.OrdinalIgnoreCase); + } + + private static int NormalizePageNum(int? pageNum) + { + return pageNum is > 0 ? pageNum.Value : 1; + } + + private static int NormalizePageSize(int? pageSize) + { + if (pageSize is null or <= 0) + { + return 20; + } + + // 页大小上限固定 50,是为了防止单次搜索返回过大结果集,把高亮和摘要处理拖慢。 + return Math.Min(pageSize.Value, 50); + } + + private static string ValidateKeyword(string keyword) + { + // Why:先把无效输入挡在入口,避免把空关键词或超长关键词直接带进数据库查询。 + string normalized = keyword?.Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw Oops.Oh("关键词不能为空"); + } + + if (normalized.Length > 50) + { + // 50 这个长度限制来自原门户实现,不是随便拍脑袋定的。 + // 它同时在兜底 SQL、摘要处理、前端输入体验上形成了统一约束。 + throw Oops.Oh("关键词长度不能超过50"); + } + + return normalized; + } + + private static Dictionary ParseRouteQueryJson(string routeQueryJson) + { + try + { + Dictionary values = JsonSerializer.Deserialize>(routeQueryJson) ?? new Dictionary(); + return values.ToDictionary(item => item.Key, item => (object)item.Value); + } + catch + { + // routeQueryJson 解析失败时直接吞掉并回退到动态拼装。 + // 这是一个有意的容错策略:索引文档里偶发脏数据不应该把整条搜索结果打挂。 + return new Dictionary(); + } + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/IHwSearchRebuildService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/IHwSearchRebuildService.cs new file mode 100644 index 0000000..2f8de21 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/IHwSearchRebuildService.cs @@ -0,0 +1,75 @@ +// ============================================================================ +// 【文件说明】IHwSearchRebuildService.cs - 搜索索引重建服务接口 +// ============================================================================ +// 这个接口定义了搜索索引重建的契约。 +// +// 【什么是索引重建?】 +// 索引重建是指: +// 1. 从业务表读取数据 +// 2. 转换成搜索文档格式 +// 3. 写入搜索索引表 +// +// 【为什么需要索引重建?】 +// 1. 初始化:新环境部署时,索引表是空的 +// 2. 修复:索引和业务数据不一致时,重建可以修复 +// 3. 升级:索引结构变化时,需要重建所有索引 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用定时任务或消息队列触发重建: +// @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 +// public void rebuildIndex() { ... } +// +// C# 这里通过服务接口,可以在需要时手动触发。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索索引重建服务接口。 +/// +/// 【服务职责】 +/// 提供搜索索引的全量重建能力: +/// - 从业务表读取所有数据 +/// - 转换成搜索文档格式 +/// - 批量写入索引表 +/// +/// +/// 【设计模式 - 策略模式】 +/// 这个接口支持多种实现: +/// - HwSearchRebuildService:真正的重建服务(在 SearchEf 中) +/// - HwNoopSearchRebuildService:空实现,用于禁用重建功能 +/// +/// 通过接口注入,调用方不需要知道具体实现。 +/// +/// +public interface IHwSearchRebuildService +{ + /// + /// 重建所有搜索索引。 + /// + /// 【重建流程】 + /// 1. 清空现有索引(或标记为删除) + /// 2. 遍历所有业务表 + /// 3. 读取数据并转换 + /// 4. 批量写入索引表 + /// + /// 【注意事项】 + /// - 重建是耗时操作,可能需要几分钟到几小时 + /// - 建议在低峰期执行 + /// - 大表重建可能需要分批处理 + /// + /// + /// 【C# 语法知识点 - Task 返回类型】 + /// Task RebuildAllAsync() 返回 Task,表示: + /// - 这是一个异步方法 + /// - 不返回具体值(类似 Java 的 void) + /// - 调用方可以 await 等待完成 + /// + /// 对比 Java: + /// Java 返回 CompletableFuture<Void>: + /// CompletableFuture rebuildAllAsync(); + /// + /// + /// 异步任务 + Task RebuildAllAsync(); +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/LegacyHwSearchQueryService.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/LegacyHwSearchQueryService.cs new file mode 100644 index 0000000..e4fea4e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/LegacyHwSearchQueryService.cs @@ -0,0 +1,175 @@ +// ============================================================================ +// 【文件说明】LegacyHwSearchQueryService.cs - 旧版搜索查询服务 +// ============================================================================ +// 这是一个"遗留系统"(Legacy)的搜索服务,使用原始 SQL 实现搜索。 +// +// 【什么是遗留系统?】 +// 遗留系统是指: +// - 旧版本的功能实现 +// - 新版本保留它作为兼容或降级方案 +// - 通常在特定场景下使用 +// +// 【为什么保留旧版搜索?】 +// 1. 兼容性:新搜索可能依赖额外的索引表,旧环境可能没有 +// 2. 降级方案:新搜索失败时,可以回退到旧版 +// 3. 平滑迁移:逐步切换到新搜索,而不是一刀切 +// +// 【与新版搜索的区别】 +// - 新版:使用 EF Core 查询索引表,支持复杂查询和排序 +// - 旧版:使用 MyBatis 风格的 SQL,直接查询业务表 +// +// 【与 Java Spring Boot 的对比】 +// Java 若依的搜索通常是直接 SQL: +// @Select("SELECT * FROM hw_web WHERE title LIKE CONCAT('%', #{keyword}, '%')") +// List searchByKeyword(@Param("keyword") String keyword); +// +// 这个类就是类似的实现方式。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 旧版搜索查询服务。 +/// +/// 【设计模式 - 适配器模式】 +/// 这个类把旧版 SQL 搜索适配到新的服务架构中: +/// - 对外提供统一的搜索接口 +/// - 内部使用旧的 SQL 实现 +/// +/// 这样可以在不修改调用方的情况下,平滑切换搜索实现。 +/// +/// +/// 【C# 语法知识点 - sealed 密封类】 +/// sealed 表示不能被继承。遗留服务通常不需要扩展。 +/// +/// +/// 【C# 语法知识点 - ITransient 瞬态服务】 +/// ITransient 表示每次请求都创建新实例。 +/// 搜索服务通常是无状态的,用 ITransient 或 IScoped 都可以。 +/// +/// +public sealed class LegacyHwSearchQueryService : ITransient +{ + /// + /// MyBatis 映射器名称。 + /// + private const string Mapper = "HwSearchMapper"; + private const string SearchByKeywordSql = + """ + SELECT * + FROM ( + SELECT CONVERT('web' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, + CONVERT(CAST(w.web_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, + CONVERT(CONCAT('页面#', w.web_code) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, + CONVERT(IFNULL(w.web_json_string, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, + CONVERT(CAST(w.web_code AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS type_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS device_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS document_id, + 80 AS score, + w.update_time AS updated_at + FROM hw_web w + WHERE w.is_delete = '0' + AND w.web_json_string LIKE CONCAT('%', @keyword, '%') + + UNION ALL + + SELECT CONVERT('web1' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, + CONVERT(CAST(w1.web_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, + CONVERT(CONCAT('详情#', w1.web_code, '-', w1.typeId, '-', w1.device_id) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, + CONVERT(IFNULL(w1.web_json_string, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, + CONVERT(CAST(w1.web_code AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, + CONVERT(CAST(w1.typeId AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS type_id, + CONVERT(CAST(w1.device_id AS CHAR) USING utf8mb4) COLLATE utf8mb4_general_ci AS device_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS document_id, + 90 AS score, + w1.update_time AS updated_at + FROM hw_web1 w1 + WHERE w1.is_delete = '0' + AND w1.web_json_string LIKE CONCAT('%', @keyword, '%') + + UNION ALL + + SELECT CONVERT('document' USING utf8mb4) COLLATE utf8mb4_general_ci AS source_type, + CONVERT(IFNULL(d.document_id, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS biz_id, + CONVERT(IFNULL(NULLIF(d.json, ''), d.document_id) USING utf8mb4) COLLATE utf8mb4_general_ci AS title, + CONVERT(IFNULL(d.json, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS content, + CONVERT(IFNULL(d.web_code, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS web_code, + CONVERT(IFNULL(d.type, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS type_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS device_id, + CAST(NULL AS CHAR CHARACTER SET utf8mb4) COLLATE utf8mb4_general_ci AS menu_id, + CONVERT(IFNULL(d.document_id, '') USING utf8mb4) COLLATE utf8mb4_general_ci AS document_id, + 70 AS score, + d.update_time AS updated_at + FROM hw_web_document d + WHERE d.is_delete = '0' + AND ( + d.json LIKE CONCAT('%', @keyword, '%') + OR d.document_id LIKE CONCAT('%', @keyword, '%') + ) + ) s + ORDER BY s.score DESC, s.updated_at DESC + LIMIT 500 + """; + + /// + /// MyBatis 执行器。 + /// + /// 【依赖注入】 + /// 通过构造函数注入 HwPortalMyBatisExecutor。 + /// 这个执行器负责解析 XML 中的 SQL 并执行。 + /// + /// + private readonly HwPortalMyBatisExecutor _executor; + private readonly ISqlSugarClient _db; + + /// + /// 构造函数(依赖注入)。 + /// + /// MyBatis 执行器 + public LegacyHwSearchQueryService(HwPortalMyBatisExecutor executor, ISqlSugarClient db) + { + _executor = executor; + _db = db; + } + + /// + /// 根据关键词搜索。 + /// + /// 【搜索逻辑】 + /// 调用 HwSearchMapper.xml 中定义的 searchByKeyword SQL。 + /// SQL 通常是 LIKE 查询: + /// SELECT * FROM hw_web WHERE title LIKE '%keyword%' + /// UNION ALL + /// SELECT * FROM hw_product WHERE title LIKE '%keyword%' + /// + /// 【性能说明】 + /// LIKE '%keyword%' 无法使用索引,性能较差。 + /// 这就是为什么需要新版搜索(使用索引表)。 + /// + /// + /// 【C# 语法知识点 - 匿名对象传参】 + /// new { keyword } 创建匿名对象,作为 SQL 参数。 + /// SQL 中的 #{keyword} 会被替换为参数值。 + /// + /// 对比 Java MyBatis: + /// Java: @Param("keyword") String keyword + /// C#: new { keyword } + /// + /// + /// 搜索关键词 + /// 搜索结果列表 + public Task> SearchAsync(string keyword) + { + // QueryListAsync<T> 执行查询并返回列表。 + // T 是结果映射类型,SearchRawRecord 是原始搜索记录。 + // 回滚到 XML 方案时可直接恢复: + // return _executor.QueryListAsync(Mapper, "searchByKeyword", new { keyword }); + return _db.Ado.SqlQueryAsync(SearchByKeywordSql, new[] + { + new SugarParameter("@keyword", keyword) + }); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs new file mode 100644 index 0000000..3b798ae --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs @@ -0,0 +1,351 @@ +// ============================================================================ +// 【文件说明】PortalSearchDocConverter.cs - 搜索文档转换器 +// ============================================================================ +// 这个类负责把业务数据转换成搜索索引文档格式。 +// +// 【核心功能】 +// 1. 从 JSON 配置中提取可搜索文本 +// 2. 过滤掉不需要搜索的字段(如 URL、图片路径) +// 3. 清理 HTML 标签和多余空白 +// +// 【为什么需要转换器?】 +// 业务数据存储格式和搜索需求不同: +// - 业务数据:结构化 JSON,包含配置、URL、图片等 +// - 搜索需求:纯文本,便于全文检索 +// +// 例如: +// - 业务数据:{ "title": "产品介绍", "icon": "/img/icon.png", "desc": "

产品描述

" } +// - 搜索文本:产品介绍 产品描述 +// +// 【与 Java Spring Boot 的对比】 +// Java 通常用工具类实现类似功能: +// public class SearchTextExtractor { +// public static String extract(String json) { ... } +// } +// +// C# 这里用实例类,便于依赖注入和测试。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 搜索文档转换器。 +/// +/// 【服务职责】 +/// 把业务实体转换成搜索索引文档: +/// 1. 提取 JSON 中的可搜索文本 +/// 2. 过滤掉 URL、图片等非搜索字段 +/// 3. 清理 HTML 标签,保留纯文本 +/// +/// +/// 【C# 语法知识点 - sealed 密封类】 +/// sealed 表示不能被继承。转换器类通常不需要扩展。 +/// +/// 注意:这个类没有实现 ITransient 等接口。 +/// 这意味着它不会被自动注册到 DI 容器。 +/// 使用时需要手动创建实例或手动注册。 +/// +/// +public sealed class PortalSearchDocConverter +{ + /// + /// 来源类型常量 - 菜单。 + /// + /// 【C# 语法知识点 - const 常量】 + /// const 是编译期常量,值在编译时就确定了。 + /// 这些常量用于标识搜索文档的来源类型。 + /// + /// 对比 Java: + /// Java: public static final String SOURCE_MENU = "menu"; + /// C#: public const string SourceMenu = "menu"; + /// + /// C# 的 const 隐式是 static 的,不能加 static 修饰符。 + /// + /// + public const string SourceMenu = "menu"; + + /// + /// 来源类型常量 - 页面。 + /// + public const string SourceWeb = "web"; + + /// + /// 来源类型常量 - 页面1。 + /// + public const string SourceWeb1 = "web1"; + + /// + /// 来源类型常量 - 文档。 + /// + public const string SourceDocument = "document"; + + /// + /// 来源类型常量 - 配置类型。 + /// + public const string SourceConfigType = "configType"; + + /// + /// 需要跳过的 JSON 字段名集合。 + /// + /// 【C# 语法知识点 - HashSet<T> 哈希集合】 + /// HashSet<T> 是不包含重复元素的集合: + /// - 查找速度快(O(1)) + /// - 自动去重 + /// - 适合存储需要快速判断是否存在的元素 + /// + /// 对比 Java: + /// Java: private static final Set<String> SKIP_KEYS = new HashSet<>(Arrays.asList(...)); + /// C#: private static readonly HashSet<string> SkipJsonKeys = new(...) { ... }; + /// + /// 【StringComparer.OrdinalIgnoreCase 参数】 + /// 创建集合时传入比较器,实现忽略大小写的比较: + /// - "Icon" 和 "icon" 被视为相同 + /// - 查找时自动忽略大小写 + /// + /// 对比 Java: + /// Java 需要用 TreeSet 或自定义 HashSet: + /// new TreeSet<>(String.CASE_INSENSITIVE_ORDER) + /// + /// + /// 【为什么跳过这些字段?】 + /// 这些字段不适合全文搜索: + /// - icon, img, banner:图片路径 + /// - url, route:URL 路径 + /// - uuid, id:技术标识符 + /// - secretkey:敏感信息 + /// + /// + private static readonly HashSet SkipJsonKeys = new(StringComparer.OrdinalIgnoreCase) + { + "icon", "url", "banner", "banner1", "img", "imglist", "type", "uuid", "filename", + "documentaddress", "secretkey", "route", "routequery", "webcode", "deviceid", "typeid", + "configtypeid", "id" + }; + + /// + /// 从文本中提取可搜索文本。 + /// + /// 【处理流程】 + /// 1. 如果文本为空,返回空字符串 + /// 2. 先尝试作为 JSON 解析 + /// 3. 如果 JSON 解析成功,提取所有文本节点 + /// 4. 如果 JSON 解析失败,清理 HTML 标签后返回 + /// + /// + /// 【C# 语法知识点 - string.IsNullOrWhiteSpace】 + /// string.IsNullOrWhiteSpace(text) 检查字符串是否: + /// - null + /// - 空字符串 "" + /// - 只包含空白字符(空格、制表符、换行等) + /// + /// 对比 Java: + /// Java: StringUtils.isBlank(text)(Apache Commons) + /// 或: text == null || text.trim().isEmpty() + /// + /// + /// 输入文本(可能是 JSON 或 HTML) + /// 提取的可搜索文本 + public string ExtractSearchableText(string text) + { + // 【空值检查】 + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + // 【HTML 标签清理】 + // 先清理 HTML,作为备用结果。 + string stripped = StripHtml(text); + + try + { + // 【JSON 解析】 + // JsonDocument 是 System.Text.Json 的高性能 JSON 解析器。 + // using 语句确保资源正确释放。 + using JsonDocument document = JsonDocument.Parse(text); + + // 【StringBuilder 文本拼接】 + // StringBuilder 用于高效拼接大量字符串。 + // 对比:string + string 每次都创建新对象,性能差。 + StringBuilder builder = new(); + + // 【递归提取文本节点】 + CollectNodeText(document.RootElement, null, builder); + + // 【规范化空白】 + string extracted = NormalizeWhitespace(builder.ToString()); + + // 【返回结果】 + // 如果提取的文本为空,返回清理后的 HTML 文本。 + return string.IsNullOrWhiteSpace(extracted) ? stripped : extracted; + } + catch + { + // 【JSON 解析失败】 + // 如果不是有效的 JSON,返回清理后的 HTML 文本。 + return stripped; + } + } + + /// + /// 递归收集 JSON 节点的文本内容。 + /// + /// 【递归遍历 JSON】 + /// JSON 可能是嵌套结构,需要递归遍历: + /// - Object:遍历每个属性 + /// - Array:遍历每个元素 + /// - String:提取文本值 + /// - Number/Boolean/Null:跳过(不适合搜索) + /// + /// + /// 【C# 语法知识点 - switch 表达式】 + /// switch (element.ValueKind) 根据 JSON 值类型分支处理: + /// - JsonValueKind.Object:JSON 对象 + /// - JsonValueKind.Array:JSON 数组 + /// - JsonValueKind.String:字符串 + /// + /// 对比 Java: + /// Java 需要用 if-else 或 switch 语句: + /// switch (element.getValueKind()) { + /// case OBJECT: ... + /// case ARRAY: ... + /// } + /// + /// + /// JSON 元素 + /// 字段名(用于判断是否跳过) + /// 输出 StringBuilder + private void CollectNodeText(JsonElement element, string fieldName, StringBuilder output) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + // 【遍历对象属性】 + // EnumerateObject() 返回对象的所有属性。 + foreach (JsonProperty property in element.EnumerateObject()) + { + // 【过滤跳过字段】 + // 如果字段名在跳过列表中,不处理。 + if (!ShouldSkip(property.Name)) + { + CollectNodeText(property.Value, property.Name, output); + } + } + break; + + case JsonValueKind.Array: + // 【遍历数组元素】 + // EnumerateArray() 返回数组的所有元素。 + foreach (JsonElement child in element.EnumerateArray()) + { + CollectNodeText(child, fieldName, output); + } + break; + + case JsonValueKind.String: + // 【提取字符串值】 + // 如果字段需要跳过,直接返回。 + if (ShouldSkip(fieldName)) + { + return; + } + + // 【获取字符串值】 + // element.GetString() 获取 JSON 字符串的值。 + string value = NormalizeWhitespace(StripHtml(element.GetString())); + + // 【过滤 URL 和空值】 + // URL 不适合搜索,跳过。 + if (string.IsNullOrWhiteSpace(value) || value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // 【追加到输出】 + // StringBuilder.Append() 追加文本。 + // .Append(' ') 追加空格分隔。 + output.Append(value).Append(' '); + break; + } + } + + /// + /// 判断字段是否应该跳过。 + /// + /// 【跳过规则】 + /// 1. 字段名在 SkipJsonKeys 集合中 + /// 2. 字段名以 "url" 结尾(如 imageUrl, videoUrl) + /// 3. 字段名以 "icon" 结尾(如 menuIcon) + /// + /// + /// 字段名 + /// 是否跳过 + private static bool ShouldSkip(string fieldName) + { + // 【空字段名检查】 + if (string.IsNullOrWhiteSpace(fieldName)) + { + return false; + } + + // 【多条件判断】 + // Contains 检查是否在跳过集合中。 + // EndsWith 检查是否以特定后缀结尾。 + return SkipJsonKeys.Contains(fieldName) + || fieldName.EndsWith("url", StringComparison.OrdinalIgnoreCase) + || fieldName.EndsWith("icon", StringComparison.OrdinalIgnoreCase); + } + + /// + /// 清理 HTML 标签。 + /// + /// 【C# 语法知识点 - 正则表达式】 + /// Regex.Replace(input, pattern, replacement) + /// - input:输入字符串 + /// - pattern:正则表达式模式 + /// - replacement:替换字符串 + /// + /// "<[^>]+>" 匹配所有 HTML 标签: + /// - <:匹配 < + /// - [^>]+:匹配一个或多个非 > 的字符 + /// - >:匹配 > + /// + /// 对比 Java: + /// Java: text.replaceAll("<[^>]+>", " ") + /// + /// C# 的 Regex.Replace 性能更好,支持编译正则。 + /// + /// + /// 输入文本 + /// 清理后的文本 + private static string StripHtml(string text) + { + // 用空格替换 HTML 标签,避免标签内容粘连。 + return NormalizeWhitespace(Regex.Replace(text ?? string.Empty, "<[^>]+>", " ")); + } + + /// + /// 规范化空白字符。 + /// + /// 【处理说明】 + /// 把连续的空白字符(空格、制表符、换行等)替换为单个空格: + /// - "a b\n\tc" → "a b c" + /// - 去除首尾空白 + /// + /// + /// 【C# 语法知识点 - 正则表达式 @"\s+"】 + /// @ 符号表示"逐字字符串"(Verbatim String): + /// - 不需要转义反斜杠 + /// - @"\s+" 等价于 "\\s+" + /// + /// \s+ 匹配一个或多个空白字符。 + /// + /// + /// 输入文本 + /// 规范化后的文本 + private static string NormalizeWhitespace(string text) + { + // 把连续空白替换为单个空格,并去除首尾空白。 + return Regex.Replace(text ?? string.Empty, @"\s+", " ").Trim(); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchRouteResolver.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchRouteResolver.cs new file mode 100644 index 0000000..5e6478b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchRouteResolver.cs @@ -0,0 +1,398 @@ +// ============================================================================ +// 【文件说明】PortalSearchRouteResolver.cs - 门户搜索路由解析器 +// ============================================================================ +// 这个服务负责解析搜索结果的前端跳转路由。 +// +// 【业务背景】 +// 用户搜索后点击结果,需要跳转到对应的页面。但问题是: +// - 搜索索引存储的是"配置分类"信息 +// - 前端需要的是"页面编码"(web_code) +// - 两者不是直接对应关系 +// +// 例如: +// - 搜索命中:配置分类"产品案例"(config_type_id = 3) +// - 前端跳转:页面编码"2"(对应"产品"栏目) +// +// 【解析逻辑】 +// 1. 根据配置分类的名称,在菜单树中查找匹配的菜单 +// 2. 优先匹配指定栏目下的菜单(避免同名菜单跳错) +// 3. 找到菜单后,返回菜单 ID 作为页面编码 +// +// 【与 Java 若依的对比】 +// Java 若依通常在搜索 SQL 中直接返回路由: +// SELECT *, 'product' as route FROM hw_product WHERE ... +// +// C# 这里用解析器动态计算,更灵活但更复杂。 +// ============================================================================ + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户搜索路由解析器。 +/// +/// 【业务说明】 +/// 配置分类命中后,前端真正需要的是页面 web_code,而不是分类主键本身。 +/// 这个服务负责把"配置分类"解析成"页面编码"。 +/// +/// +/// 【C# 语法知识点 - sealed 密封类 + ITransient】 +/// sealed + ITransient 表示: +/// - 这是一个瞬态服务(每次请求创建新实例) +/// - 不能被继承(不需要扩展) +/// +/// +public sealed class PortalSearchRouteResolver : ITransient +{ + /// + /// 配置分类到根菜单的映射表。 + /// + /// 【业务约定】 + /// 这张映射表是"配置分类 → 门户一级根菜单"的业务约定: + /// - "3" → 2L:配置分类 3 对应菜单 2(产品栏目) + /// - "4" → 7L:配置分类 4 对应菜单 7(解决方案栏目) + /// - "5" → 4L:配置分类 5 对应菜单 4(服务栏目) + /// - "6" → 24L:配置分类 6 对应菜单 24(案例栏目) + /// + /// 它不是技术规则,而是前端栏目结构和搜索跳转口径绑定出来的业务知识。 + /// 后续如果官网栏目调整,这里必须跟着业务一起改,不能只改前端路由。 + /// + /// + /// 【C# 语法知识点 - Dictionary<K,V> 字典】 + /// Dictionary<string, long> 是键值对集合: + /// - 键:string(配置分类 ID) + /// - 值:long(菜单 ID) + /// + /// StringComparer.Ordinal 参数: + /// - 使用序数比较(按字符编码比较) + /// - 区分大小写 + /// - 性能最好 + /// + /// 对比 Java: + /// Java: Map<String, Long> map = new HashMap<>(); + /// Java 默认使用 equals 比较,C# 可以指定比较器。 + /// + /// + /// 【C# 语法知识点 - 字典初始化器】 + /// new Dictionary<...> { ["3"] = 2L, ["4"] = 7L } + /// 这是集合初始化器语法,等价于: + /// var map = new Dictionary<...>(); + /// map["3"] = 2L; + /// map["4"] = 7L; + /// + /// 2L 中的 L 后缀表示 long 类型字面量。 + /// + /// + private static readonly Dictionary ConfigTypeRootMenuMap = new(StringComparer.Ordinal) + { + ["3"] = 2L, + ["4"] = 7L, + ["5"] = 4L, + ["6"] = 24L + }; + + /// + /// 菜单服务(依赖注入)。 + /// + private readonly HwWebMenuService _webMenuService; + + /// + /// 构造函数(依赖注入)。 + /// + /// 菜单服务 + public PortalSearchRouteResolver(HwWebMenuService webMenuService) + { + _webMenuService = webMenuService; + } + + /// + /// 解析配置分类的页面编码。 + /// + /// 【解析流程】 + /// 1. 获取当前有效菜单树 + /// 2. 构建候选名称列表(分类名称、首页名称) + /// 3. 在菜单树中查找匹配的菜单 + /// 4. 返回菜单 ID 作为页面编码 + /// + /// + /// 配置分类实体 + /// 页面编码(菜单 ID) + public async Task ResolveConfigTypeWebCode(HwPortalConfigType configType) + { + if (configType == null) + { + return null; + } + + // 配置分类路由解析必须依赖当前有效菜单树,而不能只看 config_type 自身主键。 + // 因为前端实际拿来跳转的是页面/菜单入口编码,不是分类表主键。 + List menus = await _webMenuService.SelectHwWebMenuList(new HwWebMenu()); + + // 这里把"中文标题 + 首页标题 + 分类归属"一起带进解析流程。 + // 目的是尽量复刻原 Java 里"按名称猜入口页面"的做法,避免迁移后搜索跳转口径变掉。 + return ResolveConfigTypeWebCode(BuildCandidateNames(configType.ConfigTypeName, configType.HomeConfigTypeName), configType.ConfigTypeClassfication, menus); + } + + /// + /// 根据标题解析页面编码(重载方法)。 + /// + /// 标题 + /// 页面编码 + public async Task ResolveConfigTypeWebCode(string title) + { + List menus = await _webMenuService.SelectHwWebMenuList(new HwWebMenu()); + return ResolveConfigTypeWebCode(BuildCandidateNames(title), null, menus); + } + + /// + /// 核心解析方法:根据候选名称和分类归属查找匹配菜单。 + /// + /// 【匹配策略】 + /// 第一轮:名称匹配 + 根菜单约束(优先) + /// - 避免不同栏目下同名菜单跳错 + /// - 优先选择直接子节点 + /// + /// 第二轮:纯名称匹配(降级) + /// - 兼容历史脏数据或未维护 ancestors 的菜单 + /// - 宁可命中一个近似入口,也不要直接返回空 + /// + /// + /// 【C# 语法知识点 - LINQ 查询链】 + /// menus.Where(...).Where(...).OrderBy(...).ThenBy(...).FirstOrDefault() + /// + /// 这是 LINQ 的链式调用: + /// - Where:过滤 + /// - OrderBy:排序(升序) + /// - ThenBy:次级排序 + /// - FirstOrDefault:取第一个,没有则返回默认值 + /// + /// 对比 Java Stream: + /// Java: menus.stream().filter(...).filter(...).sorted(...).findFirst().orElse(null) + /// + /// + /// 候选名称列表 + /// 分类归属 + /// 菜单列表 + /// 页面编码 + private static string ResolveConfigTypeWebCode(List candidateNames, string configTypeClassfication, List menus) + { + if (candidateNames.Count == 0 || menus == null || menus.Count == 0) + { + return null; + } + + // 【安全取字典值】 + // TryGetValue 是 C# 常见的"安全取字典值"写法。 + // 它不会像直接下标访问那样在 key 不存在时抛异常,更适合处理这种可空的业务映射。 + // + // 对比 Java: + // Java: Long rootId = map.get(key); // 返回 null 如果不存在 + // C#: bool found = map.TryGetValue(key, out long value); + long? expectedRootMenuId = ConfigTypeRootMenuMap.TryGetValue(configTypeClassfication ?? string.Empty, out long rootMenuId) ? rootMenuId : null; + + // 【第一轮匹配 - 带根菜单约束】 + // 这是为了避免不同一级栏目下出现同名菜单时,搜索命中后跳错页面。 + HwWebMenu matchedMenu = menus + .Where(menu => candidateNames.Contains(Normalize(menu.WebMenuName))) + .Where(menu => MatchesRootMenu(menu, expectedRootMenuId)) + .OrderBy(menu => !IsDirectChild(menu, expectedRootMenuId)) // 优先直接子节点 + .ThenBy(menu => menu.WebMenuId) + .FirstOrDefault(); + + // 【第二轮匹配 - 纯名称匹配】 + // ??= 是 C# 8 的"空合并赋值"运算符: + // - 如果 matchedMenu 不为 null,不执行赋值 + // - 如果 matchedMenu 为 null,执行赋值 + // + // 这样做是为了兼容历史脏数据或未维护 ancestors 的菜单记录, + // 宁可命中一个近似入口,也不要直接返回空。 + matchedMenu ??= menus + .Where(menu => candidateNames.Contains(Normalize(menu.WebMenuName))) + .OrderBy(menu => menu.WebMenuId) + .FirstOrDefault(); + + // 【返回结果】 + // 这里返回的是 WebMenuId 对应的页面入口编码字符串。 + // 返回 null 代表"本次无法解析到安全的前端跳转入口",后续上层会自动走兜底逻辑。 + // + // CultureInfo.InvariantCulture 用于数字转字符串: + // - 不受区域设置影响 + // - 总是使用英文格式(如 "123" 而不是 "123,00") + return matchedMenu?.WebMenuId?.ToString(CultureInfo.InvariantCulture); + } + + /// + /// 构建候选名称列表。 + /// + /// 【为什么需要候选列表?】 + /// 同一个配置分类可能有多个名称: + /// - ConfigTypeName:分类名称 + /// - HomeConfigTypeName:首页显示名称 + /// - 历史数据可能还有"XX案例"和"XX"的混用 + /// + /// 构建候选列表可以增加匹配成功率。 + /// + /// + /// 【C# 语法知识点 - params 参数数组】 + /// params string[] values 表示可变参数: + /// - 调用时可以传任意数量的参数 + /// - 编译器自动组装成数组 + /// + /// 对比 Java: + /// Java: public static List<String> buildNames(String... values) + /// + /// + /// 【C# 语法知识点 - HashSet 去重】 + /// HashSet 自动去重,避免重复比较。 + /// StringComparer.Ordinal 使用序数比较。 + /// + /// + /// 名称列表 + /// 去重后的候选名称列表 + private static List BuildCandidateNames(params string[] values) + { + // HashSet 用来做"去重后的候选集合"。 + // 搜索路由解析里经常会出现多个别名指向同一个菜单,用 HashSet 可以避免重复比较。 + HashSet candidates = new(StringComparer.Ordinal); + + // 【LINQ 过滤 + foreach】 + // Where 过滤掉空白值,然后遍历处理。 + foreach (string value in values.Where(item => !string.IsNullOrWhiteSpace(item))) + { + string normalized = Normalize(value); + if (string.IsNullOrWhiteSpace(normalized)) + { + continue; + } + + candidates.Add(normalized); + + // 【特殊处理"案例"后缀】 + // 历史数据里存在"XX案例"和"XX"混用的情况。 + // 搜索路由解析这里主动补一个去后缀别名,能降低菜单命名不统一导致的跳转失败。 + if (normalized.EndsWith("案例", StringComparison.Ordinal)) + { + // 【C# 语法知识点 - 范围操作符】 + // normalized[..^2] 是范围操作符: + // - .. 表示"从开始到" + // - ^2 表示"倒数第 2 个" + // - normalized[..^2] 表示"从开始到倒数第 2 个" + // + // 例如:"产品案例"[..^2] = "产品" + candidates.Add(Normalize(normalized[..^2])); + } + } + + return candidates.ToList(); + } + + /// + /// 检查菜单是否匹配根菜单约束。 + /// + /// 【匹配规则】 + /// 1. 如果没有根菜单约束,直接返回 true + /// 2. 如果菜单是根菜单的直接子节点,返回 true + /// 3. 如果菜单的祖先链包含根菜单,返回 true + /// + /// 这样既兼容两级菜单,也兼容更深层的树结构。 + /// + /// + /// 菜单 + /// 期望的根菜单 ID + /// 是否匹配 + private static bool MatchesRootMenu(HwWebMenu menu, long? expectedRootMenuId) + { + if (menu == null) + { + return false; + } + + if (!expectedRootMenuId.HasValue) + { + // 没有归属根菜单约束时,名称命中即可。 + // 这对应的是"未知分类/历史数据"场景,策略上更偏向尽量给出跳转结果。 + return true; + } + + // 这里允许两种命中方式: + // 1. 当前菜单就是根菜单的直接子节点 + // 2. 当前菜单虽然不是直接子节点,但 ancestors 链上包含根菜单 + // 这样既兼容两级菜单,也兼容更深层的树结构。 + return IsDirectChild(menu, expectedRootMenuId) || ContainsAncestor(menu.Ancestors, expectedRootMenuId.Value); + } + + /// + /// 检查菜单是否是根菜单的直接子节点。 + /// + /// 菜单 + /// 期望的根菜单 ID + /// 是否直接子节点 + private static bool IsDirectChild(HwWebMenu menu, long? expectedRootMenuId) + { + // menu?.Parent 使用 null 条件运算符: + // - 如果 menu 为 null,返回 null,不访问 Parent + // - 如果 menu 不为 null,返回 Parent + return menu?.Parent != null && expectedRootMenuId.HasValue && menu.Parent.Value == expectedRootMenuId.Value; + } + + /// + /// 检查祖先链是否包含指定的菜单 ID。 + /// + /// 【ancestors 格式说明】 + /// ancestors 在这套门户模型里是逗号分隔的祖先链,例如 "0,2,7": + /// - 0:根节点 + /// - 2:一级菜单 + /// - 7:二级菜单 + /// + /// 这里先拆分再精确比较,而不是直接 Contains 字符串, + /// 是为了避免 "2" 误匹配到 "12" 这类脏命中。 + /// + /// + /// 【C# 语法知识点 - String.Split 选项】 + /// Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + /// - RemoveEmptyEntries:移除空元素 + /// - TrimEntries:去除每个元素的首尾空白 + /// + /// 对比 Java: + /// Java: String[] parts = text.split(","); + // // 需要手动处理空白 + /// + /// + /// 祖先链字符串 + /// 期望的根菜单 ID + /// 是否包含 + private static bool ContainsAncestor(string ancestors, long expectedRootMenuId) + { + if (string.IsNullOrWhiteSpace(ancestors)) + { + return false; + } + + // ancestors 在这套门户模型里是逗号分隔的祖先链,例如 "0,2,7"。 + // 这里先拆分再精确比较,而不是直接 Contains 字符串,是为了避免 "2" 误匹配到 "12" 这类脏命中。 + return ancestors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Contains(expectedRootMenuId.ToString(CultureInfo.InvariantCulture), StringComparer.Ordinal); + } + + /// + /// 规范化字符串(去除空白)。 + /// + /// 【处理说明】 + /// 这里只做最保守的归一化:trim + 去连续空白。 + /// 不做大小写折叠或中文转换,避免把业务词条误伤到别的菜单名。 + /// + /// + /// 【C# 语法知识点 - 正则表达式】 + /// Regex.Replace(value?.Trim() ?? string.Empty, "\\s+", "") + /// - \\s+:匹配一个或多个空白字符 + /// - 替换为空字符串 + /// + /// + /// 输入字符串 + /// 规范化后的字符串 + private static string Normalize(string value) + { + // 这里只做最保守的归一化:trim + 去连续空白。 + // 不做大小写折叠或中文转换,避免把业务词条误伤到别的菜单名。 + return Regex.Replace(value?.Trim() ?? string.Empty, "\\s+", ""); + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoDetailMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoDetailMapper.xml new file mode 100644 index 0000000..86c769a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoDetailMapper.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + select us_info_detail_id, about_us_info_id, us_info_detail_title, us_info_detail_desc, us_info_detail_order, us_info_detail_pic, create_time, create_by, update_time, update_by from hw_about_us_info_detail + + + + + + + + insert into hw_about_us_info_detail + + about_us_info_id, + us_info_detail_title, + us_info_detail_desc, + us_info_detail_order, + us_info_detail_pic, + create_time, + create_by, + update_time, + update_by, + + + #{aboutUsInfoId}, + #{usInfoDetailTitle}, + #{usInfoDetailDesc}, + #{usInfoDetailOrder}, + #{usInfoDetailPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_about_us_info_detail + + about_us_info_id = #{aboutUsInfoId}, + us_info_detail_title = #{usInfoDetailTitle}, + us_info_detail_desc = #{usInfoDetailDesc}, + us_info_detail_order = #{usInfoDetailOrder}, + us_info_detail_pic = #{usInfoDetailPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where us_info_detail_id = #{usInfoDetailId} + + + + delete from hw_about_us_info_detail where us_info_detail_id = #{usInfoDetailId} + + + + delete from hw_about_us_info_detail where us_info_detail_id in + + #{usInfoDetailId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoMapper.xml new file mode 100644 index 0000000..935cf71 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAboutUsInfoMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + select about_us_info_id, about_us_info_type, about_us_info_title, about_us_info_etitle,about_us_info_desc, about_us_info_order,display_modal, about_us_info_pic, create_time, create_by, update_time, update_by from hw_about_us_info + + + + + + + + insert into hw_about_us_info + + about_us_info_type, + about_us_info_etitle, + about_us_info_title, + about_us_info_desc, + about_us_info_order, + display_modal, + about_us_info_pic, + create_time, + create_by, + update_time, + update_by, + + + #{aboutUsInfoType}, + #{aboutUsInfoEtitle}, + #{aboutUsInfoTitle}, + #{aboutUsInfoDesc}, + #{aboutUsInfoOrder}, + #{displayModal}, + #{aboutUsInfoPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_about_us_info + + about_us_info_type = #{aboutUsInfoType}, + about_us_info_etitle = #{aboutUsInfoEtitle}, + about_us_info_title = #{aboutUsInfoTitle}, + about_us_info_desc = #{aboutUsInfoDesc}, + about_us_info_order = #{aboutUsInfoOrder}, + display_modal = #{displayModal}, + about_us_info_pic = #{aboutUsInfoPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where about_us_info_id = #{aboutUsInfoId} + + + + delete from hw_about_us_info where about_us_info_id = #{aboutUsInfoId} + + + + delete from hw_about_us_info where about_us_info_id in + + #{aboutUsInfoId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAnalyticsMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAnalyticsMapper.xml new file mode 100644 index 0000000..f7d0d28 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwAnalyticsMapper.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO hw_web_visit_event ( + event_type, visitor_id, session_id, path, referrer, utm_source, utm_medium, utm_campaign, + keyword, ip_hash, ua, device, browser, os, stay_ms, event_time, created_at + ) VALUES ( + #{eventType}, #{visitorId}, #{sessionId}, #{path}, #{referrer}, #{utmSource}, #{utmMedium}, #{utmCampaign}, + #{keyword}, #{ipHash}, #{ua}, #{device}, #{browser}, #{os}, #{stayMs}, #{eventTime}, NOW() + ) + + + + + + + + + + + + + + + + + + + + + + INSERT INTO hw_web_visit_daily ( + stat_date, pv, uv, ip_uv, avg_stay_ms, bounce_rate, search_count, download_count, created_at, updated_at + ) VALUES ( + #{statDate}, #{pv}, #{uv}, #{ipUv}, #{avgStayMs}, #{bounceRate}, #{searchCount}, #{downloadCount}, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + pv = VALUES(pv), + uv = VALUES(uv), + ip_uv = VALUES(ip_uv), + avg_stay_ms = VALUES(avg_stay_ms), + bounce_rate = VALUES(bounce_rate), + search_count = VALUES(search_count), + download_count = VALUES(download_count), + updated_at = NOW() + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwContactUsInfoMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwContactUsInfoMapper.xml new file mode 100644 index 0000000..c257d97 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwContactUsInfoMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + select contact_us_info_id, user_name, user_email, user_phone, user_ip, remark,create_time, create_by, update_time, update_by from hw_contact_us_info + + + + + + + + insert into hw_contact_us_info + + user_name, + user_email, + user_phone, + user_ip, + remark, + create_time, + create_by, + update_time, + update_by, + + + #{userName}, + #{userEmail}, + #{userPhone}, + #{userIp}, + #{remark}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_contact_us_info + + user_name = #{userName}, + user_email = #{userEmail}, + user_phone = #{userPhone}, + user_ip = #{userIp}, + remark = #{remark}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where contact_us_info_id = #{contactUsInfoId} + + + + delete from hw_contact_us_info where contact_us_info_id = #{contactUsInfoId} + + + + delete from hw_contact_us_info where contact_us_info_id in + + #{contactUsInfoId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigMapper.xml new file mode 100644 index 0000000..b0f9fd3 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigMapper.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select portal_config_id, portal_config_type,portal_config_type_id, portal_config_title, portal_config_order, + portal_config_desc, button_name, router_address, portal_config_pic, create_time, create_by, update_time, + update_by + from hw_portal_config + + + + select hpc.portal_config_id, hpc.portal_config_type,hpc.portal_config_type_id, hpc.portal_config_title, hpc.portal_config_order, + hpc.portal_config_desc, hpc.button_name, hpc.router_address, hpc.portal_config_pic, hpc.create_time, hpc.create_by, hpc.update_time, + hpc.update_by, + hpct.config_type_name, + hpct.home_config_type_pic, + hpct.config_type_icon, + hpct.home_config_type_name, + hpct.config_type_classfication, + hpct.parent_id, + hpct.ancestors + from hw_portal_config hpc + left join hw_portal_config_type hpct on hpc.portal_config_type_id = hpct.config_type_id + + + + + + + + + + insert into hw_portal_config + + portal_config_type, + portal_config_type_id, + portal_config_title, + portal_config_order, + portal_config_desc, + button_name, + router_address, + portal_config_pic, + create_time, + create_by, + update_time, + update_by, + + + #{portalConfigType}, + #{portalConfigTypeId}, + #{portalConfigTitle}, + #{portalConfigOrder}, + #{portalConfigDesc}, + #{buttonName}, + #{routerAddress}, + #{portalConfigPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_portal_config + + portal_config_type = #{portalConfigType}, + portal_config_type_id = #{portalConfigTypeId}, + portal_config_title = #{portalConfigTitle}, + portal_config_order = #{portalConfigOrder}, + portal_config_desc = #{portalConfigDesc}, + button_name = #{buttonName}, + router_address = #{routerAddress}, + portal_config_pic = #{portalConfigPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where portal_config_id = #{portalConfigId} + + + + delete from hw_portal_config where portal_config_id = #{portalConfigId} + + + + delete from hw_portal_config where portal_config_id in + + #{portalConfigId} + + + + + + + select hpc.portal_config_id, hpc.portal_config_type,hpc.portal_config_type_id, + hpc.portal_config_title, hpc.portal_config_order, hpc.portal_config_desc, + hpc.button_name, hpc.router_address, hpc.portal_config_pic, + hpc.create_time, hpc.create_by, hpc.update_time, hpc.update_by,hpct.config_type_name from hw_portal_config hpc + left join hw_portal_config_type hpct on hpc.portal_config_type_id = hpct.config_type_id + + + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigTypeMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigTypeMapper.xml new file mode 100644 index 0000000..653eb53 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalConfigTypeMapper.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + select config_type_id, config_type_classfication, config_type_name, home_config_type_name, config_type_desc, config_type_icon, home_config_type_pic, parent_id, ancestors, create_time, create_by, update_time, update_by + from hw_portal_config_type + + + + + + + + insert into hw_portal_config_type + + config_type_classfication, + config_type_name, + home_config_type_name, + config_type_desc, + config_type_icon, + home_config_type_pic, + parent_id, + ancestors, + create_time, + create_by, + update_time, + update_by, + + + #{configTypeClassfication}, + #{configTypeName}, + #{homeConfigTypeName}, + #{configTypeDesc}, + #{configTypeIcon}, + #{homeConfigTypePic}, + #{parentId}, + #{ancestors}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_portal_config_type + + config_type_classfication = #{configTypeClassfication}, + config_type_name = #{configTypeName}, + home_config_type_name = #{homeConfigTypeName}, + config_type_desc = #{configTypeDesc}, + config_type_icon = #{configTypeIcon}, + home_config_type_pic = #{homeConfigTypePic}, + parent_id = #{parentId}, + ancestors = #{ancestors}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where config_type_id = #{configTypeId} + + + + delete from hw_portal_config_type where config_type_id = #{configTypeId} + + + + delete from hw_portal_config_type where config_type_id in + + #{configTypeId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalSearchIndex.sql b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalSearchIndex.sql new file mode 100644 index 0000000..c9842d1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwPortalSearchIndex.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS `hw_portal_search_doc` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `doc_id` VARCHAR(128) NOT NULL COMMENT '搜索文档唯一键', + `source_type` VARCHAR(32) NOT NULL COMMENT '来源类型', + `biz_id` VARCHAR(64) NULL COMMENT '业务主键', + `title` VARCHAR(500) NULL COMMENT '搜索标题', + `content` LONGTEXT NULL COMMENT '搜索正文', + `web_code` VARCHAR(64) NULL COMMENT '页面编码', + `type_id` VARCHAR(64) NULL COMMENT '类型ID', + `device_id` VARCHAR(64) NULL COMMENT '设备ID', + `menu_id` VARCHAR(64) NULL COMMENT '菜单ID', + `document_id` VARCHAR(64) NULL COMMENT '文档ID', + `base_score` INT NOT NULL DEFAULT 0 COMMENT '基础分', + `route` VARCHAR(255) NULL COMMENT '前台路由', + `route_query_json` JSON NULL COMMENT '前台路由参数', + `edit_route` VARCHAR(255) NULL COMMENT '编辑路由', + `is_delete` CHAR(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', + `updated_at` DATETIME NULL COMMENT '业务更新时间', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '索引创建时间', + `modified_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '索引更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_hw_portal_search_doc_doc_id` (`doc_id`), + KEY `idx_hw_portal_search_doc_source_type` (`source_type`), + KEY `idx_hw_portal_search_doc_updated_at` (`updated_at`), + FULLTEXT KEY `ft_hw_portal_search_doc_title_content` (`title`, `content`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='hw-portal 搜索索引表'; diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductCaseInfoMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductCaseInfoMapper.xml new file mode 100644 index 0000000..cfad79f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductCaseInfoMapper.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + select case_info_id, case_info_title, config_type_id, typical_flag, case_info_desc, case_info_pic, case_info_html, create_time, create_by, update_time, update_by from hw_product_case_info hpci + + + + select case_info_id, case_info_title, config_type_id, typical_flag, case_info_desc, case_info_pic, create_time, create_by, update_time, update_by from hw_product_case_info hpci + + + + + + + + + insert into hw_product_case_info + + case_info_title, + config_type_id, + typical_flag, + case_info_desc, + case_info_pic, + case_info_html, + create_time, + create_by, + update_time, + update_by, + + + #{caseInfoTitle}, + #{configTypeId}, + #{typicalFlag}, + #{caseInfoDesc}, + #{caseInfoPic}, + #{caseInfoHtml}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_case_info + + case_info_title = #{caseInfoTitle}, + config_type_id = #{configTypeId}, + typical_flag = #{typicalFlag}, + case_info_desc = #{caseInfoDesc}, + case_info_pic = #{caseInfoPic}, + case_info_html = #{caseInfoHtml}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where case_info_id = #{caseInfoId} + + + + delete from hw_product_case_info where case_info_id = #{caseInfoId} + + + + delete from hw_product_case_info where case_info_id in + + #{caseInfoId} + + + + + + + select hpci.case_info_id, hpci.case_info_title, hpci.config_type_id, hpci.typical_flag, hpci.case_info_desc, hpci.case_info_pic, hpci.create_time, + hpci.create_by, hpci.update_time, hpci.update_by,hpct.config_type_name from hw_product_case_info hpci left join hw_portal_config_type hpct on hpci.config_type_id=hpct.config_type_id + + + + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoDetailMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoDetailMapper.xml new file mode 100644 index 0000000..3562309 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoDetailMapper.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + select product_info_detail_id, parent_id, product_info_id, config_modal, product_info_detail_title, product_info_detail_desc, product_info_detail_order, product_info_detail_pic, ancestors, create_time, create_by, update_time, update_by + from hw_product_info_detail + + + + + + + + insert into hw_product_info_detail + + parent_id, + product_info_id, + config_modal, + product_info_detail_title, + product_info_detail_desc, + product_info_detail_order, + product_info_detail_pic, + ancestors, + create_time, + create_by, + update_time, + update_by, + + + #{parentId}, + #{productInfoId}, + #{configModal}, + #{productInfoDetailTitle}, + #{productInfoDetailDesc}, + #{productInfoDetailOrder}, + #{productInfoDetailPic}, + #{ancestors}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_info_detail + + parent_id = #{parentId}, + product_info_id = #{productInfoId}, + config_modal = #{configModal}, + product_info_detail_title = #{productInfoDetailTitle}, + product_info_detail_desc = #{productInfoDetailDesc}, + product_info_detail_order = #{productInfoDetailOrder}, + product_info_detail_pic = #{productInfoDetailPic}, + ancestors = #{ancestors}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where product_info_detail_id = #{productInfoDetailId} + + + + delete from hw_product_info_detail where product_info_detail_id = #{productInfoDetailId} + + + + delete from hw_product_info_detail where product_info_detail_id in + + #{productInfoDetailId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoMapper.xml new file mode 100644 index 0000000..7254ab4 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwProductInfoMapper.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select product_info_id, config_type_id, tab_flag, config_modal, product_info_etitle, product_info_ctitle, product_info_order, create_time, create_by, update_time, update_by from hw_product_info + + + + + + + + insert into hw_product_info + + config_type_id, + tab_flag, + config_modal, + product_info_etitle, + product_info_ctitle, + product_info_order, + create_time, + create_by, + update_time, + update_by, + + + #{configTypeId}, + #{tabFlag}, + #{configModal}, + #{productInfoEtitle}, + #{productInfoCtitle}, + #{productInfoOrder}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_info + + config_type_id = #{configTypeId}, + tab_flag = #{tabFlag}, + config_modal = #{configModal}, + product_info_etitle = #{productInfoEtitle}, + product_info_ctitle = #{productInfoCtitle}, + product_info_order = #{productInfoOrder}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where product_info_id = #{productInfoId} + + + + delete from hw_product_info where product_info_id = #{productInfoId} + + + + delete from hw_product_info where product_info_id in + + #{productInfoId} + + + + + + + + + + + + + select hpi.product_info_id, hpi.config_type_id, hpi.tab_flag, hpi.config_modal, hpi.product_info_etitle, hpi.product_info_ctitle, hpi.product_info_order, + hpi.create_time, hpi.create_by, hpi.update_time, hpi.update_by,hpct.config_type_name from hw_product_info hpi left join hw_portal_config_type hpct on hpi.config_type_id=hpct.config_type_id + + + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml new file mode 100644 index 0000000..bf6a459 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebDocumentMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebDocumentMapper.xml new file mode 100644 index 0000000..1e4e922 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebDocumentMapper.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + select document_id, tenant_id, document_address, create_time, web_code, secretKey , + json, type, + is_delete + from hw_web_document + + + + + + + + insert into hw_web_document + + document_id, + tenant_id, + document_address, + create_time, + web_code, + secretKey, + json, + type, + is_delete, + + + #{documentId}, + #{tenantId}, + #{documentAddress}, + #{createTime}, + #{webCode}, + #{secretKey}, + #{json}, + #{type}, + + #{isDelete}, + '0', + + + + + + update hw_web_document + + document_id = #{documentId}, + tenant_id = #{tenantId}, + document_address = #{documentAddress}, + create_time = #{createTime}, + web_code = #{webCode}, + + secretKey = + NULL + #{secretKey} + , + + json = #{json}, + type = #{type}, + + where document_id = #{documentId} + + + + update hw_web_document set is_delete = '1' where document_id = #{documentId} + + + + update hw_web_document set is_delete = '1' where document_id in + + #{documentId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper.xml new file mode 100644 index 0000000..c0cba77 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + select web_id, web_json, web_json_string, web_code, + web_json_english, + is_delete + from hw_web + + + + + + + + insert into hw_web + + web_json, + web_json_string, + web_code, + web_json_english, + is_delete, + + + #{webJson}, + #{webJsonString}, + #{webCode}, + #{webJsonEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web + + web_json = #{webJson}, + web_json_string = #{webJsonString}, + + web_json_english = #{webJsonEnglish}, + + where web_code = #{webCode} + + + + update hw_web set is_delete = '1' where web_id = #{webId} + + + + update hw_web set is_delete = '1' where web_id in + + #{webId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper1.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper1.xml new file mode 100644 index 0000000..0c23c9d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMapper1.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + select web_id, web_json, web_json_string, web_code, + device_id, typeId, web_json_english, + is_delete + from hw_web1 + + + + + + + + + + insert into hw_web1 + + web_json, + web_json_string, + web_code, + device_id, + typeId, + web_json_english, + is_delete, + + + #{webJson}, + #{webJsonString}, + #{webCode}, + #{deviceId}, + #{typeId}, + #{webJsonEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web1 + + web_json = #{webJson}, + web_json_string = #{webJsonString}, + + + + web_json_english = #{webJsonEnglish}, + + where web_code = #{webCode} + and device_id = #{deviceId} + and typeId = #{typeId} + + + + update hw_web1 set is_delete = '1' where web_id = #{webId} + + + + update hw_web1 set is_delete = '1' where web_id in + + #{webId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper.xml new file mode 100644 index 0000000..c0e7b37 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + select web_menu_id, parent, ancestors, status, web_menu_name, tenant_id, web_menu__pic, web_menu_type, + order, + web_menu_name_english, + is_delete + from hw_web_menu + + + + + + + + insert into hw_web_menu + + web_menu_id, + parent, + ancestors, + status, + web_menu_name, + tenant_id, + web_menu__pic, + web_menu_type, + `order`, + web_menu_name_english, + is_delete, + + + #{webMenuId}, + #{parent}, + #{ancestors}, + #{status}, + #{webMenuName}, + #{tenantId}, + #{webMenuPic}, + #{webMenuType}, + #{order}, + #{webMenuNameEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web_menu + + parent = #{parent}, + ancestors = #{ancestors}, + status = #{status}, + web_menu_name = #{webMenuName}, + tenant_id = #{tenantId}, + web_menu__pic = #{webMenuPic}, + web_menu_type = #{webMenuType}, + `order` = #{order}, + web_menu_name_english = #{webMenuNameEnglish}, + + where web_menu_id = #{webMenuId} + + + + update hw_web_menu set is_delete = '1' where web_menu_id = #{webMenuId} + + + + update hw_web_menu set is_delete = '1' where web_menu_id in + + #{webMenuId} + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper1.xml b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper1.xml new file mode 100644 index 0000000..0fc78a7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwWebMenuMapper1.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + select web_menu_id, parent, ancestors, status, web_menu_name, tenant_id, web_menu__pic, + value, + web_menu_type, + web_menu_name_english, + is_delete + from hw_web_menu1 + + + + + + + + insert into hw_web_menu1 + + web_menu_id, + parent, + ancestors, + status, + web_menu_name, + tenant_id, + web_menu__pic, + web_menu_type, + value, + web_menu_name_english, + is_delete, + + + #{webMenuId}, + #{parent}, + #{ancestors}, + #{status}, + #{webMenuName}, + #{tenantId}, + #{webMenuPic}, + #{webMenuType}, + #{value}, + #{webMenuNameEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web_menu1 + + parent = #{parent}, + ancestors = #{ancestors}, + status = #{status}, + web_menu_name = #{webMenuName}, + tenant_id = #{tenantId}, + web_menu__pic = #{webMenuPic}, + web_menu_type = #{webMenuType}, + value = #{value}, + web_menu_name_english = #{webMenuNameEnglish}, + + where web_menu_id = #{webMenuId} + + + + update hw_web_menu1 set is_delete = '1' where web_menu_id = #{webMenuId} + + + + update hw_web_menu1 set is_delete = '1' where web_menu_id in + + #{webMenuId} + + + \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs new file mode 100644 index 0000000..2f31a99 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs @@ -0,0 +1,340 @@ +// ============================================================================ +// 【文件说明】Startup.cs - 门户插件启动类 +// ============================================================================ +// 这是 Admin.NET 插件架构的核心启动类,负责注册门户模块的所有服务。 +// +// 【架构定位 - 什么是插件启动类?】 +// 在 Admin.NET/Furion 框架中,插件是一个独立的 .dll 文件,可以被主应用动态加载。 +// 每个插件都需要一个 Startup 类来告诉框架: +// 1. 我(插件)提供了哪些服务 +// 2. 我需要哪些配置 +// 3. 我的中间件应该在什么位置插入 +// +// 【对比 Java Spring Boot】 +// Java Spring Boot 的自动配置: +// @Configuration +// @EnableAutoConfiguration +// public class MyConfig { ... } +// +// 或者使用 spring.factories 文件声明自动配置类。 +// +// Admin.NET 的方式: +// [AppStartup(100)] +// public class Startup : AppStartup { ... } +// +// 两者概念相同:都是在应用启动时执行配置代码。 +// ============================================================================ + +using Admin.NET.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Admin.NET.Plugin.HwPortal; + +/// +/// 门户插件启动类。 +/// +/// 【C# 语法知识点 - AppStartup 特性】 +/// [AppStartup(100)] 是 Furion 框架的特性,标记这是一个插件启动类。 +/// +/// 参数 100 是"启动顺序": +/// - 数值越小,越早执行 +/// - 主应用通常是 0-99,插件可以是 100+ +/// - 这样可以控制依赖关系:先启动核心服务,再启动插件服务 +/// +/// 对比 Java Spring Boot: +// Spring 用 @Order 注解或 Ordered 接口控制顺序: +// @Order(100) +// public class MyConfig { ... } +/// +/// +/// 【C# 语法知识点 - 继承 AppStartup】 +/// public class Startup : AppStartup +/// +/// AppStartup 是 Furion 提供的基类/接口,定义了插件必须实现的方法: +/// - ConfigureServices:注册服务到 DI 容器 +/// - Configure:配置中间件管道 +/// +/// 这是"模板方法模式"的应用:框架定义流程,插件填充具体实现。 +/// +/// +[AppStartup(100)] +public class Startup : AppStartup +{ + /// + /// 配置服务(注册到 DI 容器)。 + /// + /// 【C# 语法知识点 - IServiceCollection】 + /// IServiceCollection 是 ASP.NET Core 的"服务注册表"(依赖注入容器)。 + /// + /// 你可以把它想象成一个"服务工厂": + /// - 在这里注册服务:"如果有人需要 IXxxService,就给他 XxxService 实例" + /// - 框架会在运行时自动解析依赖关系 + /// + /// 对比 Java Spring Boot: + /// Java 用 @Bean 注解或 @ComponentScan 自动扫描: + /// @Configuration + /// public class Config { + /// @Bean + /// public MyService myService() { + /// return new MyService(); + /// } + /// } + /// + /// C# 是显式注册(更可控),Java 是自动扫描(更便利),各有优劣。 + /// + /// + /// 【C# 语法知识点 - 三种服务生命周期】 + /// ASP.NET Core DI 容器支持三种服务生命周期: + /// + /// 1. AddSingleton(单例): + /// - 整个应用只创建一个实例 + /// - 适合:配置类、工具类、缓存 + /// - 线程安全需要考虑 + /// - 类比 Java 的 @Scope("singleton") + /// + /// 2. AddScoped(作用域): + /// - 每个 HTTP 请求创建一个实例 + /// - 适合:数据库上下文、业务服务 + /// - 同一个请求内,多次获取返回同一实例 + /// - 类比 Java 的 @Scope("request") + /// + /// 3. AddTransient(瞬态): + /// - 每次获取都创建新实例 + /// - 适合:轻量级服务、无状态服务 + /// - 类比 Java 的 @Scope("prototype") + /// + /// + /// 服务注册表 + public void ConfigureServices(IServiceCollection services) + { + // 【配置选项注册】 + // AddConfigurableOptions() 是 Furion 的扩展方法, + // 把配置类注册到 DI 容器,并绑定到配置文件(如 appsettings.json)。 + // + // 对比 Java: + // Java Spring Boot 用 @ConfigurationProperties + @EnableConfigurationProperties: + // @ConfigurationProperties(prefix = "hwportal.search") + // public class HwPortalSearchOptions { ... } + services.AddConfigurableOptions(); + + // 【Singleton 生命周期】 + // 整个应用只保留一个 HwPortalMapperRegistry 实例。 + // + // 为什么用 Singleton? + // 1. HwPortalMapperRegistry 内部缓存了 XML Mapper 定义 + // 2. 这些 XML 在应用运行期间不会变化 + // 3. 多个请求共享同一个缓存,节省内存 + // + // 对比 Java: + // Java 单例通常用 @Service(默认就是单例) + services.AddSingleton(); + + // 【Scoped 生命周期】 + // 每个 HTTP 请求创建一个 HwPortalMyBatisExecutor 实例。 + // + // 为什么用 Scoped? + // 1. 执行器依赖数据库客户端(ISqlSugarClient),通常是 Scoped + // 2. 同一个请求内多次执行 SQL,应该共用同一个连接/事务 + // 3. 请求结束后自动释放资源 + // + // 对比 Java: + // Java 通常用 @Repository 或 @Service(默认单例,但 @Autowired 注入的 SqlSession 是 request-scoped) + services.AddScoped(); + + // 搜索文本提取器是无状态对象,也注册为 Scoped。 + services.AddScoped(); + + // 【EF Core 数据库上下文注册】 + // AddDbContext() 注册 Entity Framework Core 的数据库上下文。 + // + // 为什么用 Lambda 表达式? + // 参数是一个委托(函数),在第一次需要 DbContext 时才执行配置。 + // 这叫"延迟初始化",避免应用启动时就建立数据库连接。 + // + // 对比 Java: + // Java Spring Boot 自动配置 DataSource,不需要显式注册: + // spring.datasource.url=jdbc:mysql://... + services.AddDbContext((provider, options) => + { + // 从 DI 容器获取配置选项 + // GetRequiredService():获取服务,如果不存在会抛异常 + // IOptions 是 ASP.NET Core 的配置包装接口 + HwPortalSearchOptions searchOptions = provider.GetRequiredService>().Value; + DbConnectionOptions dbOptions = provider.GetRequiredService>().Value; + + // 解析连接字符串(可能是独立配置,也可能是复用主库) + string connectionString = ResolveSearchConnectionString(searchOptions, dbOptions); + + // 配置 MySQL 连接 + // UseMySql():指定使用 MySQL 数据库提供程序 + // ServerVersion.AutoDetect():自动检测 MySQL 服务器版本 + options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => + { + // 启用失败重试:短暂网络故障时自动重试 + builder.EnableRetryOnFailure(); + }); + }); + + // 【注册搜索相关服务】 + // AddScoped() 是接口到实现的映射。 + // + // 为什么面向接口编程? + // 1. 解耦:调用者依赖接口,不依赖具体实现 + // 2. 可测试:单元测试时可以 Mock 接口 + // 3. 可替换:可以随时替换实现类 + // + // 对比 Java: + // Java Spring 自动根据类型注入,通常不需要显式注册: + // @Service + // public class HwSearchSchemaService implements IHwSearchSchemaService { ... } + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + /// + /// 配置中间件管道。 + /// + /// 【C# 语法知识点 - IApplicationBuilder】 + /// IApplicationBuilder 是 ASP.NET Core 的"中间件管道构建器"。 + /// + /// 什么是中间件? + /// 中间件是处理 HTTP 请求的"管道组件",每个中间件可以选择: + /// 1. 处理请求并返回响应(短路) + /// 2. 处理请求后,传递给下一个中间件 + /// 3. 请求返回后,执行一些后置处理 + /// + /// 中间件执行顺序(洋葱模型): + /// 请求 → 中间件1 → 中间件2 → 控制器 → 中间件2 → 中间件1 → 响应 + /// + /// 对比 Java: + /// Java Spring Boot 用 Filter 或 HandlerInterceptor: + /// @Component + /// public class MyFilter implements Filter { ... } + /// + /// + /// 【当前实现说明】 + /// 这个方法目前为空,因为门户插件不需要额外的中间件。 + /// 如果未来需要添加自定义中间件(如请求日志、权限校验),在这里添加。 + /// + /// + /// 应用构建器 + /// 主机环境信息 + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + // 这里暂时不追加自定义中间件。 + // 如果未来 hw-portal 需要自己的中间件,也是在这里接入。 + // + // 示例:添加一个自定义中间件 + // app.UseMiddleware(); + // + // 示例:添加静态文件服务 + // app.UseStaticFiles(); + } + + /// + /// 解析搜索数据库的连接字符串。 + /// + /// 【业务逻辑说明】 + /// 搜索功能可以: + /// 1. 使用独立的 MySQL 连接(推荐,隔离搜索负载) + /// 2. 复用主库连接(简单,但不适合高并发搜索场景) + /// + /// 这个方法实现"自动回退"逻辑: + /// - 如果配置了独立连接,就用独立连接 + /// - 如果没配置且允许回退,就复用主库 + /// - 如果没配置且不允许回退,抛异常 + /// + /// + /// 【C# 语法知识点 - private static 方法】 + /// private:只能在当前类内部调用 + /// static:不依赖实例状态,可以直接用 Startup.ResolveSearchConnectionString(...) 调用 + /// + /// 为什么用 static? + /// 1. 这个方法不访问任何实例成员(没有用到 this) + /// 2. 标记为 static 可以让编译器优化 + /// 3. 语义清晰:这是一个纯工具方法 + /// + /// + /// 【C# 语法知识点 - 空值检查】 + /// string.IsNullOrWhiteSpace(str) 检查: + /// - null + /// - 空字符串 "" + /// - 纯空白字符串 " " + /// + /// 这是 .NET 提供的最佳实践,比 str == null || str.Trim() == "" 更高效。 + /// + /// 对比 Java: + /// Java 需要: + /// if (str == null || str.trim().isEmpty()) { ... } + /// + /// + /// 搜索配置选项 + /// 数据库连接配置 + /// 解析后的连接字符串 + /// 配置无效时抛出 + private static string ResolveSearchConnectionString(HwPortalSearchOptions searchOptions, DbConnectionOptions dbOptions) + { + // 【第一优先级】如果配置了独立的搜索连接串,直接使用 + if (!string.IsNullOrWhiteSpace(searchOptions.ConnectionString)) + { + // .Trim() 去除首尾空格,避免配置错误 + return searchOptions.ConnectionString.Trim(); + } + + // 【检查回退策略】 + // 如果不允许回退到主库连接,直接抛异常 + // 这是"防御性编程":明确失败比隐式行为更安全 + if (!searchOptions.UseMainDbConnectionWhenEmpty) + { + throw new InvalidOperationException("HwPortalSearch.ConnectionString 未配置,且已关闭主库回退。"); + } + + // 【第二优先级】复用主库连接 + // FirstOrDefault() 是 LINQ 方法,返回第一个元素或默认值(null) + // + // 对比 Java: + // Java Stream:dbOptions.getConnectionConfigs().stream().findFirst().orElse(null); + DbConnectionConfig mainConfig = dbOptions?.ConnectionConfigs?.FirstOrDefault(); + + // 检查主库连接是否存在且有效 + if (mainConfig == null || string.IsNullOrWhiteSpace(mainConfig.ConnectionString)) + { + throw new InvalidOperationException("未找到可复用的主库连接串,请在 Search.json 中配置 HwPortalSearch.ConnectionString。"); + } + + // 【数据库类型检查】 + // 当前搜索功能只支持 MySQL,如果主库是 SQL Server/PostgreSQL 等,不能复用 + // 这是业务约束,提前检查可以避免运行时的奇怪错误 + if (mainConfig.DbType != SqlSugar.DbType.MySql && mainConfig.DbType != SqlSugar.DbType.MySqlConnector) + { + throw new InvalidOperationException("HwPortalSearch 默认只支持复用 MySQL 主库连接,请单独配置 HwPortalSearch.ConnectionString。"); + } + + // 【获取连接字符串】 + string connectionString = mainConfig.ConnectionString; + + // 【加密处理】 + // 如果主库连接串是加密的(生产环境推荐),需要解密 + // DbSettings?.EnableConnStringEncrypt 是"可空链式调用": + // - 如果 DbSettings 为 null,不会抛异常,整个表达式为 null + // - 这是 C# 的"空条件运算符"?. 语法 + // + // 对比 Java: + // Java 需要: + // if (mainConfig.getDbSettings() != null && mainConfig.getDbSettings().getEnableConnStringEncrypt()) { ... } + if (mainConfig.DbSettings?.EnableConnStringEncrypt == true) + { + // 调用框架提供的解密工具 + connectionString = CryptogramUtil.Decrypt(connectionString); + } + + return connectionString; + } +} diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Admin.NET.Plugin.K3Cloud.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Admin.NET.Plugin.K3Cloud.csproj new file mode 100644 index 0000000..3b49690 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Admin.NET.Plugin.K3Cloud.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Configuration/K3Cloud.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Configuration/K3Cloud.json new file mode 100644 index 0000000..4a7b4d0 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Configuration/K3Cloud.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "K3Cloud": { + // ERP地址 + "Url": "http://127.0.0.1/k3cloud/", + // 帐套Id(数据中心ID) + "AcctID": "XXXXXXXXX", + // 应用Id + "AppId": "XXXXXXXX", + // 应用密钥 + "AppKey": "XXX", + // 用户名称 + "UserName": "XXX", + // 用户密码 + "UserPassword": "XXXX@2024", + // 语言代码 + "LanguageCode": "2052" + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/GlobalUsings.cs new file mode 100644 index 0000000..27be574 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/GlobalUsings.cs @@ -0,0 +1,9 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Furion; +global using Furion.ConfigurableOptions; +global using Furion.HttpRemote; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Option/K3CloudOptions.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Option/K3CloudOptions.cs new file mode 100644 index 0000000..134c936 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Option/K3CloudOptions.cs @@ -0,0 +1,45 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud; + +public sealed class K3CloudOptions : IConfigurableOptions +{ + /// + /// ERP业务站点地址 + /// + public string Url { get; set; } + + /// + /// 帐套Id(数据中心ID) + /// + public string AcctID { get; set; } + + /// + /// 应用Id + /// + public string AppId { get; set; } + + /// + /// 应用密钥 + /// + public string AppKey { get; set; } + + /// + /// 用户名称 + /// + public string UserName { get; set; } + + /// + /// 用户密码 + /// + public string UserPassword { get; set; } + + /// + /// 语言代码 + /// + public string LanguageCode { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudBaeInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudBaeInput.cs new file mode 100644 index 0000000..28ae79d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudBaeInput.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud.Service; + +/// +/// ERP基础入参 +/// +public class K3CloudBaeInput +{ + /// + /// 表单Id + /// + public string formid { get; set; } + + /// + /// 数据包 + /// + public T data { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginInput.cs new file mode 100644 index 0000000..6579cb1 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginInput.cs @@ -0,0 +1,12 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud.Service; + +public class K3CloudLoginInput +{ + public List parameters { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginOutput.cs new file mode 100644 index 0000000..89c2963 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudLoginOutput.cs @@ -0,0 +1,62 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud.Service; + +public class K3CloudLoginOutput +{ + public string Message { get; set; } + public string MessageCode { get; set; } + public ErpLoginResultTypeEnum LoginResultType { get; set; } +} + +public enum ErpLoginResultTypeEnum +{ + /// + /// 激活 + /// + Activation = -7, + + /// + /// 云通行证未绑定Cloud账号 + /// + EntryCloudUnBind = -6, + + /// + /// 需要表单处理 + /// + DealWithForm = -5, + + /// + /// 登录警告 + /// + Wanning = -4, + + /// + /// 密码验证不通过(强制的) + /// + PWInvalid_Required = -3, + + /// + /// 密码验证不通过(可选的) + /// + PWInvalid_Optional = -2, + + /// + /// 登录失败 + /// + Failure = -1, + + /// + /// 用户或密码错误 + /// + PWError = 0, + + /// + /// 登录成功 + /// + Success = 1 +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudPushResultOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudPushResultOutput.cs new file mode 100644 index 0000000..d0ede43 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/Dto/K3CloudPushResultOutput.cs @@ -0,0 +1,66 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud.Service; + +public class K3CloudPushResultOutput +{ + public ErpPushResultInfo Result { get; set; } +} + +public class ErpPushResultInfo +{ + /// + /// Id + /// + public object? Id { get; set; } + + /// + /// 编码 + /// + public string? Number { get; set; } + + public ErpPushResultInfo_ResponseStatus ResponseStatus { get; set; } +} + +public class ErpPushResultInfo_ResponseStatus +{ + public bool IsSuccess { get; set; } + public int? ErrorCode { get; set; } + + /// + /// 错误代码MsgCode说明 + ///0:默认 + ///1:上下文丢失 会话过期 + ///2:没有权限 + ///3:操作标识为空 + ///4:异常 + ///5:单据标识为空 + ///6:数据库操作失败 + ///7:许可错误 + ///8:参数错误 + ///9:指定字段/值不存在 + ///10:未找到对应数据 + ///11:验证失败 + ///12:不可操作 + ///13:网控冲突 + ///14:调用限制 + ///15:禁止管理员登录 + /// + public int? MsgCode { get; set; } + + /// + /// 如果失败,具体失败原因 + /// + public List Errors { get; set; } +} + +public class ErpPushResultInfo_Errors +{ + public string FieldName { get; set; } + public string Message { get; set; } + public int DIndex { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/IK3CloudApi.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/IK3CloudApi.cs new file mode 100644 index 0000000..15e746f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Service/IK3CloudApi.cs @@ -0,0 +1,50 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.K3Cloud.Service; + +/// +/// 金蝶云星空ERP接口 +/// +[HttpClientName("K3Cloud")] +public interface IK3CloudApi : IHttpDeclarative +{ + /// + /// 验证用户 + /// + /// + /// + /// + [Post("Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc")] + Task ValidateUser([Body] K3CloudLoginInput input, Action action = default); + + /// + /// 保存表单 + /// + /// + /// + /// + [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc")] + Task Save([Body] K3CloudBaeInput input, Action action = default); + + /// + /// 提交表单 + /// + /// + /// + /// + [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Submit.common.kdsvc")] + Task Submit([Body] K3CloudBaeInput input, Action action = default); + + /// + /// 审核表单 + /// + /// + /// + /// + [Post("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Audit.common.kdsvc")] + Task Audit([Body] K3CloudBaeInput input, Action action = default); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs new file mode 100644 index 0000000..b533341 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs @@ -0,0 +1,29 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Plugin.K3Cloud; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddConfigurableOptions(); + + services.AddHttpClient("K3Cloud", client => + { + client.BaseAddress = new Uri(App.GetConfig("K3Cloud", true).Url); + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj new file mode 100644 index 0000000..b66374b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Admin.NET.Plugin.ReZero.csproj @@ -0,0 +1,36 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + + + + + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Configuration/ReZero.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Configuration/ReZero.json new file mode 100644 index 0000000..4a32c2b --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Configuration/ReZero.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "[openapi:ReZero]": { + "Group": "ReZero", + "Title": "ReZero", + "Description": "全网唯一并且免费的运行时界面创建API接口的项目,并且生成接口文档,真正的运时行创建低代码、线上建表、线上建接口、线上生成接口文档、线上测试接口、热插拔、超级API。", + "Version": "1.0.0", + "Order": 80 + }, + "ReZero": { + "AccessTokenKey": "admin.net:access-token", + "RefreshAccessTokenKey": "admin.net:x-access-token" + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/GlobalUsings.cs new file mode 100644 index 0000000..80451d5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/GlobalUsings.cs @@ -0,0 +1,8 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Furion; +global using Furion.ConfigurableOptions; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Option/ReZeroOptions.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Option/ReZeroOptions.cs new file mode 100644 index 0000000..41352f6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Option/ReZeroOptions.cs @@ -0,0 +1,20 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.ReZero; + +public sealed class ReZeroOptions : IConfigurableOptions +{ + /// + /// AccessTokenKey + /// + public string AccessTokenKey { get; set; } + + /// + /// RefreshAccessTokenKey + /// + public string RefreshAccessTokenKey { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/SeedData/SysMenuSeedData.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/SeedData/SysMenuSeedData.cs new file mode 100644 index 0000000..1f5e4d5 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/SeedData/SysMenuSeedData.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; + +namespace Admin.NET.Plugin.ReZero; + +/// +/// 超级API菜单表种子数据 +/// +public class SysMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysMenu{ Id=1310500010101, Pid=1300500000101, Title="超级API", Path="/develop/reZero", Name="sysReZero", Component="Layout", Icon="ele-MagicStick", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1310500020101, Pid=1310500010101, Title="动态接口", Path="/develop/reZero/dynamicApi", Name="sysReZeroDynamicApi", Component="layout/routerView/iframe", Icon="ele-Menu", Type=MenuTypeEnum.Menu, IsIframe=true, OutLink="http://localhost:5005/rezero/dynamic_interface.html?model=small", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1310500030101, Pid=1310500010101, Title="数据库管理", Path="/develop/reZero/database", Name="sysReZeroDatabase", Component="layout/routerView/iframe", Icon="ele-Menu", Type=MenuTypeEnum.Menu, IsIframe=true, OutLink="http://localhost:5005/rezero/database_manager.html?model=small", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1310500040101, Pid=1310500010101, Title="实体表管理", Path="/develop/reZero/entity", Name="sysReZeroEntity", Component="layout/routerView/iframe", Icon="ele-Menu", Type=MenuTypeEnum.Menu, IsIframe=true, OutLink="http://localhost:5005/rezero/entity_manager.html?model=small", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1310500050101, Pid=1310500010101, Title="接口分类", Path="/develop/reZero/apiCategory", Name="sysReZeroApiCategory", Component="layout/routerView/iframe", Icon="ele-Menu", Type=MenuTypeEnum.Menu, IsIframe=true, OutLink="http://localhost:5005/rezero/interface_categroy.html?model=small", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1310500060101, Pid=1310500010101, Title="接口定义", Path="/develop/reZero/apiDefine", Name="sysReZeroApiDefine", Component="layout/routerView/iframe", Icon="ele-Menu", Type=MenuTypeEnum.Menu, IsIframe=true, OutLink="http://localhost:5005/rezero/interface_manager.html?model=small", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Service/SuperApiAop.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Service/SuperApiAop.cs new file mode 100644 index 0000000..df6386a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Service/SuperApiAop.cs @@ -0,0 +1,110 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Furion.DataEncryption; +using Furion.FriendlyException; +using Furion.JsonSerialization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using ReZero.SuperAPI; + +namespace Admin.NET.Plugin.ReZero.Service; + +/// +/// 超级API接口拦截器 +/// +public class SuperApiAop : DefaultSuperApiAop +{ + public override async Task OnExecutingAsync(InterfaceContext aopContext) + { + //if (aopContext.InterfaceType == InterfaceType.DynamicApi) + //{ + var authenticateResult = await aopContext.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded) + throw Oops.Oh("没权限 Unauthorized"); + //} + + var accessToken = aopContext.HttpContext.Request.Headers["Authorization"].ToString(); + var (isValid, tokenData, validationResult) = JWTEncryption.Validate(accessToken.Replace("Bearer ", "")); + if (!isValid) + throw Oops.Oh("Token 无效"); + + await base.OnExecutingAsync(aopContext); + } + + public override async Task OnExecutedAsync(InterfaceContext aopContext) + { + InitLogContext(aopContext, LogLevel.Information); + + await base.OnExecutedAsync(aopContext); + } + + public override async Task OnErrorAsync(InterfaceContext aopContext) + { + InitLogContext(aopContext, LogLevel.Error); + + await base.OnErrorAsync(aopContext); + } + + /// + /// 保存超级API接口日志 + /// + /// + /// + private void InitLogContext(InterfaceContext aopContext, LogLevel logLevel) + { + var api = aopContext.InterfaceInfo; + var context = aopContext.HttpContext; + + var accessToken = context.Request.Headers["Authorization"].ToString(); + if (!string.IsNullOrWhiteSpace(accessToken) && accessToken.StartsWith("Bearer ")) + accessToken = accessToken.Replace("Bearer ", ""); + var claims = JWTEncryption.ReadJwtToken(accessToken)?.Claims; + var userName = claims?.FirstOrDefault(u => u.Type == ClaimConst.Account)?.Value; + var realName = claims?.FirstOrDefault(u => u.Type == ClaimConst.RealName)?.Value; + + var paths = api.Url.Split('/'); + var actionName = paths[paths.Length - 1]; + + var apiInfo = new + { + requestUrl = api.Url, + httpMethod = api.HttpMethod, + displayTitle = api.Name, + actionTypeName = actionName, + controllerName = aopContext.InterfaceType == InterfaceType.DynamicApi ? $"ReZero动态-{api.GroupName}" : $"ReZero系统-{api.GroupName}", + remoteIPv4 = context.GetRemoteIpAddressToIPv4(), + userAgent = context.Request.Headers["User-Agent"], + returnInformation = new + { + httpStatusCode = context.Response.StatusCode, + }, + authorizationClaims = new[] + { + new + { + type = ClaimConst.Account, + value = userName + }, + new + { + type = ClaimConst.RealName, + value = realName + }, + }, + exception = aopContext.Exception == null ? null : JSON.Serialize(aopContext.Exception) + }; + + var logger = App.GetRequiredService().CreateLogger(CommonConst.SysLogCategoryName); + using var scope = logger.ScopeContext(new Dictionary { + { "loggingMonitor", apiInfo.ToJson() } + }); + logger.Log(logLevel, "ReZero超级API接口日志"); + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Startup.cs new file mode 100644 index 0000000..c6cb664 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.ReZero/Startup.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Core; +using Admin.NET.Plugin.ReZero.Service; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using ReZero; +using ReZero.SuperAPI; + +namespace Admin.NET.Plugin.ReZero; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + var reZeroOpt = App.GetConfig("ReZero", true); + + // 获取默认数据库配置(第一个) + var dbOptions = App.GetConfig("DbConnection", true); + var superAPIOption = new SuperAPIOptions() + { + DatabaseOptions = new DatabaseOptions() + { + ConnectionConfig = new SuperAPIConnectionConfig() + { + DbType = dbOptions.ConnectionConfigs[0].DbType, + ConnectionString = dbOptions.ConnectionConfigs[0].ConnectionString + } + }, + UiOptions = new UiOptions() { DefaultIndexSource = "/index.html" }, + InterfaceOptions = new InterfaceOptions() + { + AuthorizationLocalStorageName = reZeroOpt.AccessTokenKey, // 浏览器本地存储LocalStorage存储Token的键名 + SuperApiAop = new SuperApiAop() // 超级API拦截器 + } + }; + + // 注册超级API + services.AddReZeroServices(api => + { + api.EnableSuperApi(superAPIOption); + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Admin.NET.Plugin.WorkWeixin.csproj b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Admin.NET.Plugin.WorkWeixin.csproj new file mode 100644 index 0000000..8d66519 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Admin.NET.Plugin.WorkWeixin.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net10.0 + 1701;1702;1591;8632 + enable + disable + True + Admin.NET + Admin.NET 通用权限开发平台 + + + + + PreserveNewest + + + + + + + + diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Configuration/WorkWeixin.json b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Configuration/WorkWeixin.json new file mode 100644 index 0000000..349015f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Configuration/WorkWeixin.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "WorkWeixin": { + "CorpId": "xxxx", // 企业ID + "CorpSecret": "xxxx" // 企业微信凭证密钥 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Const/WorkWeixinConst.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Const/WorkWeixinConst.cs new file mode 100644 index 0000000..b5a1858 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Const/WorkWeixinConst.cs @@ -0,0 +1,15 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Const; + +public class WorkWeixinConst +{ + /// + /// API分组名称 + /// + public const string GroupName = "WorkWeixin"; +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/GlobalUsings.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/GlobalUsings.cs new file mode 100644 index 0000000..a833f67 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/GlobalUsings.cs @@ -0,0 +1,11 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +global using Furion; +global using Furion.HttpRemote; +global using Newtonsoft.Json; +global using System.ComponentModel.DataAnnotations; +global using System.Text.Json.Serialization; \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Option/WorkWeixinOptions.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Option/WorkWeixinOptions.cs new file mode 100644 index 0000000..6c1008d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Option/WorkWeixinOptions.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Furion.ConfigurableOptions; + +namespace Admin.NET.Plugin.WorkWeixin.Option; + +/// +/// 企业微信配置项 +/// +public class WorkWeixinOptions : IConfigurableOptions +{ + /// + /// 企业ID + /// + public string CorpId { get; set; } + + /// + /// 企业微信凭证密钥 + /// + public string CorpSecret { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpInput.cs new file mode 100644 index 0000000..e664938 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpInput.cs @@ -0,0 +1,425 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 创建群聊会话输入参数 +/// +public class CreatAppChatInput +{ + /// + /// 群名称 + /// + [JsonProperty("name")] + [JsonPropertyName("name")] + [Required(ErrorMessage = "群名称不能为空"), MaxLength(50, ErrorMessage = "群名称最多不能超过50个字符")] + public string Name { get; set; } + + /// + /// 群主Id + /// + [JsonProperty("owner")] + [JsonPropertyName("owner")] + [Required(ErrorMessage = "群主Id不能为空")] + public string Owner { get; set; } + + /// + /// 群成员Id列表 + /// + [JsonProperty("userlist")] + [JsonPropertyName("userlist")] + [Core.NotEmpty(ErrorMessage = "群成员列表不能为空")] + public List UserList { get; set; } + + /// + /// 群Id + /// + [JsonProperty("chatid")] + [JsonPropertyName("chatid")] + [Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")] + public string ChatId { get; set; } +} + +/// +/// 修改群聊会话输入参数 +/// +public class UpdateAppChatInput +{ + /// + /// 群Id + /// + [JsonProperty("chatid")] + [JsonPropertyName("chatid")] + [Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")] + public string ChatId { get; set; } + + /// + /// 群名称 + /// + [JsonProperty("name")] + [JsonPropertyName("name")] + [Required(ErrorMessage = "群名称不能为空"), MaxLength(50, ErrorMessage = "群名称最多不能超过50个字符")] + public string Name { get; set; } + + /// + /// 群主Id + /// + [JsonProperty("owner")] + [JsonPropertyName("owner")] + [Required(ErrorMessage = "群主Id不能为空")] + public string Owner { get; set; } + + /// + /// 添加成员的id列表 + /// + [JsonProperty("add_user_list")] + [JsonPropertyName("add_user_list")] + public List AddUserList { get; set; } + + /// + /// 踢出成员的id列表 + /// + [JsonProperty("del_user_list")] + [JsonPropertyName("del_user_list")] + public List DelUserList { get; set; } +} + +/// +/// 应用消息推送输入基类参数 +/// +public class SendBaseAppChatInput +{ + /// + /// 群Id + /// + [JsonProperty("chatid")] + [JsonPropertyName("chatid")] + [Required(ErrorMessage = "群Id不能为空"), MaxLength(32, ErrorMessage = "群Id最多不能超过32个字符")] + public string ChatId { get; set; } + + /// + /// 消息类型 + /// + /// text:文本消息 + /// image:图片消息 + /// voice:图片消息 + /// video:视频消息 + /// file:文件消息 + /// textcard:文本卡片 + /// news:图文消息 + /// mpnews:图文消息(存储在企业微信) + /// markdown:markdown消息 + [JsonProperty("msgtype")] + [JsonPropertyName("msgtype")] + [Required(ErrorMessage = "消息类型不能为空")] + protected string MsgType { get; set; } + + /// + /// 是否是保密消息 + /// + [JsonProperty("safe")] + [JsonPropertyName("safe")] + [Required(ErrorMessage = "消息类型不能为空")] + public int Safe { get; set; } + + public SendBaseAppChatInput(string chatId, string msgType, bool safe = false) + { + ChatId = chatId; + MsgType = msgType; + Safe = safe ? 1 : 0; + } +} + +/// +/// 推送文本消息输入参数 +/// +public class SendTextAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("text")] + [JsonPropertyName("text")] + public object Text { get; set; } + + /// + /// 文本消息 + /// + /// + /// + /// + public SendTextAppChatInput(string chatId, string content, bool safe = false) : base(chatId, "text", safe) + { + Text = new { content }; + } +} + +/// +/// 推送图片消息输入参数 +/// +public class SendImageAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("image")] + [JsonPropertyName("image")] + public object Image { get; set; } + + /// + /// 图片消息 + /// + /// + /// + /// + public SendImageAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "image", safe) + { + Image = new { media_id = mediaId }; + } +} + +/// +/// 推送语音消息输入参数 +/// +public class SendVoiceAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("voice")] + [JsonPropertyName("voice")] + public object Voice { get; set; } + + /// + /// 语音消息 + /// + /// + /// + /// + public SendVoiceAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "voice", safe) + { + Voice = new { media_id = mediaId }; + } +} + +/// +/// 推送视频消息输入参数 +/// +public class SendVideoAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("video")] + [JsonPropertyName("video")] + public object Video { get; set; } + + /// + /// 视频消息 + /// + /// + /// + /// + /// + /// + public SendVideoAppChatInput(string chatId, string title, string description, string mediaId, bool safe = false) : base(chatId, "video", safe) + { + Video = new + { + media_id = mediaId, + description, + title + }; + } +} + +/// +/// 推送视频消息输入参数 +/// +public class SendFileAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("file")] + [JsonPropertyName("file")] + public object File { get; set; } + + /// + /// 文件消息 + /// + /// + /// + /// + public SendFileAppChatInput(string chatId, string mediaId, bool safe = false) : base(chatId, "video", safe) + { + File = new { media_id = mediaId }; + } +} + +/// +/// 推送文本卡片消息输入参数 +/// +public class SendTextCardAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("textcard")] + [JsonPropertyName("textcard")] + public object TextCard { get; set; } + + /// + /// 文本卡片消息 + /// + /// + /// 标题 + /// 描述 + /// 点击后跳转的链接 + /// 按钮文字 + /// + public SendTextCardAppChatInput(string chatId, string title, string description, string url, string btnTxt, bool safe = false) : base(chatId, "textcard", safe) + { + TextCard = new + { + title, + description, + url, + btntxt = btnTxt + }; + } +} + +/// +/// 图文消息项 +/// +public class SendNewsItem +{ + /// + /// 标题 + /// + [JsonProperty("title")] + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// 描述 + /// + [JsonProperty("description")] + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// 描述 + /// + [JsonProperty("url")] + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// 图文消息的图片链接(推荐大图1068 * 455,小图150 * 150) + /// + [JsonProperty("picurl")] + [JsonPropertyName("picurl")] + public string PicUrl { get; set; } +} + +/// +/// 推送图文消息输入参数 +/// +public class SendNewsAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("news")] + [JsonPropertyName("news")] + public object News { get; set; } + + /// + /// 图文消息 + /// + /// + /// 图文消息列表 + /// + public SendNewsAppChatInput(string chatId, List newsList, bool safe = false) : base(chatId, "news", safe) + { + News = new { articles = newsList }; + } +} + +/// +/// 图文消息项 +/// +public class SendMpNewsItem +{ + /// + /// 标题 + /// + [JsonProperty("title")] + [JsonPropertyName("title")] + public string Title { get; set; } + + /// + /// 缩略图media_id + /// + [JsonProperty("thumb_media_id")] + [JsonPropertyName("thumb_media_id")] + public string ThumbMediaId { get; set; } + + /// + /// 作者 + /// + [JsonProperty("author")] + [JsonPropertyName("author")] + public string Author { get; set; } + + /// + /// 点击“阅读原文”之后的页面链接 + /// + [JsonProperty("content_source_url")] + [JsonPropertyName("content_source_url")] + public string ContentSourceUrl { get; set; } + + /// + /// 图文消息的内容 + /// + [JsonProperty("content")] + [JsonPropertyName("content")] + public string Content { get; set; } + + /// + /// 图文消息的描述 + /// + [JsonProperty("digest")] + [JsonPropertyName("digest")] + public string Digest { get; set; } +} + +/// +/// 推送图文消息(存储在企业微信)输入参数 +/// +public class SendMpNewsAppChatInput : SendBaseAppChatInput +{ + /// + /// 消息内容 + /// + [JsonProperty("mpnews")] + [JsonPropertyName("mpnews")] + public object MpNews { get; set; } + + /// + /// 图文消息 + /// + /// + /// 图文消息列表 + /// + public SendMpNewsAppChatInput(string chatId, List mpNewsList, bool safe = false) : base(chatId, "mpnews", safe) + { + MpNews = new { articles = mpNewsList }; + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpOutput.cs new file mode 100644 index 0000000..888912f --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/Dto/AppChatHttpOutput.cs @@ -0,0 +1,17 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +public class CreatAppChatOutput : BaseWorkOutput +{ + /// + /// 群聊的唯一标志 + /// + [JsonProperty("chatid")] + [JsonPropertyName("chatid")] + public string ChatId { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/IWorkWeixinAppChatHttp.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/IWorkWeixinAppChatHttp.cs new file mode 100644 index 0000000..2a8256d --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/AppChat/IWorkWeixinAppChatHttp.cs @@ -0,0 +1,53 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat; + +/// +/// 群聊会话远程调用服务 +/// +public interface IWorkWeixinAppChatHttp : IHttpDeclarative +{ + /// + /// 创建群聊会话 + /// + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/create")] + Task Create([Query("access_token")] string accessToken, [Body] CreatAppChatInput body); + + /// + /// 修改群聊会话 + /// + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/update")] + Task Update([Query("access_token")] string accessToken, [Body] UpdateAppChatInput body); + + /// + /// 获取群聊会话 + /// + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/appchat/get")] + Task Get([Query("access_token")] string accessToken, [Query("chatid")] string chatId); + + /// + /// 应用推送消息 + /// + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/appchat/send")] + Task Send([Query("access_token")] string accessToken, [Body] SendBaseAppChatInput body); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/Dto/WorkWeixinAuthHttpOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/Dto/WorkWeixinAuthHttpOutput.cs new file mode 100644 index 0000000..3485140 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/Dto/WorkWeixinAuthHttpOutput.cs @@ -0,0 +1,24 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +public class AuthAccessTokenHttpOutput : BaseWorkOutput +{ + /// + /// 获取到的凭证 + /// + [JsonProperty("access_token")] + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + /// + /// 凭证的有效时间(秒) + /// + [JsonProperty("expires_in")] + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/IWorkWeixinAuthHttp.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/IWorkWeixinAuthHttp.cs new file mode 100644 index 0000000..118a08e --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Auth/IWorkWeixinAuthHttp.cs @@ -0,0 +1,23 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat; + +/// +/// 授权会话远程服务 +/// +public interface IWorkWeixinAuthHttp : IHttpDeclarative +{ + /// + /// 获取接口凭证 + /// + /// 企业ID + /// 应用的凭证密钥 + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/gettoken")] + Task GetToken([Query("corpid")] string corpId, [Query("corpsecret")] string corpSecret); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpInput.cs new file mode 100644 index 0000000..ce87de7 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpInput.cs @@ -0,0 +1,48 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 创建部门输入参数 +/// +public class DepartmentHttpInput +{ + /// + /// 部门名称 + /// + [JsonProperty("id")] + [JsonPropertyName("id")] + public long? Id { get; set; } + + /// + /// 父部门id + /// + [JsonProperty("parentid")] + [JsonPropertyName("parentid")] + public long? ParentId { get; set; } + + /// + /// 部门名称 + /// + [JsonProperty("name")] + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// 英文名称 + /// + [JsonProperty("name_en")] + [JsonPropertyName("name_en")] + public string NameEn { get; set; } + + /// + /// 序号 + /// + [JsonProperty("order")] + [JsonPropertyName("order")] + public int? Order { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpOutput.cs new file mode 100644 index 0000000..151e93c --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/Dto/DepartmentHttpOutput.cs @@ -0,0 +1,95 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 部门Id列表输出参数 +/// +public class DepartmentIdOutput : BaseWorkOutput +{ + /// + /// id + /// + [JsonProperty("department_id")] + [JsonPropertyName("department_id")] + public List DepartmentList { get; set; } +} + +/// +/// 部门Id输出参数 +/// +public class DepartmentItemOutput +{ + /// + /// 部门名称 + /// + [JsonProperty("id")] + [JsonPropertyName("id")] + public long? Id { get; set; } + + /// + /// 父部门id + /// + [JsonProperty("parentid")] + [JsonPropertyName("parentid")] + public long? ParentId { get; set; } + + /// + /// 序号 + /// + [JsonProperty("order")] + [JsonPropertyName("order")] + public int? Order { get; set; } +} + +/// +/// 部门输出参数 +/// +public class DepartmentOutput +{ + /// + /// 部门名称 + /// + [JsonProperty("id")] + [JsonPropertyName("id")] + public long? Id { get; set; } + + /// + /// 父部门id + /// + [JsonProperty("parentid")] + [JsonPropertyName("parentid")] + public long? ParentId { get; set; } + + /// + /// 部门名称 + /// + [JsonProperty("name")] + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// 英文名称 + /// + [JsonProperty("name_en")] + [JsonPropertyName("name_en")] + public string NameEn { get; set; } + + /// + /// 部门负责人列表 + /// + [JsonProperty("department_leader")] + [JsonPropertyName("department_leader")] + public List Leaders { get; set; } + + /// + /// 序号 + /// + [JsonProperty("order")] + [JsonPropertyName("order")] + public int? Order { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/IDepartmentHttp.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/IDepartmentHttp.cs new file mode 100644 index 0000000..72172e6 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Department/IDepartmentHttp.cs @@ -0,0 +1,63 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy.AppChat; + +/// +/// 部门远程调用服务 +/// +public interface IDepartmentHttp : IHttpDeclarative +{ + /// + /// 创建部门 + /// https://developer.work.weixin.qq.com/document/path/90205 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/department/create")] + Task Create([Query("access_token")] string accessToken, [Body] DepartmentHttpInput body); + + /// + /// 修改部门 + /// https://developer.work.weixin.qq.com/document/path/90206 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/department/update")] + Task Update([Query("access_token")] string accessToken, [Body] DepartmentHttpInput body); + + /// + /// 删除部门 + /// https://developer.work.weixin.qq.com/document/path/90207 + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/department/delete")] + Task Delete([Query("access_token")] string accessToken, [Query] long id); + + /// + /// 获取部门Id列表 + /// https://developer.work.weixin.qq.com/document/path/90208 + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/department/simplelist")] + Task SimpleList([Query("access_token")] string accessToken, [Query] long id); + + /// + /// 获取部门详情 + /// https://developer.work.weixin.qq.com/document/path/90208 + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/department/get")] + Task Get([Query("access_token")] string accessToken, [Query] long id); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpInput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpInput.cs new file mode 100644 index 0000000..81e0add --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpInput.cs @@ -0,0 +1,54 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 标签输入参数 +/// +public class TagHttpInput +{ + /// + /// 标签id + /// + [JsonProperty("tagid")] + [JsonPropertyName("tagid")] + public long? TagId { get; set; } + + /// + /// 标签名称 + /// + [JsonProperty("tagname")] + [JsonPropertyName("tagname")] + public string TagName { get; set; } +} + +/// +/// 增加标签成员输入参数 +/// +public class TagUsersTagInput +{ + /// + /// 标签id + /// + [JsonProperty("tagid")] + [JsonPropertyName("tagid")] + public long TagId { get; set; } + + /// + /// 企业成员ID列表 + /// + [JsonProperty("userlist")] + [JsonPropertyName("userlist")] + public List UserList { get; set; } + + /// + /// 企业部门ID列表 + /// + [JsonProperty("partylist")] + [JsonPropertyName("partylist")] + public List PartyList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpOutput.cs new file mode 100644 index 0000000..30c0ae2 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/Dto/TagHttpOutput.cs @@ -0,0 +1,33 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 新增标签输出参数 +/// +public class TagIdHttpOutput : BaseWorkOutput +{ + /// + /// 标签Id + /// + [JsonProperty("tagid")] + [JsonPropertyName("tagid")] + public long? TagId { get; set; } +} + +/// +/// 标签列表输出参数 +/// +public class TagListHttpOutput : BaseWorkOutput +{ + /// + /// 标签Id + /// + [JsonProperty("taglist")] + [JsonPropertyName("taglist")] + public List TagList { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/ITagHttp.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/ITagHttp.cs new file mode 100644 index 0000000..c72b501 --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/Tag/ITagHttp.cs @@ -0,0 +1,82 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin.Proxy; + +/// +/// 标签远程调用服务 +/// +public interface ITagHttp : IHttpDeclarative +{ + /// + /// 创建标签 + /// https://developer.work.weixin.qq.com/document/path/90210 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/tag/create")] + Task Create([Query("access_token")] string accessToken, [Body] TagHttpInput body); + + /// + /// 更新标签名字 + /// https://developer.work.weixin.qq.com/document/path/90211 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/tag/update")] + Task Update([Query("access_token")] string accessToken, [Body] TagHttpInput body); + + /// + /// 删除标签 + /// https://developer.work.weixin.qq.com/document/path/90212 + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/tag/delete")] + Task Delete([Query("access_token")] string accessToken, [Query("tagid")] long tagId); + + /// + /// 获取标签详情 + /// https://developer.work.weixin.qq.com/document/path/90213 + /// + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/tag/get")] + Task Get([Query("access_token")] string accessToken, [Query("tagid")] long tagId); + + /// + /// 增加标签成员 + /// https://developer.work.weixin.qq.com/document/path/90214 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers")] + Task AddTagUsers([Query("access_token")] string accessToken, [Body] TagUsersTagInput body); + + /// + /// 删除标签成员 + /// https://developer.work.weixin.qq.com/document/path/90215 + /// + /// + /// + /// + [Post("https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers")] + Task DelTagUsers([Query("access_token")] string accessToken, [Body] TagUsersTagInput body); + + /// + /// 获取标签列表 + /// https://developer.work.weixin.qq.com/document/path/90216 + /// + /// + /// + [Get("https://qyapi.weixin.qq.com/cgi-bin/tag/list")] + Task List([Query("access_token")] string accessToken); +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Startup.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Startup.cs new file mode 100644 index 0000000..7b6a39a --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Startup.cs @@ -0,0 +1,25 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +using Admin.NET.Plugin.WorkWeixin.Option; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Admin.NET.Plugin.WorkWeixin; + +[AppStartup(100)] +public class Startup : AppStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddConfigurableOptions(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } +} \ No newline at end of file diff --git a/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Utils/BaseHttpOutput.cs b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Utils/BaseHttpOutput.cs new file mode 100644 index 0000000..8ab0cac --- /dev/null +++ b/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Utils/BaseHttpOutput.cs @@ -0,0 +1,40 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace Admin.NET.Plugin.WorkWeixin; + +/// +/// 企业微信接口输出基类 +/// +public class BaseWorkOutput +{ + /// + /// 返回码 + /// + [JsonProperty("errcode")] + [JsonPropertyName("errcode")] + public int ErrCode { get; set; } + + /// + /// 对返回码的文本描述内容 + /// + [JsonProperty("errmsg")] + [JsonPropertyName("errmsg")] + public string ErrMsg { get; set; } +} + +/// +/// 带id的输出参数 +/// +public class BaseWorkIdOutput : BaseWorkOutput +{ + /// + /// id + /// + [JsonProperty("id")] + [JsonPropertyName("id")] + public long? Id { get; set; } +} \ No newline at end of file diff --git a/Admin.NET-v2/BACKEND_INFRA_CONTEXT.md b/Admin.NET-v2/BACKEND_INFRA_CONTEXT.md new file mode 100644 index 0000000..da79506 --- /dev/null +++ b/Admin.NET-v2/BACKEND_INFRA_CONTEXT.md @@ -0,0 +1,333 @@ +# 后端基础封装长期上下文 + +## 1. 先看哪里 + +阅读本仓库后端时,建议固定按下面顺序建立上下文: + +1. `Admin.NET/Admin.NET.Web.Core/Startup.cs` +2. `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs` +3. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +4. `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` +5. 具体能力目录:`Service/*`、`Extension/*`、`Utils/*`、`Option/*` + +说明: + +- `Admin.NET.Web.Entry` 基本只是宿主入口,核心装配在 `Admin.NET.Web.Core` +- `Admin.NET.Application` 当前更偏配置与示例开放接口,不是主要基础设施承载层 +- `Admin.NET.Core` 是后端公共封装与基础设施中心 + +## 2. 后端常见封装模式 + +本项目后端整体是 `Furion + SqlSugar + 动态 API + 统一返回 + 多租户 + 缓存/事件/调度` 的组合式封装,常见模式如下: + +- `Option/*.cs + Configuration/*.json` + - 用于强类型配置绑定 + - 统一由 `ProjectOptions.AddProjectOptions()` 注册 +- `*Setup.cs` + - 用于框架能力接入与初始化 + - 常见如缓存、SqlSugar、OAuth、日志、SignalR +- `Service/**/*Service.cs + IDynamicApiController` + - 默认服务暴露方式 + - 业务服务与基础设施服务都大量采用动态 API 控制器模式 +- `Utils/*` + - 放通用工具、统一返回、通用请求封装、导入导出、加解密 +- `Extension/*` + - 放高频扩展方法,特别是 SqlSugar 查询、仓储增强、请求上下文增强 +- `SqlSugar/*` + - 放数据库层二次封装 + - 包括切库、分页、过滤器、事务、初始化、缓存桥接 +- `Plugins/*` + - 外部系统集成通常独立在插件中 + - 常见模式是 `Option + Proxy + Dto + ResultProvider` + +## 3. 全局核心封装 + +这些类/文件是后端全局理解的关键入口: + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` + - 后端总装配入口 + - 串起缓存、Jwt、Signature 鉴权、SqlSugar、EventBus、Schedule、SignalR、OSS、统一返回 +- `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs` + - 配置选项统一注册入口 +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` + - 全局统一返回封装 + - 将成功、异常、校验失败、401/403 等全部收敛为 `AdminResult` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` + - SqlSugar 全局装配入口 + - 包含 AOP、软删过滤、租户过滤、数据权限过滤、差异日志、建库建表、视图、种子初始化 +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` + - 默认仓储入口 + - 自动处理主库、日志库、租户库切换 +- `Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs` + - 系统缓存统一访问入口 + - 也是分布式锁、缓存包装、Hash 操作入口 +- `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` + - 多租户核心服务 + - 租户初始化、租户缓存、租户库连接创建都集中在这里 +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` + - 登录、Token、RefreshToken、验证码、单点登录、黑名单等核心鉴权逻辑入口 +- `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` + - JWT 自动续期与权限校验核心处理器 + +## 4. 高频工具类位置 + +### 4.1 Utils + +- `Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs` + - 综合工具类 + - 包含固定哈希、服务地址、Excel 导入导出、设备/IP 信息等 +- `Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs` + - 统一加解密入口 + - 支持 MD5/SM2/SM4,认证、租户连接串、开放接口都会依赖它 +- `Admin.NET/Admin.NET.Core/Utils/DateTimeUtil.cs` + - 时间处理工具 +- `Admin.NET/Admin.NET.Core/Utils/FileHelper.cs` + - 文件工具 +- `Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs` + - Excel 输出辅助 +- `Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs` + - 代码生成辅助 +- `Admin.NET/Admin.NET.Core/Utils/SSHHelper.cs` + - SSH/SFTP 工具 +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` + - 全局统一返回 + +### 4.2 通用请求/返回封装 + +- `Admin.NET/Admin.NET.Core/Utils/BaseFilter.cs` + - 关键字搜索、结构化过滤模型底座 +- `Admin.NET/Admin.NET.Core/Utils/BasePageInput.cs` + - 全局分页入参底座 +- `Admin.NET/Admin.NET.Core/Utils/BaseIdInput.cs` + - 通用主键入参 +- `Admin.NET/Admin.NET.Core/Utils/BaseStatusInput.cs` + - 通用状态切换入参 +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs` + - 通用分页返回结构 + +### 4.3 Extensions + +- `Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs` + - 仓储增强最重要的扩展类 + - 包含假删除、差异日志、排序、防注入排序、多库表名 `AS`、忽略过滤器、批量列表查询 +- `Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs` + - 动态高级搜索/过滤扩展 + - 直接承接 `BaseFilter` +- `Admin.NET/Admin.NET.Core/Extension/HttpContextExtension.cs` + - 设备、浏览器、操作系统、外部登录提供者等扩展 +- `Admin.NET/Admin.NET.Core/Extension/RequestExtension.cs` + - 请求源地址扩展 + +## 5. 遇到数据访问时先查哪些类 + +如果要理解某个服务方法的数据访问,不建议一开始直接盯业务查询语句,建议按下面顺序查: + +1. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` +2. `Admin.NET/Admin.NET.Core/Extension/RepositoryExtension.cs` +3. `Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs` +4. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarPagedList.cs` +5. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` + +重点说明: + +- 仓储默认不是简单单库访问,而是带租户自动切库能力 +- 软删除、租户过滤、组织数据权限过滤,都是在 `SqlSugarSetup.SetDbAop()` 和 `SqlSugarFilter` 中统一挂载 +- 分页返回不是随意对象,而是统一使用 `SqlSugarPagedList` + +## 6. 遇到缓存时先查哪些类 + +缓存能力优先看下面几处: + +1. `Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs` +2. `Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs` +3. `Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs` +4. `Admin.NET/Admin.NET.Core/Const/CacheConst.cs` + +说明: + +- `CacheSetup` 负责接入 Redis 或内存兜底 +- `SysCacheService` 是项目内缓存统一门面,不要绕开它直接散写缓存逻辑 +- `SqlSugarCache` 是 SqlSugar 二级缓存到系统缓存的桥接层 +- 许多防重、黑名单、配置缓存、在线用户缓存都依赖 `SysCacheService` + +## 7. 遇到多租户时先查哪些类 + +多租户相关优先看: + +1. `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +2. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` +3. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +4. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` +5. `Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs` +6. `Admin.NET/Admin.NET.Core/Const/ClaimConst.cs` + +关键点: + +- 租户库连接是运行期动态创建的,不是固定写死 +- 仓储会优先判断实体特性、请求头租户、当前登录租户,再决定切哪个库 +- 数据权限与租户过滤是全局过滤器,不是每个服务手写 + +## 8. 遇到鉴权时先查哪些类 + +普通登录/JWT: + +1. `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +2. `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` +3. `Admin.NET/Admin.NET.Core/Const/ClaimConst.cs` +4. `Admin.NET/Admin.NET.Core/Const/ConfigConst.cs` + +开放接口签名鉴权: + +1. `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs` +2. `Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs` +3. `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationOptions.cs` +4. `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationEvent.cs` + +第三方 OAuth: + +1. `Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs` +2. `Admin.NET/Admin.NET.Core/Option/OAuthOptions.cs` + +## 9. Furion / SqlSugar 强相关封装 + +### 9.1 Furion 强相关 + +- 动态 API:大量服务直接实现 `IDynamicApiController` +- 配置绑定:`ProjectOptions` 中使用 `AddConfigurableOptions()` +- 统一返回:`AddInjectWithUnifyResult()` +- 工作单元:`SqlSugarUnitOfWork` 适配 Furion 的 `[UnitOfWork]` +- JWT:`AddJwt()` +- 签名鉴权:`AddSignatureAuthentication(...)` +- 事件总线:`AddEventBus(...)` +- 任务调度:`AddSchedule(...)` +- 声明式远程请求:`AddHttpRemote()` + +### 9.2 SqlSugar 强相关 + +- `SqlSugarSetup` +- `SqlSugarRepository` +- `SqlSugarUnitOfWork` +- `SqlSugarPagedList` +- `SqlSugarFilter` +- `SqlSugarCache` +- `RepositoryExtension` +- `SqlSugarExtension` + +这些共同构成了本项目的数据访问基础设施层。 + +## 10. 事件、调度、打印、远程请求入口 + +### 10.1 事件总线 + +- `Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs` +- `Admin.NET/Admin.NET.Core/EventBus/EventConsumer.cs` + +说明: + +- 当前事件总线不是裸 Furion 默认行为,做了 Redis 存储和失败重试增强 + +### 10.2 任务调度 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs` + +说明: + +- `SysScheduleService` 更偏业务日程管理 +- 框架级作业能力要看 `AddSchedule` 和 `DynamicJobCompiler` + +### 10.3 打印 + +- `Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs` + +### 10.4 远程请求与插件代理 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` 中 `AddHttpRemote()` +- `Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/*` +- `Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Utils/BaseHttpOutput.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs` + +说明: + +- 本仓库插件侧远程调用以声明式 HTTP 接口代理为主 +- 不建议新增散落式裸 `HttpClient` 封装,优先复用既有声明式模式 + +## 11. 配置封装位置 + +核心配置类主要集中在: + +- `Admin.NET/Admin.NET.Core/Option/*` +- `Admin.NET/Plugins/*/Option/*` +- `Admin.NET/Admin.NET.Application/Configuration/*.json` + +高频配置: + +- `DbConnectionOptions` +- `CacheOptions` +- `EventBusOptions` +- `OAuthOptions` +- `UploadOptions` +- 插件侧如 `WorkWeixinOptions`、`DingTalkOptions` + +## 12. 实际开发建议 + +- 扩展后端能力前,先确认是否已有 `Utils`、`Extension`、`SqlSugar`、`Service` 中的公共封装可复用 +- 不要绕开 `SysCacheService`、`SqlSugarRepository`、`AdminResultProvider` 另起一套公共实现 +- 多租户、缓存键、鉴权 Claim、系统配置键都优先复用现有常量类,不要新增硬编码字符串 +- 如果需要排查“为什么查询结果不对”,优先检查全局过滤器、租户切库、软删除、数据权限,而不是先怀疑业务查询本身 + + +## 附录 +### A. 工具类清单: +Startup、ProjectOptions;文件:Startup.cs、ProjectOptions.cs;分类:Infrastructure;主要用途:全局装配 Furion、Jwt、Signature、SqlSugar、Cache、EventBus、Schedule、SignalR、OSS、统一返回与 Options 绑定;复用:后端所有能力最终都从这里接入。 +AdminResultProvider;文件:AdminResultProvider.cs;分类:Wrapper;主要用途:全局统一返回、异常返回、校验失败返回、401/403 状态码包装为 AdminResult;复用:在 Startup 中全局注入,ExcelHelper 也直接引用。 +GoViewResultProvider;文件:GoViewResultProvider.cs;分类:Wrapper;主要用途:插件侧单独的统一返回模型;复用:GoView 插件接口。 +BaseFilter、BasePageInput、BaseIdInput、BaseStatusInput;文件:BaseFilter.cs、BasePageInput.cs、BaseIdInput.cs、BaseStatusInput.cs;分类:Wrapper/Request;主要用途:统一关键字搜索、结构化过滤、分页、主键入参、状态切换入参;复用:BasePageInput 约被 28 个 DTO 复用,BaseIdInput 约被 30 个 DTO 复用。 +SqlSugarPagedList、SqlSugarPagedExtensions;文件:SqlSugarPagedList.cs;分类:Wrapper;主要用途:统一分页结果结构与 ToPagedList* 扩展;复用:约 32 个服务/DTO 返回分页结果时复用。 +CommonUtil;文件:CommonUtil.cs;分类:Util;主要用途:固定哈希、服务地址、Excel 导入导出、XML 序列化、设备/IP 信息等综合工具;复用:登录、日志、种子数据、导入导出等多个模块。 +CryptogramUtil;文件:CryptogramUtil.cs;分类:Util;主要用途:统一密码/密钥加解密入口,兼容 MD5/SM2/SM4;复用:认证、LDAP、租户连接串、更新服务等约 10 个文件。 +DateTimeUtil;文件:DateTimeUtil.cs;分类:Util;主要用途:时间格式转换、Unix 时间处理;复用:签名鉴权、服务器信息、测试。 +FileHelper、ExcelHelper、CodeGenUtil、SSHHelper;文件:FileHelper.cs、ExcelHelper.cs、CodeGenUtil.cs、SSHHelper.cs;分类:Helper/Util;主要用途:文件处理、Excel 导出、代码生成、SSH/SFTP;复用:代码生成、语言包、更新、外部集成。 +HttpContextExtension、RequestExtension;文件:HttpContextExtension.cs、RequestExtension.cs;分类:Extensions;主要用途:扩展外部登录提供者、设备/浏览器/OS、请求源地址获取;复用:登录链路、日志链路。 +RepositoryExtension;文件:RepositoryExtension.cs;分类:Extensions;主要用途:SqlSugar 仓储增强,提供假删除、差异日志、排序、防注入排序、多库 AS、忽略租户过滤器、批量 in 查询等;复用:数据访问层核心增强。 +SqlSugarExtension;文件:SqlSugarExtension.cs;分类:Extensions;主要用途:动态高级搜索、组合过滤、字段表达式解析;复用:承接 BaseFilter 查询模型。 +CacheSetup、SqlSugarCache;文件:CacheSetup.cs、SqlSugarCache.cs;分类:Infrastructure;主要用途:统一缓存提供者接入,Redis 与内存兜底,SqlSugar 二级缓存桥接到系统缓存;复用:全局缓存与数据访问层。 +SysCacheService;文件:SysCacheService.cs;分类:Infrastructure;主要用途:缓存 CRUD、前缀删除、分布式锁、Hash、Redis 类型读取、缓存穿透包装 AdGetAsync;复用:约 33 个文件,覆盖认证、幂等、在线用户、配置、SqlSugar 缓存等。 +SqlSugarRepository;文件:SqlSugarRepository.cs;分类:Infrastructure;主要用途:按系统表/日志表/租户头/登录租户自动切库,封装分表操作;复用:约 61 个文件,是几乎所有服务的默认仓储入口。 +SqlSugarSetup;文件:SqlSugarSetup.cs;分类:Infrastructure;主要用途:SqlSugar 全局初始化、AOP、审计字段、软删/租户/数据权限过滤、差异日志、建库建表、视图、种子、租户库初始化;复用:数据库层总装配。 +SqlSugarUnitOfWork;文件:SqlSugarUnitOfWork.cs;分类:Infrastructure;主要用途:把 Furion UnitOfWork 对接到 ISqlSugarClient.AsTenant() 事务;复用:所有 [UnitOfWork] 服务方法。 +SqlSugarFilter;文件:SqlSugarFilter.cs;分类:Infrastructure;主要用途:组织数据权限、自定义实体过滤器、过滤器缓存清理;复用:SqlSugarSetup 启动时挂载,组织/角色/用户服务会主动清理相关缓存。 +SysTenantService;文件:SysTenantService.cs;分类:Infrastructure;主要用途:租户 CRUD、菜单初始化、Logo、默认角色、缓存租户、动态租户库连接;复用:认证、配置、在线用户、仓储切库。 +SysAuthService、JwtHandler;文件:SysAuthService.cs、JwtHandler.cs;分类:Infrastructure;主要用途:登录、Token/RefreshToken、单点/黑名单、验证码、租户切换,外加 JWT 自动续期与按钮权限校验;复用:全局鉴权主链路。 +SignatureAuthenticationHandler、SysOpenAccessService;文件:SignatureAuthenticationHandler.cs、SysOpenAccessService.cs;分类:Infrastructure;主要用途:开放接口签名鉴权、accessKey/accessSecret 校验、重放防护、绑定用户租户 Claim;复用:AddSignatureAuthentication 和开放接口控制器。 +OAuthSetup;文件:OAuthSetup.cs;分类:Infrastructure;主要用途:微信/Gitee 第三方登录注册与 Cookie 策略;复用:认证入口。 +LoggingSetup;文件:LoggingSetup.cs;分类:Infrastructure;主要用途:控制台、文件、数据库、ES 日志写入与 Furion Monitor Logging 配置;复用:全局日志体系。 +AppEventSubscriber、EventConsumer、RetryEventHandlerExecutor;文件:AppEventSubscriber.cs、EventConsumer.cs、RetryEventHandlerExecutor.cs;分类:Infrastructure;主要用途:事件订阅、Redis 消费桥接、失败重试/熔断回调;复用:全局 EventBus 增强。 +DynamicJobCompiler、SysScheduleService;文件:DynamicJobCompiler.cs、SysScheduleService.cs;分类:Infrastructure;主要用途:前者是框架级动态任务编译,后者是业务日程管理;复用:任务调度与日程能力。 +SignalRSetup;文件:SignalRSetup.cs;分类:Infrastructure;主要用途:SignalR 序列化、Redis Backplane、DataProtection Key 持久化;复用:在线用户/即时通讯链路。 +SysPrintService;文件:SysPrintService.cs;分类:Infrastructure;主要用途:打印模板分页、增删改查;复用:打印能力统一入口。 +IOSSServiceManager、OSSServiceManager;文件:IOSSServiceManager.cs;分类:Infrastructure;主要用途:统一 OSS 服务实例缓存、动态 provider 配置转换;复用:文件存储、MultiOSSFileProvider、SysFileProviderService。 +IWorkWeixinAuthHttp、IWorkWeixinAppChatHttp、IDepartmentHttp、ITagHttp、BaseWorkOutput;文件:IWorkWeixinAuthHttp.cs、IWorkWeixinAppChatHttp.cs、IDepartmentHttp.cs、ITagHttp.cs、BaseHttpOutput.cs;分类:Infrastructure/Remote;主要用途:通过声明式 HTTP 代理封装企业微信远程调用,统一返回基类;复用:插件远程集成入口。 +DbConnectionOptions、CacheOptions、EventBusOptions、OAuthOptions、UploadOptions、WorkWeixinOptions、DingTalkOptions;文件:DbConnectionOptions.cs、CacheOptions.cs、EventBusOptions.cs、OAuthOptions.cs、UploadOptions.cs、WorkWeixinOptions.cs、DingTalkOptions.cs;分类:Options;主要用途:把 Configuration/*.json 与插件配置绑定到强类型 Options;复用:由 ProjectOptions 统一注册。 +CacheConst、ClaimConst、ConfigConst、SqlSugarConst、ApplicationConst;文件:CacheConst.cs、ClaimConst.cs、ConfigConst.cs、SqlSugarConst.cs、ApplicationConst.cs;分类:Const;主要用途:统一缓存键、Claim 名、配置键、数据库配置 ID、应用分组名;复用:认证/缓存/多租户/配置/分页/日志全链路。 + +### B. 高价值基础封装: +全局核心封装:Startup + ProjectOptions、AdminResultProvider、SqlSugarSetup、SqlSugarRepository、SysCacheService、SysTenantService、SysAuthService、JwtHandler。这几类基本决定了“配置如何绑定、接口如何暴露、数据如何访问、用户如何鉴权、租户如何切换、返回如何统一”。 +与 Furion 强相关:IDynamicApiController 是服务层默认暴露方式;AddConfigurableOptions 负责 Options 绑定;AddInjectWithUnifyResult 负责统一返回;AddJwt、AddSignatureAuthentication、AddEventBus、AddSchedule、AddHttpRemote 都在 Startup 汇总;SqlSugarUnitOfWork 是 Furion [UnitOfWork] 到 SqlSugar 的适配层。 +与 SqlSugar 强相关:SqlSugarSetup、SqlSugarRepository、SqlSugarPagedList、RepositoryExtension、SqlSugarExtension、SqlSugarFilter、SqlSugarCache、SqlSugarUnitOfWork 是完整的二次封装链,覆盖分页、过滤、分表、租户切库、软删、差异日志、种子、AOP 与缓存。 +多租户相关:先看 SysTenantService,再看 SqlSugarRepository 的自动切库逻辑,最后看 SqlSugarSetup.SetDbAop 和 SqlSugarFilter 的租户/数据权限过滤。 +缓存相关:先看 CacheSetup,日常调用和锁都收敛到 SysCacheService,数据库二级缓存走 SqlSugarCache,缓存键规范看 CacheConst。 +鉴权相关:普通登录链路看 SysAuthService + JwtHandler,开放接口签名链路看 SignatureAuthenticationHandler + SysOpenAccessService,第三方登录看 OAuthSetup,Claim 规范看 ClaimConst。 +事件/任务/远程/打印:事件总线看 AppEventSubscriber + RetryEventHandlerExecutor + RedisEventSourceStorer;任务调度看 Startup.AddSchedule 和 DynamicJobCompiler;远程请求看 Startup.AddHttpRemote 加插件 Proxy/*Http.cs;打印入口看 SysPrintService。 + +### C. 建议写入 AGENTS.md 的内容草案 +本项目后端公共封装高度集中在 Admin.NET.Core,Admin.NET.Application 目前更多承担配置与示例开放接口,Admin.NET.Web.Core 主要负责能力装配,Admin.NET.Web.Entry 只是最薄宿主入口。 +常见封装模式是:Option/*.cs + Configuration/*.json 做配置绑定,*Setup.cs 做框架接入,Service/**/*Service.cs + IDynamicApiController 做动态 API,Utils/* 与 Extension/* 提供高频工具和查询增强,SqlSugar/* 承担数据库层二次封装。 +阅读代码的优先顺序建议固定为:Admin.NET.Web.Core/Startup.cs -> Admin.NET.Web.Core/ProjectOptions.cs -> Admin.NET.Core/SqlSugar/SqlSugarSetup.cs -> Admin.NET.Core/Utils/AdminResultProvider.cs -> 具体能力服务目录。 +高频工具类主要在 Admin.NET.Core/Utils 与 Admin.NET.Core/Extension。如果看到分页、筛选、关键字搜索,先查 BaseFilter、BasePageInput、SqlSugarExtension、SqlSugarPagedList;如果看到通用导出/文件/加密,先查 CommonUtil、ExcelHelper、FileHelper、CryptogramUtil。 +遇到缓存问题时优先看 CacheSetup、SysCacheService、SqlSugarCache、CacheConst;遇到租户与数据权限时优先看 SysTenantService、SqlSugarRepository、SqlSugarFilter、SqlSugarSetup;遇到鉴权问题时优先看 SysAuthService、JwtHandler、SignatureAuthenticationHandler、SysOpenAccessService、OAuthSetup。 +遇到数据访问问题,不要先从具体业务服务硬读 SQL,先看 SqlSugarRepository 的切库规则、RepositoryExtension 的差异日志/软删增强、SqlSugarExtension 的动态过滤,再回到业务查询。 +遇到插件与外部系统集成,先看插件自己的 Option、Proxy、Utils/BaseHttpOutput 或自定义 ResultProvider,本仓库远程调用模式偏向“声明式 HTTP 接口 + 统一输入输出 DTO”,不是散落的裸 HttpClient。 +当前代码基底明确是 Furion + SqlSugar + Furion EventBus/Schedule/RemoteRequest 组合,后续开发应优先复用这些既有封装,不要绕开它们直接造新的通用层。 diff --git a/Admin.NET-v2/BACKEND_TECH_GUIDE.md b/Admin.NET-v2/BACKEND_TECH_GUIDE.md new file mode 100644 index 0000000..d8a3d6b --- /dev/null +++ b/Admin.NET-v2/BACKEND_TECH_GUIDE.md @@ -0,0 +1,4978 @@ +# Admin.NET 后端使用指南 + +## 1. 文档定位 + +本文是当前脚手架 `Admin.NET` 的后端使用指南,只聚焦 `Admin.NET/` 目录下的后端模块,不展开前端 `Web/` 的页面实现细节。 + +文档目标不是重复 README,而是把当前仓库里已经落地的后端能力、配置入口、开发方式、扩展边界、常见注意事项整理成可直接用于二开和交接的“后台手册”。 + +本文写法参考若依文档的组织方式,采用“功能说明 + 配置说明 + 使用方式 + 开发建议 + 注意事项”的结构,便于长期维护和按章节查阅。 + +## 2. 适用范围 + +- 适用于当前仓库 `C:\D\WORK\NewP\Admin.NET-v2` +- 适用于当前后端脚手架形态:`Furion + SqlSugar + 动态 API + 统一返回 + 多租户 + 插件化` +- 适用于基于 `Admin.NET.Core` 继续扩展 `MES/WMS/QMS/DMS/EMS/ERP` 业务模块的研发场景 + +## 3. 技术基线 + +### 3.1 运行时 + +- 后端核心项目当前为 `net8.0;net10.0` 双目标 +- 宿主入口:`Admin.NET.Web.Entry` +- 启动装配:`Admin.NET.Web.Core` +- 基础设施核心:`Admin.NET.Core` +- 示例应用层:`Admin.NET.Application` + +### 3.2 核心框架 + +- Web 装配:Furion +- 数据访问:SqlSugar +- 接口暴露:`IDynamicApiController` +- 统一返回:`AdminResultProvider` +- 统一鉴权:JWT + Signature + OAuth +- 任务调度:Furion Schedule +- 事件总线:Furion EventBus +- 缓存:Memory / Redis +- 即时通讯:SignalR +- 对外集成:声明式 HTTP / HttpClient / 插件 + +### 3.3 当前应用层默认引用插件 + +当前 `Admin.NET.Application.csproj` 默认引用: + +- `Admin.NET.Plugin.ApprovalFlow` +- `Admin.NET.Plugin.DingTalk` +- `Admin.NET.Plugin.GoView` +- `Admin.NET.Plugin.HwPortal` + +存在但当前未默认引用: + +- `Admin.NET.Plugin.WorkWeixin` +- `Admin.NET.Plugin.K3Cloud` +- `Admin.NET.Plugin.ReZero` + +## 4. 后端目录说明 + +### 4.1 目录结构 + +- `Admin.NET/Admin.NET.Web.Entry` + - 最薄宿主入口 + - 负责 `Serve.Run` 启动和 Kestrel 基础限制 +- `Admin.NET/Admin.NET.Web.Core` + - 后端总装配层 + - 负责注册数据库、缓存、鉴权、日志、OSS、限流、SignalR、Swagger、任务调度等 +- `Admin.NET/Admin.NET.Core` + - 基础设施与系统公共服务中心 + - 包含 Entity、SqlSugar、Cache、Auth、Tenant、File、Template、Message、Logging、EventBus、SignalR 等 +- `Admin.NET/Admin.NET.Application` + - 示例业务层与开放接口示例 + - 包含 `Configuration` 与 `OpenApi` +- `Admin.NET/Plugins` + - 插件式扩展目录 + +### 4.2 推荐阅读顺序 + +建议阅读后端代码时固定按以下顺序建立上下文: + +1. `Admin.NET/Admin.NET.Web.Core/Startup.cs` +2. `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs` +3. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +4. `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` +5. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` +6. `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` +7. `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +8. `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +9. `Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs` +10. 对应模块的 `Service/*` 或 `Plugins/*` + +## 5. 启动流程总览 + +### 5.1 宿主启动 + +`Admin.NET.Web.Entry/Program.cs` 通过 `Serve.Run(RunOptions.Default.AddWebComponent())` 启动应用。 + +宿主层主要负责: + +- 配置日志过滤 +- 配置 Kestrel 超时与请求体大小 +- 不承载具体业务能力注册 + +### 5.2 服务注册阶段 + +真正的服务注册发生在 `Admin.NET.Web.Core/Startup.cs`: + +- `AddProjectOptions()` +- `AddCache()` +- `AddSqlSugar()` +- `AddJwt()` +- `AddSignatureAuthentication(...)` +- `AddCorsAccessor()` +- `AddHttpRemote()` +- `AddTaskQueue()` +- `AddSchedule(...)` +- `AddEventBus(...)` +- `AddOAuth()` +- `AddElasticSearchClients()` +- `AddImageSharp()` +- `AddOSSService(...)` +- `AddViewEngine()` +- `AddSignalR(...)` +- `AddLoggingSetup()` +- `AddCaptcha()` + +### 5.3 中间件阶段 + +中间件链主要包括: + +- 响应压缩 +- 反向代理头处理 +- 异常处理 +- 静态文件 +- OAuth Cookie 策略 +- 统一状态码拦截 +- 多语言 +- 路由 +- 跨域 +- 认证授权 +- 限流 +- 任务看板 +- Swagger / Scalar +- SignalR Hub +- MVC 路由 + +### 5.4 开发建议 + +- 不要把基础设施能力注册分散到业务服务类里 +- 涉及新增全局能力时,优先补在 `Startup.cs` 和 `ProjectOptions.cs` +- 涉及插件能力时,优先放入插件自己的 `Startup.cs` + +## 6. 零基础快速入门 + +这一节不讲抽象概念,只讲一个刚接手项目的人,如何从 0 到 1 把后端跑起来。 + +### 6.1 第一步:先改数据库配置 + +找到: + +- `Admin.NET/Admin.NET.Application/Configuration/Database.json` + +最小可运行配置通常先用本地 `Sqlite`,例如: + +```json +{ + "DbConnection": { + "EnableConsoleSql": false, + "ConnectionConfigs": [ + { + "DbType": "Sqlite", + "ConnectionString": "DataSource=./Admin.NET.db", + "DbSettings": { + "EnableInitDb": true, + "EnableInitView": true, + "EnableDiffLog": false, + "EnableUnderLine": false, + "EnableConnStringEncrypt": false + }, + "TableSettings": { + "EnableInitTable": true, + "EnableIncreTable": false + }, + "SeedSettings": { + "EnableInitSeed": true, + "EnableIncreSeed": false + } + } + ] + } +} +``` + +如果你只是本地熟悉项目,先不要切多数据库、日志库、租户独立库,先把主库跑起来。 + +### 6.2 第二步:先跑后端 + +在项目根目录执行: + +```powershell +dotnet restore Admin.NET\Admin.NET.sln +dotnet build Admin.NET\Admin.NET.sln -c Debug +dotnet run --project Admin.NET\Admin.NET.Web.Entry +``` + +如果数据库配置和初始化开关无误,第一次启动会自动建库、建表、建视图、写种子。 + +### 6.3 第三步:打开接口文档 + +启动成功后,优先看这两个入口: + +- Swagger +- Scalar + +在本项目中,文档能力由: + +- `Admin.NET/Admin.NET.Application/Configuration/Swagger.json` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` + +统一控制。 + +如果你只是想确认后端是否跑通,先看 Swagger 能否正常打开,再测试一个匿名接口或登录接口。 + +### 6.4 第四步:理解一个接口是怎么跑通的 + +推荐直接看: + +- `Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs` +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` + +这是最快理解“动态 API + 统一认证 + 统一返回”的入口。 + +### 6.5 第五步:新增一个最小业务模块 + +最小业务模块通常至少需要四样东西: + +1. 实体 +2. Service +3. 菜单/权限 +4. 页面或接口调试入口 + +最小实体示例: + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Entity/ExampleMaterial.cs +[SugarTable("sys_example_material", "示例物料")] +public class ExampleMaterial : EntityBaseTenantOrgDel +{ + [SugarColumn(ColumnDescription = "物料编码", Length = 64)] + public string MaterialCode { get; set; } + + [SugarColumn(ColumnDescription = "物料名称", Length = 128)] + public string MaterialName { get; set; } +} +``` + +最小服务示例: + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleMaterialService.cs +[ApiDescriptionSettings("示例模块", Name = "ExampleMaterial", Order = 100)] +public class ExampleMaterialService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _rep; + + public ExampleMaterialService(SqlSugarRepository rep) + { + _rep = rep; + } + + [HttpGet("page")] + public Task> Page([FromQuery] BasePageInput input) + { + return _rep.AsQueryable() + .OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + [HttpPost("add")] + public Task Add([FromBody] ExampleMaterial input) + { + return _rep.InsertAsync(input); + } +} +``` + +这样你就已经具备了一个最小可调试业务模块。 + +### 6.6 第六步:再考虑菜单和权限 + +很多新人会在“接口已经能调通”后忘记权限体系,结果前端页面上按钮、菜单、权限控制全部不通。 + +当前项目权限控制的关键认知是: + +- 菜单和按钮权限本质上绑定到 `SysMenu.Permission` +- `JwtHandler` 会按请求路由去匹配按钮权限 +- 所以你的接口命名和菜单权限命名要保持一致的规则 + +最小认知示例: + +```text +接口路由:/api/exampleMaterial/page +对应按钮权限:exampleMaterial:page +``` + +如果接口存在,但当前用户没有这个按钮权限,就会被 `JwtHandler` 拦截。 + +### 6.7 第七步:再上代码生成器 + +建议先手写跑通一个最小模块,再去用代码生成器。 + +原因是很多新人一开始直接用生成器,但不知道生成出来的: + +- 实体该放哪里 +- DTO 为什么这样命名 +- 菜单按钮为什么这么生成 +- 接口权限为什么是这套规则 + +先理解后再生成,后续二次收敛会轻松很多。 + +## 7. 新建业务模块完整示例 + +这一节给出一个更接近实际开发的“物料主数据”最小链路,适合做培训和交接时演示。 + +### 7.1 场景目标 + +目标是新增一个“物料主数据”模块,至少具备: + +- 物料分页查询 +- 新增物料 +- 编辑物料 +- 删除物料 +- 菜单权限可控 + +### 7.2 第一步:新增实体 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Entity/ExampleMaterial.cs +[SugarTable("sys_example_material", "示例物料")] +public class ExampleMaterial : EntityBaseTenantOrgDel +{ + [SugarColumn(ColumnDescription = "物料编码", Length = 64)] + public string MaterialCode { get; set; } + + [SugarColumn(ColumnDescription = "物料名称", Length = 128)] + public string MaterialName { get; set; } + + [SugarColumn(ColumnDescription = "状态")] + public int Status { get; set; } +} +``` + +### 7.3 第二步:新增 DTO + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/Dto/ExampleMaterialInput.cs +public class ExampleMaterialInput : BasePageInput +{ + public string Keyword { get; set; } +} +``` + +### 7.4 第三步:新增 Service + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleMaterialService.cs +[ApiDescriptionSettings("示例模块", Name = "ExampleMaterial", Order = 100)] +public class ExampleMaterialService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _rep; + + public ExampleMaterialService(SqlSugarRepository rep) + { + _rep = rep; + } + + [HttpGet("page")] + public Task> Page([FromQuery] ExampleMaterialInput input) + { + return _rep.AsQueryable() + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), x => x.MaterialCode.Contains(input.Keyword) || x.MaterialName.Contains(input.Keyword)) + .OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPagedListAsync(input.Page, input.PageSize); + } + + [HttpPost("add")] + public Task Add([FromBody] ExampleMaterial input) + { + return _rep.InsertAsync(input); + } + + [HttpPost("update")] + public Task Update([FromBody] ExampleMaterial input) + { + return _rep.UpdateAsync(input); + } + + [HttpPost("delete")] + public Task Delete([FromBody] BaseIdInput input) + { + return _rep.DeleteAsync(x => x.Id == input.Id); + } +} +``` + +### 7.5 第四步:配置菜单和按钮权限 + +如果你是手工建模块,而不是用代码生成器,就必须自己配置菜单和按钮权限。 + +最少要保证下面几个权限名和接口路由规则一致: + +- `exampleMaterial:page` +- `exampleMaterial:add` +- `exampleMaterial:update` +- `exampleMaterial:delete` + +否则即使接口开发完成,页面和权限体系也无法正常协同。 + +### 7.6 第五步:调试接口 + +用 Swagger 先验证: + +1. `page` 是否能返回分页结果 +2. `add` 后审计字段、租户字段是否自动写入 +3. `delete` 是否按软删除逻辑生效 + +### 7.7 第六步:再决定是否切回代码生成器 + +如果这个流程你已经理解了,再去使用代码生成器,你就知道: + +- 为什么生成出来的 Service 是那种结构 +- 为什么 DTO 要分输入输出 +- 为什么菜单按钮权限会按固定规则生成 +- 为什么有些通用字段不建议自己再写 + +### 7.8 新手常见错误 + +- 只建实体,不建 DTO,后面接口难维护 +- 直接用实体做所有出入参,导致字段污染 +- 忘了配置菜单权限,页面一进来就 403 +- 没注意租户基类,数据串租户 +- 为了快直接 `ClearFilter()`,后面权限全失真 + +### 7.9 建议培训顺序 + +如果你要带新人,建议按下面顺序培训: + +1. 先让他跑通登录和 Swagger +2. 再让他看一个现成 Service +3. 再让他手写一个最小模块 +4. 最后再让他用代码生成器 + +这样效果比“先教生成器”要好很多 + +--- + +# 第一章 框架简介 + +## 1.1 功能说明 + +Admin.NET 当前后端脚手架的核心特征是: + +- 以 `Furion` 作为应用启动、动态 API、统一返回、鉴权、调度、事件总线的基础框架 +- 以 `SqlSugar` 作为 ORM 与多库多租户封装核心 +- 以 `IDynamicApiController` 作为默认接口暴露方式 +- 以 `Admin.NET.Core` 作为长期复用的基础设施中心 +- 以插件方式扩展外部集成或独立业务能力 + +这意味着后端开发不是传统的“Controller + Service + Mapper”裸三层模式,而是“统一基础设施 + 业务服务动态暴露 + 系统能力门面复用”的模式。 + +## 1.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs` +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` + +## 1.3 当前脚手架特征 + +- 默认启用全局鉴权 +- 默认支持多租户 +- 默认支持代码生成 +- 默认支持文件上传下载 +- 默认支持模板渲染 +- 默认支持日志记录 +- 默认支持任务调度看板 +- 默认支持 Swagger / Scalar +- 默认支持开放接口签名鉴权 + +## 1.4 开发建议 + +- 把 `Admin.NET.Core` 视为“平台基础设施层”,不要在里面堆积强业务逻辑 +- 业务模块优先新建独立应用层或插件层 +- 复用现有封装优先级高于自行新建通用组件 + +## 1.5 注意事项 + +- README 中提到的功能并不等于全部都已经形成完整的后端开箱即用链路 +- 后端实际能力边界必须以代码实现为准,而不是以宣传文案为准 + +# 第二章 开发流程 + +## 2.1 开发思路 + +建议的开发流程如下: + +1. 阅读 `Startup`、`ProjectOptions`、`SqlSugarSetup` +2. 确认功能是否已有系统服务可复用 +3. 补配置文件与 Options 绑定 +4. 建实体并选择合适基类 +5. 建服务并实现 `IDynamicApiController` +6. 复用 `SqlSugarRepository`、`SysCacheService` 等门面 +7. 补日志、权限、租户、导入导出等周边能力 +8. 编译验证与必要测试 + +## 2.2 常用命令 + +```powershell +dotnet restore Admin.NET\Admin.NET.sln +dotnet build Admin.NET\Admin.NET.sln -c Debug +dotnet run --project Admin.NET\Admin.NET.Web.Entry +dotnet test Admin.NET\Admin.NET.Test\Admin.NET.Test.csproj +``` + +## 2.3 新增业务模块推荐路径 + +推荐优先在独立应用层或插件层新增: + +- 实体 +- DTO +- Service +- 配置文件 +- 种子数据 + +不建议直接修改平台核心,除非: + +- 现有基础设施确实缺少扩展点 +- 修改后具备明显的通用复用价值 +- 不会破坏租户、权限、日志、缓存主链路 + +## 2.4 典型开发步骤 + +### 2.4.1 新增配置 + +- 在 `Admin.NET.Application/Configuration` 增加 json +- 在 `ProjectOptions.cs` 增加 `AddConfigurableOptions()` +- 补对应 `Option/*.cs` + +### 2.4.2 新增实体 + +- 放到 `Admin.NET.Core/Entity` 或业务项目实体目录 +- 标注 `SugarTable` +- 继承合适的实体基类 + +### 2.4.3 新增服务 + +- 放到 `Service/*` +- 默认实现 `IDynamicApiController` +- 通过 `ApiDescriptionSettings` 配置分组、顺序与说明 + +### 2.4.4 新增外部集成 + +- 优先走插件目录 +- 优先走声明式 HTTP +- 优先放配置、DTO、Proxy、Service 成体系目录 + +## 2.5 注意事项 + +- 避免在业务代码中直接访问裸配置节点字符串 +- 避免绕过统一返回、统一缓存、统一仓储 +- 避免硬编码状态流转、阈值、审批规则 + +# 第三章 数据库配置 + +## 3.1 功能说明 + +当前项目数据库能力由 `SqlSugarSetup` 统一接入,支持: + +- 多连接 +- 主从读写 +- 日志库 +- 独立租户库 +- 自动建库建表 +- 自动建视图 +- 自动种子初始化 +- 全局过滤器 +- 审计字段自动赋值 +- 慢 SQL / 错误 SQL / 差异日志 + +## 3.2 关键文件 + +- `Admin.NET/Admin.NET.Application/Configuration/Database.json` +- `Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` + +## 3.3 配置说明 + +主配置节点:`DbConnection` + +重点配置项: + +- `EnableConsoleSql` +- `ConnectionConfigs` +- `DbSettings.EnableInitDb` +- `DbSettings.EnableInitView` +- `DbSettings.EnableDiffLog` +- `DbSettings.EnableUnderLine` +- `DbSettings.EnableConnStringEncrypt` +- `TableSettings.EnableInitTable` +- `TableSettings.EnableIncreTable` +- `SeedSettings.EnableInitSeed` +- `SeedSettings.EnableIncreSeed` + +## 3.4 使用方式 + +### 3.4.1 默认库 + +`ConnectionConfigs` 第一项默认作为主库使用,若 `ConfigId` 未显式配置,会在 `PostConfigure` 中自动补为主库 ID。 + +### 3.4.2 日志库 + +可配置固定日志库连接,用于写入日志与差异记录。 + +### 3.4.3 租户库 + +租户数据库隔离时,会通过 `SysTenantService.GetTenantDbConnectionScope()` 动态创建连接。 + +## 3.5 开发建议 + +- 开发环境可打开表初始化与种子初始化 +- 生产环境建议关闭自动初始化 +- 多业务库时,统一放入 `ConnectionConfigs` 管理 +- 数据库连接串加密统一走国密工具,不要自行发明一套加解密逻辑 + +## 3.6 注意事项 + +- 配置类属性实际使用的是 `EnableConnStringEncrypt` +- 示例配置文件中写成 `EnableConnEncrypt`,启用前应以代码属性为准修正 +- 涉及表结构同步前,应先确认视图是否会阻塞表更新 + +# 第四章 实体基类 + +## 4.1 功能说明 + +实体基类决定了当前业务对象是否具备: + +- 主键 +- 审计字段 +- 软删除 +- 机构过滤 +- 租户过滤 + +当前脚手架已经把大部分系统字段封装进基类,不建议重复定义。 + +## 4.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Entity/EntityBase.cs` + +## 4.3 基类说明 + +### 4.3.1 通用基类 + +- `EntityBaseId` +- `EntityBase` +- `EntityBaseDel` + +### 4.3.2 数据权限基类 + +- `EntityBaseOrg` +- `EntityBaseOrgDel` + +### 4.3.3 多租户基类 + +- `EntityBaseTenant` +- `EntityBaseTenantDel` +- `EntityBaseTenantId` +- `EntityBaseTenantOrg` +- `EntityBaseTenantOrgDel` + +## 4.4 使用方式 + +- 普通系统表:继承 `EntityBase` 或 `EntityBaseDel` +- 需要按机构做数据权限:继承 `EntityBaseOrg` 或 `EntityBaseOrgDel` +- 需要多租户隔离:继承 `EntityBaseTenant*` + +## 4.5 开发建议 + +- 主键统一使用雪花 ID +- 租户字段统一走基类,不手动重复定义 +- 软删表统一继承 `*Del`,避免一部分表软删、一部分表硬删导致行为不一致 + +## 4.6 注意事项 + +- 继承不同基类会直接影响全局过滤器生效方式 +- 选错基类,后续租户或数据权限会出现隐蔽问题 + +# 第五章 缓存管理 + +## 5.1 功能说明 + +缓存是当前脚手架的基础门面能力之一,支持: + +- Memory / Redis 切换 +- 前缀管理 +- 分布式锁 +- Hash 结构 +- 查询缓存桥接 +- 缓存穿透防护式读取 + +## 5.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs` +- `Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs` +- `Admin.NET/Admin.NET.Core/Cache/SqlSugarCache.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Cache.json` + +## 5.3 配置说明 + +主配置节点:`Cache` + +重点配置项: + +- `Prefix` +- `CacheType` +- `Redis.Configuration` +- `Redis.MaxMessageSize` +- `Redis.AutoDetect` + +集群配置节点:`Cluster` + +重点配置项: + +- `Enabled` +- `SignalR.RedisConfiguration` +- `DataProtecteKey` +- `IsSentinel` + +## 5.4 使用方式 + +### 5.4.1 基础读写 + +日常缓存统一通过 `SysCacheService.Set/Get/GetOrAdd`。 + +### 5.4.2 分布式锁 + +通过 `BeginCacheLock()` 获取锁,适合防重复提交、幂等控制、缓存重建等场景。 + +### 5.4.3 前缀清理 + +通过 `RemoveByPrefixKey()` 做批量清理,适合菜单、权限、配置、租户等缓存失效。 + +## 5.5 常见场景 + +- 登录失败次数 +- Token 黑名单 +- 开放接口 nonce +- 用户按钮权限 +- 角色数据范围 +- 多租户连接缓存 + +## 5.6 开发建议 + +- 统一使用 `CacheConst` +- 缓存 Key 命名要体现模块、对象、主键、作用域 +- 业务模块不要直接耦合具体 Redis 类型操作 + +## 5.7 注意事项 + +- 内存缓存只适合单节点或开发场景 +- 多节点部署且涉及在线用户、Token 黑名单、限流、SignalR 时应优先启用 Redis + +# 第六章 导入导出 + +## 6.1 功能说明 + +当前脚手架已形成较完整的 Excel 导入导出基础能力,支持: + +- Excel 模板导出 +- Excel 数据导出 +- Excel 导入 +- 字典值转换 +- 导入错误标记 +- 下拉模板列 +- 轻量级 MiniExcel 导入导出 + +## 6.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs` +- `Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs` +- `Admin.NET/Admin.NET.Core/Utils/MiniExcelUtil.cs` +- `Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs` + +## 6.3 现有技术实现 + +- `Magicodes.IE` +- `EPPlus` +- `MiniExcel` + +## 6.4 使用方式 + +### 6.4.1 导入模板 + +导入 DTO 继承 `BaseImportInput`,并通过 `ImporterHeader` / `ExporterHeader` 控制列行为。 + +### 6.4.2 导入校验 + +使用 `ExcelHelper.ImportData()`,可在导入后对数据做业务校验,并将错误信息回写到导出文件中。 + +### 6.4.3 字典转换 + +使用现有字典特性和 `CommonUtil` 中的导入导出映射,不要自己维护文本值与编码值转换逻辑。 + +## 6.5 开发建议 + +- 导入模板与业务表分离,避免直接暴露实体 +- 错误信息统一进入 `Error` 字段 +- 大批量导入建议加异步处理与日志记录 + +## 6.6 注意事项 + +- 导入文件应复用上传服务处理,不要绕过文件统一能力 +- 字段顺序与列名变化后,应同步更新模板与导入 DTO + +# 第七章 上传下载 + +## 7.1 功能说明 + +当前脚手架的文件能力统一由 `SysFileService` 承载,支持: + +- 单文件上传 +- 多文件上传 +- Base64 上传 +- 文件下载 +- 文件预览 +- Base64 下载 +- 头像上传 +- 电子签名上传 +- 文件与业务主键绑定 + +## 7.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs` +- `Admin.NET/Admin.NET.Core/Service/File/FileProvider/DefaultFileProvider.cs` +- `Admin.NET/Admin.NET.Core/Service/File/FileProvider/MultiOSSFileProvider.cs` +- `Admin.NET/Admin.NET.Core/Service/File/FileProvider/OSSFileProvider.cs` +- `Admin.NET/Admin.NET.Core/Service/File/FileProvider/SSHFileProvider.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Upload.json` + +## 7.3 配置说明 + +主配置节点: + +- `Upload` +- `OSSProvider` +- `SSHProvider` +- `MultiOSS` + +重点配置项: + +- `Upload.Path` +- `Upload.MaxSize` +- `Upload.ContentType` +- `Upload.EnableMd5` +- `OSSProvider.Enabled` +- `OSSProvider.Provider` +- `MultiOSS.Enabled` + +## 7.4 使用方式 + +### 7.4.1 普通业务文件 + +直接调用 `UploadFile()` 或 `UploadFiles()`。 + +### 7.4.2 图片类附件 + +可通过 `UploadAvatar()`、`UploadSignature()` 进行用户头像和签名文件处理。 + +### 7.4.3 业务实体关联文件 + +通过: + +- `UpdateFileByDataId()` +- `DeleteFileByDataId()` + +实现文件和业务主键绑定与清理。 + +## 7.5 开发建议 + +- 附件统一保存到 `SysFile` +- 不在业务表中直接存物理路径 +- 需要多存储时优先使用 MultiOSS,而不是手写多套上传逻辑 + +## 7.6 注意事项 + +- 当前校验是基于 ContentType 与后缀,不是完整的内容安全扫描 +- 高安全场景建议叠加病毒扫描、MIME 深度校验和访问鉴权控制 + +# 第八章 模板打印 + +## 8.1 功能说明 + +当前脚手架关于“模板打印”已具备两层底座: + +- 打印模板管理 +- 通用模板渲染 + +适合构建: + +- 消息模板 +- 标签模板 +- 工票模板 +- 通知模板 +- 业务文本渲染模板 + +## 8.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs` +- `Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs` +- `Admin.NET/Admin.NET.Core/Entity/SysPrint.cs` +- `Admin.NET/Admin.NET.Core/Entity/SysTemplate.cs` + +## 8.3 使用方式 + +### 8.3.1 打印模板管理 + +通过 `SysPrintService` 管理打印模板的增删改查和分页查询。 + +### 8.3.2 通用模板渲染 + +通过 `SysTemplateService.Render()` 或 `RenderByCode()` 渲染模板内容,底层依赖 `IViewEngine`。 + +### 8.3.3 模板预览 + +通过 `ProView()` 对模板内容进行预览。 + +## 8.4 当前边界 + +当前已确认的能力更偏: + +- 模板管理 +- 模板文本渲染 +- 打印模板元数据管理 + +当前未确认存在统一的: + +- 服务端 PDF 输出中心 +- 打印任务队列 +- 打印机路由管理 +- 标签批量套打总控 + +## 8.5 开发建议 + +- 业务变化频繁的打印内容优先用模板存储,不要写死在代码里 +- 模板编码统一由业务常量管理 +- 打印数据组装放业务服务,模板渲染放模板服务 + +## 8.6 注意事项 + +- 模板层适合“渲染”,不适合承载复杂业务判断 +- 真正复杂的打印排版仍建议在独立打印模块中继续扩展 + +# 第九章 统一认证 + +## 9.1 功能说明 + +当前脚手架认证体系包含三条链路: + +- JWT 认证 +- Signature 开放接口签名认证 +- OAuth 第三方登录认证 + +## 9.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +- `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` +- `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs` +- `Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs` + +## 9.3 使用方式 + +### 9.3.1 JWT 认证 + +默认全局开启,未显式标记匿名的接口均需鉴权。 + +### 9.3.2 Signature 认证 + +用于开放接口,对外部系统通过 `accessKey/accessSecret + timestamp + nonce + sign` 做签名认证。 + +### 9.3.3 OAuth + +当前默认接入: + +- Weixin +- Gitee + +## 9.4 当前能力 + +`SysAuthService` 已实现: + +- 账号密码登录 +- 手机验证码登录 +- 用户注册 +- 获取验证码 +- 退出登录 +- 获取当前登录信息 +- 刷新 Token +- Swagger 登录 + +## 9.5 开发建议 + +- 管理后台接口走 JWT +- 对外系统接口走 Signature +- 三方登录场景走 OAuth,不混用 JWT 登录入口 + +## 9.6 注意事项 + +- 全局鉴权开启后,开放接口需显式走对应认证方案 +- 不要把对外开放接口直接暴露成后台 JWT 接口 + +# 第十章 数据权限 + +## 10.1 功能说明 + +当前项目的数据权限由 SqlSugar 全局过滤器统一实现,不要求每个服务手写权限 where 条件。 + +## 10.2 关键文件 + +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/Enum/DataScopeEnum.cs` +- `Admin.NET/Admin.NET.Core/Entity/IEntityFilter.cs` + +## 10.3 当前实现内容 + +- 软删除过滤 +- 租户过滤 +- 机构过滤 +- 仅本人数据过滤 +- 自定义实体过滤 + +## 10.4 使用方式 + +### 10.4.1 默认行为 + +只要实体继承了相应基类或实现了相应过滤接口,就会在查询时自动生效。 + +### 10.4.2 自定义过滤器 + +可以实现 `IEntityFilter`,通过 `AddEntityFilter()` 增加自定义条件。 + +### 10.4.3 缓存清理 + +角色、机构、用户权限变化后,需要清理相关缓存,否则数据范围可能延迟生效。 + +## 10.5 开发建议 + +- 业务查询先考虑是否被过滤器影响 +- 不是所有跨租户、跨组织查询都应该 `ClearFilter()` +- 必须绕过过滤器时,要明确记录用途和权限边界 + +## 10.6 注意事项 + +- 查询结果“不全”时,优先排查过滤器 +- 超管与普通用户的过滤行为不同 + +# 第十一章 即时通讯 + +## 11.1 功能说明 + +当前项目使用 SignalR 承载即时通讯与实时通知能力,适合: + +- 在线用户 +- 系统通知 +- 实时消息推送 +- 看板状态刷新 + +## 11.2 关键文件 + +- `Admin.NET/Admin.NET.Core/SignalR/SignalRSetup.cs` +- `Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` + +## 11.3 使用方式 + +### 11.3.1 单机模式 + +默认直接注册 SignalR 即可运行。 + +### 11.3.2 集群模式 + +启用 Redis 缓存并开启 `Cluster.Enabled=true` 后,可使用 Redis Backplane 实现跨节点广播。 + +## 11.4 开发建议 + +- 即时消息只承载通知,不承载重业务事务 +- 重要消息应同时落业务表或消息表,不要只依赖内存推送 + +## 11.5 注意事项 + +- 多节点部署时,单机 SignalR 无法跨节点广播 +- 类名与程序集名可能影响 Redis Channel 路由隔离 + +# 第十二章 图片压缩 + +## 12.1 功能说明 + +当前项目已接入 ImageSharp 中间件,具备图片处理底座,但尚未形成统一的上传即压缩、缩略图生成、质量策略中心。 + +## 12.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs` + +## 12.3 当前状态 + +当前已接入: + +- `services.AddImageSharp()` +- `app.UseImageSharp()` + +当前未确认统一存在: + +- 上传后自动压缩 +- 多尺寸缩略图生成 +- 图片质量分级输出 +- 图片 EXIF 清理 + +## 12.4 开发建议 + +- 若业务需要现场拍照压缩,建议扩展 `SysFileService` 或具体 `FileProvider` +- 压缩策略应可配置,不应写死分辨率、质量参数 + +## 12.5 注意事项 + +- 本章节当前属于“有底座、无统一业务实现” +- 后续扩展时应保持和现有文件存储体系一致 + +# 第十三章 接口限流 + +## 13.1 功能说明 + +当前项目已接入 `AspNetCoreRateLimit`,支持: + +- IP 限流 +- ClientId 限流 +- 黑白名单 +- 自定义超限返回值 + +## 13.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Option/RateLimitOptions.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Limit.json` + +## 13.3 使用方式 + +### 13.3.1 IP 限流 + +通过 `IpRateLimiting` 配置全局或接口级访问频率。 + +### 13.3.2 客户端限流 + +通过 `ClientRateLimiting` 基于客户端标识做限流。 + +## 13.4 开发建议 + +- 登录、短信发送、开放接口、设备接入接口应单独限流 +- 生产环境建议把通用限流规则按实际流量收紧 + +## 13.5 注意事项 + +- 示例规则阈值较高,不能直接作为生产值 +- 处于反向代理后方时应正确配置真实 IP 头 + +# 第十四章 国密信创 + +## 14.1 功能说明 + +当前项目明确支持国密相关能力,已接入: + +- SM2 +- SM4 +- MD5 + +其中认证链路默认已按国密模式适配。 + +## 14.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs` +- `Admin.NET/Admin.NET.Core/Option/CryptogramOptions.cs` +- `Admin.NET/Admin.NET.Application/Configuration/App.json` +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` + +## 14.3 使用方式 + +### 14.3.1 登录密码传输 + +前端密码可使用 SM2 加密传输,后端在 `VerifyPassword()` 中解密校验。 + +### 14.3.2 连接串加密 + +数据库连接串可通过配置开关启用加密读取。 + +### 14.3.3 通用加解密 + +业务如需统一加解密,可复用 `CryptogramUtil`。 + +## 14.4 开发建议 + +- 密钥统一配置,不要散落在代码中 +- 新系统上线前应同步更新公私钥 +- 如涉及等保、密评,应配套补充日志、访问控制、密钥管理制度 + +## 14.5 注意事项 + +- 修改公私钥后,前后端必须同步 +- 历史数据如使用旧算法,需要评估平滑迁移策略 + +# 第十五章 邮件发送 + +## 15.1 功能说明 + +邮件发送能力由 `SysEmailService` 提供,当前基于 SMTP 实现。 + +## 15.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Email.json` + +## 15.3 配置说明 + +主配置节点:`Email` + +重点配置项: + +- `Host` +- `Port` +- `EnableSsl` +- `DefaultFromEmail` +- `DefaultToEmail` +- `UserName` +- `Password` + +## 15.4 使用方式 + +通过 `SendEmail(content, title, toEmail)` 直接发送邮件。 + +## 15.5 开发建议 + +- 业务邮件内容优先结合模板服务渲染 +- 告警邮件建议通过事件总线异步发送 + +## 15.6 注意事项 + +- 当前实现更适合基础通知与告警,不是完整营销邮件平台 +- 大批量邮件场景应引入异步队列和发送状态管理 + +# 第十六章 短信发送 + +## 16.1 功能说明 + +当前短信能力支持: + +- 阿里云短信 +- 腾讯云短信 +- 自定义短信 HTTP 接口 + +并支持验证码缓存与校验。 + +## 16.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/SMS.json` + +## 16.3 使用方式 + +### 16.3.1 验证码发送 + +通过 `SendSms()` 发送验证码类短信。 + +### 16.3.2 验证码校验 + +通过 `VerifyCode()` 校验缓存中的验证码。 + +### 16.3.3 模板短信 + +阿里云模板短信可通过 `AliyunSendSmsTemplate()` 发送。 + +## 16.4 开发建议 + +- 验证码短信应叠加限流与防刷 +- 模板 ID、签名、过期时间统一在配置中维护 + +## 16.5 注意事项 + +- 自定义短信接口成功标识依赖配置项 `SuccessFlag` +- 自定义短信接口如果不规范,容易出现成功失败判断误差 + +# 第十七章 微信对接 + +## 17.1 功能说明 + +当前主项目内置的是微信公众号 / 小程序 / 微信支付相关能力。 + +## 17.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs` +- `Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Wechat.json` + +## 17.3 当前能力 + +- 公众号 OAuth2 授权 URL 生成 +- 通过 code 获取 OpenId +- 微信用户入库 +- OpenId 登录 +- JSSDK 签名参数生成 +- 模板消息查询、发送、删除 +- 微信支付配置承载 + +## 17.4 开发建议 + +- C 端公众号、小程序统一复用现有服务 +- 企业微信请走 `WorkWeixin` 插件,不混在 `SysWechatService` 中 + +## 17.5 注意事项 + +- 微信支付配置涉及证书、回调地址与密钥,生产环境应走安全配置管理 +- OpenId 登录只适合明确的微信用户体系,不适合通用后台账号体系 + +# 第十八章 多租户 SAAS + +## 18.1 功能说明 + +多租户是当前脚手架核心能力之一,支持: + +- 同库租户隔离 +- 独立库租户隔离 +- 租户切换 +- 租户初始化 +- 租户菜单授权 +- 租户管理员初始化 + +## 18.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` + +## 18.3 使用方式 + +### 18.3.1 隔离模式 + +- `TenantTypeEnum.Id` +- `TenantTypeEnum.Db` + +### 18.3.2 租户连接切换 + +由 `SqlSugarRepository` 在构造阶段自动判断。 + +### 18.3.3 租户初始化 + +新增租户后会自动初始化: + +- 机构 +- 默认角色 +- 岗位 +- 管理员账号 +- 菜单授权 + +## 18.4 开发建议 + +- 所有业务表在建模初期就要决定是否租户隔离 +- 涉及租户扩容、迁移、归档时,应基于当前连接模型扩展,不要重造租户访问层 + +## 18.5 注意事项 + +- 租户切换和登录租户不是同一个概念,需关注 Claim 与实体里的 `TenantId` +- 数据权限和租户过滤同时存在时,查询结果可能叠加收缩 + +# 第十九章 远程请求 + +## 19.1 功能说明 + +当前项目统一远程调用能力已经接入,推荐优先走声明式 HTTP。 + +## 19.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Startup.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/IDingTalkApi.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/Proxy/*` +- `Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/Startup.cs` + +## 19.3 使用方式 + +### 19.3.1 声明式 HTTP + +适用于: + +- DingTalk +- WorkWeixin + +### 19.3.2 命名 HttpClient + +适用于: + +- K3Cloud + +## 19.4 开发建议 + +- 新增三方对接优先使用声明式接口 +- 将配置、DTO、代理、服务放在同一模块内 +- 对外部接口返回结果统一包装 + +## 19.5 注意事项 + +- `WorkWeixin` 当前插件虽然提供了代理接口,但默认应用层未引用,启用前需补项目引用 +- 自定义 `HttpClient` 不应在业务代码中到处散落 + +# 第二十章 消息队列 + +## 20.1 功能说明 + +当前项目的消息能力主要通过 EventBus 承载,已支持: + +- 内存事件源 +- Redis 事件源 +- 失败重试 +- 失败回调 +- 事件监视器 + +## 20.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs` +- `Admin.NET/Admin.NET.Application/Configuration/EventBus.json` + +## 20.3 当前状态 + +- 内存事件总线可直接用 +- Redis 事件源在缓存启用 Redis 时可自动接入 +- RabbitMQ 存储器已有代码,但默认未在 `Startup` 中打开 + +## 20.4 使用方式 + +- 发布事件:通过 `IEventPublisher` +- 订阅事件:通过 `IEventSubscriber` +- 异常告警、登录事件、日志扩展都可以走事件化解耦 + +## 20.5 开发建议 + +- 适合将非主事务动作改为异步事件 +- 业务事件命名应稳定,避免后期难以追踪 + +## 20.6 注意事项 + +- 当前更偏事件驱动底座,不是完整企业级 MQ 平台 +- 需要严格投递确认、死信队列、消费组管理时,应在现有基础上继续封装 + +# 第二十一章 定时任务 + +## 21.1 功能说明 + +当前项目有两类时间相关能力: + +- 框架级定时任务 +- 业务级日程管理 + +## 21.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs` +- `Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs` + +## 21.3 使用方式 + +### 21.3.1 框架级定时任务 + +通过 `AddSchedule(...)` 注册,支持: + +- 持久化 +- 监控 +- 调度看板 +- 动态作业编译 + +### 21.3.2 业务日程 + +通过 `SysScheduleService` 管理个人或业务日程。 + +## 21.4 开发建议 + +- 系统级同步、统计、清理任务走调度器 +- 个人提醒、业务跟进走日程服务 + +## 21.5 注意事项 + +- 两类能力不要混用 +- 动态作业代码需做好来源控制与安全约束 + +# 第二十二章 令牌 Token + +## 22.1 功能说明 + +Token 体系由 `SysAuthService + JwtHandler` 统一控制,包含: + +- AccessToken +- RefreshToken +- Token 黑名单 +- 自动续期 + +## 22.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +- `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` + +## 22.3 使用方式 + +### 22.3.1 登录签发 + +`CreateToken()` 统一生成 AccessToken 与 RefreshToken。 + +### 22.3.2 自动续期 + +`JwtHandler` 在认证管线中自动处理。 + +### 22.3.3 退出失效 + +`Logout()` 将旧 Token 写入黑名单缓存。 + +## 22.4 开发建议 + +- Token 生命周期与业务安全等级匹配 +- 统一在响应头传递 Token + +## 22.5 注意事项 + +- 黑名单依赖缓存,单机模式下跨节点无共享 +- 不建议业务层自行生成或解析 JWT + +# 第二十三章 日志记录 + +## 23.1 功能说明 + +当前日志体系支持: + +- 控制台日志 +- 文件日志 +- 数据库日志 +- ES 日志 +- 访问日志 +- 操作日志 +- 异常日志 +- 差异日志 +- 慢 SQL / 错误 SQL + +## 23.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Logging/LoggingSetup.cs` +- `Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs` +- `Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs` +- `Admin.NET/Admin.NET.Core/Service/Log/*` +- `Admin.NET/Admin.NET.Application/Configuration/Logging.json` + +## 23.3 使用方式 + +- 全局日志由框架自动接入 +- 业务日志可通过系统日志服务查询 +- SQL 日志由 SqlSugar AOP 自动挂接 + +## 23.4 开发建议 + +- 关键业务动作记录操作日志 +- 重要数据变更考虑开启差异日志 +- 批量处理任务避免全量打印大量明细日志 + +## 23.5 注意事项 + +- 全局监控日志开启会有性能成本 +- ES 日志需确认存储生命周期与索引策略 + +# 第二十四章 代码生成 + +## 24.1 功能说明 + +代码生成是当前脚手架成熟度较高的能力,支持: + +- 表导入 +- 字段同步 +- 代码预览 +- 本地生成 +- ZIP 打包 +- 菜单按钮自动生成 +- 前后端联动生成 + +## 24.2 关键文件 + +- `Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs` +- `Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs` +- `Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs` +- `Admin.NET/Admin.NET.Application/Configuration/CodeGen.json` + +## 24.3 使用方式 + +### 24.3.1 基础流程 + +1. 导入表结构 +2. 编辑生成配置 +3. 同步字段 +4. 预览模板 +5. 生成本地代码或 ZIP + +### 24.3.2 生成范围 + +- 后端 Service / DTO +- 前端页面 +- API 文件 +- 菜单与按钮权限 + +## 24.4 开发建议 + +- 代码生成适合标准 CRUD +- 复杂业务流程不要强压模板层 +- 生成后的代码应进入业务应用层继续手工收敛 + +## 24.5 注意事项 + +- 表字段变更后要及时同步生成配置 +- 命名空间、页面路径、菜单路径要在生成前确认清楚 + +# 第二十五章 可视化大屏 + +## 25.1 功能说明 + +当前大屏能力由 `GoView` 插件提供,已经具备基础后端支撑。 + +## 25.2 关键文件 + +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Startup.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/GoViewProService.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs` + +## 25.3 当前能力 + +- 大屏项目管理 +- 大屏内容保存 +- 发布状态管理 +- 预览图上传 +- 背景图上传 +- 独立返回模型 + +## 25.4 开发建议 + +- 大屏数据聚合逻辑放业务服务 +- 大屏插件负责项目管理和内容持久化 +- 生产、设备、能源、质量等指标查询建议由独立接口层聚合输出 + +## 25.5 注意事项 + +- 当前插件默认已引用,可直接启用 +- 图片数据目前可直接保存在插件实体中,后续如需统一对象存储,可再向 `SysFileService` 收敛 + +# 第二十六章 OpenAPI + +## 26.1 功能说明 + +当前项目的 OpenAPI 包含两部分: + +- 内部接口文档 +- 对外开放接口 + +## 26.2 关键文件 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Swagger.json` +- `Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs` +- `Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs` + +## 26.3 当前能力 + +### 26.3.1 内部接口文档 + +- Swagger +- Scalar +- 接口分组 +- Swagger 登录 + +### 26.3.2 对外开放接口 + +- AccessKey / AccessSecret 管理 +- HMAC-SHA256 签名 +- 时间戳校验 +- nonce 防重放 +- 认证成功后 Claim 注入 + +## 26.4 使用方式 + +### 26.4.1 内部接口 + +通过 Swagger / Scalar 调试和查看文档。 + +### 26.4.2 对外接口 + +通过 Signature 认证方案暴露接口。 + +示例接口:`DemoOpenApi` + +## 26.5 开发建议 + +- 对外系统接口使用独立开放身份 +- 结合限流、日志、租户绑定一起设计 +- 外部系统不要直接使用后台 JWT + +## 26.6 注意事项 + +- 开放接口默认也要考虑多租户与数据权限问题 +- 签名算法、时间偏差、nonce 过期时间应与对接方提前约定 + +--- + +## 附录一:常用配置清单 + +当前后端高频配置文件位于 `Admin.NET/Admin.NET.Application/Configuration/`: + +- `App.json` +- `Database.json` +- `Cache.json` +- `Upload.json` +- `Email.json` +- `SMS.json` +- `Wechat.json` +- `OAuth.json` +- `EventBus.json` +- `Logging.json` +- `CodeGen.json` +- `Swagger.json` +- `Limit.json` + +## 附录二:推荐二开约束 + +### A. 分层约束 + +- 平台基础设施放 `Admin.NET.Core` +- 业务应用服务放应用层或插件层 +- 第三方集成优先插件化 + +### B. 复用约束 + +优先复用: + +- `SqlSugarRepository` +- `SysCacheService` +- `SysAuthService` +- `SysTenantService` +- `SysFileService` +- `SysTemplateService` +- `AdminResultProvider` + +### C. 安全约束 + +- 不硬编码连接串、密钥、账号、审批规则 +- 不直接信任请求输入的排序、过滤、路径、文件类型 +- 对开放接口、短信、登录、上传统一加限流与日志 + +### D. 可配置约束 + +- 状态流转、审批、阈值、公式优先配置化 +- 多变规则优先落表、字典或 JSON 规则,而不是堆 if-else + +## 附录三:当前脚手架能力结论 + +### 已形成较完整后端底座的能力 + +- 框架启动装配 +- 数据库与多租户 +- 统一鉴权 +- Token +- 缓存 +- 文件 +- 模板 +- 短信 +- 邮件 +- 日志 +- 限流 +- 代码生成 +- 可视化大屏 +- OpenAPI + +### 已有底座但通常还需继续业务化封装的能力 + +- 图片压缩 +- 服务端打印编排 +- 企业级消息队列治理 +- 复杂设备接入编排 +- 复杂第三方集成编排 + +## 附录四:后续建议 + +如果你准备继续把这套脚手架用于制造业项目,建议下一步按业务域继续拆文档: + +1. `MES` 生产执行模块设计指南 +2. `WMS` 库存与条码模块设计指南 +3. `QMS` 质量与检验模块设计指南 +4. `DMS` 设备与点检模块设计指南 +5. `EMS` 能源采集与统计模块设计指南 + +这样可以把“脚手架手册”和“业务域手册”彻底分开,后续维护成本更低。 + +## 附录五:26章深度补充 + +### 1. 框架简介补充 + +#### 1.1 统一技术路线 + +当前脚手架不是传统意义上“只给一套目录结构”的空框架,而是已经把平台常见基础能力做了统一门面和统一入口: + +- 数据库统一入口:`SqlSugarSetup` + `SqlSugarRepository` +- 缓存统一入口:`SysCacheService` +- 鉴权统一入口:`SysAuthService` + `JwtHandler` +- 租户统一入口:`SysTenantService` +- 文件统一入口:`SysFileService` +- 模板统一入口:`SysTemplateService` +- 日志统一入口:`LoggingSetup` + `SysLog*Service` +- OpenAPI 统一入口:`SysOpenAccessService` + +实际意义是:后续二开时,不应该把这些能力再拆成第二套公共层,否则维护成本会快速失控。 + +#### 1.2 与传统 MVC 项目的差异 + +和常见 ASP.NET Core 三层结构相比,本项目有几个关键差异: + +- 很多服务直接实现 `IDynamicApiController`,无需单独建 Controller +- 返回值默认统一包装,不建议业务方法自己构造自定义响应对象 +- 数据权限、租户过滤、软删除过滤由 ORM 全局挂载,不建议每个查询手写 +- 配置通过 Options 绑定,不建议业务层直接到处 `GetSection("xxx")` +- 插件不是样例性质,而是正式扩展模式 + +#### 1.3 适合的项目类型 + +这套后端脚手架比较适合: + +- 多组织、多角色、多菜单权限的后台系统 +- 存在租户隔离诉求的 SaaS 系统 +- 需要代码生成加速标准 CRUD 的管理系统 +- 需要文件、模板、消息、日志、调度等平台能力的中大型业务系统 +- 制造业场景下需要长期演进的平台化项目 + +不太适合: + +- 极致轻量、极少模块、无权限体系的简单单页接口服务 +- 强调极致低延迟的纯网关型服务 +- 完全事件驱动且几乎无后台管理页面的系统 + +### 2. 开发流程补充 + +#### 2.1 建议的二开顺序 + +如果你要在当前脚手架上开发一个新业务模块,建议按这个顺序做: + +1. 先确认该模块是否需要租户隔离 +2. 再确认该模块是否存在组织数据权限 +3. 再确认是否要纳入代码生成 +4. 再定义实体、枚举、字典、状态机和规则配置 +5. 最后再写服务、接口、导入导出和联动消息 + +这样做的原因是:一旦实体基类、权限边界、租户模型选错,后面返工成本很高。 + +#### 2.2 一个标准模块的最小落地清单 + +一个标准后端业务模块建议至少包含: + +- 实体类 +- 输入 DTO / 输出 DTO +- Service +- 必要枚举 +- 必要字典配置 +- 菜单权限 +- 日志埋点 +- 导入导出能力 +- 单元测试或集成验证清单 + +#### 2.3 制造业项目的额外建议 + +对于 `MES/WMS/QMS/DMS/EMS` 场景,建议在开发流程里固定加上三项: + +- 状态定义表或字典表设计 +- 设备、工单、批次、库位等主数据边界确认 +- 追溯字段和操作日志字段确认 + +原因是这些模块后期最容易因为“状态硬编码”和“追溯字段缺失”导致无法扩展。 + +### 3. 数据库配置补充 + +#### 3.1 `SqlSugarSetup` 的实际职责 + +`SqlSugarSetup` 不只是“注册 ORM”,它实际上承担了数据库基础设施的大部分核心动作: + +- 注册雪花 ID +- 处理多数据库连接配置 +- 初始化 `SqlSugarScope` +- 为每个连接挂 AOP +- 统一执行审计字段自动赋值 +- 统一处理软删过滤 +- 统一处理租户过滤 +- 统一处理数据权限过滤 +- 统一处理 SQL 错误与超时日志 +- 初始化库、表、视图、种子 + +换句话说,数据库行为的很多“默认规则”不是散落在业务里,而是集中在这里。 + +#### 3.2 库表初始化建议 + +当前脚手架支持自动建表、自动建视图、自动种子。建议按环境控制: + +- 本地开发环境:可开启表和种子初始化 +- 测试环境:仅在结构变更时短期开启 +- 生产环境:默认关闭,改用正式发布脚本 + +如果长期在生产环境开启: + +- 可能因误改实体导致结构被自动同步 +- 可能在高并发启动时造成视图/表锁问题 +- 可能因种子更新影响线上数据 + +#### 3.3 数据库类型与信创兼容 + +代码已经兼容多种数据库类型,配置层面对国产数据库也预留了能力,例如: + +- PostgreSQL +- Oracle +- SqlServer +- MySql +- Sqlite +- Kdbndp +- Dm + +如果要真正落地信创,需要额外补以下验证: + +- 字段类型映射验证 +- 视图初始化语法兼容验证 +- 差异日志和分页 SQL 验证 +- 索引策略验证 +- 连接串与驱动兼容验证 + +### 4. 实体基类补充 + +#### 4.1 为什么必须优先选对基类 + +本项目的实体基类不是简单字段复用,而是直接参与下面几类行为: + +- 审计字段自动赋值 +- 逻辑删除过滤 +- 租户过滤 +- 机构过滤 +- 所属人过滤 + +如果基类选错,会出现下面几类典型问题: + +- 业务数据没有自动带上 `TenantId` +- 软删记录被误查出来 +- 组织数据权限不生效 +- 审计字段为空 + +#### 4.2 建模建议 + +针对制造业系统,建议按对象类型选择基类: + +- 平台基础表:`EntityBase` / `EntityBaseDel` +- 组织敏感业务表:`EntityBaseOrg` / `EntityBaseOrgDel` +- 租户业务主表:`EntityBaseTenant` / `EntityBaseTenantDel` +- 既要租户又要机构:`EntityBaseTenantOrg` / `EntityBaseTenantOrgDel` + +例如: + +- 工单、批次、质检单、出入库单:通常建议走租户业务基类 +- 组织维度统计表、岗位权限表:通常建议走带机构基类 + +#### 4.3 实体层扩展原则 + +业务实体扩展时建议: + +- 状态字段用枚举 + 字典组合,不只用裸字符串 +- 金额、数量、重量统一考虑精度 +- 追溯字段如批次、工序、设备、库位要在设计初期补齐 + +### 5. 缓存管理补充 + +#### 5.1 `SysCacheService` 的实际定位 + +`SysCacheService` 不是简单包装 Redis,而是平台缓存访问门面。它封装的价值包括: + +- 屏蔽 Memory / Redis 差异 +- 统一缓存前缀 +- 统一锁模型 +- 统一 Hash 操作 +- 统一缓存穿透防护式读取 + +因此业务代码应依赖这个门面,而不是依赖具体缓存实现。 + +#### 5.2 典型缓存键分类建议 + +建议按以下结构设计缓存 Key: + +- 系统参数类:`config:*` +- 权限类:`menu:*`、`role:*`、`button:*` +- 用户会话类:`token:*`、`blacklist:*` +- 防重复类:`nonce:*`、`lock:*` +- 业务快照类:`biz:模块:主键` + +#### 5.3 多节点场景注意点 + +以下能力如果部署为多节点,强烈建议切到 Redis: + +- Token 黑名单 +- 登录失败次数 +- 开放接口 nonce +- SignalR Backplane +- 任务状态共享 +- 设备或消息中心的实时状态缓存 + +#### 5.4 不推荐的做法 + +- 在业务代码中使用 `Cache.Default` 作为默认缓存入口 +- 手动拼接不规范缓存键 +- 不设置过期时间就缓存高频波动业务数据 +- 用缓存替代正式主数据存储 + +### 6. 导入导出补充 + +#### 6.1 导入设计建议 + +对于导入功能,建议始终分三层: + +1. 模板层:列头、格式、必填项 +2. 解析层:Excel 到 DTO +3. 入库层:DTO 到实体,再做业务校验 + +不要把所有校验都写进 Excel 解析回调里,否则后续复用性很差。 + +#### 6.2 导入常见错误分类 + +建议把导入错误区分为: + +- 模板错误 +- 格式错误 +- 字典值错误 +- 主数据不存在 +- 业务规则冲突 +- 重复数据 + +当前脚手架已经能较好承载: + +- 模板错误 +- 行级字段错误 +- 错误信息回写 + +后续可继续扩展: + +- 唯一性校验明细 +- 错误分级 +- 导入任务批次号 + +#### 6.3 导出设计建议 + +导出时建议不要直接导实体,优先导专用输出 DTO,原因是: + +- 可控制字段范围 +- 可控制字典值转换 +- 可规避敏感字段误导出 +- 可避免导航属性或多余字段污染 + +### 7. 上传下载补充 + +#### 7.1 文件体系的角色分工 + +当前文件体系可理解为三层: + +- 文件元数据层:`SysFile` +- 文件服务层:`SysFileService` +- 存储适配层:`ICustomFileProvider` 及其实现 + +这样设计的好处是:业务只关心文件逻辑,不关心底层到底是本地、OSS 还是 SSH。 + +#### 7.2 `SysFileProviderService` 的意义 + +当前脚手架除了统一上传服务,还增加了 `SysFileProviderService` 作为多存储提供者配置中心,具备: + +- 提供者列表管理 +- 默认提供者切换 +- 提供者缓存 +- 存储桶管理 +- 配置合法性校验 + +这说明文件体系已经开始朝“平台级文件中心”演进,不只是单纯上传接口。 + +#### 7.3 制造业项目常见文件场景 + +你们后续很可能会遇到: + +- 质检附件 +- 设备巡检照片 +- 不合格品图片 +- 工艺文件 +- 作业指导书 +- 标签或单据模板 +- 批量导入文件 + +这些都建议统一走 `SysFileService`,不要每个模块各自存磁盘路径。 + +### 8. 模板打印补充 + +#### 8.1 `SysPrintService` 与 `SysTemplateService` 的差异 + +二者不要混淆: + +- `SysPrintService` 更偏“打印模板对象管理” +- `SysTemplateService` 更偏“通用文本/内容模板渲染” + +如果做标签打印、工票打印、发料单打印,通常是: + +1. `SysPrintService` 存模板定义 +2. 业务服务组装打印数据 +3. `SysTemplateService` 负责动态渲染 +4. 最终输出给前端打印控件或后端文件服务 + +#### 8.2 复杂打印建议 + +如果后续你们要支持: + +- 标签模板版本管理 +- 多纸张尺寸 +- 多打印机路由 +- 打印失败重试 +- 批量套打任务 + +建议在现有模板能力上单独再封装一个 `PrintModule`,而不是把所有逻辑堆在 `SysPrintService`。 + +### 9. 统一认证补充 + +#### 9.1 登录链路 + +一个标准后台登录请求大致经过: + +1. 验证码校验 +2. 登录失败次数校验 +3. 用户与租户获取 +4. 密码或域登录校验 +5. 生成 AccessToken / RefreshToken +6. 设置响应头 +7. 更新用户最后登录信息 +8. 发布登录事件 + +这条链路已经基本完整,后续新增登录方式时应尽量保持这一套主线不变。 + +#### 9.2 `JwtHandler` 的价值 + +`JwtHandler` 不只是鉴权,还承担: + +- 黑名单校验 +- 自动刷新 Token +- 路由按钮权限判断 + +这意味着权限不是“登录成功就算结束”,而是在每次请求阶段继续判定。 + +#### 9.3 开放接口认证建议 + +开放接口建议固定遵循: + +- 给每个外部系统单独发一组 `accessKey/accessSecret` +- 绑定具体用户和租户 +- 配套限流 +- 配套日志 +- 配套 nonce 防重放 + +不要多个系统共用一套开放凭据,否则审计和追责会非常麻烦。 + +### 10. 数据权限补充 + +#### 10.1 过滤器真正的执行位置 + +当前数据权限不是在 Controller 或 Service 里做,而是在 SqlSugar 查询执行前统一挂入。 + +所以出现权限问题时,排查顺序建议是: + +1. 是否继承了正确实体基类 +2. 当前登录用户角色数据范围是什么 +3. 当前请求头 TenantId 是什么 +4. 当前 Claim TenantId / UserId / OrgId 是什么 +5. 查询是否调用了 `ClearFilter()` + +#### 10.2 常见误区 + +- 以为查不到数据是 SQL 有问题,实际上是过滤器生效 +- 以为租户过滤和机构过滤只能选一个,实际上是会叠加 +- 以为超管一定无过滤,实际上软删过滤可配置是否忽略 + +### 11. 即时通讯补充 + +#### 11.1 在线用户的实际用途 + +SignalR 当前不只是聊天能力,更适合做: + +- 在线状态监控 +- 实时公告 +- 审批/待办提醒 +- 设备告警推送 +- 长流程业务节点反馈 + +#### 11.2 业务使用建议 + +即时通讯适合作为“通知层”,不适合作为“最终状态层”。 + +例如: + +- 设备告警消息可以实时推送 +- 但设备告警状态仍应落数据库 + +这样即使连接中断,也不会丢失关键业务状态。 + +### 12. 图片压缩补充 + +#### 12.1 为什么当前只能说“有底座” + +因为当前看到的是: + +- 注册了 ImageSharp +- 启用了 ImageSharp 中间件 + +但没有看到在 `SysFileService` 中统一定义: + +- 压缩比例 +- 是否生成缩略图 +- 是否保留原图 +- 图片质量参数 +- 不同业务目录的不同策略 + +所以当前不能把它描述成“完整图片压缩模块”。 + +#### 12.2 建议的扩展方向 + +后续如果你要落地这一章,建议新增: + +- 图片处理配置表或配置项 +- 上传后处理管线 +- 缩略图命名规范 +- 原图保留策略 +- 图像水印策略 + +### 13. 接口限流补充 + +#### 13.1 限流落点建议 + +建议把限流对象分成三类: + +- 后台用户接口 +- 开放接口 +- 高风险接口 + +高风险接口包括: + +- 登录 +- 获取验证码 +- 短信发送 +- 文件上传 +- 开放接口签名入口 +- 对外数据上报入口 + +#### 13.2 与网关限流的关系 + +如果后续前面还有网关或 API 管理层: + +- 网关负责粗粒度限流 +- 后端负责业务粒度限流 + +不要完全依赖网关,也不要在后端做得过重导致配置难维护。 + +### 14. 国密信创补充 + +#### 14.1 当前国密链路已落地的点 + +从代码看,当前国密并非只停留在工具类: + +- 登录密码传输已在认证服务中落地 +- 连接串加密能力已在数据库配置中预留 +- `App.json` 已有默认 `CryptoType` +- `CryptogramUtil` 已覆盖多个算法 + +这说明国密能力已经融入主链路,不是单独的实验性工具。 + +#### 14.2 后续信创项目建议 + +若用于正式信创项目,建议补齐: + +- 密钥轮换方案 +- 证书管理方案 +- 加密失败日志与告警 +- 国产数据库性能验证报告 +- 国产服务器部署基线 + +### 15. 邮件发送补充 + +#### 15.1 邮件能力的典型用法 + +当前邮件服务适合做: + +- 系统异常告警 +- 审批提醒 +- 定时报表发送 +- 注册或找回相关通知 + +不适合直接拿来做: + +- 大规模营销邮件 +- 邮件追踪统计平台 +- 多模板 A/B 测试平台 + +#### 15.2 与模板服务联动 + +比较推荐的方式是: + +1. 业务数据组装 +2. 调 `SysTemplateService.RenderByCode()` +3. 调 `SysEmailService.SendEmail()` + +这样可以把文案与逻辑分开维护。 + +### 16. 短信发送补充 + +#### 16.1 验证码链路建议 + +当前验证码短信功能具备基本闭环,但生产建议再加: + +- 发送频率限制 +- 每日发送上限 +- 图形验证码前置 +- 号码黑名单 +- 渠道异常告警 + +#### 16.2 多短信通道建议 + +当前已支持阿里、腾讯、自定义接口。后续如果要做主备通道,建议增加: + +- 通道优先级 +- 故障切换策略 +- 通道发送日志 +- 成本统计 + +### 17. 微信对接补充 + +#### 17.1 公众号、小程序、企业微信要分层理解 + +当前代码中实际是三套概念: + +- `SysWechatService`:公众号 / 小程序用户能力 +- `SysWechatPayService`:微信支付能力 +- `WorkWeixin` 插件:企业微信能力 + +这三者不要在文档和设计里混为一谈。 + +#### 17.2 微信支付现有能力 + +`SysWechatPayService` 当前已经具备: + +- JSAPI 下单 +- Native 下单 +- 服务商模式下单 +- 本地支付单查询 +- 微信侧支付单查询 +- 退款申请 +- 退款查询 +- 支付回调 +- 退款回调 + +这说明本章不仅有“微信用户对接”,其实还覆盖了支付链路。 + +### 18. 多租户 SAAS 补充 + +#### 18.1 租户新增后的默认初始化内容 + +当前新增租户后会自动初始化: + +- 机构 +- 默认角色 +- 岗位 +- 管理员账号 +- 菜单授权 + +这套初始化逻辑对 SaaS 项目非常关键,因为它保证租户开通后能直接登录和使用,而不需要人工一步步初始化基础权限。 + +#### 18.2 租户库模式的关键风险点 + +如果选择独立库隔离,需要额外关注: + +- 租户连接串安全 +- 租户库建库耗时 +- 多租户连接池数量 +- 租户迁移与归档 +- 库级备份恢复策略 + +### 19. 远程请求补充 + +#### 19.1 当前项目里的三类对外集成方式 + +可以把现有集成方式分成三类: + +- Furion 声明式 HTTP:如 DingTalk、WorkWeixin +- 命名 `HttpClient`:如 K3Cloud +- 插件式集成服务:如 DingTalkService、GoView、ReZero + +#### 19.2 插件能力概览 + +- `DingTalk`:已默认引用,可直接做钉钉接口和审批流程对接 +- `WorkWeixin`:已提供代理接口与配置类,但默认应用层未引用 +- `K3Cloud`:已提供配置与命名客户端,适合 ERP 集成入口 +- `ReZero`:提供超级 API 能力,并通过 `SuperApiAop` 复用 JWT 校验和日志链路 + +#### 19.3 集成规范建议 + +外部系统集成时建议统一做到: + +- 配置化 +- DTO 化 +- 结果统一封装 +- 错误日志统一记录 +- 与业务逻辑解耦 + +### 20. 消息队列补充 + +#### 20.1 事件总线和传统 MQ 的边界 + +当前脚手架更准确的说法是“具备事件驱动能力”,而不是已经完整搭好企业 MQ 平台。 + +因为它已具备: + +- 发布订阅 +- 重试 +- fallback +- Redis 事件源 + +但未完整体现: + +- 可视化消费组管理 +- 死信队列治理 +- 顺序消费控制 +- 分区消费治理 +- 大规模消息堆积治理 + +#### 20.2 适合接入的业务场景 + +适合: + +- 登录事件 +- 异常告警 +- 审批通知 +- 第三方异步同步 +- 文件处理后事件 + +不建议一开始就用它承载超大规模设备遥测主链路。 + +### 21. 定时任务补充 + +#### 21.1 当前任务体系的两条主线 + +很多项目会把“任务调度”和“业务日程”写在一起,但当前脚手架其实已经分清楚了: + +- 调度器:系统级后台作业 +- 日程服务:用户或业务日程数据 + +这个区分是对的,后续扩展时要保持。 + +#### 21.2 制造业场景适合的调度任务 + +常见适合放入调度器的任务: + +- 班次统计汇总 +- 设备状态同步 +- 库存快照生成 +- 质检逾期预警 +- 日报周报生成 +- ERP / MES / WMS 数据对账 + +### 22. 令牌 Token 补充 + +#### 22.1 Token 相关默认行为 + +当前默认行为包括: + +- 登录签发 AccessToken 和 RefreshToken +- 在响应头回传 +- 鉴权时做黑名单校验 +- 在需要时自动续期 +- 退出时写入黑名单 + +这意味着前后端配合时,响应头 Token 处理规则必须稳定。 + +#### 22.2 高安全项目建议 + +如果后续安全要求更高,建议再加: + +- 设备指纹 +- 登录地异常校验 +- 多端登录策略配置 +- RefreshToken 单独存储和吊销策略 +- Token 审计日志 + +### 23. 日志记录补充 + +#### 23.1 日志应如何分层看 + +当前日志体系可以拆成四层: + +- 平台运行日志 +- 业务操作日志 +- 异常日志 +- 数据差异日志 + +其中差异日志对制造业很重要,因为很多主数据和工单、质检数据要求可追溯。 + +#### 23.2 建议重点保留的日志 + +对于制造业平台,建议重点保留: + +- 主数据变更日志 +- 审批操作日志 +- 工单状态变更日志 +- 库存调整日志 +- 质检结论变更日志 +- 接口异常日志 + +### 24. 代码生成补充 + +#### 24.1 当前代码生成的真正价值 + +代码生成的价值不是“生成一次后永远不用管”,而是: + +- 快速建立标准模块骨架 +- 统一后端目录和前端目录 +- 统一菜单和按钮权限命名 +- 统一 DTO 和页面结构 + +因此它更适合当“模块起步工具”,而不是“复杂业务最终实现工具”。 + +#### 24.2 适合生成的模块 + +适合: + +- 主数据管理 +- 参数管理 +- 基础业务台账 +- 标准 CRUD 配置类模块 + +不适合直接完全生成: + +- 复杂审批流 +- 多状态机驱动业务 +- 多表联动强事务业务 +- 复杂排程类业务 + +### 25. 可视化大屏补充 + +#### 25.1 大屏插件的定位 + +GoView 插件更像“项目承载层”,而不是“业务计算引擎”。 + +它负责: + +- 项目内容保存 +- 发布状态管理 +- 预览图与背景图 + +业务实际要展示的数据应由业务 API 提供。 + +#### 25.2 制造业大屏落地建议 + +如果用于制造业大屏,建议数据来源按主题拆接口: + +- 生产主题 +- 质量主题 +- 仓储主题 +- 设备主题 +- 能源主题 + +不要把所有统计塞到一个超大接口里。 + +### 26. OpenAPI 补充 + +#### 26.1 内部文档与开放接口要分开治理 + +当前项目同时具备: + +- 内部接口文档系统 +- 对外开放接口身份体系 + +这两个能力可以共存,但治理方式不同: + +- 内部文档重点是开发调试效率 +- 对外开放接口重点是安全、审计、稳定性 + +#### 26.2 对外开放接口建议的最小治理清单 + +建议至少做到: + +- 每个系统独立凭据 +- 请求签名 +- nonce 防重放 +- 时间戳校验 +- 调用日志 +- 限流 +- 租户绑定 +- 用户绑定 + +#### 26.3 `AdminResultProvider` 的补充价值 + +当前统一返回不仅影响普通接口,也影响: + +- 鉴权失败 +- 校验失败 +- 系统异常 +- 401 / 403 / 302 + +这对开放接口很重要,因为它保证对外响应结构一致,不会因为不同异常路径而返回完全不同的格式。 + +## 附录六:发布版章节补充 + +### 1. 框架简介发布版补充 + +#### 常见问题 + +- 不清楚项目真正的后端入口在哪里 +- 误以为 `Admin.NET.Application` 是全部后端核心 +- 新人不知道应该先看哪几个文件建立上下文 + +#### 排错建议 + +- 先确认启动注册是否在 `Admin.NET.Web.Core/Startup.cs` +- 再确认全局配置绑定是否在 `ProjectOptions.cs` +- 再确认能力最终归属是在 `Admin.NET.Core` 还是插件 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Web.Entry/Program.cs` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs` +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` + +#### 推荐扩展写法 + +- 平台基础能力放 `Admin.NET.Core` +- 行业集成或外部系统对接优先做插件 +- 强业务服务优先放应用层或新增业务工程 + +### 2. 开发流程发布版补充 + +#### 常见问题 + +- 业务开发直接改核心层,后期升级困难 +- 新功能上来就写 SQL,没有先确认是否已有封装 +- 只开发接口,不补字典、权限、日志和导入导出 + +#### 排错建议 + +- 新模块开发前,先搜索是否已有同类系统服务 +- 如果某能力看似缺失,先查 `Service`、`Utils`、`Extension`、`Plugins` +- 如果改动涉及全局行为,先确认是否应进 `Startup` 或 `ProjectOptions` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/*` +- `Admin.NET/Admin.NET.Core/Utils/*` +- `Admin.NET/Admin.NET.Core/Extension/*` +- `Admin.NET/Plugins/*` + +#### 推荐扩展写法 + +- 新模块至少补齐实体、DTO、Service、菜单权限、日志点 +- 规则多变的业务先做配置模型,再写执行逻辑 +- 对标准 CRUD 模块优先考虑代码生成,再二次收敛 + +### 3. 数据库配置发布版补充 + +#### 常见问题 + +- 启动时反复初始化表结构或种子数据 +- 查询到了错误的数据库连接 +- 开启连接串加密后数据库连不上 +- 生产环境误开自动建表 + +#### 排错建议 + +- 检查 `Database.json` 中 `EnableInitDb/EnableInitTable/EnableInitSeed/EnableInitView` +- 检查实体是否带 `TenantAttribute`、`SysTableAttribute`、`LogTableAttribute` +- 检查连接串加密配置键是否与 `DbConnectionOptions` 一致 +- 检查 `SqlSugarRepository` 是否因为请求头或 Claim 切到了租户库 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Application/Configuration/Database.json` +- `Admin.NET/Admin.NET.Core/Option/DbConnectionOptions.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` + +#### 推荐扩展写法 + +- 多数据库场景统一扩展 `ConnectionConfigs` +- 需要特殊数据库行为时优先补 `SqlSugarSetup.SetDbConfig()` +- 生产环境使用正式 SQL 脚本,不依赖自动建表 + +### 4. 实体基类发布版补充 + +#### 常见问题 + +- 新增业务表后租户字段不自动写入 +- 审计字段为空 +- 软删除逻辑不生效 +- 数据权限过滤不生效 + +#### 排错建议 + +- 检查是否继承了正确基类 +- 检查实体是否真的包含 `TenantId`、`OrgId`、`IsDelete` +- 检查 `SqlSugarSetup.DataExecuting` 是否会自动填充对应字段 +- 检查业务查询是否调用了 `ClearFilter()` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Entity/EntityBase.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` + +#### 推荐扩展写法 + +- 新业务实体统一基于现有基类扩展 +- 不要复制系统字段到业务实体里重写 +- 需要自定义过滤标记时,可新增接口或特性并在过滤器层统一处理 + +### 5. 缓存管理发布版补充 + +#### 常见问题 + +- 多节点环境下缓存不一致 +- 清理了缓存但业务看起来仍然没更新 +- 分布式锁没生效 +- Redis 切换后部分功能异常 + +#### 排错建议 + +- 检查当前 `CacheType` 是 `Memory` 还是 `Redis` +- 检查缓存 Key 是否带统一前缀 +- 检查是否通过 `SysCacheService` 操作,而不是直接用底层缓存 +- 检查是否清理了多个关联 Key,而不是只删一个主 Key + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Application/Configuration/Cache.json` +- `Admin.NET/Admin.NET.Core/Cache/CacheSetup.cs` +- `Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs` +- `Admin.NET/Admin.NET.Core/Const/CacheConst.cs` + +#### 推荐扩展写法 + +- 所有业务缓存统一定义常量前缀 +- 复杂缓存场景封装成专用缓存服务,不直接散写在业务方法里 +- 对需要跨节点一致性的功能强制要求 Redis + +### 6. 导入导出发布版补充 + +#### 常见问题 + +- Excel 导入模板和实际字段不一致 +- 字典文本导入后无法正确映射 +- 导入失败但错误信息不清晰 +- 直接导实体导致导出列过多 + +#### 排错建议 + +- 检查 DTO 是否继承 `BaseImportInput` +- 检查 `ImporterHeader/ExporterHeader` 是否和模板列一致 +- 检查字典字段是否正确配置 `ImportDictAttribute` +- 检查错误回写是否进入 `Error` 字段 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Utils/CommonUtil.cs` +- `Admin.NET/Admin.NET.Core/Utils/ExcelHelper.cs` +- `Admin.NET/Admin.NET.Core/Utils/BaseImportInput.cs` +- `Admin.NET/Admin.NET.Core/Attribute/ImportDictAttribute.cs` + +#### 推荐扩展写法 + +- 业务导入导出一律使用专用 DTO +- 错误标记与业务校验分层处理 +- 大批量导入可继续扩展成异步任务模式 + +### 7. 上传下载发布版补充 + +#### 常见问题 + +- 文件上传成功但预览或下载失败 +- 本地和 OSS 场景切换后 URL 异常 +- 文件删除了数据库记录但物理文件未删除 +- 多存储提供者下默认桶配置混乱 + +#### 排错建议 + +- 检查 `SysFileService` 最终选择了哪个 `FileProvider` +- 检查 `Upload.json` 中 `OSSProvider`、`SSHProvider`、`MultiOSS` +- 检查 `SysFileProviderService` 的默认提供者配置 +- 检查文件记录中的 `BucketName`、`Url`、`FilePath` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs` +- `Admin.NET/Admin.NET.Core/Service/File/SysFileProviderService.cs` +- `Admin.NET/Admin.NET.Core/Service/File/FileProvider/*` +- `Admin.NET/Admin.NET.Application/Configuration/Upload.json` + +#### 推荐扩展写法 + +- 多业务线文件统一接入 `SysFileService` +- 多存储场景统一通过 `SysFileProviderService` 管理 +- 高安全附件可在 Provider 层增加水印、病毒扫描、权限校验 + +### 8. 模板打印发布版补充 + +#### 常见问题 + +- 模板能保存但渲染结果不符合预期 +- 模板变量缺失导致运行异常 +- 打印模板和消息模板概念混用 +- 复杂打印需求误以为当前脚手架已完整支持 + +#### 排错建议 + +- 检查模板变量名与传入数据对象字段是否一致 +- 检查 `SysTemplateService.RenderAsync()` 的渲染表达式 +- 区分是 `SysPrintService` 的打印模板,还是 `SysTemplateService` 的通用模板 +- 对 PDF、套打、打印路由需求,先确认当前代码是否已有统一实现 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Print/SysPrintService.cs` +- `Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs` +- `Admin.NET/Admin.NET.Core/Entity/SysPrint.cs` +- `Admin.NET/Admin.NET.Core/Entity/SysTemplate.cs` + +#### 推荐扩展写法 + +- 打印模板元数据走 `SysPrintService` +- 动态内容渲染走 `SysTemplateService` +- 复杂打印流程单独再封装打印模块,不把逻辑堆在现有两个服务中 + +### 9. 统一认证发布版补充 + +#### 常见问题 + +- 登录成功但后续接口仍返回 401 +- 开放接口签名认证失败 +- OAuth 登录成功但后台权限链路不通 +- Swagger 登录可用但普通接口授权失败 + +#### 排错建议 + +- 检查响应头里是否正确返回了 `access-token` / `refresh-token` +- 检查前端是否按 Bearer 规范携带 Token +- 检查 `JwtHandler` 是否因为黑名单或权限判定导致失败 +- 检查开放接口请求头中的 `accessKey/timestamp/nonce/sign` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +- `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` +- `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs` +- `Admin.NET/Admin.NET.Core/Service/OAuth/OAuthSetup.cs` + +#### 推荐扩展写法 + +- 新增登录方式时复用 `CreateToken()` 主链路 +- 后台接口继续走 JWT,不要自行另发 Token +- 开放接口继续走 Signature,不和后台用户鉴权混用 + +### 10. 数据权限发布版补充 + +#### 常见问题 + +- 查询结果变少或为空 +- 超级管理员和普通管理员看到的数据差异异常 +- 自定义过滤器加了但没有生效 +- 调试时怀疑 SQL 有问题,实际是全局过滤导致 + +#### 排错建议 + +- 先看实体基类是否正确 +- 再看当前用户角色的数据范围缓存 +- 再看租户 Claim、组织 Claim、用户 Claim +- 最后检查是否显式使用了 `ClearFilter()` 或 `IgnoreColumns` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/Enum/DataScopeEnum.cs` +- `Admin.NET/Admin.NET.Core/Entity/IEntityFilter.cs` + +#### 推荐扩展写法 + +- 行业特殊过滤规则统一实现 `IEntityFilter` +- 角色、机构、用户权限调整后统一清缓存 +- 避免每个业务服务自己拼数据权限 where 条件 + +### 11. 即时通讯发布版补充 + +#### 常见问题 + +- 本地单机可推送,多节点后消息不同步 +- 连接正常但广播不到目标用户 +- 在线用户状态不准确 +- 实时通知丢失后无法追溯 + +#### 排错建议 + +- 检查 Redis Backplane 是否启用 +- 检查 `Cluster.Enabled`、`SignalR.RedisConfiguration` +- 检查客户端连接是否带上了正确身份上下文 +- 检查重要消息是否也落了数据库或通知表 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/SignalR/SignalRSetup.cs` +- `Admin.NET/Admin.NET.Core/Hub/OnlineUserHub.cs` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` + +#### 推荐扩展写法 + +- 实时消息只做通知层,正式状态落库 +- 多节点场景统一启用 Redis +- 高价值通知可增加“未读消息中心”补偿机制 + +### 12. 图片压缩发布版补充 + +#### 常见问题 + +- 误以为当前已经支持上传自动压缩 +- 图片上传后体积仍很大 +- 不同业务需要不同压缩策略但没有统一入口 + +#### 排错建议 + +- 确认当前是否只是注册了 ImageSharp 中间件 +- 确认 `SysFileService` 中是否真的有压缩逻辑 +- 确认业务是否自行在前端做了压缩,从而误判后端能力 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs` + +#### 推荐扩展写法 + +- 在 `SysFileService` 增加统一图片处理管线 +- 图片压缩参数配置化 +- 可按业务目录、文件类型、像素区间定义不同策略 + +### 13. 接口限流发布版补充 + +#### 常见问题 + +- 本地测试总是触发 429 +- 生产环境明明流量不大却误伤合法请求 +- 反向代理后限流识别到的 IP 不正确 + +#### 排错建议 + +- 检查 `Limit.json` 中 `EnableEndpointRateLimiting` +- 检查 `RealIpHeader` 是否与网关头一致 +- 检查黑名单策略是否误配为 `Limit=0` +- 检查客户端是否共用了同一个 `X-ClientId` + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Application/Configuration/Limit.json` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Option/RateLimitOptions.cs` + +#### 推荐扩展写法 + +- 登录、短信、开放接口用独立限流规则 +- 把限流策略和环境区分开 +- 高风险接口可同时叠加设备指纹或验证码策略 + +### 14. 国密信创发布版补充 + +#### 常见问题 + +- 前端加密和后端解密不一致 +- 换了公私钥后登录全部失败 +- 连接串加密后读取失败 + +#### 排错建议 + +- 检查前端是否同步了新的公钥 +- 检查后端 `Cryptogram.CryptoType` 是否与实际一致 +- 检查历史数据是否仍使用旧算法 +- 检查连接串是否经过同一算法加密 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Utils/CryptogramUtil.cs` +- `Admin.NET/Admin.NET.Core/Option/CryptogramOptions.cs` +- `Admin.NET/Admin.NET.Application/Configuration/App.json` +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` + +#### 推荐扩展写法 + +- 密钥统一配置和版本管理 +- 需要平滑升级时增加算法兼容判定层 +- 国密能力的使用场景统一走工具类,不散写加解密代码 + +### 15. 邮件发送发布版补充 + +#### 常见问题 + +- 邮件发送报 SMTP 认证失败 +- 本地测试能发,服务器发不出去 +- 模板邮件文案和业务逻辑耦合太深 + +#### 排错建议 + +- 检查 SMTP 服务器、端口、SSL、授权码 +- 检查部署环境网络出口和防火墙 +- 检查是否误把邮箱密码当成授权码 +- 检查是否应该先通过模板服务生成内容 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Message/SysEmailService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Email.json` +- `Admin.NET/Admin.NET.Core/Service/Template/SysTemplateService.cs` + +#### 推荐扩展写法 + +- 邮件内容先模板化 +- 发信动作事件化或异步化 +- 后续可扩展发信日志、失败重试和多模板管理 + +### 16. 短信发送发布版补充 + +#### 常见问题 + +- 验证码短信能发但校验失败 +- 自定义短信接口返回成功但系统判断失败 +- 高并发下同一手机号被重复发送 + +#### 排错建议 + +- 检查验证码缓存 Key 是否一致 +- 检查 `VerifyCodeExpireSeconds` +- 检查自定义短信接口 `SuccessFlag` +- 检查是否对发送接口做了限流和防重复 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Message/SysSmsService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/SMS.json` +- `Admin.NET/Admin.NET.Core/Const/CacheConst.cs` + +#### 推荐扩展写法 + +- 验证码类短信统一封装发送频率控制 +- 多短信通道增加主备切换 +- 短信发送明细和状态单独落日志表 + +### 17. 微信对接发布版补充 + +#### 常见问题 + +- 公众号、小程序、企业微信接口混用 +- OpenId 登录成功但无法映射到业务用户 +- 微信支付回调到了但订单状态没更新 + +#### 排错建议 + +- 先区分是 `SysWechatService`、`SysWechatPayService` 还是 `WorkWeixin` 插件 +- 检查支付回调地址是否和配置一致 +- 检查本地订单号与微信交易号的映射 +- 检查支付和退款回调中的商户号、应用号是否匹配 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatService.cs` +- `Admin.NET/Admin.NET.Core/Service/Wechat/SysWechatPayService.cs` +- `Admin.NET/Admin.NET.Application/Configuration/Wechat.json` +- `Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/*` + +#### 推荐扩展写法 + +- 微信用户、支付单、退款单分别建清晰的业务边界 +- 企业微信场景继续插件化 +- 支付状态同步建议增加幂等与补偿查询任务 + +### 18. 多租户 SAAS发布版补充 + +#### 常见问题 + +- 租户切换后数据仍然是旧租户的 +- 新增租户后菜单和管理员没初始化完整 +- 独立库租户有时连不上 + +#### 排错建议 + +- 检查请求头 `TenantId` 和当前登录 Claim `TenantId` +- 检查 `SysTenantService.CacheTenant()` 是否执行 +- 检查租户库连接串是否已加密或已解密 +- 检查实体是否继承租户基类 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarRepository.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs` +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarFilter.cs` + +#### 推荐扩展写法 + +- 新业务从建模阶段就明确租户模式 +- 租户初始化逻辑集中在租户服务,不分散到业务模块 +- 独立库模式可继续补租户迁移、备份和回收策略 + +### 19. 远程请求发布版补充 + +#### 常见问题 + +- 第三方接口调用超时或结果格式不统一 +- 插件提供了代理接口但系统没真正启用 +- 业务里到处出现裸 `HttpClient` + +#### 排错建议 + +- 检查插件是否被 `Application.csproj` 引用 +- 检查插件 `Startup.cs` 是否注册了对应代理 +- 检查配置类是否已绑定 +- 检查 DTO 和返回模型是否与对方接口版本一致 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/*` +- `Admin.NET/Plugins/Admin.NET.Plugin.WorkWeixin/*` +- `Admin.NET/Plugins/Admin.NET.Plugin.K3Cloud/*` +- `Admin.NET/Plugins/Admin.NET.Plugin.ReZero/*` + +#### 推荐扩展写法 + +- 所有三方系统集成做成“配置 + DTO + Proxy + Service”四件套 +- 异常、超时、签名、重试统一封装 +- 新插件如需鉴权,尽量复用现有 JWT 或开放接口机制 + +### 20. 消息队列发布版补充 + +#### 常见问题 + +- 事件发布了但订阅器没执行 +- Redis 模式下消息行为和单机不一致 +- 失败重试后仍无结果,不知道在哪里排查 + +#### 排错建议 + +- 检查事件是否真的通过 `IEventPublisher` 发布 +- 检查订阅器是否正确标注 `EventSubscribe` +- 检查 Redis 缓存是否启用,事件源是否已替换 +- 检查 `RetryEventHandlerExecutor` 的重试日志和 fallback + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/EventBus/AppEventSubscriber.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs` +- `Admin.NET/Admin.NET.Core/EventBus/RabbitMQEventSourceStore.cs` + +#### 推荐扩展写法 + +- 事件用于解耦非主流程动作 +- 高可靠消息场景继续在现有基础上补正式 MQ 治理能力 +- 关键事件建议加业务日志与重放能力 + +### 21. 定时任务发布版补充 + +#### 常见问题 + +- 作业能在本地跑,部署后不执行 +- 任务看板登录异常 +- 动态作业编译成功但执行报错 + +#### 排错建议 + +- 检查 `JobSchedule.Enabled` +- 检查作业持久化与监控是否已注册 +- 检查任务代码是否实现 `IJob` +- 检查 `/schedule` 登录时是否依赖后台账号认证链路 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` +- `Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs` +- `Admin.NET/Admin.NET.Core/Service/Schedule/SysScheduleService.cs` + +#### 推荐扩展写法 + +- 系统级任务统一走调度器 +- 用户日程统一走业务日程服务 +- 动态任务建议加来源审计和发布审批 + +### 22. 令牌 Token发布版补充 + +#### 常见问题 + +- 刷新 Token 后旧 Token 还能用或不能用 +- 退出登录后依然能访问部分接口 +- 自动续期和前端 Token 刷新策略冲突 + +#### 排错建议 + +- 检查 `Logout()` 是否写入黑名单缓存 +- 检查 `JwtHandler` 是否真正参与授权链路 +- 检查前端是否及时替换响应头里的新 Token +- 检查刷新逻辑是否走了系统自带接口 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +- `Admin.NET/Admin.NET.Web.Core/Handlers/JwtHandler.cs` +- `Admin.NET/Admin.NET.Core/Const/CacheConst.cs` + +#### 推荐扩展写法 + +- Token 体系统一由认证服务维护 +- 多端登录策略配置化 +- 高安全项目增加 Token 审计与设备指纹校验 + +### 23. 日志记录发布版补充 + +#### 常见问题 + +- 数据库日志或 ES 日志没写入 +- SQL 执行异常但看不到关键定位信息 +- 业务操作日志不全,后续无法追溯 + +#### 排错建议 + +- 检查 `Logging.json` 是否开启对应日志通道 +- 检查 ES 或数据库写入器配置是否正确 +- 检查 `SqlSugarSetup` 的 AOP 是否已挂上 +- 检查关键业务动作是否真的打了操作日志 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Logging/LoggingSetup.cs` +- `Admin.NET/Admin.NET.Core/Logging/DatabaseLoggingWriter.cs` +- `Admin.NET/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs` +- `Admin.NET/Admin.NET.Core/Service/Log/*` +- `Admin.NET/Admin.NET.Application/Configuration/Logging.json` + +#### 推荐扩展写法 + +- 重要业务动作统一补操作日志 +- 关键主数据启用差异日志 +- 针对制造业追溯需求,可扩展业务轨迹日志表 + +### 24. 代码生成发布版补充 + +#### 常见问题 + +- 代码生成出的模块路径不对 +- 同步字段后配置丢失 +- 菜单和按钮权限重复 + +#### 排错建议 + +- 检查 `CodeGen.json` 中命名空间和前端根目录配置 +- 检查生成前是否已正确设置页面路径、模块名、菜单父级 +- 检查表字段变更后是否重新同步字段配置 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs` +- `Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenConfigService.cs` +- `Admin.NET/Admin.NET.Core/Utils/CodeGenUtil.cs` +- `Admin.NET/Admin.NET.Application/Configuration/CodeGen.json` + +#### 推荐扩展写法 + +- 代码生成只做骨架和标准模块起步 +- 生成后进入业务层继续收敛 +- 复杂模块单独维护模板或直接手写 + +### 25. 可视化大屏发布版补充 + +#### 常见问题 + +- 大屏项目数据保存了但预览图不显示 +- 大屏插件能用,但业务数据接口没有统一规划 +- 误把大屏插件当成统计引擎 + +#### 排错建议 + +- 检查 `GoViewProService` 中预览图和背景图的 Base64 存储 +- 检查项目内容与项目元数据是否同时存在 +- 检查大屏消费的数据接口是否由业务服务正确提供 + +#### 代码定位路径 + +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Startup.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/GoViewPro/GoViewProService.cs` +- `Admin.NET/Plugins/Admin.NET.Plugin.GoView/Util/GoViewResultProvider.cs` + +#### 推荐扩展写法 + +- 大屏插件继续只做项目管理层 +- 统计、聚合、指标统一从业务后端接口输出 +- 可按生产、质量、设备、能源拆分大屏 API + +### 26. OpenAPI发布版补充 + +#### 常见问题 + +- Swagger 可见但部分接口调试失败 +- 开放接口签名通过但业务身份不正确 +- 对外开放接口返回结构不统一 + +#### 排错建议 + +- 检查 `Swagger.json` 登录配置和生产环境开关 +- 检查 `SysOpenAccessService` 是否正确绑定用户和租户 +- 检查开放接口是否使用了 `SignatureAuthenticationDefaults.AuthenticationScheme` +- 检查统一返回是否被 `NonUnify` 绕过 + +#### 代码定位路径 + +- `Admin.NET/Admin.NET.Application/Configuration/Swagger.json` +- `Admin.NET/Admin.NET.Application/OpenApi/DemoOpenApi.cs` +- `Admin.NET/Admin.NET.Core/Service/OpenAccess/SysOpenAccessService.cs` +- `Admin.NET/Admin.NET.Core/SignatureAuth/SignatureAuthenticationHandler.cs` +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` + +#### 推荐扩展写法 + +- 对外开放接口使用独立分组和独立认证方案 +- 开放接口接入时同步给出签名示例、错误码和调用频率约束 +- 内外部接口文档分开治理,避免后台接口暴露过多 + +## 附录七:统一术语表 + +| 术语 | 含义 | 当前脚手架落点 | 备注 | +| --- | --- | --- | --- | +| 宿主 | 应用启动入口与 Web 容器承载层 | `Admin.NET.Web.Entry` | 只做最薄启动,不承载业务 | +| 启动装配层 | 全局注册服务与中间件链的层 | `Admin.NET.Web.Core` | 后端能力总入口 | +| 基础设施层 | 平台公共能力沉淀层 | `Admin.NET.Core` | 数据库、缓存、鉴权、文件、模板等 | +| 应用层 | 业务接口暴露和配置承载层 | `Admin.NET.Application` | 当前更偏示例与默认应用 | +| 插件 | 可独立启停的扩展模块 | `Admin.NET/Plugins/*` | 适合集成和独立业务域 | +| 动态 API | 通过服务类自动生成接口的模式 | `IDynamicApiController` | 不强制手写 Controller | +| 统一返回 | 后端统一响应结构 | `AdminResultProvider` | 成功、异常、401/403 统一处理 | +| 仓储 | 默认数据访问入口 | `SqlSugarRepository` | 自动切库、承接过滤器 | +| 切库 | 按租户或特性切换数据库连接 | `SqlSugarRepository` | 主库、日志库、租户库 | +| 全局过滤器 | 统一附加到查询上的过滤条件 | `SqlSugarFilter` | 软删、租户、数据权限 | +| 租户隔离 | 按租户区分数据边界 | `SysTenantService` + `SqlSugar*` | 支持同库和独立库 | +| 数据权限 | 按组织、角色、本人范围过滤数据 | `SqlSugarFilter` | 与租户过滤可叠加 | +| 开放接口 | 面向外部系统的签名接口 | `SysOpenAccessService` | 不走后台 JWT | +| Signature 鉴权 | `accessKey/accessSecret` 签名认证 | `SignatureAuthenticationHandler` | 含时间戳与 nonce | +| JWT 鉴权 | 后台用户访问认证方式 | `SysAuthService` + `JwtHandler` | 支持刷新和黑名单 | +| Token 黑名单 | 失效 Token 缓存记录 | `SysCacheService` | 登出后写入 | +| 任务调度 | 系统后台作业调度能力 | `AddSchedule` | 与业务日程不同 | +| 业务日程 | 用户或业务提醒计划数据 | `SysScheduleService` | 不是系统作业引擎 | +| 事件总线 | 异步事件发布订阅机制 | `AddEventBus` | 可走内存或 Redis | +| 声明式 HTTP | 用接口声明远程请求 | `AddHttpRemote` | 适合 DingTalk/WorkWeixin | +| 文件提供者 | 文件存储适配实现 | `ICustomFileProvider` | 本地、OSS、SSH、多存储 | +| 多存储 | 多桶、多提供者文件方案 | `SysFileProviderService` | 可配置默认提供者 | +| 模板渲染 | 将模板内容与数据合成结果 | `SysTemplateService` | 适合消息、打印文本 | +| 打印模板 | 打印场景模板元数据管理 | `SysPrintService` | 偏管理,不是完整打印中心 | +| 差异日志 | 记录字段修改前后差异 | `SqlSugarSetup` + `SysLogDiff` | 适合追溯要求高的业务 | +| 代码生成 | 根据表结构生成前后端骨架 | `SysCodeGenService` | 适合标准 CRUD | +| 可视化大屏 | 大屏项目内容管理能力 | `GoView` 插件 | 不是统计引擎本身 | +| 超级 API | 面向库表的快速接口能力 | `ReZero` 插件 | 默认应用层未启用 | + +## 附录八:架构图 + +### 1. 总体架构图 + +```mermaid +flowchart TB + U["用户 / 外部系统 / 三方平台"] --> E["Admin.NET.Web.Entry
宿主入口"] + E --> W["Admin.NET.Web.Core
启动装配层"] + W --> A["Admin.NET.Application
默认应用层"] + W --> C["Admin.NET.Core
基础设施中心"] + A --> C + W --> P["Plugins
插件扩展层"] + + C --> DB["SqlSugar / 多数据库 / 多租户"] + C --> CA["Cache / Redis / Memory"] + C --> AU["JWT / Signature / OAuth"] + C --> FS["File / OSS / MultiOSS / SSH"] + C --> EV["EventBus / Schedule / SignalR"] + C --> LG["Logging / DiffLog / ES"] + + P --> DT["DingTalk"] + P --> GV["GoView"] + P --> WW["WorkWeixin"] + P --> K3["K3Cloud"] + P --> RZ["ReZero"] +``` + +### 2. 分层职责图 + +```mermaid +flowchart LR + ENTRY["Web.Entry
负责启动"] --> COREWEB["Web.Core
负责注册服务和中间件"] + COREWEB --> APP["Application
负责默认业务接口和配置"] + COREWEB --> CORE["Core
负责实体、基础设施、系统服务"] + APP --> CORE + APP --> PLUGIN["Plugins
按需扩展能力"] +``` + +### 3. 核心能力归属图 + +```mermaid +mindmap + root(("Admin.NET 后端")) + 启动 + Web.Entry + Web.Core + 数据 + SqlSugarSetup + SqlSugarRepository + SqlSugarFilter + 鉴权 + SysAuthService + JwtHandler + SignatureAuthenticationHandler + SysOpenAccessService + 租户 + SysTenantService + 缓存 + CacheSetup + SysCacheService + 文件 + SysFileService + SysFileProviderService + 模板 + SysTemplateService + SysPrintService + 调度与事件 + AddSchedule + DynamicJobCompiler + AddEventBus + 日志 + LoggingSetup + SysLogExService + SysLogOpService + SysLogDiffService +``` + +## 附录九:请求链路图 + +### 1. 后台普通请求链路 + +```mermaid +sequenceDiagram + participant Client as 前端/客户端 + participant Middleware as 中间件链 + participant Auth as JwtHandler + participant Api as IDynamicApiController + participant Service as 业务服务 + participant Repo as SqlSugarRepository + participant Db as Database + + Client->>Middleware: HTTP 请求 + Bearer Token + Middleware->>Auth: 认证授权 + Auth->>Auth: 黑名单校验 / 自动续期 / 按钮权限 + Auth-->>Middleware: 通过 + Middleware->>Api: 路由到动态接口 + Api->>Service: 调用服务方法 + Service->>Repo: 仓储访问 + Repo->>Repo: 判断系统表/日志表/租户表/请求头TenantId + Repo->>Db: 执行查询或写入 + Db-->>Repo: 返回结果 + Repo-->>Service: 返回实体/分页结果 + Service-->>Api: 返回业务结果 + Api-->>Middleware: 统一返回包装 + Middleware-->>Client: AdminResult +``` + +### 2. 开放接口请求链路 + +```mermaid +sequenceDiagram + participant Partner as 外部系统 + participant Sig as SignatureAuthenticationHandler + participant OpenSvc as SysOpenAccessService + participant Api as OpenApi服务 + participant Repo as SqlSugarRepository + participant Db as Database + + Partner->>Sig: accessKey + timestamp + nonce + sign + Sig->>Sig: 时间戳校验 + Sig->>OpenSvc: 根据accessKey读取accessSecret + OpenSvc-->>Sig: accessSecret + 绑定用户/租户 + Sig->>Sig: 签名校验 + nonce防重放 + Sig-->>Api: 注入Claims并通过认证 + Api->>Repo: 执行业务查询 + Repo->>Db: 自动套用租户/权限逻辑 + Db-->>Repo: 返回结果 + Repo-->>Api: 返回数据 + Api-->>Partner: 统一结果 +``` + +### 3. 文件上传链路 + +```mermaid +flowchart LR + A["客户端上传文件"] --> B["SysFileService"] + B --> C["校验文件名/大小/类型/后缀/MD5"] + C --> D["选择FileProvider"] + D --> E["本地 / OSS / SSH / MultiOSS"] + E --> F["保存SysFile元数据"] + F --> G["返回文件URL/Id/路径信息"] +``` + +## 附录十:角色与权限关系图 + +### 1. 角色权限主关系 + +```mermaid +flowchart TB + U["用户 SysUser"] --> UR["用户角色关系 SysUserRole"] + UR --> R["角色 SysRole"] + R --> RM["角色菜单关系 SysRoleMenu"] + RM --> M["菜单 / 按钮 SysMenu"] + + U --> O["机构 SysOrg"] + R --> DS["数据范围 DataScope"] + DS --> DF["SqlSugarFilter 数据权限过滤"] + M --> BTN["按钮权限集合"] + BTN --> JH["JwtHandler 路由按钮鉴权"] +``` + +### 2. 权限判定简图 + +```mermaid +flowchart LR + Req["请求路由"] --> Jwt["JwtHandler"] + Jwt --> Super{"是否超管"} + Super -- 是 --> Pass["直接放行"] + Super -- 否 --> Btn["获取用户按钮权限"] + Btn --> Match{"路由是否命中按钮权限"} + Match -- 是 --> Pass + Match -- 否 --> AllBtn["获取系统全部按钮权限"] + AllBtn --> Public{"是否非受控按钮路由"} + Public -- 是 --> Pass + Public -- 否 --> Deny["403 / 无权限"] +``` + +## 附录十一:租户隔离说明图 + +### 1. 租户隔离模型图 + +```mermaid +flowchart TB + T["租户访问请求"] --> R["SqlSugarRepository"] + R --> A{"实体是否系统表"} + A -- 是 --> MAIN["主库 MainConfig"] + A -- 否 --> B{"实体是否日志表"} + B -- 是 --> LOG["日志库 LogConfig"] + B -- 否 --> C{"实体是否显式指定TenantAttribute"} + C -- 是 --> SPEC["指定库连接"] + C -- 否 --> D{"请求头/Claim中是否存在TenantId"} + D -- 否 --> MAIN + D -- 是 --> E{"租户模式"} + E -- ID隔离 --> MAIN + E -- DB隔离 --> TENANTDB["租户独立库"] +``` + +### 2. 租户请求与过滤器关系图 + +```mermaid +flowchart LR + H["请求头 TenantId"] --> Repo["SqlSugarRepository"] + C["用户Claim TenantId"] --> Repo + Repo --> Scope["连接作用域"] + Scope --> Filter["SqlSugarFilter"] + Filter --> Soft["软删过滤"] + Filter --> Tenant["租户过滤"] + Filter --> Org["机构过滤"] + Filter --> Self["仅本人过滤"] + Filter --> Custom["自定义实体过滤"] +``` + +### 3. 租户初始化图 + +```mermaid +flowchart TD + Add["新增租户"] --> Init["SysTenantService.InitNewTenant"] + Init --> Org["初始化机构"] + Init --> Role["初始化默认角色"] + Init --> Pos["初始化岗位"] + Init --> User["初始化管理员账号"] + Init --> Menu["初始化租户菜单授权"] + Init --> Cache["刷新租户缓存"] +``` + +## 附录十二:插件启用矩阵表 + +### 1. 插件矩阵 + +| 插件 | 目录 | 当前默认应用层是否引用 | Startup 是否存在 | 主要能力 | 当前状态 | 启用建议 | +| --- | --- | --- | --- | --- | --- | --- | +| ApprovalFlow | `Admin.NET.Plugin.ApprovalFlow` | 是 | 是 | 审批流中间件 | 默认启用 | 审批场景可直接基于此扩展 | +| DingTalk | `Admin.NET.Plugin.DingTalk` | 是 | 是 | 钉钉接口、审批、组织同步、消息卡片 | 默认启用 | 钉钉集成优先复用该插件 | +| GoView | `Admin.NET.Plugin.GoView` | 是 | 是 | 大屏项目管理、发布、预览图 | 默认启用 | 可直接作为大屏后端支撑 | +| HwPortal | `Admin.NET.Plugin.HwPortal` | 是 | 以项目引用为准 | 行业门户相关能力 | 默认引用 | 使用前建议补专项文档 | +| WorkWeixin | `Admin.NET.Plugin.WorkWeixin` | 否 | 是 | 企业微信代理接口与配置 | 代码存在未默认启用 | 需要企业微信时先补项目引用 | +| K3Cloud | `Admin.NET.Plugin.K3Cloud` | 否 | 是 | 金蝶云星空命名客户端与配置 | 代码存在未默认启用 | ERP 集成时按需启用 | +| ReZero | `Admin.NET.Plugin.ReZero` | 否 | 是 | 超级 API、JWT 校验 AOP、日志接入 | 代码存在未默认启用 | 仅在需要超级 API 时启用 | + +### 2. 默认启用判断依据 + +默认是否启用,以 `Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj` 中是否存在对应 `ProjectReference` 为准。 + +### 3. 插件启用步骤建议 + +1. 在 `Application.csproj` 中增加对应插件引用 +2. 确认插件有无专属配置文件和配置项 +3. 确认插件 `Startup.cs` 是否注册了必要服务 +4. 检查插件是否依赖第三方服务、证书、账号或回调地址 +5. 补充模块级验证与接入文档 + +## 附录十三:培训与交接版最小示例代码片段 + +### 1. 框架简介示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Application/Example/ExampleService.cs +[ApiDescriptionSettings("示例分组", Name = "Example", Order = 100)] +public class ExampleService : IDynamicApiController +{ + [HttpGet("ping")] + public string Ping() + { + return "pong"; + } +} +``` + +#### 变更注意事项 + +- 新增服务优先实现 `IDynamicApiController` +- 分组、名称、顺序统一通过 `ApiDescriptionSettings` 管理 +- 不要轻易改动对外暴露路径风格,避免影响前端与培训材料 + +### 2. 开发流程示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Web.Core/ProjectOptions.cs +public static IServiceCollection AddProjectOptions(this IServiceCollection services) +{ + services.AddConfigurableOptions(); + return services; +} +``` + +#### 变更注意事项 + +- 新增配置类时必须同步注册 Options 绑定 +- 公共流程改动前先确认是否影响已有插件和现有业务模块 +- 培训资料中的开发顺序一旦确定,尽量不要频繁调整 + +### 3. 数据库配置示例 + +#### 最小示例代码片段 + +```json +{ + "DbConnection": { + "EnableConsoleSql": false, + "ConnectionConfigs": [ + { + "ConfigId": "1300000000001", + "DbType": "Sqlite", + "ConnectionString": "DataSource=./Admin.NET.db", + "DbSettings": { + "EnableInitDb": true, + "EnableInitView": true, + "EnableDiffLog": false, + "EnableUnderLine": false, + "EnableConnStringEncrypt": false + } + } + ] + } +} +``` + +#### 变更注意事项 + +- 自动建表、种子、视图开关变更要明确环境范围 +- 连接串加密配置键必须与代码属性保持一致 +- 新增连接配置后要同步更新部署文档和实施手册 + +### 4. 实体基类示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Entity/ExampleOrder.cs +[SugarTable("sys_example_order", "示例订单")] +public class ExampleOrder : EntityBaseTenantOrgDel +{ + [SugarColumn(ColumnDescription = "单号", Length = 64)] + public string OrderNo { get; set; } + + [SugarColumn(ColumnDescription = "状态")] + public int Status { get; set; } +} +``` + +#### 变更注意事项 + +- 基类变更会直接影响租户、机构、软删和审计行为 +- 已上线表修改基类前必须做数据兼容评估 +- 培训时要明确不同业务对象应该继承哪类基类 + +### 5. 缓存管理示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleCacheService.cs +public class ExampleCacheService +{ + private readonly SysCacheService _sysCacheService; + + public ExampleCacheService(SysCacheService sysCacheService) + { + _sysCacheService = sysCacheService; + } + + public string GetOrCreateName(long id) + { + return _sysCacheService.GetOrAdd($"example:name:{id}", _ => $"NAME-{id}", 300); + } +} +``` + +#### 变更注意事项 + +- 缓存 Key 规则变更要同步清理历史 Key +- Memory 切 Redis 后要补多节点回归测试 +- 缓存前缀调整要同步培训文档和运维脚本 + +### 6. 导入导出示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/Dto/ExampleImportInput.cs +public class ExampleImportInput : BaseImportInput +{ + [ImporterHeader(Name = "单号")] + [ExporterHeader("单号")] + public string OrderNo { get; set; } +} +``` + +#### 变更注意事项 + +- 模板列名变更要同步 DTO 特性和培训模板 +- 导入 DTO 不建议直接复用实体 +- 错误标记格式变更要同步实施与用户培训说明 + +### 7. 上传下载示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleFileService.cs +public class ExampleFileService : IDynamicApiController +{ + private readonly SysFileService _sysFileService; + + public ExampleFileService(SysFileService sysFileService) + { + _sysFileService = sysFileService; + } + + [HttpPost("uploadAttachment")] + public Task UploadAttachment(IFormFile file) + { + return _sysFileService.UploadFile(new UploadFileInput { File = file }, "upload/example"); + } +} +``` + +#### 变更注意事项 + +- 存储路径规则调整要考虑历史文件兼容 +- OSS、SSH、多存储切换后要回归预览、下载、删除 +- 文件 URL 生成规则变更要同步前端和培训资料 + +### 8. 模板打印示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleTemplateFacade.cs +public class ExampleTemplateFacade +{ + private readonly SysTemplateService _sysTemplateService; + + public ExampleTemplateFacade(SysTemplateService sysTemplateService) + { + _sysTemplateService = sysTemplateService; + } + + public Task RenderNotice() + { + return _sysTemplateService.RenderByCode("example_notice", new Dictionary + { + { "OrderNo", "MO-20260315-001" }, + { "Status", "已完成" } + }); + } +} +``` + +#### 变更注意事项 + +- 模板变量名变更要同步业务渲染代码 +- 模板编码建议一经发布尽量稳定 +- 打印模板和消息模板改动要分开发布说明 + +### 9. 统一认证示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Auth/ExampleAuthFacade.cs +public class ExampleAuthFacade : IDynamicApiController +{ + private readonly SysAuthService _sysAuthService; + + public ExampleAuthFacade(SysAuthService sysAuthService) + { + _sysAuthService = sysAuthService; + } + + [AllowAnonymous] + [HttpPost("loginByAccount")] + public Task LoginByAccount(LoginInput input) + { + return _sysAuthService.Login(input); + } +} +``` + +#### 变更注意事项 + +- 登录入参结构变更要同步前端、Swagger 和培训材料 +- Token 响应头名称不要频繁调整 +- 新增认证方式时要优先复用现有主链路 + +### 10. 数据权限示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Entity/ExampleTask.cs +[SugarTable("sys_example_task", "示例任务")] +public class ExampleTask : EntityBaseTenantOrg +{ + [SugarColumn(ColumnDescription = "任务名称", Length = 128)] + public string Name { get; set; } +} +``` + +#### 变更注意事项 + +- 从无组织基类切到带组织基类会改变查询结果 +- 权限相关缓存清理要纳入变更步骤 +- 自定义过滤器上线前要先评估历史报表和统计口径 + +### 11. 即时通讯示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleNoticeService.cs +public class ExampleNoticeService +{ + private readonly IHubContext _hubContext; + + public ExampleNoticeService(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public Task Broadcast(string message) + { + return _hubContext.Clients.All.ReceiveMessage(message); + } +} +``` + +#### 变更注意事项 + +- Hub 方法名变更要同步前端订阅代码 +- 多节点部署前必须验证 Redis Backplane +- 培训中要强调“通知层”和“状态层”分离 + +### 12. 图片压缩示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleImageService.cs +public class ExampleImageService +{ + // 建议在此处继续封装统一图片压缩逻辑, + // 不建议在各业务服务中散写处理代码。 +} +``` + +#### 变更注意事项 + +- 该章节当前属于二开能力,不能按现成功能培训为“已完整支持” +- 图片压缩上线后要评估原图兼容策略 +- 质量和尺寸参数必须可配置 + +### 13. 接口限流示例 + +#### 最小示例代码片段 + +```json +{ + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "GeneralRules": [ + { + "Endpoint": "*:/api/example/sendCode", + "Period": "1m", + "Limit": 5 + } + ] + } +} +``` + +#### 变更注意事项 + +- 限流阈值变更要同步测试和生产配置 +- 代理头配置变更要与网关一起发布 +- 开放接口限流规则调整要提前通知对接方 + +### 14. 国密信创示例 + +#### 最小示例代码片段 + +```json +{ + "Cryptogram": { + "CryptoType": "SM2", + "PublicKey": "your_public_key", + "PrivateKey": "your_private_key" + } +} +``` + +#### 变更注意事项 + +- 公私钥切换必须前后端同步 +- 算法调整前要评估历史数据兼容 +- 培训时要明确密钥和普通配置的权限边界 + +### 15. 邮件发送示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleMailFacade.cs +public class ExampleMailFacade +{ + private readonly SysEmailService _sysEmailService; + + public ExampleMailFacade(SysEmailService sysEmailService) + { + _sysEmailService = sysEmailService; + } + + public Task SendAlarm() + { + return _sysEmailService.SendEmail("设备告警", "设备告警通知", "alarm@example.com"); + } +} +``` + +#### 变更注意事项 + +- SMTP 通道调整要同步运维配置 +- 邮件模板或标题规范变更要同步培训资料 +- 上线前建议增加发送日志和失败回溯能力 + +### 16. 短信发送示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleSmsFacade.cs +public class ExampleSmsFacade : IDynamicApiController +{ + private readonly SysSmsService _sysSmsService; + + public ExampleSmsFacade(SysSmsService sysSmsService) + { + _sysSmsService = sysSmsService; + } + + [AllowAnonymous] + [HttpPost("sendSmsCode")] + public Task SendSmsCode(string phone) + { + return _sysSmsService.SendSms(phone); + } +} +``` + +#### 变更注意事项 + +- 短信模板 ID、签名、通道切换要同步实施说明 +- 验证码时效变更要同步前端倒计时 +- 自定义短信接口成功判定规则变更要先联调验证 + +### 17. 微信对接示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleWechatFacade.cs +public class ExampleWechatFacade : IDynamicApiController +{ + private readonly SysWechatService _sysWechatService; + + public ExampleWechatFacade(SysWechatService sysWechatService) + { + _sysWechatService = sysWechatService; + } + + [AllowAnonymous] + [HttpGet("genAuthUrl")] + public string GenAuthUrl(string redirectUrl) + { + return _sysWechatService.GenAuthUrl(new GenAuthUrlInput { RedirectUrl = redirectUrl, Scope = "snsapi_userinfo" }); + } +} +``` + +#### 变更注意事项 + +- 公众号、小程序、企业微信培训内容必须分开 +- 支付和回调地址调整要做完整联调 +- OpenId 映射策略变更要评估存量用户兼容 + +### 18. 多租户 SAAS示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleTenantFacade.cs +public class ExampleTenantFacade : IDynamicApiController +{ + private readonly SysTenantService _sysTenantService; + + public ExampleTenantFacade(SysTenantService sysTenantService) + { + _sysTenantService = sysTenantService; + } + + [HttpPost("switchTenant")] + public Task SwitchTenant(BaseIdInput input) + { + return _sysTenantService.ChangeTenant(input); + } +} +``` + +#### 变更注意事项 + +- 租户模式调整会影响数据库、缓存、权限和文件逻辑 +- 初始化策略改动要同步运维与实施流程 +- 独立库模式变更前要准备迁移和回滚方案 + +### 19. 远程请求示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Plugins/Admin.NET.Plugin.DingTalk/Service/ExampleDingTalkFacade.cs +public class ExampleDingTalkFacade +{ + private readonly IDingTalkApi _dingTalkApi; + + public ExampleDingTalkFacade(IDingTalkApi dingTalkApi) + { + _dingTalkApi = dingTalkApi; + } + + public Task GetToken(string appKey, string appSecret) + { + return _dingTalkApi.GetDingTalkToken(appKey, appSecret); + } +} +``` + +#### 变更注意事项 + +- 第三方接口版本变更要同步 DTO 和培训示例 +- 插件是否默认启用变更要同步插件矩阵表 +- 回调地址、密钥、域名变更要做环境区分 + +### 20. 消息队列示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/EventBus/ExampleEventPublisher.cs +public class ExampleEventPublisher +{ + private readonly IEventPublisher _eventPublisher; + + public ExampleEventPublisher(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + } + + public Task PublishOrderCreated(object payload) + { + return _eventPublisher.PublishAsync("example:order:created", payload); + } +} +``` + +#### 变更注意事项 + +- 事件名不要随意改 +- 事件源切 Redis 要同步监控和排障手册 +- 高可靠事件需继续补偿机制和回放能力 + +### 21. 定时任务示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Job/ExampleJob.cs +public class ExampleJob : IJob +{ + public Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken) + { + Console.WriteLine("example job running"); + return Task.CompletedTask; + } +} +``` + +#### 变更注意事项 + +- 作业频率调整要评估对业务峰谷的影响 +- 动态作业上线要配权限和审计 +- 培训时要区分调度器作业和业务日程 + +### 22. 令牌 Token示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleTokenFacade.cs +public class ExampleTokenFacade : IDynamicApiController +{ + private readonly SysAuthService _sysAuthService; + + public ExampleTokenFacade(SysAuthService sysAuthService) + { + _sysAuthService = sysAuthService; + } + + [HttpGet("refreshToken")] + public Task RefreshToken(string accessToken) + { + return _sysAuthService.GetRefreshToken(accessToken); + } +} +``` + +#### 变更注意事项 + +- Token 时效调整要同步前端刷新机制 +- 黑名单规则变更要同步缓存和培训说明 +- Bearer 使用规范尽量长期稳定 + +### 23. 日志记录示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleLogFacade.cs +public class ExampleLogFacade +{ + public void WriteBusinessLog() + { + var logger = App.GetRequiredService().CreateLogger(CommonConst.SysLogCategoryName); + logger.LogInformation("示例业务日志"); + } +} +``` + +#### 变更注意事项 + +- 日志级别调整会影响成本和排障方式 +- 差异日志开启前要确认敏感字段策略 +- 培训时应明确业务日志和平台日志的区别 + +### 24. 代码生成示例 + +#### 最小示例代码片段 + +```json +{ + "CodeGen": { + "EntityAssemblyNames": [ "Admin.NET.Core", "Admin.NET.Application" ], + "FrontRootPath": "Web", + "BackendApplicationNamespaces": [ "Admin.NET.Application" ] + } +} +``` + +#### 变更注意事项 + +- 代码生成模板路径和命名空间规则变更会影响后续所有新模块 +- 字段同步前建议备份历史配置 +- 培训资料中的生成流程一旦成型,尽量少改顺序 + +### 25. 可视化大屏示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Plugins/Admin.NET.Plugin.GoView/Service/ExampleGoViewFacade.cs +public class ExampleGoViewFacade : IDynamicApiController +{ + private readonly GoViewProService _goViewProService; + + public ExampleGoViewFacade(GoViewProService goViewProService) + { + _goViewProService = goViewProService; + } + + [HttpGet("projectList")] + public Task> ProjectList(int page = 1, int limit = 12) + { + return _goViewProService.GetList(page, limit); + } +} +``` + +#### 变更注意事项 + +- 大屏项目结构调整要同步前端模板 +- 预览图和背景图策略变更要考虑历史项目兼容 +- 统计口径变化要同步现场培训资料 + +### 26. OpenAPI示例 + +#### 最小示例代码片段 + +```csharp +// 文件:Admin.NET/Admin.NET.Application/OpenApi/ExampleOpenApi.cs +[ApiDescriptionSettings("开放接口", Name = "Example", Order = 200)] +[Authorize(AuthenticationSchemes = SignatureAuthenticationDefaults.AuthenticationScheme)] +public class ExampleOpenApi : IDynamicApiController +{ + [HttpGet("status")] + public string Status() + { + return "ok"; + } +} +``` + +#### 变更注意事项 + +- 开放接口路径、签名规则、返回结构尽量保持长期稳定 +- 凭据绑定关系变更要先评估对接系统影响 +- 对外接口文档更新要同步通知外部对接方 + +## 附录十四:章节培训导航表 + +这张表用于培训、交接和新人自学时快速跳转: + +- 想先理解原理:看“主体章节” +- 想查坑位:看“发布版补充” +- 想直接照着写:看“最小示例” +- 想配合图理解:看“附录八~十一” + +| 章节 | 主体章节 | 发布版补充 | 最小示例 | 推荐联读图表/附录 | +| --- | --- | --- | --- | --- | +| 1. 框架简介 | 第一章 | 附录六 第1节 | 附录十三 第1节 | 附录八 总体架构图 | +| 2. 开发流程 | 第二章 | 附录六 第2节 | 附录十三 第2节 | `6. 零基础快速入门`、`7. 新建业务模块完整示例` | +| 3. 数据库配置 | 第三章 | 附录六 第3节 | 附录十三 第3节 | 附录八 核心能力归属图、附录十一 租户隔离模型 | +| 4. 实体基类 | 第四章 | 附录六 第4节 | 附录十三 第4节 | `7. 新建业务模块完整示例` | +| 5. 缓存管理 | 第五章 | 附录六 第5节 | 附录十三 第5节 | 附录八 核心能力归属图 | +| 6. 导入导出 | 第六章 | 附录六 第6节 | 附录十三 第6节 | `7. 新建业务模块完整示例` | +| 7. 上传下载 | 第七章 | 附录六 第7节 | 附录十三 第7节 | 附录九 文件上传链路图 | +| 8. 模板打印 | 第八章 | 附录六 第8节 | 附录十三 第8节 | 附录八 核心能力归属图 | +| 9. 统一认证 | 第九章 | 附录六 第9节 | 附录十三 第9节 | 附录九 后台普通请求链路图 | +| 10. 数据权限 | 第十章 | 附录六 第10节 | 附录十三 第10节 | 附录十 角色与权限关系图、附录十一 过滤关系图 | +| 11. 即时通讯 | 第十一章 | 附录六 第11节 | 附录十三 第11节 | 附录八 总体架构图 | +| 12. 图片压缩 | 第十二章 | 附录六 第12节 | 附录十三 第12节 | 第七章 文件体系一起看 | +| 13. 接口限流 | 第十三章 | 附录六 第13节 | 附录十三 第13节 | 附录九 后台普通请求链路图 | +| 14. 国密信创 | 第十四章 | 附录六 第14节 | 附录十三 第14节 | 第九章 统一认证、第三章 数据库配置 | +| 15. 邮件发送 | 第十五章 | 附录六 第15节 | 附录十三 第15节 | 第八章 模板打印一起看 | +| 16. 短信发送 | 第十六章 | 附录六 第16节 | 附录十三 第16节 | 第十三章 接口限流一起看 | +| 17. 微信对接 | 第十七章 | 附录六 第17节 | 附录十三 第17节 | 第十九章 远程请求、附录十二 插件矩阵 | +| 18. 多租户 SAAS | 第十八章 | 附录六 第18节 | 附录十三 第18节 | 附录十一 全部图 | +| 19. 远程请求 | 第十九章 | 附录六 第19节 | 附录十三 第19节 | 附录十二 插件启用矩阵表 | +| 20. 消息队列 | 第二十章 | 附录六 第20节 | 附录十三 第20节 | 附录九 请求链路图 | +| 21. 定时任务 | 第二十一章 | 附录六 第21节 | 附录十三 第21节 | 附录八 总体架构图 | +| 22. 令牌 Token | 第二十二章 | 附录六 第22节 | 附录十三 第22节 | 第九章 统一认证、附录九 请求链路图 | +| 23. 日志记录 | 第二十三章 | 附录六 第23节 | 附录十三 第23节 | 附录八 核心能力归属图 | +| 24. 代码生成 | 第二十四章 | 附录六 第24节 | 附录十三 第24节 | `7. 新建业务模块完整示例` | +| 25. 可视化大屏 | 第二十五章 | 附录六 第25节 | 附录十三 第25节 | 附录十二 插件启用矩阵表 | +| 26. OpenAPI | 第二十六章 | 附录六 第26节 | 附录十三 第26节 | 附录九 开放接口请求链路图 | + +## 附录十五:事务管理与高并发专栏 + +### 1. 当前脚手架已具备的事务能力 + +当前项目已经具备两种事务用法: + +- 基于 Furion 的 `[UnitOfWork]` 特性事务 +- 基于 `ISqlSugarClient.AsTenant().BeginTran()` 的手动事务 + +对应关键实现: + +- `Admin.NET/Admin.NET.Core/SqlSugar/SqlSugarUnitOfWork.cs` +- `Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs` +- `Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs` + +`SqlSugarUnitOfWork` 已经把 Furion 的工作单元与 SqlSugar 事务适配起来: + +- `BeginTransaction()` -> `BeginTran()` +- `CommitTransaction()` -> `CommitTran()` +- `RollbackTransaction()` -> `RollbackTran()` + +### 2. 单库事务推荐写法 + +#### 2.1 事务型 Service 示例 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleOrderService.cs +[ApiDescriptionSettings("示例模块", Name = "ExampleOrder", Order = 120)] +public class ExampleOrderService : IDynamicApiController, ITransient +{ + private readonly SqlSugarRepository _orderRep; + private readonly SqlSugarRepository _detailRep; + + public ExampleOrderService( + SqlSugarRepository orderRep, + SqlSugarRepository detailRep) + { + _orderRep = orderRep; + _detailRep = detailRep; + } + + [UnitOfWork] + [HttpPost("saveOrder")] + public async Task SaveOrder([FromBody] SaveOrderInput input) + { + var order = input.Adapt(); + await _orderRep.InsertAsync(order); + + foreach (var item in input.Details) + { + var detail = item.Adapt(); + detail.OrderId = order.Id; + await _detailRep.InsertAsync(detail); + } + } +} +``` + +#### 2.2 适用场景 + +- 单据主子表保存 +- 库存主表和流水表同时写入 +- 工单和工序任务一起落库 +- 质检单和不合格品明细一起保存 + +### 3. 手动事务推荐写法 + +当你需要更细粒度控制时,可使用手动事务: + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleStockService.cs +public class ExampleStockService +{ + private readonly ISqlSugarClient _db; + + public ExampleStockService(ISqlSugarClient db) + { + _db = db; + } + + public async Task DoBizAsync() + { + try + { + _db.AsTenant().BeginTran(); + + // 业务操作 1 + // 业务操作 2 + // 业务操作 3 + + _db.AsTenant().CommitTran(); + } + catch + { + _db.AsTenant().RollbackTran(); + throw; + } + } +} +``` + +### 4. 多库事务说明 + +当前脚手架已具备: + +- 主库 +- 日志库 +- 租户独立库 + +但从当前代码看,事务适配核心仍然基于 `ISqlSugarClient.AsTenant()`,适合单进程内同类数据库连接的事务控制。 + +对于以下场景,当前文档建议不要误判为“已经具备完整分布式事务能力”: + +- 跨主库和日志库强一致提交 +- 跨微服务事务 +- 跨消息系统和数据库的强一致事务 + +### 5. 当前推荐的一致性方案 + +如果跨多个资源,建议优先采用: + +- 主业务事务内只提交核心数据 +- 日志、通知、同步动作走事件总线异步化 +- 外部系统同步采用“最终一致性 + 重试 + 补偿” + +这也是当前脚手架更自然的工程方式。 + +### 6. 并发防重推荐写法 + +针对同一工单、同一入库单被重复点击提交,推荐使用 `SysCacheService.BeginCacheLock()`: + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleSubmitService.cs +public class ExampleSubmitService : IDynamicApiController, ITransient +{ + private readonly SysCacheService _sysCacheService; + + public ExampleSubmitService(SysCacheService sysCacheService) + { + _sysCacheService = sysCacheService; + } + + [HttpPost("submitOrder")] + public Task SubmitOrder([FromBody] BaseIdInput input) + { + using var cacheLock = _sysCacheService.BeginCacheLock($"lock:submit:order:{input.Id}", 500, 10000, true); + if (cacheLock == null) + throw Oops.Oh("单据正在处理中,请勿重复提交"); + + // 这里执行真正的提交逻辑 + return Task.CompletedTask; + } +} +``` + +### 7. 常见问题 + +- `[UnitOfWork]` 标了但事务没生效 +- 事务里写了日志库或外部服务,出现部分成功、部分失败 +- 用户重复点击导致重复入库、重复发起审批 + +### 8. 排错建议 + +- 检查方法是否真的通过 IoC 调用,而不是类内自调用 +- 检查事务方法里是否混入了外部 HTTP 调用 +- 检查并发场景是否使用了缓存锁或幂等键 +- 检查多租户场景中事务是否都在同一连接作用域 + +### 9. 变更注意事项 + +- 核心事务逻辑变更前要先补并发用例 +- `[UnitOfWork]` 使用范围扩大会影响异常回滚行为 +- 引入外部服务调用进入事务前,要先评估是否改为事件驱动 + +## 附录十六:异常处理与统一返回落地指南 + +### 1. 当前脚手架的异常主线 + +当前项目的统一异常与返回链路已经比较完整: + +- 业务方法中通过 `Oops.Oh(...)` 抛业务异常 +- `AdminResultProvider` 统一包装成功、异常、校验失败、401/403 +- `LoggingSetup`、异常日志服务和 SqlSugar AOP 负责记录运行信息 + +关键文件: + +- `Admin.NET/Admin.NET.Core/Utils/AdminResultProvider.cs` +- `Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs` +- `Admin.NET/Admin.NET.Web.Core/Startup.cs` + +### 2. 业务抛错规范 + +#### 2.1 推荐写法 + +不要写: + +```csharp +return false; +``` + +也不要写: + +```csharp +return "工单状态不合法"; +``` + +推荐统一使用: + +```csharp +throw Oops.Oh("工单状态不合法"); +``` + +或: + +```csharp +throw Oops.Oh(ErrorCodeEnum.D3005); +``` + +#### 2.2 业务校验示例 + +```csharp +// 文件:Admin.NET/Admin.NET.Core/Service/Example/ExampleWorkOrderService.cs +public class ExampleWorkOrderService : IDynamicApiController, ITransient +{ + [HttpPost("complete")] + public Task Complete([FromBody] CompleteWorkOrderInput input) + { + if (input == null || input.Id <= 0) + throw Oops.Oh("工单参数不能为空"); + + if (input.Status != 2) + throw Oops.Oh("当前工单不是可完工状态"); + + return Task.CompletedTask; + } +} +``` + +### 3. 状态码规约建议 + +当前统一返回结构里的 `Code` 本质上是状态码承载位,建议团队内形成最小规约: + +- 200:成功 +- 400:参数校验或业务前置条件失败 +- 401:未登录、登录过期、签名认证失败 +- 403:无权限 +- 429:限流 +- 500:系统异常 + +如果业务侧还需要更细粒度编码,建议: + +- 对外给前端仍保持统一 HTTP 语义 +- 细粒度业务错误通过 `ErrorCodeEnum` 或 `Message` 承载 + +### 4. 全局异常捕获验证 + +当前一旦抛出异常: + +1. 中间件链或统一返回管道捕获异常 +2. `AdminResultProvider.OnException()` 输出标准结果 +3. 若启用监控日志,则进入日志体系 +4. SQL 异常也会通过 `SqlSugarSetup.OnError` 记录 + +### 5. 前端收到的标准格式 + +典型统一返回结构: + +```json +{ + "code": 400, + "type": "error", + "message": "工单状态不合法", + "result": null, + "extras": null, + "time": "2026-03-15 10:00:00" +} +``` + +### 6. 常见问题 + +- 业务方法抛了异常,但前端收到非统一格式 +- 401、403、302 的返回和业务异常不一致 +- 某些接口用了 `NonUnify`,导致前端处理异常困难 + +### 7. 排错建议 + +- 检查接口是否被 `NonUnify` 标记绕过统一返回 +- 检查异常是业务异常还是原生未处理异常 +- 检查 Swagger / 开放接口 / 文件下载这类特殊接口是否有自定义返回策略 + +### 8. 变更注意事项 + +- 不要随意改 `AdminResultProvider` 的返回结构 +- 错误码规约调整要同步前端、测试和实施文档 +- 引入新的异常类型前要确认是否能被统一返回正确承接 + +## 附录十七:单元测试与接口测试模板 + +### 1. 当前测试项目现状 + +当前仓库已有: + +- `Admin.NET/Admin.NET.Test/Admin.NET.Test.csproj` +- `Furion.Xunit` +- `xunit` +- Selenium WebDriver + +现有测试更偏: + +- 工具类测试 +- 部分 UI 自动化测试 + +从当前代码看,**完整的 Service 级业务单测模板还不充分**,这正是后续建议补齐的部分。 + +### 2. 现有测试文件 + +- `Admin.NET/Admin.NET.Test/BaseTest.cs` +- `Admin.NET/Admin.NET.Test/User/UserTest.cs` +- `Admin.NET/Admin.NET.Test/Utils/DateTimeUtilTests.cs` +- `Admin.NET/Admin.NET.Test/Utils/SafeMathTests.cs` + +### 3. Service 测试推荐模板 + +下面给出一个适合后续扩展的 Service 测试骨架示例: + +```csharp +// 文件:Admin.NET/Admin.NET.Test/Example/ExampleMaterialServiceTests.cs +using Xunit; + +namespace Admin.NET.Test.Example; + +public class ExampleMaterialServiceTests +{ + [Fact] + public async Task Page_Should_Return_Result() + { + // Arrange + var input = new ExampleMaterialInput + { + Page = 1, + PageSize = 10, + Keyword = "MAT" + }; + + // Act + // 这里建议后续在测试基类中统一构建 ServiceProvider,再解析真实 Service + await Task.CompletedTask; + + // Assert + Assert.True(input.Page == 1); + } +} +``` + +### 4. 数据库测试建议 + +当前文档建议按这两种方式二选一: + +- 连测试库 +- 使用轻量 Sqlite 测试库 + +对制造业核心业务,建议优先连独立测试库,因为: + +- 可以更贴近真实 SQL 和索引行为 +- 可以验证事务和并发 +- 可以验证租户过滤和数据权限 + +如果只是工具类或简单服务逻辑,可先用轻量测试方式。 + +### 5. Mock 外部请求建议 + +对于钉钉、企业微信、K3Cloud 等外部依赖,不建议单测直接请求真实接口。 + +建议做法: + +- 抽离业务服务和外部代理接口 +- 在测试里替换代理接口的实现 +- 只验证业务编排逻辑 + +伪代码示例: + +```csharp +public class FakeDingTalkApi : IDingTalkApi +{ + public Task GetDingTalkToken(string appkey, string appsecret) + { + return Task.FromResult(new GetDingTalkTokenOutput + { + AccessToken = "fake-token", + ErrCode = 0 + }); + } + + // 其它接口按测试需要继续补假实现 +} +``` + +### 6. UI 测试说明 + +当前 `BaseTest` 和 `UserTest` 使用 Selenium + EdgeDriver,适合: + +- 验证登录 +- 验证用户管理页面 +- 做简单页面回归 + +但它不是 Service 单测替代品。 + +### 7. 常见问题 + +- `dotnet test` 能跑,但业务逻辑没有真正被断言 +- UI 测试依赖本地环境,稳定性有限 +- 外部系统联调导致测试不稳定 + +### 8. 变更注意事项 + +- 核心库存、工单、质检逻辑变更必须补测试 +- 引入外部依赖到测试中前,要先区分是集成测试还是单元测试 +- 不要把 Selenium UI 测试当成全部测试体系 + +## 附录十八:生产环境发布与部署清单 + +### 1. 生产环境安全阻断清单 + +生产环境至少应确认以下配置: + +- `EnableInitDb = false` +- `EnableInitTable = false` +- `EnableInitSeed = false` +- `EnableInitView = false` +- `EnableConsoleSql = false` +- Swagger 生产开关按需关闭 +- 测试账号和默认密码清理或强制修改 + +### 2. 当前仓库的 docker 目录 + +当前仓库已经提供: + +- `docker/docker-compose.yml` +- `docker/docker-compose-builder.yml` +- `docker/README.md` +- `docker/nginx/conf/nginx.conf` +- `docker/app/Configuration/*` + +说明当前项目已经有基础容器化部署样板。 + +### 3. Nginx 代理关键点 + +当前 `docker/nginx/conf/nginx.conf` 已给出关键代理配置: + +- `/prod-api/` 反向代理到后端 +- `/prod-api/hubs` 额外配置了 WebSocket 升级 +- 已透传: + - `X-Real-IP` + - `X-Forwarded-For` + +这对下面两类能力非常关键: + +- SignalR +- 接口限流与真实 IP 获取 + +### 4. SignalR / WebSocket 配置提醒 + +如果前面有 Nginx 或网关,必须保留类似配置: + +```nginx +location /prod-api/hubs { + proxy_pass http://adminNet:5005/hubs; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +} +``` + +否则: + +- 实时通知可能失效 +- 在线用户功能可能异常 + +### 5. Dockerfile 建议模板 + +当前仓库主要基于 docker-compose 构建。若后续需要单独维护后端镜像,可参考精简模板: + +```dockerfile +# 请按项目真实目标框架替换 sdk/runtime 镜像标签 +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . . +RUN dotnet publish Admin.NET/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +ENV TZ=Asia/Shanghai +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Admin.NET.Web.Entry.dll"] +``` + +### 6. 生产检查清单 + +上线前建议逐项核对: + +1. 数据库初始化开关是否关闭 +2. Swagger 是否符合生产策略 +3. Redis 是否启用并验证通过 +4. 真实 IP 头是否透传 +5. SignalR WebSocket 是否可用 +6. 连接串、密钥、短信、邮件配置是否使用生产参数 +7. 默认管理员密码是否变更 +8. 日志目录、磁盘空间、ES 索引策略是否确认 + +### 7. 常见问题 + +- 线上获取到的永远是 `127.0.0.1` +- WebSocket 连接被网关吃掉 +- 生产环境误开建表 +- 容器时区不对导致时间错乱 + +### 8. 变更注意事项 + +- 发布链路改动要同步实施和运维手册 +- 代理层变更会影响限流、日志 IP、SignalR 三条链路 +- 生产配置和开发配置必须物理隔离 + +## 附录十九:高频性能瓶颈排查指南 + +### 1. 慢 SQL 排查 + +当前项目已在 `SqlSugarSetup.SetDbAop()` 中挂了慢 SQL 记录逻辑: + +- 当 SQL 执行时间超过 5 秒 +- 会记录文件名、行号、方法名和原生 SQL + +这意味着慢 SQL 的第一排查入口不是业务猜测,而是: + +- 查看慢 SQL 日志 +- 定位文件、方法和执行语句 + +### 2. 常见 SQL 性能问题 + +最常见的几类问题: + +- N+1 查询 +- 大表无条件分页 +- 模糊查询未走索引 +- 联表字段和过滤字段未建索引 +- 报表导出未限制数据量 + +### 3. 如何快速定位 N+1 问题 + +建议看两个地方: + +- 某个接口是否循环内反复查库 +- 慢 SQL 日志是否出现同模板语句短时间重复执行很多次 + +排查顺序: + +1. 看业务 Service +2. 看仓储调用次数 +3. 看是否能合并查询、预加载或批量查询 + +### 4. 内存激增排查 + +后端内存激增常见原因: + +- 导出超大 Excel +- 查询全表不分页 +- 文件或图片大对象长期驻留内存 +- 大批量日志对象 JSON 化 + +特别提醒: + +- 第六章导入导出 +- 第七章文件上传下载 + +这两章最容易出现大对象内存占用问题。 + +### 5. 分页与导出边界建议 + +- 页面查询必须分页 +- 后台导出必须限制最大记录数 +- 报表类接口建议异步生成文件 +- 大批量统计建议落中间结果表或缓存快照 + +### 6. 当前 Trace 能力说明 + +从当前仓库代码看,已经有: + +- 统一日志分类 +- SQL 日志 +- 统一返回 +- 反向代理头透传 + +但**尚未确认已完整集成**以下任一全链路追踪方案: + +- SkyWalking +- OpenTelemetry +- Jaeger +- Zipkin + +因此当前建议写法是: + +- 先利用日志、SQL AOP、统一请求入口做手动链路排查 +- 如后续接入 TraceId 体系,可把网关、后端、SQL、日志串起来 + +### 7. 高并发热点排查建议 + +重点关注: + +- 登录接口 +- 短信发送接口 +- 文件上传接口 +- 开放接口签名认证入口 +- 单据提交和库存扣减接口 + +这些接口建议同时检查: + +- 限流 +- 缓存锁 +- 事务粒度 +- SQL 索引 +- 幂等控制 + +### 8. 常见问题 + +- SQL 看着不复杂但接口就是慢 +- 导出时服务器内存飙升 +- 多节点后性能下降明显 +- 外部系统同步拖慢主流程 + +### 9. 变更注意事项 + +- 核心查询改写前要先保留慢 SQL 对比基线 +- 大批量导出策略调整要同步培训和运维说明 +- 如后续引入全链路追踪,要同步更新日志规范和排障手册 diff --git a/Admin.NET-v2/DISCLAIMER.md b/Admin.NET-v2/DISCLAIMER.md new file mode 100644 index 0000000..2bb1fc5 --- /dev/null +++ b/Admin.NET-v2/DISCLAIMER.md @@ -0,0 +1,52 @@ +

Admin.NET 开源框架法律声明与授权条款

+ +``` +任何用户在使用由 Admin.NET 技术开发团队(以下简称「本团队」)研发的系列框架(以下简称「Admin.NET 通用权限开发框架」)前,请您仔细阅读并透彻理解本声明。若您一旦使用 Admin.NET 通用权限开发框架,您的使用行为即被视为对本声明全部内容的认可和接受。 +``` + +**免责声明** + +1. Admin.NET 通用权限开发框架仅属于快速开发框架并不涉及具体业务应用场景,其尊重并保护所有用户的个人隐私权,不窃取任何用户计算机中的信息,更不具备用户数据存储等网络传输功能。 + +2. 任何单位或个人因下载使用 Admin.NET 通用权限开发框架而产生的任何意外、疏忽、合约毁坏、诽谤、数据丢失、系统故障、服务中断、经济损失、商誉损害、版权或知识产权侵犯及其造成的损失 (包括但不限于直接、间接、附带或衍生的损失等),本团队不承担任何法律责任。 + +3. 任何单位或个人在阅读本免责声明后,应在开源许可证所允许的范围内进行合法的发布、传播和使用 Admin.NET 通用权限开发框架等行为,若违反本免责声明条款或违反法律法规所造成的法律责任(包括但不限于民事赔偿和刑事责任),由违约者自行承担,本团队不承担任何法律责任。 + +4. 本团队对 Admin.NET 通用权限开发框架拥有知识产权(包括但不限于商标权、专利权、著作权、商业秘密等),上述产品均受到相关法律法规的保护。任何单位或个人不得在未经本团队书面授权的情况下对 Admin.NET 通用权限开发框架本身申请相关的知识产权。 + +5. 使用者必须在适用法律和法规允许的范围内正确使用 Admin.NET,严禁将其用于非法、欺诈、恶意或侵犯他人合法权益的目的,亦不将运用于任何违反我国法律法规的平台。若发现任何未经授权或违法使用本框架的情况,我们将依据相关法律追究责任,并有权采取必要的措施予以制止。 + +6. 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动。任何基于本项目二次开发而产生的一切法律纠纷和责任,均与作者无关。 + +7. 用户明确并同意本声明条款列举的全部内容,对使用 Admin.NET 通用权限开发框架可能存在的风险和相关后果将完全由用户自行承担,本团队不承担任何法律责任。 + +8. 如果本声明的任何部分被认为无效或不可执行,则该部分将被解释为反映本团队的初衷,其余部分仍具有完全效力。不可执行的部分声明,并不构成我们放弃执行该声明的权利。 + + +**授权条款** + +Admin.NET 是一个基于 .NET 构建的开源通用权限开发框架,我们采用了双重授权条款,您可以在 Apache 许可证(版本 2.0)或 MIT 许可证的条款下自由地使用、复制、分发、修改和贡献此项目。这意味着您可以根据自身需求和法律要求选择更适合您的许可条款: + +1. **Apache 许可证(版本 2.0)**:您可以通过访问 [LICENSE-APACHE](https://gitee.com/zuohuaijun/Admin.NET/blob/next/LICENSE-APACHE) 获取详细信息,并遵照其条款使用本框架。 + +2. **MIT 许可证**:另一种选择是遵循 MIT 许可协议,详情参见 [LICENSE-MIT](https://gitee.com/zuohuaijun/Admin.NET/blob/next/LICENSE-MIT)。 + + +**责任限制** + +Admin.NET 团队及社区成员尽力提供完善的文档和技术支持,但并不对因使用框架过程中产生的问题提供绝对的解决方案保障。所有用户提供或推荐的解决方案、代码片段、最佳实践等均“按原样”提供,使用者须自行判断并承担使用后的一切风险。 + + +**法律义务与合规性** + +使用者在利用 Admin.NET 开发应用程序时,负有确保其应用程序符合所有适用法律、行业标准以及信息安全规范的全部责任。使用者应自行评估并确保其产品不会滥用框架功能,尤其是防止被用于潜在有害或不道德的目的。 + + +**技术交流** + +Admin.NET 交流群提供的支持和资源旨在辅助开发过程,但不应视为全面的技术指导或保证。我们鼓励用户积极参与开源过程,同时也提醒用户充分测试其开发成果,确保其安全性和稳定性。 + + +**变更说明** + +本团队有权随时对声明条款及附件内容进行单方面的变更或更新,变更或更新后立即自动生效,无需另行单独通知您或任何第三方;若您在声明内容公告变更后继续使用的,表示您已充分阅读、理解并接受修改后的声明内容。 diff --git a/Admin.NET-v2/HW_PORTAL_抽离迁移指导.md b/Admin.NET-v2/HW_PORTAL_抽离迁移指导.md new file mode 100644 index 0000000..a2e4494 --- /dev/null +++ b/Admin.NET-v2/HW_PORTAL_抽离迁移指导.md @@ -0,0 +1,13245 @@ +# hw-portal 抽离迁移指导 + +## 1. 文档目的 + +- 本文档基于仓库内源模块 `ruoyi-portal` 做静态分析,目标是指导其按“`hw-portal` 独立代码模块”方式迁移到当前 `Admin.NET`(.NET 10)项目。 +- 约束前提:数据库表结构不变、SQL 不变、方法业务逻辑不变;允许替换工具类、框架胶水代码、统一返回包装、分页封装、鉴权与注解实现。 +- 当前仓库里还没有现成的 `hw-portal` 模块,因此本文档既是源码分析结果,也是后续抽离实施的基线。 + +## 2. 已确认事实与不确定点 + +- 源模块路径:`C:/D/WORK/NewP/Admin.NET-v2/ruoyi-portal`。 +- `pom.xml` 可直接确认:父工程 `com.ruoyi:ruoyi:3.9.1`,模块坐标 `ruoyi-portal`。 +- 模块 `pom.xml` 直接声明的依赖只有:`com.ruoyi:ruoyi-common`。 +- 由于 Spring Boot、MyBatis、Easy-ES 等版本由父 POM 继承,当前模块自身 `pom.xml` 里看不到完整版本;因此迁移指导按“版本无关 + 保留现有行为”编写。 +- 你补充说明源系统是 `Spring Boot 3.5.8 + MyBatis + JDK17`;源码中也确实出现了 `jakarta.servlet`,说明至少已经是 Jakarta 体系。 +- `ruoyi-portal/src/test/java` 下未发现测试源码,因此本次分析全部基于生产代码。 + +## 3. 模块总体画像 + +- Java 源文件数:`92`。 +- MyBatis XML 文件数:`15`。 +- 参与源码附录的文件总数(含 `pom.xml`):`108`。 +- 涉及主要数据库表:`hw_about_us_info_detail`, `hw_about_us_info`, `hw_web_visit_event`, `hw_web_visit_daily`, `hw_contact_us_info`, `hw_portal_config`, `hw_portal_config_type`, `hw_product_case_info`, `hw_product_info_detail`, `hw_product_info`, `hw_web_menu`, `hw_web`, `hw_web1`, `hw_web_document`, `hw_web_menu1`。 + +| 包/目录 | 文件数 | +| --- | ---: | +| `controller` | 17 | +| `domain` | 16 | +| `domain/dto` | 6 | +| `domain/vo` | 1 | +| `mapper` | 15 | +| `search/convert` | 1 | +| `search/es/entity` | 1 | +| `search/es/mapper` | 1 | +| `search/service` | 1 | +| `search/service/impl` | 1 | +| `service` | 16 | +| `service/impl` | 16 | + +### 3.1 功能分块 + +- 门户前台聚合:`HwPortalController`。 +- 后台内容维护:关于我们、联系我们、门户配置、配置类型、产品、案例、页面 JSON、菜单、资料文档。 +- 搜索:MySQL 联合搜索 + Easy-ES 可选搜索 + 索引重建。 +- 官网分析:匿名埋点采集、日报聚合、看板排行。 +- 树结构数据:配置类型树、产品明细树、菜单树。 + +### 3.2 当前 Admin.NET 的推荐落点 + +- 结合本仓库现状,最小阻力方案是把 `hw-portal` 放进 `Admin.NET.Core`: + - 实体建议放 `Admin.NET/Admin.NET.Core/Entity/HwPortal/`。 + - 服务建议放 `Admin.NET/Admin.NET.Core/Service/HwPortal/`。 + - DTO 建议放 `Admin.NET/Admin.NET.Core/Service/HwPortal/Dto/`。 + - 若要完全隔离,也可以单独建 `Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/`,但这会增加宿主注册、配置扫描、发布结构复杂度。 +- 从当前项目惯例看,`IDynamicApiController + SqlSugarRepository/ISqlSugarClient + AdminResultProvider` 是最贴合现有骨架的实现方式。 +- 因为你要求“SQL 不变”,所以仓储层不要把 XML SQL 改写成 SqlSugar LINQ;优先保留原 SQL 文本,通过 `ISqlSugarClient.Ado` 执行。 + +## 4. 接口清单 + +| 控制器 | 方法名 | HTTP | 路径 | 方法签名 | 注解 | 主要调用链 | +| --- | --- | --- | --- | --- | --- | --- | +| `HwAboutUsInfoController` | `list` | `GET` | `/portal/aboutUsInfo/list` | `public TableDataInfo list(HwAboutUsInfo hwAboutUsInfo)` | - | hwAboutUsInfoService.selectHwAboutUsInfoList | +| `HwAboutUsInfoController` | `export` | `POST` | `/portal/aboutUsInfo/export` | `public void export(HttpServletResponse response, HwAboutUsInfo hwAboutUsInfo)` | RepeatSubmit | hwAboutUsInfoService.selectHwAboutUsInfoList | +| `HwAboutUsInfoController` | `getInfo` | `GET` | `/portal/aboutUsInfo/{aboutUsInfoId}` | `public AjaxResult getInfo(@PathVariable("aboutUsInfoId") Long aboutUsInfoId)` | - | hwAboutUsInfoService.selectHwAboutUsInfoByAboutUsInfoId | +| `HwAboutUsInfoController` | `add` | `POST` | `/portal/aboutUsInfo` | `public AjaxResult add(@RequestBody HwAboutUsInfo hwAboutUsInfo)` | RepeatSubmit | hwAboutUsInfoService.insertHwAboutUsInfo | +| `HwAboutUsInfoController` | `edit` | `PUT` | `/portal/aboutUsInfo` | `public AjaxResult edit(@RequestBody HwAboutUsInfo hwAboutUsInfo)` | RepeatSubmit | hwAboutUsInfoService.updateHwAboutUsInfo | +| `HwAboutUsInfoController` | `remove` | `DELETE` | `/portal/aboutUsInfo/{aboutUsInfoIds}` | `public AjaxResult remove(@PathVariable Long[] aboutUsInfoIds)` | RepeatSubmit | hwAboutUsInfoService.deleteHwAboutUsInfoByAboutUsInfoIds | +| `HwAboutUsInfoDetailController` | `list` | `GET` | `/portal/aboutUsInfoDetail/list` | `public TableDataInfo list(HwAboutUsInfoDetail hwAboutUsInfoDetail)` | - | hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList | +| `HwAboutUsInfoDetailController` | `export` | `POST` | `/portal/aboutUsInfoDetail/export` | `public void export(HttpServletResponse response, HwAboutUsInfoDetail hwAboutUsInfoDetail)` | RepeatSubmit | hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList | +| `HwAboutUsInfoDetailController` | `getInfo` | `GET` | `/portal/aboutUsInfoDetail/{usInfoDetailId}` | `public AjaxResult getInfo(@PathVariable("usInfoDetailId") Long usInfoDetailId)` | - | hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailByUsInfoDetailId | +| `HwAboutUsInfoDetailController` | `add` | `POST` | `/portal/aboutUsInfoDetail` | `public AjaxResult add(@RequestBody HwAboutUsInfoDetail hwAboutUsInfoDetail)` | RepeatSubmit | hwAboutUsInfoDetailService.insertHwAboutUsInfoDetail | +| `HwAboutUsInfoDetailController` | `edit` | `PUT` | `/portal/aboutUsInfoDetail` | `public AjaxResult edit(@RequestBody HwAboutUsInfoDetail hwAboutUsInfoDetail)` | RepeatSubmit | hwAboutUsInfoDetailService.updateHwAboutUsInfoDetail | +| `HwAboutUsInfoDetailController` | `remove` | `DELETE` | `/portal/aboutUsInfoDetail/{usInfoDetailIds}` | `public AjaxResult remove(@PathVariable Long[] usInfoDetailIds)` | RepeatSubmit | hwAboutUsInfoDetailService.deleteHwAboutUsInfoDetailByUsInfoDetailIds | +| `HwAnalyticsController` | `collect` | `POST` | `/portal/analytics/collect` | `public AjaxResult collect(@RequestBody(required = false) String body, HttpServletRequest request)` | Anonymous / RateLimiter | hwAnalyticsService.collect | +| `HwAnalyticsController` | `dashboard` | `GET` | `/portal/analytics/dashboard` | `public AjaxResult dashboard(@RequestParam(value = "statDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate statDate, @RequestParam(value = "top", required = false) Integer top)` | Log | hwAnalyticsService.getDashboard | +| `HwAnalyticsController` | `refreshDaily` | `POST` | `/portal/analytics/refreshDaily` | `public AjaxResult refreshDaily(@RequestParam(value = "statDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate statDate)` | Log | hwAnalyticsService.refreshDailyStat | +| `HwContactUsInfoController` | `list` | `GET` | `/portal/contactUsInfo/list` | `public TableDataInfo list(HwContactUsInfo hwContactUsInfo)` | - | hwContactUsInfoService.selectHwContactUsInfoList | +| `HwContactUsInfoController` | `export` | `POST` | `/portal/contactUsInfo/export` | `public void export(HttpServletResponse response, HwContactUsInfo hwContactUsInfo)` | RepeatSubmit | hwContactUsInfoService.selectHwContactUsInfoList | +| `HwContactUsInfoController` | `getInfo` | `GET` | `/portal/contactUsInfo/{contactUsInfoId}` | `public AjaxResult getInfo(@PathVariable("contactUsInfoId") Long contactUsInfoId)` | - | hwContactUsInfoService.selectHwContactUsInfoByContactUsInfoId | +| `HwContactUsInfoController` | `add` | `POST` | `/portal/contactUsInfo` | `public AjaxResult add(@RequestBody HwContactUsInfo hwContactUsInfo)` | RepeatSubmit | hwContactUsInfoService.insertHwContactUsInfo | +| `HwContactUsInfoController` | `edit` | `PUT` | `/portal/contactUsInfo` | `public AjaxResult edit(@RequestBody HwContactUsInfo hwContactUsInfo)` | RepeatSubmit | hwContactUsInfoService.updateHwContactUsInfo | +| `HwContactUsInfoController` | `remove` | `DELETE` | `/portal/contactUsInfo/{contactUsInfoIds}` | `public AjaxResult remove(@PathVariable Long[] contactUsInfoIds)` | RepeatSubmit | hwContactUsInfoService.deleteHwContactUsInfoByContactUsInfoIds | +| `HwPortalConfigController` | `list` | `GET` | `/portal/portalConfig/list` | `public TableDataInfo list(HwPortalConfig hwPortalConfig)` | - | hwPortalConfigService.selectHwPortalConfigJoinList | +| `HwPortalConfigController` | `export` | `POST` | `/portal/portalConfig/export` | `public void export(HttpServletResponse response, HwPortalConfig hwPortalConfig)` | RepeatSubmit | hwPortalConfigService.selectHwPortalConfigList | +| `HwPortalConfigController` | `getInfo` | `GET` | `/portal/portalConfig/{portalConfigId}` | `public AjaxResult getInfo(@PathVariable("portalConfigId") Long portalConfigId)` | - | hwPortalConfigService.selectHwPortalConfigByPortalConfigId | +| `HwPortalConfigController` | `add` | `POST` | `/portal/portalConfig` | `public AjaxResult add(@RequestBody HwPortalConfig hwPortalConfig)` | RepeatSubmit | hwPortalConfigService.insertHwPortalConfig | +| `HwPortalConfigController` | `edit` | `PUT` | `/portal/portalConfig` | `public AjaxResult edit(@RequestBody HwPortalConfig hwPortalConfig)` | RepeatSubmit | hwPortalConfigService.updateHwPortalConfig | +| `HwPortalConfigController` | `remove` | `DELETE` | `/portal/portalConfig/{portalConfigIds}` | `public AjaxResult remove(@PathVariable Long[] portalConfigIds)` | RepeatSubmit | hwPortalConfigService.deleteHwPortalConfigByPortalConfigIds | +| `HwPortalConfigController` | `portalConfigTypeTree` | `GET` | `/portal/portalConfig/portalConfigTypeTree` | `public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectPortalConfigTypeTreeList | +| `HwPortalConfigTypeController` | `list` | `GET` | `/portal/portalConfigType/list` | `public AjaxResult list(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectHwPortalConfigTypeList | +| `HwPortalConfigTypeController` | `export` | `POST` | `/portal/portalConfigType/export` | `public void export(HttpServletResponse response, HwPortalConfigType hwPortalConfigType)` | RepeatSubmit | hwPortalConfigTypeService.selectHwPortalConfigTypeList | +| `HwPortalConfigTypeController` | `getInfo` | `GET` | `/portal/portalConfigType/{configTypeId}` | `public AjaxResult getInfo(@PathVariable("configTypeId") Long configTypeId)` | - | hwPortalConfigTypeService.selectHwPortalConfigTypeByConfigTypeId | +| `HwPortalConfigTypeController` | `add` | `POST` | `/portal/portalConfigType` | `public AjaxResult add(@RequestBody HwPortalConfigType hwPortalConfigType)` | RepeatSubmit | hwPortalConfigTypeService.insertHwPortalConfigType | +| `HwPortalConfigTypeController` | `edit` | `PUT` | `/portal/portalConfigType` | `public AjaxResult edit(@RequestBody HwPortalConfigType hwPortalConfigType)` | RepeatSubmit | hwPortalConfigTypeService.updateHwPortalConfigType | +| `HwPortalConfigTypeController` | `remove` | `DELETE` | `/portal/portalConfigType/{configTypeIds}` | `public AjaxResult remove(@PathVariable Long[] configTypeIds)` | RepeatSubmit | hwPortalConfigTypeService.deleteHwPortalConfigTypeByConfigTypeIds | +| `HwPortalController` | `getPortalConfigList` | `GET` | `/portal/portal/getPortalConfigList` | `public TableDataInfo getPortalConfigList(HwPortalConfig hwPortalConfig)` | - | hwPortalConfigService.selectHwPortalConfigList | +| `HwPortalController` | `getPortalConfigTypeList` | `GET` | `/portal/portal/getPortalConfigTypeList` | `public TableDataInfo getPortalConfigTypeList(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectHwPortalConfigTypeList | +| `HwPortalController` | `selectConfigTypeList` | `GET` | `/portal/portal/selectConfigTypeList` | `public TableDataInfo selectConfigTypeList(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectConfigTypeList | +| `HwPortalController` | `getHomeCaseTitleList` | `GET` | `/portal/portal/getHomeCaseTitleList` | `public TableDataInfo getHomeCaseTitleList(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectHwPortalConfigTypeList | +| `HwPortalController` | `getTypicalHomeCaseInfo` | `GET` | `/portal/portal/getTypicalHomeCaseInfo` | `public AjaxResult getTypicalHomeCaseInfo(HwProductCaseInfo queryProductCaseInfo)` | - | hwProductCaseInfoService.getTypicalHomeCaseInfo | +| `HwPortalController` | `addContactUsInfo` | `POST` | `/portal/portal/addContactUsInfo` | `public AjaxResult addContactUsInfo(@RequestBody HwContactUsInfo hwContactUsInfo)` | RepeatSubmit | hwContactUsInfoService.insertHwContactUsInfo | +| `HwPortalController` | `getProductCenterProductInfos` | `GET` | `/portal/portal/getProductCenterProductInfos` | `public AjaxResult getProductCenterProductInfos(HwProductInfo hwProductInfo)` | - | productInfoService.selectHwProductInfoJoinDetailList | +| `HwPortalController` | `getProductCenterProductDetailInfos` | `GET` | `/portal/portal/getProductCenterProductDetailInfos` | `public AjaxResult getProductCenterProductDetailInfos(HwProductInfoDetail hwProductInfoDetail)` | - | hwProductInfoDetailService.selectHwProductInfoDetailList | +| `HwPortalController` | `getCaseCenterCaseInfos` | `GET` | `/portal/portal/getCaseCenterCaseInfos` | `public AjaxResult getCaseCenterCaseInfos(HwProductCaseInfo hwProductCaseInfo)` | - | hwProductCaseInfoService.selectHwProductCaseInfoList | +| `HwPortalController` | `getCaseCenterCaseInfo` | `GET` | `/portal/portal/getCaseCenterCaseInfo/{caseInfoId}` | `public AjaxResult getCaseCenterCaseInfo(@PathVariable("caseInfoId") Long caseInfoId)` | - | hwProductCaseInfoService.selectHwProductCaseInfoByCaseInfoId | +| `HwPortalController` | `getAboutUsInfo` | `GET` | `/portal/portal/getAboutUsInfo` | `public AjaxResult getAboutUsInfo(HwAboutUsInfo hwAboutUsInfo)` | - | hwAboutUsInfoService.selectHwAboutUsInfoList | +| `HwPortalController` | `getAboutUsInfoDetails` | `GET` | `/portal/portal/getAboutUsInfoDetails` | `public AjaxResult getAboutUsInfoDetails(HwAboutUsInfoDetail hwAboutUsInfoDetail)` | - | hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList | +| `HwProductCaseInfoController` | `list` | `GET` | `/portal/productCaseInfo/list` | `public TableDataInfo list(HwProductCaseInfo hwProductCaseInfo)` | - | hwProductCaseInfoService.selectHwProductCaseInfoList | +| `HwProductCaseInfoController` | `export` | `POST` | `/portal/productCaseInfo/export` | `public void export(HttpServletResponse response, HwProductCaseInfo hwProductCaseInfo)` | RepeatSubmit | hwProductCaseInfoService.selectHwProductCaseInfoList | +| `HwProductCaseInfoController` | `getInfo` | `GET` | `/portal/productCaseInfo/{caseInfoId}` | `public AjaxResult getInfo(@PathVariable("caseInfoId") Long caseInfoId)` | - | hwProductCaseInfoService.selectHwProductCaseInfoByCaseInfoId | +| `HwProductCaseInfoController` | `add` | `POST` | `/portal/productCaseInfo` | `public AjaxResult add(@RequestBody HwProductCaseInfo hwProductCaseInfo)` | RepeatSubmit | hwProductCaseInfoService.insertHwProductCaseInfo | +| `HwProductCaseInfoController` | `edit` | `PUT` | `/portal/productCaseInfo` | `public AjaxResult edit(@RequestBody HwProductCaseInfo hwProductCaseInfo)` | RepeatSubmit | hwProductCaseInfoService.updateHwProductCaseInfo | +| `HwProductCaseInfoController` | `remove` | `DELETE` | `/portal/productCaseInfo/{caseInfoIds}` | `public AjaxResult remove(@PathVariable Long[] caseInfoIds)` | RepeatSubmit | hwProductCaseInfoService.deleteHwProductCaseInfoByCaseInfoIds | +| `HwProductCaseInfoController` | `portalConfigTypeTree` | `GET` | `/portal/productCaseInfo/portalConfigTypeTree` | `public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectPortalConfigTypeTreeList | +| `HwProductInfoController` | `list` | `GET` | `/portal/productInfo/list` | `public TableDataInfo list(HwProductInfo hwProductInfo)` | - | hwProductInfoService.selectHwProductInfoJoinList | +| `HwProductInfoController` | `export` | `POST` | `/portal/productInfo/export` | `public void export(HttpServletResponse response, HwProductInfo hwProductInfo)` | RepeatSubmit | hwProductInfoService.selectHwProductInfoList | +| `HwProductInfoController` | `getInfo` | `GET` | `/portal/productInfo/{productInfoId}` | `public AjaxResult getInfo(@PathVariable("productInfoId") Long productInfoId)` | - | hwProductInfoService.selectHwProductInfoByProductInfoId | +| `HwProductInfoController` | `add` | `POST` | `/portal/productInfo` | `public AjaxResult add(@RequestBody HwProductInfo hwProductInfo)` | RepeatSubmit | hwProductInfoService.insertHwProductInfo | +| `HwProductInfoController` | `edit` | `PUT` | `/portal/productInfo` | `public AjaxResult edit(@RequestBody HwProductInfo hwProductInfo)` | RepeatSubmit | hwProductInfoService.updateHwProductInfo | +| `HwProductInfoController` | `remove` | `DELETE` | `/portal/productInfo/{productInfoIds}` | `public AjaxResult remove(@PathVariable Long[] productInfoIds)` | RepeatSubmit | hwProductInfoService.deleteHwProductInfoByProductInfoIds | +| `HwProductInfoController` | `portalConfigTypeTree` | `GET` | `/portal/productInfo/portalConfigTypeTree` | `public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType)` | - | hwPortalConfigTypeService.selectPortalConfigTypeTreeList | +| `HwProductInfoDetailController` | `list` | `GET` | `/portal/productInfoDetail/list` | `public AjaxResult list(HwProductInfoDetail hwProductInfoDetail)` | - | hwProductInfoDetailService.selectHwProductInfoDetailList | +| `HwProductInfoDetailController` | `export` | `POST` | `/portal/productInfoDetail/export` | `public void export(HttpServletResponse response, HwProductInfoDetail hwProductInfoDetail)` | RepeatSubmit | hwProductInfoDetailService.selectHwProductInfoDetailList | +| `HwProductInfoDetailController` | `getInfo` | `GET` | `/portal/productInfoDetail/{productInfoDetailId}` | `public AjaxResult getInfo(@PathVariable("productInfoDetailId") Long productInfoDetailId)` | - | hwProductInfoDetailService.selectHwProductInfoDetailByProductInfoDetailId | +| `HwProductInfoDetailController` | `add` | `POST` | `/portal/productInfoDetail` | `public AjaxResult add(@RequestBody HwProductInfoDetail hwProductInfoDetail)` | RepeatSubmit | hwProductInfoDetailService.insertHwProductInfoDetail | +| `HwProductInfoDetailController` | `edit` | `PUT` | `/portal/productInfoDetail` | `public AjaxResult edit(@RequestBody HwProductInfoDetail hwProductInfoDetail)` | RepeatSubmit | hwProductInfoDetailService.updateHwProductInfoDetail | +| `HwProductInfoDetailController` | `remove` | `DELETE` | `/portal/productInfoDetail/{productInfoDetailIds}` | `public AjaxResult remove(@PathVariable Long[] productInfoDetailIds)` | RepeatSubmit | hwProductInfoDetailService.deleteHwProductInfoDetailByProductInfoDetailIds | +| `HwSearchAdminController` | `rebuild` | `POST` | `/portal/search/admin/rebuild` | `public AjaxResult rebuild()` | Log | hwSearchRebuildService.rebuildAll | +| `HwSearchController` | `search` | `GET` | `/portal/search` | `public AjaxResult search(@RequestParam("keyword") String keyword, @RequestParam(value = "pageNum", required = false) Integer pageNum, @RequestParam(value = "pageSize", required = false) Integer pageSize)` | RateLimiter | hwSearchService.search | +| `HwSearchController` | `editSearch` | `GET` | `/portal/search/edit` | `public AjaxResult editSearch(@RequestParam("keyword") String keyword, @RequestParam(value = "pageNum", required = false) Integer pageNum, @RequestParam(value = "pageSize", required = false) Integer pageSize)` | RateLimiter | hwSearchService.searchForEdit | +| `HwWebController` | `list` | `GET` | `/portal/hwWeb/list` | `public TableDataInfo list(HwWeb hwWeb)` | - | hwWebService.selectHwWebList | +| `HwWebController` | `export` | `POST` | `/portal/hwWeb/export` | `public void export(HttpServletResponse response, HwWeb hwWeb)` | RepeatSubmit | hwWebService.selectHwWebList | +| `HwWebController` | `getInfo` | `GET` | `/portal/hwWeb/{webCode}` | `public AjaxResult getInfo(@PathVariable("webCode") Long webCode)` | - | hwWebService.selectHwWebByWebcode | +| `HwWebController` | `add` | `POST` | `/portal/hwWeb` | `public AjaxResult add(@RequestBody HwWeb hwWeb)` | RepeatSubmit | hwWebService.insertHwWeb | +| `HwWebController` | `edit` | `PUT` | `/portal/hwWeb` | `public AjaxResult edit(@RequestBody HwWeb hwWeb)` | RepeatSubmit | hwWebService.updateHwWeb | +| `HwWebController` | `remove` | `DELETE` | `/portal/hwWeb/{webIds}` | `public AjaxResult remove(@PathVariable Long[] webIds)` | RepeatSubmit | hwWebService.deleteHwWebByWebIds | +| `HwWebController` | `getHwWebList` | `GET` | `/portal/hwWeb/getHwWebList` | `public AjaxResult getHwWebList(HwWeb HwWeb)` | - | hwWebService.selectHwWebList | +| `HwWebController1` | `list` | `GET` | `/portal/hwWeb1/list` | `public TableDataInfo list(HwWeb1 HwWeb1)` | - | - | +| `HwWebController1` | `export` | `POST` | `/portal/hwWeb1/export` | `public void export(HttpServletResponse response, HwWeb1 HwWeb1)` | RepeatSubmit | - | +| `HwWebController1` | `getInfo` | `GET` | `/portal/hwWeb1/{webCode}` | `public AjaxResult getInfo(@PathVariable("webCode") Long webCode)` | - | - | +| `HwWebController1` | `add` | `POST` | `/portal/hwWeb1` | `public AjaxResult add(@RequestBody HwWeb1 HwWeb1)` | RepeatSubmit | - | +| `HwWebController1` | `edit` | `PUT` | `/portal/hwWeb1` | `public AjaxResult edit(@RequestBody HwWeb1 HwWeb1)` | RepeatSubmit | - | +| `HwWebController1` | `remove` | `DELETE` | `/portal/hwWeb1/{webIds}` | `public AjaxResult remove(@PathVariable Long[] webIds)` | RepeatSubmit | - | +| `HwWebController1` | `getHwWeb1List` | `GET` | `/portal/hwWeb1/getHwWeb1List` | `public AjaxResult getHwWeb1List(HwWeb1 HwWeb1)` | - | - | +| `HwWebDocumentController` | `list` | `GET` | `/portal/hwWebDocument/list` | `public TableDataInfo list(HwWebDocument hwWebDocument)` | - | hwWebDocumentService.selectHwWebDocumentList | +| `HwWebDocumentController` | `export` | `POST` | `/portal/hwWebDocument/export` | `public void export(HttpServletResponse response, HwWebDocument hwWebDocument)` | RepeatSubmit | hwWebDocumentService.selectHwWebDocumentList | +| `HwWebDocumentController` | `getInfo` | `GET` | `/portal/hwWebDocument/{documentId}` | `public AjaxResult getInfo(@PathVariable("documentId") String documentId)` | - | hwWebDocumentService.selectHwWebDocumentByDocumentId | +| `HwWebDocumentController` | `add` | `POST` | `/portal/hwWebDocument` | `public AjaxResult add(@RequestBody HwWebDocument hwWebDocument)` | RepeatSubmit | hwWebDocumentService.insertHwWebDocument | +| `HwWebDocumentController` | `edit` | `PUT` | `/portal/hwWebDocument` | `public AjaxResult edit(@RequestBody HwWebDocument hwWebDocument)` | RepeatSubmit | hwWebDocumentService.updateHwWebDocument | +| `HwWebDocumentController` | `remove` | `DELETE` | `/portal/hwWebDocument/{documentIds}` | `public AjaxResult remove(@PathVariable String[] documentIds)` | RepeatSubmit | hwWebDocumentService.deleteHwWebDocumentByDocumentIds | +| `HwWebDocumentController` | `getSecureDocumentAddress` | `POST` | `/portal/hwWebDocument/getSecureDocumentAddress` | `public AjaxResult getSecureDocumentAddress(@RequestBody SecureDocumentRequest request)` | RepeatSubmit | hwWebDocumentService.verifyAndGetDocumentAddress | +| `HwWebMenuController` | `list` | `GET` | `/portal/hwWebMenu/list` | `public AjaxResult list(HwWebMenu hwWebMenu)` | - | hwWebMenuService.selectHwWebMenuList | +| `HwWebMenuController` | `export` | `POST` | `/portal/hwWebMenu/export` | `public void export(HttpServletResponse response, HwWebMenu hwWebMenu)` | RepeatSubmit | hwWebMenuService.selectHwWebMenuList | +| `HwWebMenuController` | `getInfo` | `GET` | `/portal/hwWebMenu/{webMenuId}` | `public AjaxResult getInfo(@PathVariable("webMenuId") Long webMenuId)` | - | hwWebMenuService.selectHwWebMenuByWebMenuId | +| `HwWebMenuController` | `add` | `POST` | `/portal/hwWebMenu` | `public AjaxResult add(@RequestBody HwWebMenu hwWebMenu)` | RepeatSubmit | hwWebMenuService.insertHwWebMenu | +| `HwWebMenuController` | `edit` | `PUT` | `/portal/hwWebMenu` | `public AjaxResult edit(@RequestBody HwWebMenu hwWebMenu)` | RepeatSubmit | hwWebMenuService.updateHwWebMenu | +| `HwWebMenuController` | `remove` | `DELETE` | `/portal/hwWebMenu/{webMenuIds}` | `public AjaxResult remove(@PathVariable Long[] webMenuIds)` | RepeatSubmit | hwWebMenuService.deleteHwWebMenuByWebMenuIds | +| `HwWebMenuController` | `selectMenuTree` | `GET` | `/portal/hwWebMenu/selectMenuTree` | `public AjaxResult selectMenuTree(HwWebMenu hwWebMenu)` | - | hwWebMenuService.selectMenuTree | +| `HwWebMenuController1` | `list` | `GET` | `/portal/hwWebMenu1/list` | `public AjaxResult list(HwWebMenu1 hwWebMenu1)` | - | - | +| `HwWebMenuController1` | `export` | `POST` | `/portal/hwWebMenu1/export` | `public void export(HttpServletResponse response, HwWebMenu1 hwWebMenu1)` | RepeatSubmit | - | +| `HwWebMenuController1` | `getInfo` | `GET` | `/portal/hwWebMenu1/{webMenuId}` | `public AjaxResult getInfo(@PathVariable("webMenuId") Long webMenuId)` | - | - | +| `HwWebMenuController1` | `add` | `POST` | `/portal/hwWebMenu1` | `public AjaxResult add(@RequestBody HwWebMenu1 hwWebMenu1)` | RepeatSubmit | - | +| `HwWebMenuController1` | `edit` | `PUT` | `/portal/hwWebMenu1` | `public AjaxResult edit(@RequestBody HwWebMenu1 hwWebMenu1)` | RepeatSubmit | - | +| `HwWebMenuController1` | `remove` | `DELETE` | `/portal/hwWebMenu1/{webMenuIds}` | `public AjaxResult remove(@PathVariable Long[] webMenuIds)` | RepeatSubmit | - | +| `HwWebMenuController1` | `selectMenuTree` | `GET` | `/portal/hwWebMenu1/selectMenuTree` | `public AjaxResult selectMenuTree(HwWebMenu1 hwWebMenu1)` | - | - | + +## 5. 服务接口与 Mapper 方法 + +### 5.1 Service 接口 + +| 接口 | 方法 | 返回值 | 参数 | 源文件 | +| --- | --- | --- | --- | --- | +| `IHwAboutUsInfoDetailService` | `deleteHwAboutUsInfoDetailByUsInfoDetailId` | `public int` | `Long usInfoDetailId` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoDetailService` | `deleteHwAboutUsInfoDetailByUsInfoDetailIds` | `public int` | `Long[] usInfoDetailIds` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoDetailService` | `insertHwAboutUsInfoDetail` | `public int` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoDetailService` | `selectHwAboutUsInfoDetailByUsInfoDetailId` | `public HwAboutUsInfoDetail` | `Long usInfoDetailId` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoDetailService` | `selectHwAboutUsInfoDetailList` | `public List` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoDetailService` | `updateHwAboutUsInfoDetail` | `public int` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` | +| `IHwAboutUsInfoService` | `deleteHwAboutUsInfoByAboutUsInfoId` | `public int` | `Long aboutUsInfoId` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAboutUsInfoService` | `deleteHwAboutUsInfoByAboutUsInfoIds` | `public int` | `Long[] aboutUsInfoIds` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAboutUsInfoService` | `insertHwAboutUsInfo` | `public int` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAboutUsInfoService` | `selectHwAboutUsInfoByAboutUsInfoId` | `public HwAboutUsInfo` | `Long aboutUsInfoId` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAboutUsInfoService` | `selectHwAboutUsInfoList` | `public List` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAboutUsInfoService` | `updateHwAboutUsInfo` | `public int` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` | +| `IHwAnalyticsService` | `collect` | `void` | `AnalyticsCollectRequest request, String requestIp, String requestUserAgent` | `src/main/java/com/ruoyi/portal/service/IHwAnalyticsService.java` | +| `IHwAnalyticsService` | `getDashboard` | `AnalyticsDashboardDTO` | `LocalDate statDate, Integer rankLimit` | `src/main/java/com/ruoyi/portal/service/IHwAnalyticsService.java` | +| `IHwAnalyticsService` | `refreshDailyStat` | `void` | `LocalDate statDate` | `src/main/java/com/ruoyi/portal/service/IHwAnalyticsService.java` | +| `IHwContactUsInfoService` | `deleteHwContactUsInfoByContactUsInfoId` | `public int` | `Long contactUsInfoId` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwContactUsInfoService` | `deleteHwContactUsInfoByContactUsInfoIds` | `public int` | `Long[] contactUsInfoIds` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwContactUsInfoService` | `insertHwContactUsInfo` | `public int` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwContactUsInfoService` | `selectHwContactUsInfoByContactUsInfoId` | `public HwContactUsInfo` | `Long contactUsInfoId` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwContactUsInfoService` | `selectHwContactUsInfoList` | `public List` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwContactUsInfoService` | `updateHwContactUsInfo` | `public int` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` | +| `IHwPortalConfigService` | `deleteHwPortalConfigByPortalConfigId` | `public int` | `Long portalConfigId` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `deleteHwPortalConfigByPortalConfigIds` | `public int` | `Long[] portalConfigIds` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `insertHwPortalConfig` | `public int` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `selectHwPortalConfigByPortalConfigId` | `public HwPortalConfig` | `Long portalConfigId` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `selectHwPortalConfigJoinList` | `public List` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `selectHwPortalConfigList` | `public List` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigService` | `updateHwPortalConfig` | `public int` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` | +| `IHwPortalConfigTypeService` | `buildPortalConfigTypeTree` | `public List` | `List portalConfigTypes` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `buildPortalConfigTypeTreeSelect` | `public List` | `List portalConfigTypes` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `deleteHwPortalConfigTypeByConfigTypeId` | `public int` | `Long configTypeId` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `deleteHwPortalConfigTypeByConfigTypeIds` | `public int` | `Long[] configTypeIds` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `insertHwPortalConfigType` | `public int` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `selectConfigTypeList` | `public List` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `selectHwPortalConfigTypeByConfigTypeId` | `public HwPortalConfigType` | `Long configTypeId` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `selectHwPortalConfigTypeList` | `public List` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `selectPortalConfigTypeTreeList` | `public List` | `HwPortalConfigType portalConfigType` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwPortalConfigTypeService` | `updateHwPortalConfigType` | `public int` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` | +| `IHwProductCaseInfoService` | `deleteHwProductCaseInfoByCaseInfoId` | `public int` | `Long caseInfoId` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `deleteHwProductCaseInfoByCaseInfoIds` | `public int` | `Long[] caseInfoIds` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `getTypicalHomeCaseInfo` | `public HwProductCaseInfo` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `insertHwProductCaseInfo` | `public int` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `selectHwProductCaseInfoByCaseInfoId` | `public HwProductCaseInfo` | `Long caseInfoId` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `selectHwProductCaseInfoJoinList` | `public List` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `selectHwProductCaseInfoList` | `public List` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductCaseInfoService` | `updateHwProductCaseInfo` | `public int` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` | +| `IHwProductInfoDetailService` | `deleteHwProductInfoDetailByProductInfoDetailId` | `public int` | `Long productInfoDetailId` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoDetailService` | `deleteHwProductInfoDetailByProductInfoDetailIds` | `public int` | `Long[] productInfoDetailIds` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoDetailService` | `insertHwProductInfoDetail` | `public int` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoDetailService` | `selectHwProductInfoDetailByProductInfoDetailId` | `public HwProductInfoDetail` | `Long productInfoDetailId` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoDetailService` | `selectHwProductInfoDetailList` | `public List` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoDetailService` | `updateHwProductInfoDetail` | `public int` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` | +| `IHwProductInfoService` | `deleteHwProductInfoByProductInfoId` | `public int` | `Long productInfoId` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `deleteHwProductInfoByProductInfoIds` | `public int` | `Long[] productInfoIds` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `insertHwProductInfo` | `public int` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `selectHwProductInfoByProductInfoId` | `public HwProductInfo` | `Long productInfoId` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `selectHwProductInfoJoinDetailList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `selectHwProductInfoJoinList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `selectHwProductInfoList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwProductInfoService` | `updateHwProductInfo` | `public int` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` | +| `IHwSearchRebuildService` | `rebuildAll` | `void` | `` | `src/main/java/com/ruoyi/portal/service/IHwSearchRebuildService.java` | +| `IHwSearchService` | `search` | `SearchPageDTO` | `String keyword, Integer pageNum, Integer pageSize` | `src/main/java/com/ruoyi/portal/service/IHwSearchService.java` | +| `IHwSearchService` | `searchForEdit` | `SearchPageDTO` | `String keyword, Integer pageNum, Integer pageSize` | `src/main/java/com/ruoyi/portal/service/IHwSearchService.java` | +| `IHwWebDocumentService` | `deleteHwWebDocumentByDocumentId` | `public int` | `String documentId` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebDocumentService` | `deleteHwWebDocumentByDocumentIds` | `public int` | `String[] documentIds` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebDocumentService` | `insertHwWebDocument` | `public int` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebDocumentService` | `selectHwWebDocumentByDocumentId` | `public HwWebDocument` | `String documentId` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebDocumentService` | `selectHwWebDocumentList` | `public List` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebDocumentService` | `updateHwWebDocument` | `public int` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` | +| `IHwWebMenuService` | `deleteHwWebMenuByWebMenuId` | `public int` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `deleteHwWebMenuByWebMenuIds` | `public int` | `Long[] webMenuIds` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `insertHwWebMenu` | `public int` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `selectHwWebMenuByWebMenuId` | `public HwWebMenu` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `selectHwWebMenuList` | `public List` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `selectMenuTree` | `public List` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService` | `updateHwWebMenu` | `public int` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` | +| `IHwWebMenuService1` | `deleteHwWebMenuByWebMenuId` | `public int` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `deleteHwWebMenuByWebMenuIds` | `public int` | `Long[] webMenuIds` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `insertHwWebMenu` | `public int` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `selectHwWebMenuByWebMenuId` | `public HwWebMenu1` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `selectHwWebMenuList` | `public List` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `selectMenuTree` | `public List` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebMenuService1` | `updateHwWebMenu` | `public int` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` | +| `IHwWebService` | `deleteHwWebByWebId` | `public int` | `Long webId` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService` | `deleteHwWebByWebIds` | `public int` | `Long[] webIds` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService` | `insertHwWeb` | `public int` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService` | `selectHwWebByWebcode` | `public HwWeb` | `Long webCode` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService` | `selectHwWebList` | `public List` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService` | `updateHwWeb` | `public int` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/service/IHwWebService.java` | +| `IHwWebService1` | `deleteHwWebByWebId` | `public int` | `Long webId` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `deleteHwWebByWebIds` | `public int` | `Long[] webIds` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `insertHwWeb` | `public int` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `selectHwWebByWebcode` | `public HwWeb1` | `Long webCode` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `selectHwWebList` | `public List` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `selectHwWebOne` | `public HwWeb1` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `IHwWebService1` | `updateHwWeb` | `public int` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/service/IHwWebService1.java` | +| `PortalSearchEsService` | `search` | `SearchPageDTO` | `String keyword, Integer pageNum, Integer pageSize, boolean editMode` | `src/main/java/com/ruoyi/portal/search/service/PortalSearchEsService.java` | + +### 5.2 Mapper 接口 + +| Mapper | 方法 | 返回值 | 参数 | 源文件 | +| --- | --- | --- | --- | --- | +| `HwAboutUsInfoDetailMapper` | `deleteHwAboutUsInfoDetailByUsInfoDetailId` | `public int` | `Long usInfoDetailId` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoDetailMapper` | `deleteHwAboutUsInfoDetailByUsInfoDetailIds` | `public int` | `Long[] usInfoDetailIds` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoDetailMapper` | `insertHwAboutUsInfoDetail` | `public int` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoDetailMapper` | `selectHwAboutUsInfoDetailByUsInfoDetailId` | `public HwAboutUsInfoDetail` | `Long usInfoDetailId` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoDetailMapper` | `selectHwAboutUsInfoDetailList` | `public List` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoDetailMapper` | `updateHwAboutUsInfoDetail` | `public int` | `HwAboutUsInfoDetail hwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` | +| `HwAboutUsInfoMapper` | `deleteHwAboutUsInfoByAboutUsInfoId` | `public int` | `Long aboutUsInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAboutUsInfoMapper` | `deleteHwAboutUsInfoByAboutUsInfoIds` | `public int` | `Long[] aboutUsInfoIds` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAboutUsInfoMapper` | `insertHwAboutUsInfo` | `public int` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAboutUsInfoMapper` | `selectHwAboutUsInfoByAboutUsInfoId` | `public HwAboutUsInfo` | `Long aboutUsInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAboutUsInfoMapper` | `selectHwAboutUsInfoList` | `public List` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAboutUsInfoMapper` | `updateHwAboutUsInfo` | `public int` | `HwAboutUsInfo hwAboutUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` | +| `HwAnalyticsMapper` | `avgStayMs` | `Long` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `countDistinctIp` | `Long` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `countDistinctSessions` | `Long` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `countDistinctVisitor` | `Long` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `countEventByType` | `Long` | `@Param("statDate") LocalDate statDate, @Param("eventType") String eventType` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `countSinglePageSessions` | `Long` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `insertVisitEvent` | `int` | `HwWebVisitEvent event` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `selectDailyByDate` | `HwWebVisitDaily` | `@Param("statDate") LocalDate statDate` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `selectTopEntryPages` | `List` | `@Param("statDate") LocalDate statDate, @Param("limit") int limit` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `selectTopHotPages` | `List` | `@Param("statDate") LocalDate statDate, @Param("limit") int limit` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `selectTopKeywords` | `List` | `@Param("statDate") LocalDate statDate, @Param("limit") int limit` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwAnalyticsMapper` | `upsertDaily` | `int` | `HwWebVisitDaily daily` | `src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` | +| `HwContactUsInfoMapper` | `deleteHwContactUsInfoByContactUsInfoId` | `public int` | `Long contactUsInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwContactUsInfoMapper` | `deleteHwContactUsInfoByContactUsInfoIds` | `public int` | `Long[] contactUsInfoIds` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwContactUsInfoMapper` | `insertHwContactUsInfo` | `public int` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwContactUsInfoMapper` | `selectHwContactUsInfoByContactUsInfoId` | `public HwContactUsInfo` | `Long contactUsInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwContactUsInfoMapper` | `selectHwContactUsInfoList` | `public List` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwContactUsInfoMapper` | `updateHwContactUsInfo` | `public int` | `HwContactUsInfo hwContactUsInfo` | `src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` | +| `HwPortalConfigMapper` | `deleteHwPortalConfigByPortalConfigId` | `public int` | `Long portalConfigId` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `deleteHwPortalConfigByPortalConfigIds` | `public int` | `Long[] portalConfigIds` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `insertHwPortalConfig` | `public int` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `selectHwPortalConfigByPortalConfigId` | `public HwPortalConfig` | `Long portalConfigId` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `selectHwPortalConfigJoinList` | `public List` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `selectHwPortalConfigList` | `public List` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `selectHwPortalConfigList2` | `public List` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigMapper` | `updateHwPortalConfig` | `public int` | `HwPortalConfig hwPortalConfig` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` | +| `HwPortalConfigTypeMapper` | `deleteHwPortalConfigTypeByConfigTypeId` | `public int` | `Long configTypeId` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwPortalConfigTypeMapper` | `deleteHwPortalConfigTypeByConfigTypeIds` | `public int` | `Long[] configTypeIds` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwPortalConfigTypeMapper` | `insertHwPortalConfigType` | `public int` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwPortalConfigTypeMapper` | `selectHwPortalConfigTypeByConfigTypeId` | `public HwPortalConfigType` | `Long configTypeId` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwPortalConfigTypeMapper` | `selectHwPortalConfigTypeList` | `public List` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwPortalConfigTypeMapper` | `updateHwPortalConfigType` | `public int` | `HwPortalConfigType hwPortalConfigType` | `src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` | +| `HwProductCaseInfoMapper` | `deleteHwProductCaseInfoByCaseInfoId` | `public int` | `Long caseInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `deleteHwProductCaseInfoByCaseInfoIds` | `public int` | `Long[] caseInfoIds` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `insertHwProductCaseInfo` | `public int` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `selectHwProductCaseInfoByCaseInfoId` | `public HwProductCaseInfo` | `Long caseInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `selectHwProductCaseInfoJoinList` | `public List` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `selectHwProductCaseInfoList` | `public List` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductCaseInfoMapper` | `updateHwProductCaseInfo` | `public int` | `HwProductCaseInfo hwProductCaseInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` | +| `HwProductInfoDetailMapper` | `deleteHwProductInfoDetailByProductInfoDetailId` | `public int` | `Long productInfoDetailId` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoDetailMapper` | `deleteHwProductInfoDetailByProductInfoDetailIds` | `public int` | `Long[] productInfoDetailIds` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoDetailMapper` | `insertHwProductInfoDetail` | `public int` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoDetailMapper` | `selectHwProductInfoDetailByProductInfoDetailId` | `public HwProductInfoDetail` | `Long productInfoDetailId` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoDetailMapper` | `selectHwProductInfoDetailList` | `public List` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoDetailMapper` | `updateHwProductInfoDetail` | `public int` | `HwProductInfoDetail hwProductInfoDetail` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` | +| `HwProductInfoMapper` | `deleteHwProductInfoByProductInfoId` | `public int` | `Long productInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `deleteHwProductInfoByProductInfoIds` | `public int` | `Long[] productInfoIds` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `insertHwProductInfo` | `public int` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `selectHwProductInfoByProductInfoId` | `public HwProductInfo` | `Long productInfoId` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `selectHwProductInfoJoinDetailList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `selectHwProductInfoJoinList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `selectHwProductInfoList` | `public List` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwProductInfoMapper` | `updateHwProductInfo` | `public int` | `HwProductInfo hwProductInfo` | `src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` | +| `HwSearchMapper` | `searchByKeyword` | `List` | `@Param("keyword") String keyword` | `src/main/java/com/ruoyi/portal/mapper/HwSearchMapper.java` | +| `HwWebDocumentMapper` | `deleteHwWebDocumentByDocumentId` | `public int` | `String documentId` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebDocumentMapper` | `deleteHwWebDocumentByDocumentIds` | `public int` | `String[] documentIds` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebDocumentMapper` | `insertHwWebDocument` | `public int` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebDocumentMapper` | `selectHwWebDocumentByDocumentId` | `public HwWebDocument` | `String documentId` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebDocumentMapper` | `selectHwWebDocumentList` | `public List` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebDocumentMapper` | `updateHwWebDocument` | `public int` | `HwWebDocument hwWebDocument` | `src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` | +| `HwWebMapper` | `deleteHwWebByWebId` | `public int` | `Long webId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper` | `deleteHwWebByWebIds` | `public int` | `Long[] webIds` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper` | `insertHwWeb` | `public int` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper` | `selectHwWebByWebcode` | `public HwWeb` | `Long webCode` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper` | `selectHwWebList` | `public List` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper` | `updateHwWeb` | `public int` | `HwWeb hwWeb` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` | +| `HwWebMapper1` | `deleteHwWebByWebId` | `public int` | `Long webId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `deleteHwWebByWebIds` | `public int` | `Long[] webIds` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `insertHwWeb` | `public int` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `selectHwWebByWebcode` | `public HwWeb1` | `Long webCode` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `selectHwWebList` | `public List` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `selectHwWebOne` | `public HwWeb1` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMapper1` | `updateHwWeb` | `public int` | `HwWeb1 hwWeb1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` | +| `HwWebMenuMapper` | `deleteHwWebMenuByWebMenuId` | `public int` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper` | `deleteHwWebMenuByWebMenuIds` | `public int` | `Long[] webMenuIds` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper` | `insertHwWebMenu` | `public int` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper` | `selectHwWebMenuByWebMenuId` | `public HwWebMenu` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper` | `selectHwWebMenuList` | `public List` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper` | `updateHwWebMenu` | `public int` | `HwWebMenu hwWebMenu` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` | +| `HwWebMenuMapper1` | `deleteHwWebMenuByWebMenuId` | `public int` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `deleteHwWebMenuByWebMenuIds` | `public int` | `Long[] webMenuIds` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `insertHwWebMenu` | `public int` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `selectHwWebMenuByWebMenuId` | `public HwWebMenu1` | `Long webMenuId` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `selectHwWebMenuList` | `public List` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `selectMenuTree` | `public List` | `HwWebMenu1 hwWebMenu` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | +| `HwWebMenuMapper1` | `updateHwWebMenu` | `public int` | `HwWebMenu1 HwWebMenu1` | `src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` | + +### 5.3 MyBatis XML 操作 + +| XML | 操作 | id | 涉及表 | 源文件 | +| --- | --- | --- | --- | --- | +| `HwAboutUsInfoDetailMapper.xml` | `DELETE` | `deleteHwAboutUsInfoDetailByUsInfoDetailId` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoDetailMapper.xml` | `DELETE` | `deleteHwAboutUsInfoDetailByUsInfoDetailIds` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoDetailMapper.xml` | `INSERT` | `insertHwAboutUsInfoDetail` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoDetailMapper.xml` | `SELECT` | `selectHwAboutUsInfoDetailByUsInfoDetailId` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoDetailMapper.xml` | `SELECT` | `selectHwAboutUsInfoDetailList` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoDetailMapper.xml` | `UPDATE` | `updateHwAboutUsInfoDetail` | hw_about_us_info_detail | `src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `DELETE` | `deleteHwAboutUsInfoByAboutUsInfoId` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `DELETE` | `deleteHwAboutUsInfoByAboutUsInfoIds` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `INSERT` | `insertHwAboutUsInfo` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `SELECT` | `selectHwAboutUsInfoByAboutUsInfoId` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `SELECT` | `selectHwAboutUsInfoList` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAboutUsInfoMapper.xml` | `UPDATE` | `updateHwAboutUsInfo` | hw_about_us_info | `src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` | +| `HwAnalyticsMapper.xml` | `INSERT` | `insertVisitEvent` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `INSERT` | `upsertDaily` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `avgStayMs` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `countDistinctIp` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `countDistinctSessions` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `countDistinctVisitor` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `countEventByType` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `countSinglePageSessions` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `selectDailyByDate` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `selectTopEntryPages` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `selectTopHotPages` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwAnalyticsMapper.xml` | `SELECT` | `selectTopKeywords` | hw_web_visit_event, hw_web_visit_daily | `src/main/resources/mapper/portal/HwAnalyticsMapper.xml` | +| `HwContactUsInfoMapper.xml` | `DELETE` | `deleteHwContactUsInfoByContactUsInfoId` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwContactUsInfoMapper.xml` | `DELETE` | `deleteHwContactUsInfoByContactUsInfoIds` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwContactUsInfoMapper.xml` | `INSERT` | `insertHwContactUsInfo` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwContactUsInfoMapper.xml` | `SELECT` | `selectHwContactUsInfoByContactUsInfoId` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwContactUsInfoMapper.xml` | `SELECT` | `selectHwContactUsInfoList` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwContactUsInfoMapper.xml` | `UPDATE` | `updateHwContactUsInfo` | hw_contact_us_info | `src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` | +| `HwPortalConfigMapper.xml` | `DELETE` | `deleteHwPortalConfigByPortalConfigId` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `DELETE` | `deleteHwPortalConfigByPortalConfigIds` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `INSERT` | `insertHwPortalConfig` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `SELECT` | `selectHwPortalConfigByPortalConfigId` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `SELECT` | `selectHwPortalConfigJoinList` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `SELECT` | `selectHwPortalConfigList` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `SELECT` | `selectHwPortalConfigList2` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigMapper.xml` | `UPDATE` | `updateHwPortalConfig` | hw_portal_config, hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `DELETE` | `deleteHwPortalConfigTypeByConfigTypeId` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `DELETE` | `deleteHwPortalConfigTypeByConfigTypeIds` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `INSERT` | `insertHwPortalConfigType` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `SELECT` | `selectHwPortalConfigTypeByConfigTypeId` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `SELECT` | `selectHwPortalConfigTypeList` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwPortalConfigTypeMapper.xml` | `UPDATE` | `updateHwPortalConfigType` | hw_portal_config_type | `src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `DELETE` | `deleteHwProductCaseInfoByCaseInfoId` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `DELETE` | `deleteHwProductCaseInfoByCaseInfoIds` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `INSERT` | `insertHwProductCaseInfo` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `SELECT` | `selectHwProductCaseInfoByCaseInfoId` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `SELECT` | `selectHwProductCaseInfoJoinList` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `SELECT` | `selectHwProductCaseInfoList` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductCaseInfoMapper.xml` | `UPDATE` | `updateHwProductCaseInfo` | hw_product_case_info, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `DELETE` | `deleteHwProductInfoDetailByProductInfoDetailId` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `DELETE` | `deleteHwProductInfoDetailByProductInfoDetailIds` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `INSERT` | `insertHwProductInfoDetail` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `SELECT` | `selectHwProductInfoDetailByProductInfoDetailId` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `SELECT` | `selectHwProductInfoDetailList` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoDetailMapper.xml` | `UPDATE` | `updateHwProductInfoDetail` | hw_product_info_detail | `src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` | +| `HwProductInfoMapper.xml` | `DELETE` | `deleteHwProductInfoByProductInfoId` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `DELETE` | `deleteHwProductInfoByProductInfoIds` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `INSERT` | `insertHwProductInfo` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `SELECT` | `selectHwProductInfoByProductInfoId` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `SELECT` | `selectHwProductInfoJoinDetailList` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `SELECT` | `selectHwProductInfoJoinList` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `SELECT` | `selectHwProductInfoList` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwProductInfoMapper.xml` | `UPDATE` | `updateHwProductInfo` | hw_product_info, hw_product_info_detail, hw_portal_config_type | `src/main/resources/mapper/portal/HwProductInfoMapper.xml` | +| `HwSearchMapper.xml` | `SELECT` | `searchByKeyword` | hw_web_menu, hw_web, hw_web1, hw_web_document, hw_portal_config_type | `src/main/resources/mapper/portal/HwSearchMapper.xml` | +| `HwWebDocumentMapper.xml` | `INSERT` | `insertHwWebDocument` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebDocumentMapper.xml` | `SELECT` | `selectHwWebDocumentByDocumentId` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebDocumentMapper.xml` | `SELECT` | `selectHwWebDocumentList` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebDocumentMapper.xml` | `UPDATE` | `deleteHwWebDocumentByDocumentId` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebDocumentMapper.xml` | `UPDATE` | `deleteHwWebDocumentByDocumentIds` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebDocumentMapper.xml` | `UPDATE` | `updateHwWebDocument` | hw_web_document | `src/main/resources/mapper/portal/HwWebDocumentMapper.xml` | +| `HwWebMapper.xml` | `INSERT` | `insertHwWeb` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper.xml` | `SELECT` | `selectHwWebByWebcode` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper.xml` | `SELECT` | `selectHwWebList` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper.xml` | `UPDATE` | `deleteHwWebByWebId` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper.xml` | `UPDATE` | `deleteHwWebByWebIds` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper.xml` | `UPDATE` | `updateHwWeb` | hw_web | `src/main/resources/mapper/portal/HwWebMapper.xml` | +| `HwWebMapper1.xml` | `INSERT` | `insertHwWeb` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `SELECT` | `selectHwWebByWebcode` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `SELECT` | `selectHwWebList` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `SELECT` | `selectHwWebOne` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `UPDATE` | `deleteHwWebByWebId` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `UPDATE` | `deleteHwWebByWebIds` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMapper1.xml` | `UPDATE` | `updateHwWeb` | hw_web1 | `src/main/resources/mapper/portal/HwWebMapper1.xml` | +| `HwWebMenuMapper.xml` | `INSERT` | `insertHwWebMenu` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper.xml` | `SELECT` | `selectHwWebMenuByWebMenuId` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper.xml` | `SELECT` | `selectHwWebMenuList` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper.xml` | `UPDATE` | `deleteHwWebMenuByWebMenuId` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper.xml` | `UPDATE` | `deleteHwWebMenuByWebMenuIds` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper.xml` | `UPDATE` | `updateHwWebMenu` | hw_web_menu | `src/main/resources/mapper/portal/HwWebMenuMapper.xml` | +| `HwWebMenuMapper1.xml` | `INSERT` | `insertHwWebMenu` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | +| `HwWebMenuMapper1.xml` | `SELECT` | `selectHwWebMenuByWebMenuId` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | +| `HwWebMenuMapper1.xml` | `SELECT` | `selectHwWebMenuList` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | +| `HwWebMenuMapper1.xml` | `UPDATE` | `deleteHwWebMenuByWebMenuId` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | +| `HwWebMenuMapper1.xml` | `UPDATE` | `deleteHwWebMenuByWebMenuIds` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | +| `HwWebMenuMapper1.xml` | `UPDATE` | `updateHwWebMenu` | hw_web_menu1 | `src/main/resources/mapper/portal/HwWebMenuMapper1.xml` | + +### 5.4 实体 / DTO / VO / 搜索文档清单 + +| 类型 | 源文件 | 直接依赖的 `ruoyi-common` 能力 | +| --- | --- | --- | +| `AnalyticsCollectRequest` | `src/main/java/com/ruoyi/portal/domain/dto/AnalyticsCollectRequest.java` | - | +| `AnalyticsDashboardDTO` | `src/main/java/com/ruoyi/portal/domain/dto/AnalyticsDashboardDTO.java` | - | +| `AnalyticsRankItemDTO` | `src/main/java/com/ruoyi/portal/domain/dto/AnalyticsRankItemDTO.java` | - | +| `HwAboutUsInfo` | `src/main/java/com/ruoyi/portal/domain/HwAboutUsInfo.java` | Excel, BaseEntity | +| `HwAboutUsInfoDetail` | `src/main/java/com/ruoyi/portal/domain/HwAboutUsInfoDetail.java` | Excel, BaseEntity | +| `HwContactUsInfo` | `src/main/java/com/ruoyi/portal/domain/HwContactUsInfo.java` | Excel, BaseEntity | +| `HwPortalConfig` | `src/main/java/com/ruoyi/portal/domain/HwPortalConfig.java` | Excel, BaseEntity | +| `HwPortalConfigType` | `src/main/java/com/ruoyi/portal/domain/HwPortalConfigType.java` | Excel, BaseEntity, TreeEntity | +| `HwProductCaseInfo` | `src/main/java/com/ruoyi/portal/domain/HwProductCaseInfo.java` | Excel, BaseEntity | +| `HwProductInfo` | `src/main/java/com/ruoyi/portal/domain/HwProductInfo.java` | Excel, BaseEntity | +| `HwProductInfoDetail` | `src/main/java/com/ruoyi/portal/domain/HwProductInfoDetail.java` | Excel, TreeEntity | +| `HwWeb` | `src/main/java/com/ruoyi/portal/domain/HwWeb.java` | Excel, BaseEntity | +| `HwWeb1` | `src/main/java/com/ruoyi/portal/domain/HwWeb1.java` | Excel, BaseEntity | +| `HwWebDocument` | `src/main/java/com/ruoyi/portal/domain/HwWebDocument.java` | Excel, BaseEntity | +| `HwWebMenu` | `src/main/java/com/ruoyi/portal/domain/HwWebMenu.java` | Excel, TreeEntity | +| `HwWebMenu1` | `src/main/java/com/ruoyi/portal/domain/HwWebMenu1.java` | Excel, TreeEntity | +| `HwWebVisitDaily` | `src/main/java/com/ruoyi/portal/domain/HwWebVisitDaily.java` | - | +| `HwWebVisitEvent` | `src/main/java/com/ruoyi/portal/domain/HwWebVisitEvent.java` | BaseEntity | +| `PortalSearchDoc` | `src/main/java/com/ruoyi/portal/search/es/entity/PortalSearchDoc.java` | - | +| `SearchPageDTO` | `src/main/java/com/ruoyi/portal/domain/dto/SearchPageDTO.java` | - | +| `SearchRawRecord` | `src/main/java/com/ruoyi/portal/domain/dto/SearchRawRecord.java` | - | +| `SearchResultDTO` | `src/main/java/com/ruoyi/portal/domain/dto/SearchResultDTO.java` | - | +| `SecureDocumentRequest` | `src/main/java/com/ruoyi/portal/domain/SecureDocumentRequest.java` | - | +| `TreeSelect` | `src/main/java/com/ruoyi/portal/domain/vo/TreeSelect.java` | - | + +## 6. 关键代码逻辑 + +### 6.1 `HwAnalyticsServiceImpl.java` + +- 埋点事件白名单固定为 `page_view/page_leave/search_submit/download_click/contact_submit`。 +- IP 不直接存库,而是用 `SHA-256(ip + "|hw-portal-analytics")` 计算哈希;这个固定盐值必须保持不变,否则历史统计口径会断裂。 +- `collect` 每落一条事件就立即刷新当天汇总表。 +- `refreshDailyStat` 的跳出率算法是 `singlePageSessionCount * 100 / sessionCount`,并四舍五入到两位小数。 + +### 6.2 `HwPortalConfigServiceImpl.java` + +- `selectHwPortalConfigList` 根据 `portalConfigType == "2"` 在两套 SQL 之间切换:普通列表 SQL 与 `selectHwPortalConfigList2`。 +- 这类“按字段切换 SQL 模板”的行为必须保留,不要在 C# 中合并成单条动态拼装 SQL。 + +### 6.3 `HwPortalConfigTypeServiceImpl.java` + +- `selectConfigTypeList` 在带 `configTypeClassfication` 过滤时,先查全量、再补齐所有子孙节点、最后组树;这不是普通 where 过滤。 +- `insertHwPortalConfigType` 会根据父节点补 `parentId/ancestors/createTime/createBy`,新增逻辑不能只做简单 Insert。 +- 树构造规则是“父节点为空、0、或父节点不在当前集合中即视为根”。 + +### 6.4 `HwPortalController.java` + +- 这是门户前台聚合控制器,不自己落库,只组合 `config/configType/product/case/contact/aboutUs` 多个服务。 +- 其中 `getPortalConfigList`、`getHomeCaseTitleList` 调用了 `startPage()`;迁移到 Admin.NET 时如果要保持返回契约,需要保留分页行为而不是直接返回全部。 +- `addContactUsInfo` 会在入库前写入客户端 IP,属于方法逻辑的一部分,不能省略。 + +### 6.5 `HwProductCaseInfoServiceImpl.java` + +- `getTypicalHomeCaseInfo` 会先强制设置 `homeTypicalFlag=1`,再查询列表,然后优先返回 `typicalFlag==1` 的首条,否则回退到列表首条。 +- 没有命中数据时返回的是新的空对象,不是 `null`。 + +### 6.6 `HwProductInfoDetailServiceImpl.java` + +- `insertHwProductInfoDetail` 会根据父节点补 `parentId/ancestors/createTime/createBy`。 +- 这里 `parentId == null` 和 `parentId == 0` 都被归一成根节点,迁移时要保持相同行为。 + +### 6.7 `HwProductInfoServiceImpl.java` + +- `selectHwProductInfoJoinDetailList` 先跑 join SQL,再在内存中按 `configModal == 13` 或明细 `configModel == 13` 把明细列表转换为树。 +- 树化后同时写入 `TreeEntity.children` 与 `hwProductInfoDetailList`,前端可能依赖两套字段。 + +### 6.8 `HwSearchMapper.xml` + +- MySQL 搜索 SQL 由 5 段 `UNION ALL` 组成,只覆盖 `menu/web/web1/document/configType`。 +- 每段都带固定基础分值:menu=110、web1=90、web=80、document=70、configType=65;随后服务层再叠加标题/内容命中分。 + +### 6.9 `HwSearchRebuildServiceImpl.java` + +- 索引名固定为 `hw_portal_content`。重建流程是:存在则删索引 -> 创建索引 -> 收集文档 -> 批量写入。 +- 当前仅重建 `menu/web/web1/document/configType` 五类文档,没有把 product/case/aboutUs 纳入 ES。 + +### 6.10 `HwSearchServiceImpl.java` + +- 搜索引擎选择规则是:`search.es.enabled=true` 且 `search.engine=es` 且 ES 服务存在时优先走 ES;任一条件不满足或 ES 异常时回退 MySQL。 +- MySQL 搜索先跑 `HwSearchMapper.searchByKeyword`,再在内存中做二次过滤、打分、摘要截取、高亮、分页。 +- 搜索关键字限制长度 50,页大小上限 50,MySQL SQL 内部结果上限 500。 + +### 6.11 `HwWebDocumentController.java` + +- 列表和详情接口都会把 `secretKey` 置空;当 `hasSecret=true` 时还会把 `documentAddress` 置空。 +- `getSecureDocumentAddress` 捕获异常后直接把错误信息返回前端,这也是现有接口契约的一部分。 + +### 6.12 `HwWebDocumentServiceImpl.java` + +- `updateHwWebDocument` 对 `secretKey == null` 做了特殊归一:改成空字符串,以便触发 Mapper 更新逻辑清空数据库密钥。 +- `verifyAndGetDocumentAddress` 的判定规则是:数据库密钥为空则直接放行;否则要求前端传入去空格后的完全相等密钥。 +- 增删改成功后会静默重建搜索索引。 + +### 6.13 `HwWebMenuServiceImpl.java` + +- 菜单列表的树化发生在服务层,不在 SQL 里。 +- 增删改成功后会静默触发搜索索引重建,失败只记日志不回滚主业务。 + +### 6.14 `HwWebMenuServiceImpl1.java` + +- `HwWebMenu1` 是另一套菜单模型,保留了同样的树化规则,但没有搜索索引重建逻辑。 + +### 6.15 `HwWebServiceImpl.java` + +- `updateHwWeb` 不是更新原记录,而是按 `webCode` 查重后逻辑删除旧记录,再插入一条新记录。 +- 新插入前显式设置 `isDelete = "0"`,并且整个过程包在事务里。 + +### 6.16 `HwWebServiceImpl1.java` + +- `updateHwWeb` 的唯一键是 `webCode + typeId + deviceId`,处理方式同 `HwWebServiceImpl`:先逻辑删除旧记录,再插入新记录。 + +### 6.17 `PortalSearchDocConverter.java` + +- 对 `web/web1` 的 JSON 内容会先去 HTML,再按 JSON 递归提取可搜索文本,并跳过 `icon/url/banner/img/secretKey` 等字段。 +- 路由和路由参数是在建索引阶段就写进文档,不是在查询阶段临时拼装。 + +## 7. `ruoyi-common` / Spring 能力替换为 Admin.NET 的映射 + +| 源能力 | Admin.NET 建议替代 | +| --- | --- | +| `BaseController.startPage()/getDataTable()` | `SqlSugarPagedList` 或自定义分页 DTO,分页参数建议继承 `BasePageInput` | +| `AjaxResult/success/error/toAjax` | `AdminResultProvider` 统一返回;业务层直接返回对象/分页对象,异常用 `Oops.Oh(...)` 或统一异常 | +| `@Anonymous` | Admin.NET 的 `[AllowAnonymous]` | +| `@RepeatSubmit` | Admin.NET 的 `[Idempotent]`,默认基于分布式锁与缓存去重 | +| `@RateLimiter` | 当前项目已启用 `AspNetCoreRateLimit`;若要保持按接口/IP 限流,需要在中间件策略或自定义特性中落地 | +| `DateUtils.getNowDate()` | `.NET` 的 `DateTime.Now` 或统一时间工具,但要保持本地时区语义 | +| `SecurityUtils.getUsername()` | `UserManager.Account` / `UserManager.RealName` 视原字段含义选择 | +| `IpUtils.getIpAddr()` | `HttpContext.GetRemoteIpAddressToIPv4(true)` | +| `UserAgentUtils.getBrowser()/getOperatingSystem()` | `HttpContextExtension.GetClientBrowser()/GetClientOs()` 或 UA Parser | +| `StringUtils` | 优先用 `string`/`string.IsNullOrWhiteSpace` 与现有扩展;需要保留 `defaultIfBlank/containsIgnoreCase` 语义 | +| `MyBatis XML` | 若要求 SQL 文本完全不变,优先在 C# 仓库层用 `ISqlSugarClient.Ado.SqlQueryAsync/ExecuteCommandAsync` 执行原 SQL,避免改写成 LINQ | + +### 7.1 当前模块对 `com.ruoyi.common` 的直接依赖分布 + +| 依赖 | 使用文件数 | +| --- | ---: | +| `com.ruoyi.common.annotation.Anonymous` | 16 | +| `com.ruoyi.common.annotation.Excel` | 13 | +| `com.ruoyi.common.annotation.Log` | 16 | +| `com.ruoyi.common.annotation.RateLimiter` | 2 | +| `com.ruoyi.common.annotation.RepeatSubmit` | 14 | +| `com.ruoyi.common.constant.HwPortalConstants` | 1 | +| `com.ruoyi.common.constant.UserConstants` | 1 | +| `com.ruoyi.common.core.controller.BaseController` | 17 | +| `com.ruoyi.common.core.domain.AjaxResult` | 17 | +| `com.ruoyi.common.core.domain.BaseEntity` | 11 | +| `com.ruoyi.common.core.domain.TreeEntity` | 4 | +| `com.ruoyi.common.core.page.TableDataInfo` | 10 | +| `com.ruoyi.common.enums.BusinessType` | 16 | +| `com.ruoyi.common.enums.LimitType` | 2 | +| `com.ruoyi.common.exception.ServiceException` | 5 | +| `com.ruoyi.common.utils.DateUtils` | 9 | +| `com.ruoyi.common.utils.SecurityUtils` | 2 | +| `com.ruoyi.common.utils.StringUtils` | 8 | +| `com.ruoyi.common.utils.http.UserAgentUtils` | 1 | +| `com.ruoyi.common.utils.ip.IpUtils` | 2 | +| `com.ruoyi.common.utils.poi.ExcelUtil` | 13 | + +## 8. 抽离到 `hw-portal` 的实施建议 + +### 8.1 分层建议 + +1. `Entity` 层:按原表一比一建立 C# 实体,字段名、表名、主键、逻辑删除字段保持原样,避免任何列名重命名。 +2. `Repository` 层:每个 Java Mapper + XML 对应一个 C# Repository;把 XML 中的 SQL 原样保存在 `.sql` 资源、常量字符串或独立类中,通过 `ISqlSugarClient.Ado` 执行。 +3. `Service` 层:严格按 Java `ServiceImpl` 的方法流程翻译,尤其是树构造、字段归一、软删再插入、索引静默重建、密钥校验、搜索打分与摘要。 +4. `Api` 层:如果要兼容原前端,优先保留原 URL、HTTP Method、入参与出参结构;若统一为 Admin.NET 动态 API,需要额外做路由别名或兼容层。 + +### 8.2 必须保持不变的“行为点” + +- 原 SQL 文本。 +- 原接口路径与 HTTP 方法。 +- `HwWeb/HwWeb1` 的“逻辑删旧记录再插入新记录”更新语义。 +- `ancestors` 的拼接规则。 +- 树根识别规则。 +- 搜索路由、路由参数、打分规则、摘要截断规则、高亮 HTML。 +- Analytics 的白名单事件、IP 哈希算法和固定盐值、日报刷新算法。 +- 文档密钥为空时直接放行、有密钥时精确匹配。 + +### 8.3 当前源码中需要特别留意的事实差异 + +- `SearchResultDTO` 的注释声称会覆盖 `productInfo/productDetail/caseInfo/aboutUs` 等来源,但实际 MySQL 搜索 SQL 和 ES 重建只覆盖 `menu/web/web1/document/configType`。迁移时应以“实际代码行为”为准。 +- `HwWebDocumentController` 与 `HwPortalController` 都标了匿名访问,这意味着迁移后要显式放开访问,而不是默认套用后台鉴权。 +- `RepeatSubmit` 在原系统大量使用,迁移后如果不补幂等控制,会出现行为退化。 + +## 9. 验证清单 + +- 分别对 17 个 Controller 路由做契约对比,确认 URL、HTTP Method、参数名、空值行为、返回 JSON 结构一致。 +- 对 15 份 Mapper XML 的 SQL 做逐条对照,确认迁移后执行的 SQL 文本与原 XML 一致。 +- 对 `HwWeb/HwWeb1` 更新接口做集成测试:旧记录应被逻辑删除,新记录新增成功。 +- 对配置类型树、菜单树、产品明细树做结构对比测试,确认 children 层级与根节点判定一致。 +- 对搜索做双通道验证:MySQL 模式与 ES 模式都验证关键字命中、分页、高亮、编辑端路由。 +- 对 Analytics 做口径验证:`collect -> daily` 的 PV/UV/IP UV/停留时长/跳出率 与原算法一致。 +- 对文档密钥场景做安全验证:无密钥直接放行;有密钥时空字符串报错、错误密钥报错、正确密钥返回地址。 + +## 10. 附录:完整源码基线 + +以下附录直接贴出 `ruoyi-portal` 当前源码与 XML,作为后续 `hw-portal` 抽离的原始对照基线。 + +### `ruoyi-portal/pom.xml` + +```xml + + + + ruoyi + com.ruoyi + 3.9.1 + + 4.0.0 + + ruoyi-portal + + + portal系统模块 + + + + + + com.ruoyi + ruoyi-common + + + + + +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwAboutUsInfoController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwAboutUsInfo; +import com.ruoyi.portal.service.IHwAboutUsInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 关于我们信息Controller + * + * @author xins + * @date 2024-12-01 + */ +@RestController +@Anonymous//不需要登陆任何人都可以操作 +@RequestMapping("/portal/aboutUsInfo") +public class HwAboutUsInfoController extends BaseController +{ + @Autowired + private IHwAboutUsInfoService hwAboutUsInfoService; + + /** + * 查询关于我们信息列表 + */ + //@RequiresPermissions("portalaboutUsInfo:list") + @GetMapping("/list") + public TableDataInfo list(HwAboutUsInfo hwAboutUsInfo) + { + startPage(); + List list = hwAboutUsInfoService.selectHwAboutUsInfoList(hwAboutUsInfo); + return getDataTable(list); + } + + /** + * 导出关于我们信息列表 + */ + //@RequiresPermissions("portalaboutUsInfo:export") + //@Log(title = "关于我们信息", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwAboutUsInfo hwAboutUsInfo) + { + List list = hwAboutUsInfoService.selectHwAboutUsInfoList(hwAboutUsInfo); + ExcelUtil util = new ExcelUtil(HwAboutUsInfo.class); + util.exportExcel(response, list, "关于我们信息数据"); + } + + /** + * 获取关于我们信息详细信息 + */ + //@RequiresPermissions("portalaboutUsInfo:query") + @GetMapping(value = "/{aboutUsInfoId}") + public AjaxResult getInfo(@PathVariable("aboutUsInfoId") Long aboutUsInfoId) + { + return success(hwAboutUsInfoService.selectHwAboutUsInfoByAboutUsInfoId(aboutUsInfoId)); + } + + /** + * 新增关于我们信息 + */ + //@RequiresPermissions("portalaboutUsInfo:add") + //@Log(title = "关于我们信息", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwAboutUsInfo hwAboutUsInfo) + { + return toAjax(hwAboutUsInfoService.insertHwAboutUsInfo(hwAboutUsInfo)); + } + + /** + * 修改关于我们信息 + */ + //@RequiresPermissions("portalaboutUsInfo:edit") + //@Log(title = "关于我们信息", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwAboutUsInfo hwAboutUsInfo) + { + return toAjax(hwAboutUsInfoService.updateHwAboutUsInfo(hwAboutUsInfo)); + } + + /** + * 删除关于我们信息 + */ + //@RequiresPermissions("portalaboutUsInfo:remove") + //@Log(title = "关于我们信息", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{aboutUsInfoIds}") + public AjaxResult remove(@PathVariable Long[] aboutUsInfoIds) + { + return toAjax(hwAboutUsInfoService.deleteHwAboutUsInfoByAboutUsInfoIds(aboutUsInfoIds)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwAboutUsInfoDetailController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwAboutUsInfoDetail; +import com.ruoyi.portal.service.IHwAboutUsInfoDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 关于我们信息明细Controller + * + * @author ruoyi + * @date 2024-12-01 + */ +@RestController +@RequestMapping("/portal/aboutUsInfoDetail") +@Anonymous//不需要登陆任何人都可以操作 +public class HwAboutUsInfoDetailController extends BaseController +{ + @Autowired + private IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; + + /** + * 查询关于我们信息明细列表 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:list") + @GetMapping("/list") + public TableDataInfo list(HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + startPage(); + List list = hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList(hwAboutUsInfoDetail); + return getDataTable(list); + } + + /** + * 导出关于我们信息明细列表 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:export") + //@Log(title = "关于我们信息明细", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + List list = hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList(hwAboutUsInfoDetail); + ExcelUtil util = new ExcelUtil(HwAboutUsInfoDetail.class); + util.exportExcel(response, list, "关于我们信息明细数据"); + } + + /** + * 获取关于我们信息明细详细信息 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:query") + @GetMapping(value = "/{usInfoDetailId}") + public AjaxResult getInfo(@PathVariable("usInfoDetailId") Long usInfoDetailId) + { + return success(hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailByUsInfoDetailId(usInfoDetailId)); + } + + /** + * 新增关于我们信息明细 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:add") + //@Log(title = "关于我们信息明细", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + return toAjax(hwAboutUsInfoDetailService.insertHwAboutUsInfoDetail(hwAboutUsInfoDetail)); + } + + /** + * 修改关于我们信息明细 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:edit") + //@Log(title = "关于我们信息明细", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + return toAjax(hwAboutUsInfoDetailService.updateHwAboutUsInfoDetail(hwAboutUsInfoDetail)); + } + + /** + * 删除关于我们信息明细 + */ + //@RequiresPermissions("portalaboutUsInfoDetail:remove") + //@Log(title = "关于我们信息明细", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{usInfoDetailIds}") + public AjaxResult remove(@PathVariable Long[] usInfoDetailIds) + { + return toAjax(hwAboutUsInfoDetailService.deleteHwAboutUsInfoDetailByUsInfoDetailIds(usInfoDetailIds)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwAnalyticsController.java` + +```java +package com.ruoyi.portal.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.portal.domain.dto.AnalyticsCollectRequest; +import com.ruoyi.portal.service.IHwAnalyticsService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; + +/** + * 官网匿名访问监控 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/portal/analytics") +public class HwAnalyticsController extends BaseController +{ + private final IHwAnalyticsService hwAnalyticsService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public HwAnalyticsController(IHwAnalyticsService hwAnalyticsService) + { + this.hwAnalyticsService = hwAnalyticsService; + } + + /** + * 匿名访问事件采集 + */ + @Anonymous + @RateLimiter(key = "portal_analytics_collect", time = 60, count = 240, limitType = LimitType.IP) + @PostMapping(value = "/collect", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE}) + public AjaxResult collect(@RequestBody(required = false) String body, HttpServletRequest request) + { + if (body == null || body.trim().isEmpty()) + { + return error("请求体不能为空"); + } + try + { + AnalyticsCollectRequest collectRequest = objectMapper.readValue(body, AnalyticsCollectRequest.class); + hwAnalyticsService.collect(collectRequest, IpUtils.getIpAddr(request), request.getHeader("User-Agent")); + return success(); + } + catch (Exception e) + { + return error("采集失败: " + e.getMessage()); + } + } + + /** + * 监控看板数据 + */ + @Log(title = "官网访问监控", businessType = BusinessType.OTHER) + @GetMapping("/dashboard") + public AjaxResult dashboard(@RequestParam(value = "statDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate statDate, + @RequestParam(value = "top", required = false) Integer top) + { + return success(hwAnalyticsService.getDashboard(statDate, top)); + } + + /** + * 刷新某日统计数据 + */ + @Log(title = "官网访问监控", businessType = BusinessType.OTHER) + @PostMapping("/refreshDaily") + public AjaxResult refreshDaily(@RequestParam(value = "statDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate statDate) + { + hwAnalyticsService.refreshDailyStat(statDate); + return success("刷新成功"); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwContactUsInfoController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwContactUsInfo; +import com.ruoyi.portal.service.IHwContactUsInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 联系我们信息Controller + * + * @author xins + * @date 2024-12-01 + */ +@RestController +@RequestMapping("/portal/contactUsInfo") +@Anonymous//不需要登陆任何人都可以操作 +public class HwContactUsInfoController extends BaseController +{ + @Autowired + private IHwContactUsInfoService hwContactUsInfoService; + + /** + * 查询联系我们信息列表 + */ + //@RequiresPermissions("portalcontactUsInfo:list") + @GetMapping("/list") + public TableDataInfo list(HwContactUsInfo hwContactUsInfo) + { + startPage(); + List list = hwContactUsInfoService.selectHwContactUsInfoList(hwContactUsInfo); + return getDataTable(list); + } + + /** + * 导出联系我们信息列表 + */ + //@RequiresPermissions("portalcontactUsInfo:export") + //@Log(title = "联系我们信息", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwContactUsInfo hwContactUsInfo) + { + List list = hwContactUsInfoService.selectHwContactUsInfoList(hwContactUsInfo); + ExcelUtil util = new ExcelUtil(HwContactUsInfo.class); + util.exportExcel(response, list, "联系我们信息数据"); + } + + /** + * 获取联系我们信息详细信息 + */ + //@RequiresPermissions("portalcontactUsInfo:query") + @GetMapping(value = "/{contactUsInfoId}") + public AjaxResult getInfo(@PathVariable("contactUsInfoId") Long contactUsInfoId) + { + return success(hwContactUsInfoService.selectHwContactUsInfoByContactUsInfoId(contactUsInfoId)); + } + + /** + * 新增联系我们信息 + */ + //@RequiresPermissions("portalcontactUsInfo:add") + //@Log(title = "联系我们信息", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwContactUsInfo hwContactUsInfo) + { + return toAjax(hwContactUsInfoService.insertHwContactUsInfo(hwContactUsInfo)); + } + + /** + * 修改联系我们信息 + */ + //@RequiresPermissions("portalcontactUsInfo:edit") + //@Log(title = "联系我们信息", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwContactUsInfo hwContactUsInfo) + { + return toAjax(hwContactUsInfoService.updateHwContactUsInfo(hwContactUsInfo)); + } + + /** + * 删除联系我们信息 + */ + //@RequiresPermissions("portalcontactUsInfo:remove") + //@Log(title = "联系我们信息", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{contactUsInfoIds}") + public AjaxResult remove(@PathVariable Long[] contactUsInfoIds) + { + return toAjax(hwContactUsInfoService.deleteHwContactUsInfoByContactUsInfoIds(contactUsInfoIds)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwPortalConfigController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwPortalConfig; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.service.IHwPortalConfigService; +import com.ruoyi.portal.service.IHwPortalConfigTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 门户网站配置Controller + * + * @author xins + * @date 2024-12-01 + */ +@RestController +@RequestMapping("/portal/portalConfig") +@Anonymous//不需要登陆任何人都可以操作 +public class HwPortalConfigController extends BaseController +{ + @Autowired + private IHwPortalConfigService hwPortalConfigService; + + @Autowired + private IHwPortalConfigTypeService hwPortalConfigTypeService; + + /** + * 查询门户网站配置列表 + */ + //@RequiresPermissions("portalportalConfig:list") + @GetMapping("/list") + public TableDataInfo list(HwPortalConfig hwPortalConfig) + { + startPage(); + List list = hwPortalConfigService.selectHwPortalConfigJoinList(hwPortalConfig); + return getDataTable(list); + } + + /** + * 导出门户网站配置列表 + */ + //@RequiresPermissions("portalportalConfig:export") + //@Log(title = "门户网站配置", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwPortalConfig hwPortalConfig) + { + List list = hwPortalConfigService.selectHwPortalConfigList(hwPortalConfig); + ExcelUtil util = new ExcelUtil(HwPortalConfig.class); + util.exportExcel(response, list, "门户网站配置数据"); + } + + /** + * 获取门户网站配置详细信息 + */ + //@RequiresPermissions("portalportalConfig:query") + @GetMapping(value = "/{portalConfigId}") + public AjaxResult getInfo(@PathVariable("portalConfigId") Long portalConfigId) + { + return success(hwPortalConfigService.selectHwPortalConfigByPortalConfigId(portalConfigId)); + } + + /** + * 新增门户网站配置 + */ + //@RequiresPermissions("portalportalConfig:add") + //@Log(title = "门户网站配置", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwPortalConfig hwPortalConfig) + { + return toAjax(hwPortalConfigService.insertHwPortalConfig(hwPortalConfig)); + } + + /** + * 修改门户网站配置 + */ + //@RequiresPermissions("portalportalConfig:edit") + //@Log(title = "门户网站配置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwPortalConfig hwPortalConfig) + { + return toAjax(hwPortalConfigService.updateHwPortalConfig(hwPortalConfig)); + } + + /** + * 删除门户网站配置 + */ + //@RequiresPermissions("portalportalConfig:remove") + //@Log(title = "门户网站配置", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{portalConfigIds}") + public AjaxResult remove(@PathVariable Long[] portalConfigIds) + { + return toAjax(hwPortalConfigService.deleteHwPortalConfigByPortalConfigIds(portalConfigIds)); + } + + + /** + * 获取门户网站配置树列表 + */ + //@RequiresPermissions("portalportalConfig:list") + @GetMapping("/portalConfigTypeTree") + public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType) { + + return success(hwPortalConfigTypeService.selectPortalConfigTypeTreeList(hwPortalConfigType)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwPortalConfigTypeController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.service.IHwPortalConfigTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 门户网站配置类型Controller + * + * @author xins + * @date 2024-12-11 + */ +@RestController +@RequestMapping("/portal/portalConfigType") +@Anonymous//不需要登陆任何人都可以操作 +public class HwPortalConfigTypeController extends BaseController +{ + @Autowired + private IHwPortalConfigTypeService hwPortalConfigTypeService; + + /** + * 查询门户网站配置类型列表 + */ + //@RequiresPermissions("portalportalConfigType:list") + @GetMapping("/list") + public AjaxResult list(HwPortalConfigType hwPortalConfigType) + { + List list = hwPortalConfigTypeService.selectHwPortalConfigTypeList(hwPortalConfigType); + return success(list); + } + + /** + * 导出门户网站配置类型列表 + */ + //@RequiresPermissions("portalportalConfigType:export") + //@Log(title = "门户网站配置类型", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwPortalConfigType hwPortalConfigType) + { + List list = hwPortalConfigTypeService.selectHwPortalConfigTypeList(hwPortalConfigType); + ExcelUtil util = new ExcelUtil(HwPortalConfigType.class); + util.exportExcel(response, list, "门户网站配置类型数据"); + } + + /** + * 获取门户网站配置类型详细信息 + */ + //@RequiresPermissions("portalportalConfigType:query") + @GetMapping(value = "/{configTypeId}") + public AjaxResult getInfo(@PathVariable("configTypeId") Long configTypeId) + { + return success(hwPortalConfigTypeService.selectHwPortalConfigTypeByConfigTypeId(configTypeId)); + } + + /** + * 新增门户网站配置类型 + */ + //@RequiresPermissions("portalportalConfigType:add") + //@Log(title = "门户网站配置类型", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwPortalConfigType hwPortalConfigType) + { + return toAjax(hwPortalConfigTypeService.insertHwPortalConfigType(hwPortalConfigType)); + } + + /** + * 修改门户网站配置类型 + */ + //@RequiresPermissions("portalportalConfigType:edit") + //@Log(title = "门户网站配置类型", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwPortalConfigType hwPortalConfigType) + { + return toAjax(hwPortalConfigTypeService.updateHwPortalConfigType(hwPortalConfigType)); + } + + /** + * 删除门户网站配置类型 + */ + //@RequiresPermissions("portalportalConfigType:remove") + //@Log(title = "门户网站配置类型", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{configTypeIds}") + public AjaxResult remove(@PathVariable Long[] configTypeIds) + { + return toAjax(hwPortalConfigTypeService.deleteHwPortalConfigTypeByConfigTypeIds(configTypeIds)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwPortalController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.ip.IpUtils; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.*; +import com.ruoyi.portal.service.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 门户网站前端Controller + * + * @author xins + * @date 2024-12-12 + */ +@RestController +@RequestMapping("/portal/portal") +@Anonymous//不需要登陆任何人都可以操作 +public class HwPortalController extends BaseController +{ + @Autowired + private IHwPortalConfigService hwPortalConfigService; + + @Autowired + private IHwPortalConfigTypeService hwPortalConfigTypeService; + + @Autowired + private IHwProductCaseInfoService hwProductCaseInfoService; + + @Autowired + private IHwContactUsInfoService hwContactUsInfoService; + + @Autowired + private IHwProductInfoService productInfoService; + + @Autowired + private IHwProductInfoDetailService hwProductInfoDetailService; + + @Autowired + private IHwAboutUsInfoService hwAboutUsInfoService; + + @Autowired + private IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; + + + /** + * 查询门户网站配置列表(首页大图,portal_config_type为1;产品中心大图,portal_config_type为2,并且需要根据portalConfigTypeId获取) + */ + @GetMapping("/getPortalConfigList") + public TableDataInfo getPortalConfigList(HwPortalConfig hwPortalConfig) + { + startPage(); + List list = hwPortalConfigService.selectHwPortalConfigList(hwPortalConfig); + return getDataTable(list); + } + + + /** + * 门户网站配置类型(首页产品中心,config_type_classfication为1,按homeConfigTypeName显示;产品中心页面上面的按configTypeName显示) + */ + @GetMapping("/getPortalConfigTypeList") + public TableDataInfo getPortalConfigTypeList(HwPortalConfigType hwPortalConfigType) + { + List list = hwPortalConfigTypeService.selectHwPortalConfigTypeList(hwPortalConfigType); + return getDataTable(list); + } + + + /** + * 门户网站配置类型(首页产品中心,config_type_classfication为1,按homeConfigTypeName显示;产品中心页面上面的按configTypeName显示) + */ + @GetMapping("/selectConfigTypeList") + public TableDataInfo selectConfigTypeList(HwPortalConfigType hwPortalConfigType) + { + List list = hwPortalConfigTypeService.selectConfigTypeList(hwPortalConfigType); + return getDataTable(list); + } + + /** + * 获取首页案例tab title(例如物联网、制造中心和快递物流) + */ + @GetMapping("/getHomeCaseTitleList") + public TableDataInfo getHomeCaseTitleList(HwPortalConfigType hwPortalConfigType) + { + startPage(); + List list = hwPortalConfigTypeService.selectHwPortalConfigTypeList(hwPortalConfigType); + return getDataTable(list); + } + + + /** + * 获取首页案例信息 + */ + @GetMapping("/getTypicalHomeCaseInfo") + public AjaxResult getTypicalHomeCaseInfo(HwProductCaseInfo queryProductCaseInfo) + { + HwProductCaseInfo hwProductCaseInfo = hwProductCaseInfoService.getTypicalHomeCaseInfo(queryProductCaseInfo); + + return success(hwProductCaseInfo); + } + + + + /** + * 新增联系我们 + */ + //@Log(title = "联系我们", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping("/addContactUsInfo") + public AjaxResult addContactUsInfo(@RequestBody HwContactUsInfo hwContactUsInfo) + { + hwContactUsInfo.setUserIp(IpUtils.getIpAddr()); + return toAjax(hwContactUsInfoService.insertHwContactUsInfo(hwContactUsInfo)); + } + + + + /** + * 获取产品中心产品信息(平台简介,hw_product_info获取,(配置模式2左标题+内容,右图片)读取中文标题和英文标题,下面内容从hw_product_info_detail获取,读取标题,内容和图片) + */ + @GetMapping("/getProductCenterProductInfos") + public AjaxResult getProductCenterProductInfos(HwProductInfo hwProductInfo) + { +// 配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8左图右列表9上图下内容,一行4个) + return success(productInfoService.selectHwProductInfoJoinDetailList(hwProductInfo)); + } + + /** + * 产品中心如果tab的话,根据tab的product_info_detail_id获取children productinfodetail + * @param hwProductInfoDetail + * @return + */ + @GetMapping("/getProductCenterProductDetailInfos") + public AjaxResult getProductCenterProductDetailInfos(HwProductInfoDetail hwProductInfoDetail) + { +// productinfodetail的config_modal 配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个) + return success(hwProductInfoDetailService.selectHwProductInfoDetailList(hwProductInfoDetail)); + } + + + /** + * 产品中心如果tab的话,根据tab的portalconfigtypeid获取productcaseinfo + * @param hwProductCaseInfo + * @return + */ + @GetMapping("/getCaseCenterCaseInfos") + public AjaxResult getCaseCenterCaseInfos(HwProductCaseInfo hwProductCaseInfo) + { + return success(hwProductCaseInfoService.selectHwProductCaseInfoList(hwProductCaseInfo)); + } + + /** + * 根据案例ID获取案例详情 + * @param caseInfoId + * @return + */ + @GetMapping("/getCaseCenterCaseInfo/{caseInfoId}") + public AjaxResult getCaseCenterCaseInfo(@PathVariable("caseInfoId") Long caseInfoId) + { + return success(hwProductCaseInfoService.selectHwProductCaseInfoByCaseInfoId(caseInfoId)); + } + + + /** + * 获取关于我们信息 + * @param hwAboutUsInfo + * @return + */ + @GetMapping("/getAboutUsInfo") + public AjaxResult getAboutUsInfo(HwAboutUsInfo hwAboutUsInfo) + { + return success(hwAboutUsInfoService.selectHwAboutUsInfoList(hwAboutUsInfo)); + } + + + /** + * 获取关于我们信息详情 + * @param hwAboutUsInfoDetail + * @return + */ + @GetMapping("/getAboutUsInfoDetails") + public AjaxResult getAboutUsInfoDetails(HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + return success(hwAboutUsInfoDetailService.selectHwAboutUsInfoDetailList(hwAboutUsInfoDetail)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwProductCaseInfoController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.HwProductCaseInfo; +import com.ruoyi.portal.service.IHwPortalConfigTypeService; +import com.ruoyi.portal.service.IHwProductCaseInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 案例内容Controller + * + * @author xins + * @date 2024-12-01 + */ +@RestController +@RequestMapping("/portal/productCaseInfo") +@Anonymous//不需要登陆任何人都可以操作 +public class HwProductCaseInfoController extends BaseController +{ + @Autowired + private IHwProductCaseInfoService hwProductCaseInfoService; + + @Autowired + private IHwPortalConfigTypeService hwPortalConfigTypeService; + + /** + * 查询案例内容列表 + */ + //@RequiresPermissions("portalproductCaseInfo:list") + @GetMapping("/list") + public TableDataInfo list(HwProductCaseInfo hwProductCaseInfo) + { + startPage(); + List list = hwProductCaseInfoService.selectHwProductCaseInfoList(hwProductCaseInfo); + return getDataTable(list); + } + + /** + * 导出案例内容列表 + */ + //@RequiresPermissions("portalproductCaseInfo:export") + //@Log(title = "案例内容", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwProductCaseInfo hwProductCaseInfo) + { + List list = hwProductCaseInfoService.selectHwProductCaseInfoList(hwProductCaseInfo); + ExcelUtil util = new ExcelUtil(HwProductCaseInfo.class); + util.exportExcel(response, list, "案例内容数据"); + } + + /** + * 获取案例内容详细信息 + */ + //@RequiresPermissions("portalproductCaseInfo:query") + @GetMapping(value = "/{caseInfoId}") + public AjaxResult getInfo(@PathVariable("caseInfoId") Long caseInfoId) + { + return success(hwProductCaseInfoService.selectHwProductCaseInfoByCaseInfoId(caseInfoId)); + } + + /** + * 新增案例内容 + */ + //@RequiresPermissions("portalproductCaseInfo:add") + //@Log(title = "案例内容", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwProductCaseInfo hwProductCaseInfo) + { + return toAjax(hwProductCaseInfoService.insertHwProductCaseInfo(hwProductCaseInfo)); + } + + /** + * 修改案例内容 + */ + //@RequiresPermissions("portalproductCaseInfo:edit") + //@Log(title = "案例内容", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwProductCaseInfo hwProductCaseInfo) + { + return toAjax(hwProductCaseInfoService.updateHwProductCaseInfo(hwProductCaseInfo)); + } + + /** + * 删除案例内容 + */ + //@RequiresPermissions("portalproductCaseInfo:remove") + //@Log(title = "案例内容", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{caseInfoIds}") + public AjaxResult remove(@PathVariable Long[] caseInfoIds) + { + return toAjax(hwProductCaseInfoService.deleteHwProductCaseInfoByCaseInfoIds(caseInfoIds)); + } + + + /** + * 查询门户网站配置类型s树列表 + */ + /** + * 获取门户网站配置树列表 + */ + //@RequiresPermissions("portalproductCaseInfo:list") + @GetMapping("/portalConfigTypeTree") + public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType) { + + return success(hwPortalConfigTypeService.selectPortalConfigTypeTreeList(hwPortalConfigType)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwProductInfoController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.HwProductInfo; +import com.ruoyi.portal.service.IHwPortalConfigTypeService; +import com.ruoyi.portal.service.IHwProductInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 产品信息配置Controller + * + * @author xins + * @date 2024-12-01 + */ +@RestController +@RequestMapping("/portal/productInfo") +@Anonymous//不需要登陆任何人都可以操作 +public class HwProductInfoController extends BaseController +{ + @Autowired + private IHwProductInfoService hwProductInfoService; + + @Autowired + private IHwPortalConfigTypeService hwPortalConfigTypeService; + + + /** + * 查询产品信息配置列表 + */ + //@RequiresPermissions("portalproductInfo:list") + @GetMapping("/list") + public TableDataInfo list(HwProductInfo hwProductInfo) + { + startPage(); + List list = hwProductInfoService.selectHwProductInfoJoinList(hwProductInfo); + return getDataTable(list); + } + + /** + * 导出产品信息配置列表 + */ + //@RequiresPermissions("portalproductInfo:export") + //@Log(title = "产品信息配置", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwProductInfo hwProductInfo) + { + List list = hwProductInfoService.selectHwProductInfoList(hwProductInfo); + ExcelUtil util = new ExcelUtil(HwProductInfo.class); + util.exportExcel(response, list, "产品信息配置数据"); + } + + /** + * 获取产品信息配置详细信息 + */ + //@RequiresPermissions("portalproductInfo:query") + @GetMapping(value = "/{productInfoId}") + public AjaxResult getInfo(@PathVariable("productInfoId") Long productInfoId) + { + return success(hwProductInfoService.selectHwProductInfoByProductInfoId(productInfoId)); + } + + /** + * 新增产品信息配置 + */ + //@RequiresPermissions("portalproductInfo:add") + //@Log(title = "产品信息配置", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwProductInfo hwProductInfo) + { + return toAjax(hwProductInfoService.insertHwProductInfo(hwProductInfo)); + } + + /** + * 修改产品信息配置 + */ + //@RequiresPermissions("portalproductInfo:edit") + //@Log(title = "产品信息配置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwProductInfo hwProductInfo) + { + return toAjax(hwProductInfoService.updateHwProductInfo(hwProductInfo)); + } + + /** + * 删除产品信息配置 + */ + //@RequiresPermissions("portalproductInfo:remove") + //@Log(title = "产品信息配置", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{productInfoIds}") + public AjaxResult remove(@PathVariable Long[] productInfoIds) + { + return toAjax(hwProductInfoService.deleteHwProductInfoByProductInfoIds(productInfoIds)); + } + + + /** + * 查询门户网站配置类型s树列表 + */ + /** + * 获取门户网站配置树列表 + */ + //@RequiresPermissions("portalproductInfo:list") + @GetMapping("/portalConfigTypeTree") + public AjaxResult portalConfigTypeTree(HwPortalConfigType hwPortalConfigType) { + + return success(hwPortalConfigTypeService.selectPortalConfigTypeTreeList(hwPortalConfigType)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwProductInfoDetailController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwProductInfoDetail; +import com.ruoyi.portal.service.IHwProductInfoDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * 产品信息明细配置Controller + * + * @author xins + * @date 2024-12-11 + */ +@RestController +@RequestMapping("/portal/productInfoDetail") +@Anonymous//不需要登陆任何人都可以操作 +public class HwProductInfoDetailController extends BaseController +{ + @Autowired + private IHwProductInfoDetailService hwProductInfoDetailService; + + /** + * 查询产品信息明细配置列表 + */ + //@RequiresPermissions("portalproductInfoDetail:list") + @GetMapping("/list") + public AjaxResult list(HwProductInfoDetail hwProductInfoDetail) + { + List list = hwProductInfoDetailService.selectHwProductInfoDetailList(hwProductInfoDetail); + return success(list); + } + + /** + * 导出产品信息明细配置列表 + */ + //@RequiresPermissions("portalproductInfoDetail:export") + //@Log(title = "产品信息明细配置", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwProductInfoDetail hwProductInfoDetail) + { + List list = hwProductInfoDetailService.selectHwProductInfoDetailList(hwProductInfoDetail); + ExcelUtil util = new ExcelUtil(HwProductInfoDetail.class); + util.exportExcel(response, list, "产品信息明细配置数据"); + } + + /** + * 获取产品信息明细配置详细信息 + */ + //@RequiresPermissions("portalproductInfoDetail:query") + @GetMapping(value = "/{productInfoDetailId}") + public AjaxResult getInfo(@PathVariable("productInfoDetailId") Long productInfoDetailId) + { + return success(hwProductInfoDetailService.selectHwProductInfoDetailByProductInfoDetailId(productInfoDetailId)); + } + + /** + * 新增产品信息明细配置 + */ + //@RequiresPermissions("portalproductInfoDetail:add") + //@Log(title = "产品信息明细配置", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwProductInfoDetail hwProductInfoDetail) + { + return toAjax(hwProductInfoDetailService.insertHwProductInfoDetail(hwProductInfoDetail)); + } + + /** + * 修改产品信息明细配置 + */ + //@RequiresPermissions("portalproductInfoDetail:edit") + //@Log(title = "产品信息明细配置", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwProductInfoDetail hwProductInfoDetail) + { + return toAjax(hwProductInfoDetailService.updateHwProductInfoDetail(hwProductInfoDetail)); + } + + /** + * 删除产品信息明细配置 + */ + //@RequiresPermissions("portalproductInfoDetail:remove") + //@Log(title = "产品信息明细配置", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{productInfoDetailIds}") + public AjaxResult remove(@PathVariable Long[] productInfoDetailIds) + { + return toAjax(hwProductInfoDetailService.deleteHwProductInfoDetailByProductInfoDetailIds(productInfoDetailIds)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwSearchAdminController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 搜索索引管理接口 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/portal/search/admin") +public class HwSearchAdminController extends BaseController +{ + private final IHwSearchRebuildService hwSearchRebuildService; + + public HwSearchAdminController(IHwSearchRebuildService hwSearchRebuildService) + { + this.hwSearchRebuildService = hwSearchRebuildService; + } + + /** + * 全量重建 ES 索引 + */ + @Log(title = "门户搜索索引", businessType = BusinessType.OTHER) + @PostMapping("/rebuild") + public AjaxResult rebuild() + { + hwSearchRebuildService.rebuildAll(); + return success("重建完成"); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwSearchController.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RateLimiter; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.enums.LimitType; +import com.ruoyi.portal.service.IHwSearchService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 门户关键词搜索 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/portal/search") +@Anonymous +public class HwSearchController extends BaseController +{ + private final IHwSearchService hwSearchService; + + public HwSearchController(IHwSearchService hwSearchService) + { + this.hwSearchService = hwSearchService; + } + + /** + * 门户搜索(展示端) + */ + @GetMapping + @RateLimiter(key = "portal_search", time = 60, count = 120, limitType = LimitType.IP) + public AjaxResult search(@RequestParam("keyword") String keyword, + @RequestParam(value = "pageNum", required = false) Integer pageNum, + @RequestParam(value = "pageSize", required = false) Integer pageSize) + { + return success(hwSearchService.search(keyword, pageNum, pageSize)); + } + + /** + * 编辑端搜索 + */ + @GetMapping("/edit") + @RateLimiter(key = "portal_search_edit", time = 60, count = 120, limitType = LimitType.IP) + public AjaxResult editSearch(@RequestParam("keyword") String keyword, + @RequestParam(value = "pageNum", required = false) Integer pageNum, + @RequestParam(value = "pageSize", required = false) Integer pageSize) + { + return success(hwSearchService.searchForEdit(keyword, pageNum, pageSize)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwWebController.java` + +```java +package com.ruoyi.portal.controller; + +import java.util.List; +import java.io.IOException; +import jakarta.servlet.http.HttpServletResponse; + +import com.ruoyi.portal.domain.HwWeb1; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwWeb; +import com.ruoyi.portal.service.IHwWebService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * haiwei官网jsonController + * + * @author ruoyi + * @date 2025-08-18 + */ +@RestController +@RequestMapping("/portal/hwWeb") +@Anonymous//不需要登陆任何人都可以操作 +public class HwWebController extends BaseController +{ + @Autowired + private IHwWebService hwWebService; + + /** + * 查询haiwei官网json列表 + */ + //@RequiresPermissions("portalhwWeb:list") + @GetMapping("/list") + public TableDataInfo list(HwWeb hwWeb) + { +// startPage(); + List list = hwWebService.selectHwWebList(hwWeb); + return getDataTable(list); + } + + /** + * 导出haiwei官网json列表 + */ + //@RequiresPermissions("portalhwWeb:export") + //@Log(title = "haiwei官网json", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwWeb hwWeb) + { + List list = hwWebService.selectHwWebList(hwWeb); + ExcelUtil util = new ExcelUtil(HwWeb.class); + util.exportExcel(response, list, "haiwei官网json数据"); + } + + /** + * 获取haiwei官网json详细信息 + */ + //@RequiresPermissions("portalhwWeb:query") + @GetMapping(value = "/{webCode}") + public AjaxResult getInfo(@PathVariable("webCode") Long webCode) + { + return success(hwWebService.selectHwWebByWebcode(webCode)); + } + + /** + * 新增haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:add") + //@Log(title = "haiwei官网json", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwWeb hwWeb) + { + return toAjax(hwWebService.insertHwWeb(hwWeb)); + } + + /** + * 修改haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:edit") + //@Log(title = "haiwei官网json", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwWeb hwWeb) + { + int i = hwWebService.updateHwWeb(hwWeb); + return toAjax(i); + } + + /** + * 删除haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:remove") + //@Log(title = "haiwei官网json", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{webIds}") + public AjaxResult remove(@PathVariable Long[] webIds) + { + return toAjax(hwWebService.deleteHwWebByWebIds(webIds)); + } + + @GetMapping("/getHwWebList") + public AjaxResult getHwWebList(HwWeb HwWeb) + { + return success(hwWebService.selectHwWebList(HwWeb)) ; + } + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwWebController1.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwWeb1; +import com.ruoyi.portal.service.IHwWebService1; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * haiwei官网jsonController + * + * @author ruoyi + * @date 2025-08-18 + */ +@RestController +@RequestMapping("/portal/hwWeb1") +@Anonymous//不需要登陆任何人都可以操作 +public class HwWebController1 extends BaseController +{ + + @Autowired + private IHwWebService1 hwWebService1; + + /** + * 查询haiwei官网json列表 + */ + //@RequiresPermissions("portalhwWeb:list") + @GetMapping("/list") + public TableDataInfo list(HwWeb1 HwWeb1) + { +// startPage(); + List list = hwWebService1.selectHwWebList(HwWeb1); + return getDataTable(list); + } + + /** + * 导出haiwei官网json列表 + */ + //@RequiresPermissions("portalhwWeb:export") + //@Log(title = "haiwei官网json", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwWeb1 HwWeb1) + { + List list = hwWebService1.selectHwWebList(HwWeb1); + ExcelUtil util = new ExcelUtil(HwWeb1.class); + util.exportExcel(response, list, "haiwei官网json数据"); + } + + /** + * 获取haiwei官网json详细信息 + */ + //@RequiresPermissions("portalhwWeb:query") + @GetMapping(value = "/{webCode}") + public AjaxResult getInfo(@PathVariable("webCode") Long webCode) + { + return success(hwWebService1.selectHwWebByWebcode(webCode)); + } + + /** + * 新增haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:add") + //@Log(title = "haiwei官网json", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwWeb1 HwWeb1) + { + return toAjax(hwWebService1.insertHwWeb(HwWeb1)); + } + + /** + * 修改haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:edit") + //@Log(title = "haiwei官网json", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwWeb1 HwWeb1) + { + int i = hwWebService1.updateHwWeb(HwWeb1); + return toAjax(i); + } + + /** + * 删除haiwei官网json + */ + //@RequiresPermissions("portalhwWeb:remove") + //@Log(title = "haiwei官网json", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{webIds}") + public AjaxResult remove(@PathVariable Long[] webIds) + { + return toAjax(hwWebService1.deleteHwWebByWebIds(webIds)); + } + + + @GetMapping("/getHwWeb1List") + public AjaxResult getHwWeb1List(HwWeb1 HwWeb1) + { + return success(hwWebService1.selectHwWebList(HwWeb1)) ; + } + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwWebDocumentController.java` + +```java +package com.ruoyi.portal.controller; + +import java.util.List; +import java.io.IOException; +import jakarta.servlet.http.HttpServletResponse; + +import com.ruoyi.portal.domain.SecureDocumentRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwWebDocument; +import com.ruoyi.portal.service.IHwWebDocumentService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * Hw资料文件Controller + * + * @author zch + * @date 2025-09-22 + */ +@RestController +@RequestMapping("/portal/hwWebDocument") +@Anonymous//不需要登陆任何人都可以操作 +public class HwWebDocumentController extends BaseController +{ + @Autowired + private IHwWebDocumentService hwWebDocumentService; + + /** + * 查询Hw资料文件列表 + */ +// @RequiresPermissions("portal:hwWebDocument:list") + @GetMapping("/list") + public TableDataInfo list(HwWebDocument hwWebDocument) + { + startPage(); + List list = hwWebDocumentService.selectHwWebDocumentList(hwWebDocument); + for (HwWebDocument doc : list) { + // 隐藏密钥,若设置了密钥则隐藏文件地址 + doc.setSecretKey(null); + if (doc.getHasSecret()) { + doc.setDocumentAddress(null); + } + } + return getDataTable(list); + } + + /** + * 导出Hw资料文件列表 + */ +// @RequiresPermissions("portal:hwWebDocument:export") + //@Log(title = "Hw资料文件", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwWebDocument hwWebDocument) + { + List list = hwWebDocumentService.selectHwWebDocumentList(hwWebDocument); + ExcelUtil util = new ExcelUtil(HwWebDocument.class); + util.exportExcel(response, list, "Hw资料文件数据"); + } + + /** + * 获取Hw资料文件详细信息 + */ +// @RequiresPermissions("portal:hwWebDocument:query") + @GetMapping(value = "/{documentId}") + public AjaxResult getInfo(@PathVariable("documentId") String documentId) + { + HwWebDocument doc = hwWebDocumentService.selectHwWebDocumentByDocumentId(documentId); + if (doc != null) { + // 隐藏密钥,若设置了密钥则隐藏文件地址 + doc.setSecretKey(null); + if (doc.getHasSecret()) { + doc.setDocumentAddress(null); + } + } + return success(doc); + } + + /** + * 新增Hw资料文件 + */ +// @RequiresPermissions("portal:hwWebDocument:add") + //@Log(title = "Hw资料文件", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwWebDocument hwWebDocument) + { + return toAjax(hwWebDocumentService.insertHwWebDocument(hwWebDocument)); + } + + /** + * 修改Hw资料文件 + */ +// @RequiresPermissions("portal:hwWebDocument:edit") + //@Log(title = "Hw资料文件", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwWebDocument hwWebDocument) + { + System.out.println(hwWebDocument.getSecretKey()); + return toAjax(hwWebDocumentService.updateHwWebDocument(hwWebDocument)); + } + + /** + * 删除Hw资料文件 + */ +// @RequiresPermissions("portal:hwWebDocument:remove") + //@Log(title = "Hw资料文件", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{documentIds}") + public AjaxResult remove(@PathVariable String[] documentIds) + { + return toAjax(hwWebDocumentService.deleteHwWebDocumentByDocumentIds(documentIds)); + } + + /** + * 获取安全文件地址 + */ +// @RequiresPermissions("portal:hwWebDocument:query") + //@Log(title = "获取安全文件地址", businessType = BusinessType.OTHER) + @RepeatSubmit + @PostMapping("/getSecureDocumentAddress") + public AjaxResult getSecureDocumentAddress(@RequestBody SecureDocumentRequest request) + { + try { + String address = hwWebDocumentService.verifyAndGetDocumentAddress(request.getDocumentId(), request.getProvidedKey()); + return success(address); + } catch (Exception e) { + return error(e.getMessage()); + } + } + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwWebMenuController.java` + +```java +package com.ruoyi.portal.controller; + +import java.util.List; +import java.io.IOException; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwWebMenu; +import com.ruoyi.portal.service.IHwWebMenuService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * haiwei官网菜单Controller + * + * @author zch + * @date 2025-08-18 + */ +@RestController +@RequestMapping("/portal/hwWebMenu") +@Anonymous//不需要登陆任何人都可以操作 +public class HwWebMenuController extends BaseController +{ + @Autowired + private IHwWebMenuService hwWebMenuService; + + /** + * 查询haiwei官网菜单列表 + */ + //@RequiresPermissions("portalhwWebMenu:list") + @GetMapping("/list") + public AjaxResult list(HwWebMenu hwWebMenu) + { + List list = hwWebMenuService.selectHwWebMenuList(hwWebMenu); + return success(list); + } + + /** + * 导出haiwei官网菜单列表 + */ + //@RequiresPermissions("portalhwWebMenu:export") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwWebMenu hwWebMenu) + { + List list = hwWebMenuService.selectHwWebMenuList(hwWebMenu); + ExcelUtil util = new ExcelUtil(HwWebMenu.class); + util.exportExcel(response, list, "haiwei官网菜单数据"); + } + + /** + * 获取haiwei官网菜单详细信息 + */ + //@RequiresPermissions("portalhwWebMenu:query") + @GetMapping(value = "/{webMenuId}") + public AjaxResult getInfo(@PathVariable("webMenuId") Long webMenuId) + { + return success(hwWebMenuService.selectHwWebMenuByWebMenuId(webMenuId)); + } + + /** + * 新增haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:add") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwWebMenu hwWebMenu) + { + return toAjax(hwWebMenuService.insertHwWebMenu(hwWebMenu)); + } + + /** + * 修改haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:edit") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwWebMenu hwWebMenu) + { + return toAjax(hwWebMenuService.updateHwWebMenu(hwWebMenu)); + } + + /** + * 删除haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:remove") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{webMenuIds}") + public AjaxResult remove(@PathVariable Long[] webMenuIds) + { + return toAjax(hwWebMenuService.deleteHwWebMenuByWebMenuIds(webMenuIds)); + } + + /** + * 获取菜单树列表 + */ + @GetMapping("/selectMenuTree") + public AjaxResult selectMenuTree(HwWebMenu hwWebMenu){ + return success(hwWebMenuService.selectMenuTree(hwWebMenu)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/controller/HwWebMenuController1.java` + +```java +package com.ruoyi.portal.controller; + +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.portal.domain.HwWebMenu1; +import com.ruoyi.portal.service.IHwWebMenuService1; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.annotation.RepeatSubmit; + +/** + * haiwei官网菜单Controller + * + * @author zch + * @date 2025-08-18 + */ +@RestController +@RequestMapping("/portal/hwWebMenu1") +@Anonymous//不需要登陆任何人都可以操作 +public class HwWebMenuController1 extends BaseController +{ + @Autowired + private IHwWebMenuService1 hwWebMenuService1; + + /** + * 查询haiwei官网菜单列表 + */ + //@RequiresPermissions("portalhwWebMenu:list") + @GetMapping("/list") + public AjaxResult list(HwWebMenu1 hwWebMenu1) + { + List list = hwWebMenuService1.selectHwWebMenuList(hwWebMenu1); + return success(list); + } + + /** + * 导出haiwei官网菜单列表 + */ + //@RequiresPermissions("portalhwWebMenu:export") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.EXPORT) + @RepeatSubmit + @PostMapping("/export") + public void export(HttpServletResponse response, HwWebMenu1 hwWebMenu1) + { + List list = hwWebMenuService1.selectHwWebMenuList(hwWebMenu1); + ExcelUtil util = new ExcelUtil(HwWebMenu1.class); + util.exportExcel(response, list, "haiwei官网菜单数据"); + } + + /** + * 获取haiwei官网菜单详细信息 + */ + //@RequiresPermissions("portalhwWebMenu:query") + @GetMapping(value = "/{webMenuId}") + public AjaxResult getInfo(@PathVariable("webMenuId") Long webMenuId) + { + return success(hwWebMenuService1.selectHwWebMenuByWebMenuId(webMenuId)); + } + + /** + * 新增haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:add") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.INSERT) + @RepeatSubmit + @PostMapping + public AjaxResult add(@RequestBody HwWebMenu1 hwWebMenu1) + { + return toAjax(hwWebMenuService1.insertHwWebMenu(hwWebMenu1)); + } + + /** + * 修改haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:edit") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping + public AjaxResult edit(@RequestBody HwWebMenu1 hwWebMenu1) + { + return toAjax(hwWebMenuService1.updateHwWebMenu(hwWebMenu1)); + } + + /** + * 删除haiwei官网菜单 + */ + //@RequiresPermissions("portalhwWebMenu:remove") + //@Log(title = "haiwei官网菜单", businessType = BusinessType.DELETE) + @RepeatSubmit + @DeleteMapping("/{webMenuIds}") + public AjaxResult remove(@PathVariable Long[] webMenuIds) + { + return toAjax(hwWebMenuService1.deleteHwWebMenuByWebMenuIds(webMenuIds)); + } + + /** + * 获取菜单树列表 + */ + @GetMapping("/selectMenuTree") + public AjaxResult selectMenuTree(HwWebMenu1 hwWebMenu1){ + return success(hwWebMenuService1.selectMenuTree(hwWebMenu1)); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/AnalyticsCollectRequest.java` + +```java +package com.ruoyi.portal.domain.dto; + +/** + * 匿名访问采集请求 + * + * @author ruoyi + */ +public class AnalyticsCollectRequest +{ + private String visitorId; + + private String sessionId; + + private String eventType; + + private String path; + + private String referrer; + + private String utmSource; + + private String utmMedium; + + private String utmCampaign; + + private String keyword; + + private String ua; + + private String device; + + private String browser; + + private String os; + + private Long stayMs; + + /** 毫秒时间戳 */ + private Long eventTime; + + public String getVisitorId() + { + return visitorId; + } + + public void setVisitorId(String visitorId) + { + this.visitorId = visitorId; + } + + public String getSessionId() + { + return sessionId; + } + + public void setSessionId(String sessionId) + { + this.sessionId = sessionId; + } + + public String getEventType() + { + return eventType; + } + + public void setEventType(String eventType) + { + this.eventType = eventType; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public String getReferrer() + { + return referrer; + } + + public void setReferrer(String referrer) + { + this.referrer = referrer; + } + + public String getUtmSource() + { + return utmSource; + } + + public void setUtmSource(String utmSource) + { + this.utmSource = utmSource; + } + + public String getUtmMedium() + { + return utmMedium; + } + + public void setUtmMedium(String utmMedium) + { + this.utmMedium = utmMedium; + } + + public String getUtmCampaign() + { + return utmCampaign; + } + + public void setUtmCampaign(String utmCampaign) + { + this.utmCampaign = utmCampaign; + } + + public String getKeyword() + { + return keyword; + } + + public void setKeyword(String keyword) + { + this.keyword = keyword; + } + + public String getUa() + { + return ua; + } + + public void setUa(String ua) + { + this.ua = ua; + } + + public String getDevice() + { + return device; + } + + public void setDevice(String device) + { + this.device = device; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getStayMs() + { + return stayMs; + } + + public void setStayMs(Long stayMs) + { + this.stayMs = stayMs; + } + + public Long getEventTime() + { + return eventTime; + } + + public void setEventTime(Long eventTime) + { + this.eventTime = eventTime; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/AnalyticsDashboardDTO.java` + +```java +package com.ruoyi.portal.domain.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * 官网访问监控看板 + * + * @author ruoyi + */ +public class AnalyticsDashboardDTO +{ + private String statDate; + + private Long pv; + + private Long uv; + + private Long ipUv; + + private Long avgStayMs; + + private Double bounceRate; + + private Long searchCount; + + private Long downloadCount; + + private List entryPages = new ArrayList<>(); + + private List hotPages = new ArrayList<>(); + + private List hotKeywords = new ArrayList<>(); + + public String getStatDate() + { + return statDate; + } + + public void setStatDate(String statDate) + { + this.statDate = statDate; + } + + public Long getPv() + { + return pv; + } + + public void setPv(Long pv) + { + this.pv = pv; + } + + public Long getUv() + { + return uv; + } + + public void setUv(Long uv) + { + this.uv = uv; + } + + public Long getIpUv() + { + return ipUv; + } + + public void setIpUv(Long ipUv) + { + this.ipUv = ipUv; + } + + public Long getAvgStayMs() + { + return avgStayMs; + } + + public void setAvgStayMs(Long avgStayMs) + { + this.avgStayMs = avgStayMs; + } + + public Double getBounceRate() + { + return bounceRate; + } + + public void setBounceRate(Double bounceRate) + { + this.bounceRate = bounceRate; + } + + public Long getSearchCount() + { + return searchCount; + } + + public void setSearchCount(Long searchCount) + { + this.searchCount = searchCount; + } + + public Long getDownloadCount() + { + return downloadCount; + } + + public void setDownloadCount(Long downloadCount) + { + this.downloadCount = downloadCount; + } + + public List getEntryPages() + { + return entryPages; + } + + public void setEntryPages(List entryPages) + { + this.entryPages = entryPages; + } + + public List getHotPages() + { + return hotPages; + } + + public void setHotPages(List hotPages) + { + this.hotPages = hotPages; + } + + public List getHotKeywords() + { + return hotKeywords; + } + + public void setHotKeywords(List hotKeywords) + { + this.hotKeywords = hotKeywords; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/AnalyticsRankItemDTO.java` + +```java +package com.ruoyi.portal.domain.dto; + +/** + * 统计排行项 + * + * @author ruoyi + */ +public class AnalyticsRankItemDTO +{ + private String name; + + private Long value; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Long getValue() + { + return value; + } + + public void setValue(Long value) + { + this.value = value; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/SearchPageDTO.java` + +```java +package com.ruoyi.portal.domain.dto; + +import java.util.ArrayList; +import java.util.List; + +/** + * 搜索分页数据 + * + * @author ruoyi + */ +public class SearchPageDTO +{ + /** 总记录数 */ + private long total; + + /** 结果列表 */ + private List rows = new ArrayList<>(); + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/SearchRawRecord.java` + +```java +package com.ruoyi.portal.domain.dto; + +import java.util.Date; + +/** + * 搜索原始记录 + * + * @author ruoyi + */ +public class SearchRawRecord +{ + private String sourceType; + + private String bizId; + + private String title; + + private String content; + + private String webCode; + + private String typeId; + + private String deviceId; + + private String menuId; + + private String documentId; + + private Integer score; + + private Date updatedAt; + + public String getSourceType() + { + return sourceType; + } + + public void setSourceType(String sourceType) + { + this.sourceType = sourceType; + } + + public String getBizId() + { + return bizId; + } + + public void setBizId(String bizId) + { + this.bizId = bizId; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getContent() + { + return content; + } + + public void setContent(String content) + { + this.content = content; + } + + public String getWebCode() + { + return webCode; + } + + public void setWebCode(String webCode) + { + this.webCode = webCode; + } + + public String getTypeId() + { + return typeId; + } + + public void setTypeId(String typeId) + { + this.typeId = typeId; + } + + public String getDeviceId() + { + return deviceId; + } + + public void setDeviceId(String deviceId) + { + this.deviceId = deviceId; + } + + public String getMenuId() + { + return menuId; + } + + public void setMenuId(String menuId) + { + this.menuId = menuId; + } + + public String getDocumentId() + { + return documentId; + } + + public void setDocumentId(String documentId) + { + this.documentId = documentId; + } + + public Integer getScore() + { + return score; + } + + public void setScore(Integer score) + { + this.score = score; + } + + public Date getUpdatedAt() + { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) + { + this.updatedAt = updatedAt; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/dto/SearchResultDTO.java` + +```java +package com.ruoyi.portal.domain.dto; + +import java.util.Map; + +/** + * 门户搜索结果 + * + * @author ruoyi + */ +public class SearchResultDTO +{ + /** 来源类型 menu/web/web1/document/configType/productInfo/productDetail/caseInfo/aboutUs */ + private String sourceType; + + /** 标题 */ + private String title; + + /** 命中摘要 */ + private String snippet; + + /** 相关度分值 */ + private Integer score; + + /** 展示端跳转路由 */ + private String route; + + /** 展示端路由参数 */ + private Map routeQuery; + + /** 编辑端跳转路由 */ + private String editRoute; + + public String getSourceType() + { + return sourceType; + } + + public void setSourceType(String sourceType) + { + this.sourceType = sourceType; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getSnippet() + { + return snippet; + } + + public void setSnippet(String snippet) + { + this.snippet = snippet; + } + + public Integer getScore() + { + return score; + } + + public void setScore(Integer score) + { + this.score = score; + } + + public String getRoute() + { + return route; + } + + public void setRoute(String route) + { + this.route = route; + } + + public Map getRouteQuery() + { + return routeQuery; + } + + public void setRouteQuery(Map routeQuery) + { + this.routeQuery = routeQuery; + } + + public String getEditRoute() + { + return editRoute; + } + + public void setEditRoute(String editRoute) + { + this.editRoute = editRoute; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwAboutUsInfo.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 关于我们信息对象 hw_about_us_info + * + * @author xins + * @date 2024-12-01 + */ +public class HwAboutUsInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long aboutUsInfoId; + + /** 类型(1关于我们页面上大图2公司简介3企业资质4认证证书5团队风貌) */ + @Excel(name = "类型", readConverterExp = "1=关于我们页面上大图2公司简介3企业资质4认证证书5团队风貌") + private String aboutUsInfoType; + + /** 英文标题 */ + @Excel(name = "英文标题") + private String aboutUsInfoEtitle; + + + /** 中文标题 */ + @Excel(name = "中文标题") + private String aboutUsInfoTitle; + + /** 内容 */ + @Excel(name = "内容") + private String aboutUsInfoDesc; + + /** 顺序 */ + @Excel(name = "顺序") + private Long aboutUsInfoOrder; + + /** 显示模式 */ + @Excel(name = "显示模式") + private String displayModal; + + /** 图片地址 */ + @Excel(name = "图片地址") + private String aboutUsInfoPic; + + public void setAboutUsInfoId(Long aboutUsInfoId) + { + this.aboutUsInfoId = aboutUsInfoId; + } + + public Long getAboutUsInfoId() + { + return aboutUsInfoId; + } + public void setAboutUsInfoType(String aboutUsInfoType) + { + this.aboutUsInfoType = aboutUsInfoType; + } + + public String getAboutUsInfoType() + { + return aboutUsInfoType; + } + + public String getAboutUsInfoEtitle() { + return aboutUsInfoEtitle; + } + + public void setAboutUsInfoEtitle(String aboutUsInfoEtitle) { + this.aboutUsInfoEtitle = aboutUsInfoEtitle; + } + + public void setAboutUsInfoTitle(String aboutUsInfoTitle) + { + this.aboutUsInfoTitle = aboutUsInfoTitle; + } + + public String getAboutUsInfoTitle() + { + return aboutUsInfoTitle; + } + public void setAboutUsInfoDesc(String aboutUsInfoDesc) + { + this.aboutUsInfoDesc = aboutUsInfoDesc; + } + + public String getAboutUsInfoDesc() + { + return aboutUsInfoDesc; + } + public void setAboutUsInfoOrder(Long aboutUsInfoOrder) + { + this.aboutUsInfoOrder = aboutUsInfoOrder; + } + + public Long getAboutUsInfoOrder() + { + return aboutUsInfoOrder; + } + + public String getDisplayModal() { + return displayModal; + } + + public void setDisplayModal(String displayModal) { + this.displayModal = displayModal; + } + + public void setAboutUsInfoPic(String aboutUsInfoPic) + { + this.aboutUsInfoPic = aboutUsInfoPic; + } + + public String getAboutUsInfoPic() + { + return aboutUsInfoPic; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("aboutUsInfoId", getAboutUsInfoId()) + .append("aboutUsInfoType", getAboutUsInfoType()) + .append("aboutUsInfoTitle", getAboutUsInfoTitle()) + .append("aboutUsInfoDesc", getAboutUsInfoDesc()) + .append("aboutUsInfoOrder", getAboutUsInfoOrder()) + .append("aboutUsInfoPic", getAboutUsInfoPic()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwAboutUsInfoDetail.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 关于我们信息明细对象 hw_about_us_info_detail + * + * @author ruoyi + * @date 2024-12-01 + */ +public class HwAboutUsInfoDetail extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long usInfoDetailId; + + /** 关于我们信息ID */ + @Excel(name = "关于我们信息ID") + private Long aboutUsInfoId; + + /** 标题 */ + @Excel(name = "标题") + private String usInfoDetailTitle; + + /** 内容 */ + @Excel(name = "内容") + private String usInfoDetailDesc; + + /** 顺序 */ + @Excel(name = "顺序") + private Long usInfoDetailOrder; + + /** 图片地址 */ + @Excel(name = "图片地址") + private String usInfoDetailPic; + + public void setUsInfoDetailId(Long usInfoDetailId) + { + this.usInfoDetailId = usInfoDetailId; + } + + public Long getUsInfoDetailId() + { + return usInfoDetailId; + } + public void setAboutUsInfoId(Long aboutUsInfoId) + { + this.aboutUsInfoId = aboutUsInfoId; + } + + public Long getAboutUsInfoId() + { + return aboutUsInfoId; + } + public void setUsInfoDetailTitle(String usInfoDetailTitle) + { + this.usInfoDetailTitle = usInfoDetailTitle; + } + + public String getUsInfoDetailTitle() + { + return usInfoDetailTitle; + } + public void setUsInfoDetailDesc(String usInfoDetailDesc) + { + this.usInfoDetailDesc = usInfoDetailDesc; + } + + public String getUsInfoDetailDesc() + { + return usInfoDetailDesc; + } + public void setUsInfoDetailOrder(Long usInfoDetailOrder) + { + this.usInfoDetailOrder = usInfoDetailOrder; + } + + public Long getUsInfoDetailOrder() + { + return usInfoDetailOrder; + } + public void setUsInfoDetailPic(String usInfoDetailPic) + { + this.usInfoDetailPic = usInfoDetailPic; + } + + public String getUsInfoDetailPic() + { + return usInfoDetailPic; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("usInfoDetailId", getUsInfoDetailId()) + .append("aboutUsInfoId", getAboutUsInfoId()) + .append("usInfoDetailTitle", getUsInfoDetailTitle()) + .append("usInfoDetailDesc", getUsInfoDetailDesc()) + .append("usInfoDetailOrder", getUsInfoDetailOrder()) + .append("usInfoDetailPic", getUsInfoDetailPic()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwContactUsInfo.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 联系我们信息对象 hw_contact_us_info + * + * @author xins + * @date 2024-12-01 + */ +public class HwContactUsInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long contactUsInfoId; + + /** 用户姓名 */ + @Excel(name = "用户姓名") + private String userName; + + /** 邮箱 */ + @Excel(name = "邮箱") + private String userEmail; + + /** 手机号 */ + @Excel(name = "手机号") + private String userPhone; + + /** IP地址 */ + @Excel(name = "IP地址") + private String userIp; + + private String remark; + + public void setContactUsInfoId(Long contactUsInfoId) + { + this.contactUsInfoId = contactUsInfoId; + } + + public Long getContactUsInfoId() + { + return contactUsInfoId; + } + public void setUserName(String userName) + { + this.userName = userName; + } + + public String getUserName() + { + return userName; + } + public void setUserEmail(String userEmail) + { + this.userEmail = userEmail; + } + + public String getUserEmail() + { + return userEmail; + } + public void setUserPhone(String userPhone) + { + this.userPhone = userPhone; + } + + public String getUserPhone() + { + return userPhone; + } + public void setUserIp(String userIp) + { + this.userIp = userIp; + } + + public String getUserIp() + { + return userIp; + } + + @Override + public String getRemark() { + return remark; + } + + @Override + public void setRemark(String remark) { + this.remark = remark; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("contactUsInfoId", getContactUsInfoId()) + .append("userName", getUserName()) + .append("userEmail", getUserEmail()) + .append("userPhone", getUserPhone()) + .append("userIp", getUserIp()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwPortalConfig.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 门户网站配置对象 hw_portal_config + * + * @author xins + * @date 2024-12-01 + */ +public class HwPortalConfig extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long portalConfigId; + + /** 类型(1首页大图 2产品中心大图) */ + @Excel(name = "类型(1首页大图 2产品中心大图)") + private String portalConfigType; + + /**如果类型是2的,则需要关联hw_portal_config_type*/ + private Long portalConfigTypeId; + + /** 标题 */ + @Excel(name = "标题") + private String portalConfigTitle; + + /** 顺序 */ + @Excel(name = "顺序") + private Long portalConfigOrder; + + /** 内容 */ + @Excel(name = "内容") + private String portalConfigDesc; + + /** 按钮名称 */ + @Excel(name = "按钮名称") + private String buttonName; + + /** 按钮跳转地址 */ + @Excel(name = "按钮跳转地址") + private String routerAddress; + + /** 主图地址 */ + @Excel(name = "主图地址") + private String portalConfigPic; + + private String configTypeName; + + + private String homeConfigTypePic; + private String homeConfigTypeIcon; + private String homeConfigTypeName; + private String homeConfigTypeClassfication; + private Long parentId; + private String ancestors; + + public void setPortalConfigId(Long portalConfigId) + { + this.portalConfigId = portalConfigId; + } + + public Long getPortalConfigId() + { + return portalConfigId; + } + public void setPortalConfigType(String portalConfigType) + { + this.portalConfigType = portalConfigType; + } + + public String getPortalConfigType() + { + return portalConfigType; + } + + public Long getPortalConfigTypeId() { + return portalConfigTypeId; + } + + public void setPortalConfigTypeId(Long portalConfigTypeId) { + this.portalConfigTypeId = portalConfigTypeId; + } + + public void setPortalConfigTitle(String portalConfigTitle) + { + this.portalConfigTitle = portalConfigTitle; + } + + public String getPortalConfigTitle() + { + return portalConfigTitle; + } + public void setPortalConfigOrder(Long portalConfigOrder) + { + this.portalConfigOrder = portalConfigOrder; + } + + public Long getPortalConfigOrder() + { + return portalConfigOrder; + } + public void setPortalConfigDesc(String portalConfigDesc) + { + this.portalConfigDesc = portalConfigDesc; + } + + public String getPortalConfigDesc() + { + return portalConfigDesc; + } + public void setButtonName(String buttonName) + { + this.buttonName = buttonName; + } + + public String getButtonName() + { + return buttonName; + } + public void setRouterAddress(String routerAddress) + { + this.routerAddress = routerAddress; + } + + public String getRouterAddress() + { + return routerAddress; + } + public void setPortalConfigPic(String portalConfigPic) + { + this.portalConfigPic = portalConfigPic; + } + + public String getPortalConfigPic() + { + return portalConfigPic; + } + + public String getConfigTypeName() { + return configTypeName; + } + + public void setConfigTypeName(String configTypeName) { + this.configTypeName = configTypeName; + } + + + public String getHomeConfigTypePic() { + return homeConfigTypePic; + } + + public void setHomeConfigTypePic(String homeConfigTypePic) { + this.homeConfigTypePic = homeConfigTypePic; + } + + public String getHomeConfigTypeIcon() { + return homeConfigTypeIcon; + } + + public void setHomeConfigTypeIcon(String homeConfigTypeIcon) { + this.homeConfigTypeIcon = homeConfigTypeIcon; + } + + public String getHomeConfigTypeName() { + return homeConfigTypeName; + } + + public void setHomeConfigTypeName(String homeConfigTypeName) { + this.homeConfigTypeName = homeConfigTypeName; + } + + public String getHomeConfigTypeClassfication() { + return homeConfigTypeClassfication; + } + + public void setHomeConfigTypeClassfication(String homeConfigTypeClassfication) { + this.homeConfigTypeClassfication = homeConfigTypeClassfication; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getAncestors() { + return ancestors; + } + + public void setAncestors(String ancestors) { + this.ancestors = ancestors; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("portalConfigId", getPortalConfigId()) + .append("portalConfigType", getPortalConfigType()) + .append("portalConfigTitle", getPortalConfigTitle()) + .append("portalConfigOrder", getPortalConfigOrder()) + .append("portalConfigDesc", getPortalConfigDesc()) + .append("buttonName", getButtonName()) + .append("routerAddress", getRouterAddress()) + .append("portalConfigPic", getPortalConfigPic()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .append("configTypeName", getConfigTypeName()) + .append("homeConfigTypePic", getHomeConfigTypePic()) + .append("homeConfigTypeIcon", getHomeConfigTypeIcon()) + .append("homeConfigTypeName", getHomeConfigTypeName()) + .append("homeConfigTypeClassfication", getHomeConfigTypeClassfication()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwPortalConfigType.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.common.core.domain.TreeEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.ArrayList; +import java.util.List; + +/** + * 门户网站配置类型对象 hw_portal_config_type + * + * @author xins + * @date 2024-12-11 + */ +public class HwPortalConfigType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long configTypeId; + + /** 大类(1产品中心,2案例) */ + @Excel(name = "大类(1产品中心,2案例)") + private String configTypeClassfication; + + /** 名称 */ + @Excel(name = "名称") + private String configTypeName; + + /** 首页名称 */ + @Excel(name = "首页名称") + private String homeConfigTypeName; + + /** 备注 */ + @Excel(name = "备注") + private String configTypeDesc; + + /** 图标地址 */ + @Excel(name = "图标地址") + private String configTypeIcon; + + /** 首页图片地址 */ + @Excel(name = "首页图片地址") + private String homeConfigTypePic; + + private Long parentId; + + private String ancestors; + + private List hwProductCaseInfoList; + + /** 子类型 */ + private List children = new ArrayList(); + + public void setConfigTypeId(Long configTypeId) + { + this.configTypeId = configTypeId; + } + + public Long getConfigTypeId() + { + return configTypeId; + } + public void setConfigTypeClassfication(String configTypeClassfication) + { + this.configTypeClassfication = configTypeClassfication; + } + + public String getConfigTypeClassfication() + { + return configTypeClassfication; + } + public void setConfigTypeName(String configTypeName) + { + this.configTypeName = configTypeName; + } + + public String getConfigTypeName() + { + return configTypeName; + } + public void setHomeConfigTypeName(String homeConfigTypeName) + { + this.homeConfigTypeName = homeConfigTypeName; + } + + public String getHomeConfigTypeName() + { + return homeConfigTypeName; + } + public void setConfigTypeDesc(String configTypeDesc) + { + this.configTypeDesc = configTypeDesc; + } + + public String getConfigTypeDesc() + { + return configTypeDesc; + } + public void setConfigTypeIcon(String configTypeIcon) + { + this.configTypeIcon = configTypeIcon; + } + + public String getConfigTypeIcon() + { + return configTypeIcon; + } + public void setHomeConfigTypePic(String homeConfigTypePic) + { + this.homeConfigTypePic = homeConfigTypePic; + } + + public String getHomeConfigTypePic() + { + return homeConfigTypePic; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getAncestors() { + return ancestors; + } + + public void setAncestors(String ancestors) { + this.ancestors = ancestors; + } + + public List getHwProductCaseInfoList() { + return hwProductCaseInfoList; + } + + public void setHwProductCaseInfoList(List hwProductCaseInfoList) { + this.hwProductCaseInfoList = hwProductCaseInfoList; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("configTypeId", getConfigTypeId()) + .append("configTypeClassfication", getConfigTypeClassfication()) + .append("configTypeName", getConfigTypeName()) + .append("homeConfigTypeName", getHomeConfigTypeName()) + .append("configTypeDesc", getConfigTypeDesc()) + .append("configTypeIcon", getConfigTypeIcon()) + .append("homeConfigTypePic", getHomeConfigTypePic()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwProductCaseInfo.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 案例内容对象 hw_product_case_info + * + * @author xins + * @date 2024-12-01 + */ +public class HwProductCaseInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long caseInfoId; + + /** 案例标题 */ + @Excel(name = "案例标题") + private String caseInfoTitle; + + /** 配置类型ID */ + @Excel(name = "配置类型ID") + private Long configTypeId; + + /** 典型案例标识(1是0否) */ + @Excel(name = "典型案例标识(1是0否)") + private String typicalFlag; + + /** 案例内容 */ + @Excel(name = "案例内容") + private String caseInfoDesc; + + /** 案例内容图片 */ + @Excel(name = "案例内容图片") + private String caseInfoPic; + + /** 案例详情 */ + @Excel(name = "案例详情") + private String caseInfoHtml; + + private String homeTypicalFlag; + + + public void setCaseInfoId(Long caseInfoId) + { + this.caseInfoId = caseInfoId; + } + + public Long getCaseInfoId() + { + return caseInfoId; + } + public void setCaseInfoTitle(String caseInfoTitle) + { + this.caseInfoTitle = caseInfoTitle; + } + + public String getCaseInfoTitle() + { + return caseInfoTitle; + } + + public Long getConfigTypeId() { + return configTypeId; + } + + public void setConfigTypeId(Long configTypeId) { + this.configTypeId = configTypeId; + } + + public void setTypicalFlag(String typicalFlag) + { + this.typicalFlag = typicalFlag; + } + + public String getTypicalFlag() + { + return typicalFlag; + } + public void setCaseInfoDesc(String caseInfoDesc) + { + this.caseInfoDesc = caseInfoDesc; + } + + public String getCaseInfoDesc() + { + return caseInfoDesc; + } + public void setCaseInfoPic(String caseInfoPic) + { + this.caseInfoPic = caseInfoPic; + } + + public String getCaseInfoPic() + { + return caseInfoPic; + } + public void setCaseInfoHtml(String caseInfoHtml) + { + this.caseInfoHtml = caseInfoHtml; + } + + public String getCaseInfoHtml() + { + return caseInfoHtml; + } + + public String getHomeTypicalFlag() { + return homeTypicalFlag; + } + + public void setHomeTypicalFlag(String homeTypicalFlag) { + this.homeTypicalFlag = homeTypicalFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("caseInfoId", getCaseInfoId()) + .append("caseInfoTitle", getCaseInfoTitle()) + .append("configTypeId", getConfigTypeId()) + .append("typicalFlag", getTypicalFlag()) + .append("caseInfoDesc", getCaseInfoDesc()) + .append("caseInfoPic", getCaseInfoPic()) + .append("caseInfoHtml", getCaseInfoHtml()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwProductInfo.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +/** + * 产品信息配置对象 hw_product_info + * + * @author xins + * @date 2024-12-01 + */ +public class HwProductInfo extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long productInfoId; + + /** 配置类型(例如物联网解决方案下的物联网平台和物联网硬件产品系列) */ + @Excel(name = "配置类型", readConverterExp = "例=如物联网解决方案下的物联网平台和物联网硬件产品系列") + private String configTypeId; + + /** 是否按tab显示(1是0否) */ + @Excel(name = "是否按tab显示", readConverterExp = "1=是0否") + private String tabFlag; + + /** 配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8左图右图9上图下内容,一行4个) */ + @Excel(name = "配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8左图右图9上图下内容,一行4个),13为hw官网配置模式") + private String configModal; + + /** 英文标题 */ + @Excel(name = "英文标题") + private String productInfoEtitle; + + /** 中文标题 */ + @Excel(name = "中文标题") + private String productInfoCtitle; + + /** 顺序 */ + @Excel(name = "顺序") + private Long productInfoOrder; + + /** 产品信息明细配置信息 */ + private List hwProductInfoDetailList; + + private Long parentId; + + private String configTypeName; + public void setProductInfoId(Long productInfoId) + { + this.productInfoId = productInfoId; + } + + public Long getProductInfoId() + { + return productInfoId; + } + public void setConfigTypeId(String configTypeId) + { + this.configTypeId = configTypeId; + } + + public String getConfigTypeId() + { + return configTypeId; + } + public void setTabFlag(String tabFlag) + { + this.tabFlag = tabFlag; + } + + public String getTabFlag() + { + return tabFlag; + } + public void setConfigModal(String configModal) + { + this.configModal = configModal; + } + + public String getConfigModal() + { + return configModal; + } + public void setProductInfoEtitle(String productInfoEtitle) + { + this.productInfoEtitle = productInfoEtitle; + } + + public String getProductInfoEtitle() + { + return productInfoEtitle; + } + public void setProductInfoCtitle(String productInfoCtitle) + { + this.productInfoCtitle = productInfoCtitle; + } + + public String getProductInfoCtitle() + { + return productInfoCtitle; + } + public void setProductInfoOrder(Long productInfoOrder) + { + this.productInfoOrder = productInfoOrder; + } + + public Long getProductInfoOrder() + { + return productInfoOrder; + } + + public List getHwProductInfoDetailList() + { + return hwProductInfoDetailList; + } + + public void setHwProductInfoDetailList(List hwProductInfoDetailList) + { + this.hwProductInfoDetailList = hwProductInfoDetailList; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getConfigTypeName() { + return configTypeName; + } + + public void setConfigTypeName(String configTypeName) { + this.configTypeName = configTypeName; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("productInfoId", getProductInfoId()) + .append("configTypeId", getConfigTypeId()) + .append("tabFlag", getTabFlag()) + .append("configModal", getConfigModal()) + .append("productInfoEtitle", getProductInfoEtitle()) + .append("productInfoCtitle", getProductInfoCtitle()) + .append("productInfoOrder", getProductInfoOrder()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .append("hwProductInfoDetailList", getHwProductInfoDetailList()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwProductInfoDetail.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.TreeEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + + +/** + * 产品信息明细配置对象 hw_product_info_detail + * + * @author xins + * @date 2024-12-11 + */ +public class HwProductInfoDetail extends TreeEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键标识 */ + private Long productInfoDetailId; + + /** 产品信息配置ID */ + @Excel(name = "产品信息配置ID") + private Long productInfoId; + + /** 配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个);针对右children时配置的 */ + @Excel(name = "配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个);针对右children时配置的") + private String configModal; + + /** 配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个);针对右children时配置的 */ + @Excel(name = "配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个);针对右children时配置的") + private String configModel; + + /** 标题 */ + @Excel(name = "标题") + private String productInfoDetailTitle; + + /** 内容 */ + @Excel(name = "内容") + private String productInfoDetailDesc; + + /** 顺序 */ + @Excel(name = "顺序") + private Long productInfoDetailOrder; + + /** 图片地址 */ + @Excel(name = "图片地址") + private String productInfoDetailPic; + + /** 产品信息明细配置信息 */ + private List hwProductInfoDetailList; + + public void setProductInfoDetailId(Long productInfoDetailId) + { + this.productInfoDetailId = productInfoDetailId; + } + + public Long getProductInfoDetailId() + { + return productInfoDetailId; + } + public void setProductInfoId(Long productInfoId) + { + this.productInfoId = productInfoId; + } + + public Long getProductInfoId() + { + return productInfoId; + } + public void setConfigModal(String configModal) + { + this.configModal = configModal; + } + + public String getConfigModal() + { + return configModal; + } + public void setProductInfoDetailTitle(String productInfoDetailTitle) + { + this.productInfoDetailTitle = productInfoDetailTitle; + } + + public String getProductInfoDetailTitle() + { + return productInfoDetailTitle; + } + public void setProductInfoDetailDesc(String productInfoDetailDesc) + { + this.productInfoDetailDesc = productInfoDetailDesc; + } + + public String getProductInfoDetailDesc() + { + return productInfoDetailDesc; + } + public void setProductInfoDetailOrder(Long productInfoDetailOrder) + { + this.productInfoDetailOrder = productInfoDetailOrder; + } + + public Long getProductInfoDetailOrder() + { + return productInfoDetailOrder; + } + public void setProductInfoDetailPic(String productInfoDetailPic) + { + this.productInfoDetailPic = productInfoDetailPic; + } + + public String getProductInfoDetailPic() + { + return productInfoDetailPic; + } + + public List getHwProductInfoDetailList() { + return hwProductInfoDetailList; + } + + public void setHwProductInfoDetailList(List hwProductInfoDetailList) { + this.hwProductInfoDetailList = hwProductInfoDetailList; + } + + public String getConfigModel() { + return configModel; + } + + public void setConfigModel(String configModel) { + this.configModel = configModel; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("productInfoDetailId", getProductInfoDetailId()) + .append("parentId", getParentId()) + .append("productInfoId", getProductInfoId()) + .append("configModal", getConfigModal()) + .append("productInfoDetailTitle", getProductInfoDetailTitle()) + .append("productInfoDetailDesc", getProductInfoDetailDesc()) + .append("productInfoDetailOrder", getProductInfoDetailOrder()) + .append("productInfoDetailPic", getProductInfoDetailPic()) + .append("ancestors", getAncestors()) + .append("createTime", getCreateTime()) + .append("createBy", getCreateBy()) + .append("updateTime", getUpdateTime()) + .append("updateBy", getUpdateBy()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWeb.java` + +```java +package com.ruoyi.portal.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * haiwei官网json对象 hw_web + * + * @author ruoyi + * @date 2025-08-18 + */ +public class HwWeb extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + private Long webId; + + /** json */ + @Excel(name = "json") + private String webJson; + + /** json字符串 */ + @Excel(name = "json字符串") + private String webJsonString; + + /** 页面 */ + @Excel(name = "页面") + private Long webCode; + + /** 逻辑删除标志:'0'未删除,'1'已删除 */ + private String isDelete; + + /** json字符串 */ + @Excel(name = "字符串") + private String webJsonEnglish; + + public void setWebId(Long webId) + { + this.webId = webId; + } + + public Long getWebId() + { + return webId; + } + public void setWebJson(String webJson) + { + this.webJson = webJson; + } + + public String getWebJson() + { + return webJson; + } + public void setWebJsonString(String webJsonString) + { + this.webJsonString = webJsonString; + } + + public String getWebJsonString() + { + return webJsonString; + } + public void setWebCode(Long webCode) + { + this.webCode = webCode; + } + + public Long getWebCode() + { + return webCode; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + public String getwebJsonEnglish() { + return webJsonEnglish; + } + + public void setwebJsonEnglish(String webJsonEnglish) { + this.webJsonEnglish = webJsonEnglish; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("webId", getWebId()) + .append("webJson", getWebJson()) + .append("webJsonString", getWebJsonString()) + .append("webCode", getWebCode()) + .append("isDelete", getIsDelete()) + .append("webJsonEnglish", getwebJsonEnglish()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWeb1.java` + +```java +package com.ruoyi.portal.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * haiwei官网json对象 hw_web1 + * + * @author ruoyi + * @date 2025-08-18 + */ +public class HwWeb1 extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + private Long webId; + + /** json */ + @Excel(name = "json") + private String webJson; + + /** json字符串 */ + @Excel(name = "json字符串") + private String webJsonString; + + /** 页面 */ + @Excel(name = "页面") + private Long webCode; + + private Long deviceId; + + private Long typeId; + + /** 逻辑删除标志:'0'未删除,'1'已删除 */ + private String isDelete; + + /** json字符串 */ + @Excel(name = "字符串") + private String webJsonEnglish; + + public void setWebId(Long webId) + { + this.webId = webId; + } + + public Long getWebId() + { + return webId; + } + public void setWebJson(String webJson) + { + this.webJson = webJson; + } + + public String getWebJson() + { + return webJson; + } + public void setWebJsonString(String webJsonString) + { + this.webJsonString = webJsonString; + } + + public String getWebJsonString() + { + return webJsonString; + } + public void setWebCode(Long webCode) + { + this.webCode = webCode; + } + + public Long getWebCode() + { + return webCode; + } + + public Long getDeviceId() { + return deviceId; + } + + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + + public Long getTypeId() { + return typeId; + } + + public void setTypeId(Long typeId) { + this.typeId = typeId; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + public String getwebJsonEnglish() { + return webJsonEnglish; + } + + public void setwebJsonEnglish(String webJsonEnglish) { + this.webJsonEnglish = webJsonEnglish; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("webId", getWebId()) + .append("webJson", getWebJson()) + .append("webJsonString", getWebJsonString()) + .append("webCode", getWebCode()) + .append("deviceId", getDeviceId()) + .append("typeId", getTypeId()) + .append("isDelete", getIsDelete()) + .append("webJsonEnglish", getwebJsonEnglish()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWebDocument.java` + +```java +package com.ruoyi.portal.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Hw资料文件对象 hw_web_document + * + * @author zch + * @date 2025-09-22 + */ +public class HwWebDocument extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 主键 */ + private String documentId; + + /** 租户id */ + @Excel(name = "租户id") + private Long tenantId; + + /** 文件存储地址 */ + @Excel(name = "文件存储地址") + private String documentAddress; + + /** 页面编码,用来连表查询 */ + @Excel(name = "页面编码,用来连表查询") + private String webCode; + + /** 密钥 */ + // @Excel(name = "密钥") + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String secretKey; + + /** json */ + private String json; + + /** 文件类型 */ + private String type; + + /** 逻辑删除标志:'0'未删除,'1'已删除 */ + private String isDelete; + + public void setDocumentId(String documentId) + { + this.documentId = documentId; + } + + public String getDocumentId() + { + return documentId; + } + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + + public Long getTenantId() + { + return tenantId; + } + public void setDocumentAddress(String documentAddress) + { + this.documentAddress = documentAddress; + } + + public String getDocumentAddress() + { + return documentAddress; + } + public void setWebCode(String webCode) + { + this.webCode = webCode; + } + + public String getWebCode() + { + return webCode; + } + public void setSecretKey(String secretKey) + { + this.secretKey = secretKey; + } + + public String getSecretKey() + { + return secretKey; + } + + public boolean getHasSecret() { + return secretKey != null && !secretKey.trim().isEmpty(); + } + + public String getJson() { + return json; + } + + public void setJson(String json) { + this.json = json; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("documentId", getDocumentId()) + .append("tenantId", getTenantId()) + .append("documentAddress", getDocumentAddress()) + .append("createTime", getCreateTime()) + .append("webCode", getWebCode()) + .append("hasSecret", getHasSecret()) + .append("json", getJson()) + .append("type", getType()) + .append("isDelete", getIsDelete()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWebMenu.java` + +```java +package com.ruoyi.portal.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.TreeEntity; + +/** + * haiwei官网菜单对象 hw_web_menu + * + * @author zch + * @date 2025-08-18 + */ +public class HwWebMenu extends TreeEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单主键id */ + private Long webMenuId; + + /** 父节点 */ + @Excel(name = "父节点") + private Long parent; + + /** 状态 */ + @Excel(name = "状态") + private String status; + + /** 菜单名称 */ + @Excel(name = "菜单名称") + private String webMenuName; + + /** 租户 */ + @Excel(name = "租户") + private Long tenantId; + + /** 图片地址 */ + @Excel(name = "图片地址") + private String webMenuPic; + + /** 官网菜单类型 */ + @Excel(name = "官网菜单类型") + private Long webMenuType; + + /** 排序 */ + @Excel(name = "排序") + private Integer order; + + /** 逻辑删除标志:'0'未删除,'1'已删除 */ + private String isDelete; + + private String webMenuNameEnglish; + + public void setWebMenuId(Long webMenuId) + { + this.webMenuId = webMenuId; + } + + public Long getWebMenuId() + { + return webMenuId; + } + public void setParent(Long parent) + { + this.parent = parent; + } + + public Long getParent() + { + return parent; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setWebMenuName(String webMenuName) + { + this.webMenuName = webMenuName; + } + + public String getWebMenuName() + { + return webMenuName; + } + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + + public Long getTenantId() + { + return tenantId; + } + public void setWebMenuPic(String webMenuPic) + { + this.webMenuPic = webMenuPic; + } + + public String getWebMenuPic() + { + return webMenuPic; + } + public void setWebMenuType(Long webMenuType) + { + this.webMenuType = webMenuType; + } + + public Long getWebMenuType() + { + return webMenuType; + } + + public Integer getOrder() { + return order; + } + + public void setOrder(Integer order) { + this.order = order; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + public String getWebMenuNameEnglish() { + return webMenuNameEnglish; + } + + public void setWebMenuNameEnglish(String webMenuNameEnglish) { + this.webMenuNameEnglish = webMenuNameEnglish; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("webMenuId", getWebMenuId()) + .append("parent", getParent()) + .append("ancestors", getAncestors()) + .append("status", getStatus()) + .append("webMenuName", getWebMenuName()) + .append("tenantId", getTenantId()) + .append("webMenuPic", getWebMenuPic()) + .append("webMenuType", getWebMenuType()) + .append("order", getOrder()) + .append("isDelete", getIsDelete()) + .append("webMenuNameEnglish", getWebMenuNameEnglish()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWebMenu1.java` + +```java +package com.ruoyi.portal.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.TreeEntity; + +import java.util.List; + +/** + * haiwei官网菜单对象 hw_web_menu1 + * + * @author zch + * @date 2025-08-18 + */ +public class HwWebMenu1 extends TreeEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单主键id */ + private Long webMenuId; + + /** 父节点 */ + @Excel(name = "父节点") + private Long parent; + + /** 状态 */ + @Excel(name = "状态") + private String status; + + /** 菜单名称 */ + @Excel(name = "菜单名称") + private String webMenuName; + + /** 租户 */ + @Excel(name = "租户") + private Long tenantId; + + /** 图片地址 */ + @Excel(name = "图片地址") + private String webMenuPic; + + /** 官网菜单类型 */ + @Excel(name = "官网菜单类型") + private Long webMenuType; + + private String valuel; + + /** 逻辑删除标志:'0'未删除,'1'已删除 */ + private String isDelete; + + private String webMenuNameEnglish; + + public void setWebMenuId(Long webMenuId) + { + this.webMenuId = webMenuId; + } + + public Long getWebMenuId() + { + return webMenuId; + } + public void setParent(Long parent) + { + this.parent = parent; + } + + public Long getParent() + { + return parent; + } + public void setStatus(String status) + { + this.status = status; + } + + public String getStatus() + { + return status; + } + public void setWebMenuName(String webMenuName) + { + this.webMenuName = webMenuName; + } + + public String getWebMenuName() + { + return webMenuName; + } + public void setTenantId(Long tenantId) + { + this.tenantId = tenantId; + } + + public Long getTenantId() + { + return tenantId; + } + public void setWebMenuPic(String webMenuPic) + { + this.webMenuPic = webMenuPic; + } + + public String getWebMenuPic() + { + return webMenuPic; + } + public void setWebMenuType(Long webMenuType) + { + this.webMenuType = webMenuType; + } + + public Long getWebMenuType() + { + return webMenuType; + } + + public String getValuel() { + return valuel; + } + public void setValuel(String valuel) { + this.valuel = valuel; + } + + public String getIsDelete() { + return isDelete; + } + + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + public String getWebMenuNameEnglish() { + return webMenuNameEnglish; + } + + public void setWebMenuNameEnglish(String webMenuNameEnglish) { + this.webMenuNameEnglish = webMenuNameEnglish; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("webMenuId", getWebMenuId()) + .append("parent", getParent()) + .append("ancestors", getAncestors()) + .append("status", getStatus()) + .append("webMenuName", getWebMenuName()) + .append("tenantId", getTenantId()) + .append("webMenuPic", getWebMenuPic()) + .append("webMenuType", getWebMenuType()) + .append("valuel", getValuel()) + .append("isDelete", getIsDelete()) + .append("webMenuNameEnglish", getWebMenuNameEnglish()) + .toString(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWebVisitDaily.java` + +```java +package com.ruoyi.portal.domain; + +import java.util.Date; + +/** + * 官网访问日汇总 + * + * @author ruoyi + */ +public class HwWebVisitDaily +{ + private Date statDate; + + private Long pv; + + private Long uv; + + private Long ipUv; + + private Long avgStayMs; + + private Double bounceRate; + + private Long searchCount; + + private Long downloadCount; + + private Date createdAt; + + private Date updatedAt; + + public Date getStatDate() + { + return statDate; + } + + public void setStatDate(Date statDate) + { + this.statDate = statDate; + } + + public Long getPv() + { + return pv; + } + + public void setPv(Long pv) + { + this.pv = pv; + } + + public Long getUv() + { + return uv; + } + + public void setUv(Long uv) + { + this.uv = uv; + } + + public Long getIpUv() + { + return ipUv; + } + + public void setIpUv(Long ipUv) + { + this.ipUv = ipUv; + } + + public Long getAvgStayMs() + { + return avgStayMs; + } + + public void setAvgStayMs(Long avgStayMs) + { + this.avgStayMs = avgStayMs; + } + + public Double getBounceRate() + { + return bounceRate; + } + + public void setBounceRate(Double bounceRate) + { + this.bounceRate = bounceRate; + } + + public Long getSearchCount() + { + return searchCount; + } + + public void setSearchCount(Long searchCount) + { + this.searchCount = searchCount; + } + + public Long getDownloadCount() + { + return downloadCount; + } + + public void setDownloadCount(Long downloadCount) + { + this.downloadCount = downloadCount; + } + + public Date getCreatedAt() + { + return createdAt; + } + + public void setCreatedAt(Date createdAt) + { + this.createdAt = createdAt; + } + + public Date getUpdatedAt() + { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) + { + this.updatedAt = updatedAt; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/HwWebVisitEvent.java` + +```java +package com.ruoyi.portal.domain; + +import com.ruoyi.common.core.domain.BaseEntity; + +import java.util.Date; + +/** + * 官网匿名访问事件明细 + * + * @author ruoyi + */ +public class HwWebVisitEvent extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long id; + + private String eventType; + + private String visitorId; + + private String sessionId; + + private String path; + + private String referrer; + + private String utmSource; + + private String utmMedium; + + private String utmCampaign; + + private String keyword; + + private String ipHash; + + private String ua; + + private String device; + + private String browser; + + private String os; + + private Long stayMs; + + private Date eventTime; + + private Date createdAt; + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getEventType() + { + return eventType; + } + + public void setEventType(String eventType) + { + this.eventType = eventType; + } + + public String getVisitorId() + { + return visitorId; + } + + public void setVisitorId(String visitorId) + { + this.visitorId = visitorId; + } + + public String getSessionId() + { + return sessionId; + } + + public void setSessionId(String sessionId) + { + this.sessionId = sessionId; + } + + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + public String getReferrer() + { + return referrer; + } + + public void setReferrer(String referrer) + { + this.referrer = referrer; + } + + public String getUtmSource() + { + return utmSource; + } + + public void setUtmSource(String utmSource) + { + this.utmSource = utmSource; + } + + public String getUtmMedium() + { + return utmMedium; + } + + public void setUtmMedium(String utmMedium) + { + this.utmMedium = utmMedium; + } + + public String getUtmCampaign() + { + return utmCampaign; + } + + public void setUtmCampaign(String utmCampaign) + { + this.utmCampaign = utmCampaign; + } + + public String getKeyword() + { + return keyword; + } + + public void setKeyword(String keyword) + { + this.keyword = keyword; + } + + public String getIpHash() + { + return ipHash; + } + + public void setIpHash(String ipHash) + { + this.ipHash = ipHash; + } + + public String getUa() + { + return ua; + } + + public void setUa(String ua) + { + this.ua = ua; + } + + public String getDevice() + { + return device; + } + + public void setDevice(String device) + { + this.device = device; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getStayMs() + { + return stayMs; + } + + public void setStayMs(Long stayMs) + { + this.stayMs = stayMs; + } + + public Date getEventTime() + { + return eventTime; + } + + public void setEventTime(Date eventTime) + { + this.eventTime = eventTime; + } + + public Date getCreatedAt() + { + return createdAt; + } + + public void setCreatedAt(Date createdAt) + { + this.createdAt = createdAt; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/SecureDocumentRequest.java` + +```java +package com.ruoyi.portal.domain; + +public class SecureDocumentRequest +{ + private String documentId; + private String providedKey; + + public String getDocumentId() + { + return documentId; + } + + public void setDocumentId(String documentId) + { + this.documentId = documentId; + } + + public String getProvidedKey() + { + return providedKey; + } + + public void setProvidedKey(String providedKey) + { + this.providedKey = providedKey; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/domain/vo/TreeSelect.java` + +```java +package com.ruoyi.portal.domain.vo; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.ruoyi.portal.domain.HwPortalConfigType; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(HwPortalConfigType portalConfigType) + { + this.id = portalConfigType.getConfigTypeId(); + this.label = portalConfigType.getConfigTypeName(); + this.children = portalConfigType.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoDetailMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwAboutUsInfoDetail; + +import java.util.List; + +/** + * 关于我们信息明细Mapper接口 + * + * @author ruoyi + * @date 2024-12-01 + */ +public interface HwAboutUsInfoDetailMapper +{ + /** + * 查询关于我们信息明细 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 关于我们信息明细 + */ + public HwAboutUsInfoDetail selectHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId); + + /** + * 查询关于我们信息明细列表 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 关于我们信息明细集合 + */ + public List selectHwAboutUsInfoDetailList(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 新增关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + public int insertHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 修改关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + public int updateHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 删除关于我们信息明细 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 结果 + */ + public int deleteHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId); + + /** + * 批量删除关于我们信息明细 + * + * @param usInfoDetailIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwAboutUsInfoDetailByUsInfoDetailIds(Long[] usInfoDetailIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwAboutUsInfoMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwAboutUsInfo; + +import java.util.List; + +/** + * 关于我们信息Mapper接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface HwAboutUsInfoMapper +{ + /** + * 查询关于我们信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 关于我们信息 + */ + public HwAboutUsInfo selectHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId); + + /** + * 查询关于我们信息列表 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 关于我们信息集合 + */ + public List selectHwAboutUsInfoList(HwAboutUsInfo hwAboutUsInfo); + + /** + * 新增关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + public int insertHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo); + + /** + * 修改关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + public int updateHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo); + + /** + * 删除关于我们信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 结果 + */ + public int deleteHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId); + + /** + * 批量删除关于我们信息 + * + * @param aboutUsInfoIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwAboutUsInfoByAboutUsInfoIds(Long[] aboutUsInfoIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwAnalyticsMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwWebVisitDaily; +import com.ruoyi.portal.domain.HwWebVisitEvent; +import com.ruoyi.portal.domain.dto.AnalyticsRankItemDTO; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +/** + * 官网访问监控 Mapper + * + * @author ruoyi + */ +public interface HwAnalyticsMapper +{ + /** + * 新增访问事件 + * + * @param event 事件 + * @return 结果 + */ + int insertVisitEvent(HwWebVisitEvent event); + + /** + * 查询指定日期事件总数 + * + * @param statDate 日期 + * @param eventType 事件类型 + * @return 数量 + */ + Long countEventByType(@Param("statDate") LocalDate statDate, @Param("eventType") String eventType); + + /** + * 查询指定日期访客 UV + * + * @param statDate 日期 + * @return UV + */ + Long countDistinctVisitor(@Param("statDate") LocalDate statDate); + + /** + * 查询指定日期 IP UV + * + * @param statDate 日期 + * @return IP UV + */ + Long countDistinctIp(@Param("statDate") LocalDate statDate); + + /** + * 查询指定日期平均停留时长 + * + * @param statDate 日期 + * @return 平均停留时长 + */ + Long avgStayMs(@Param("statDate") LocalDate statDate); + + /** + * 查询指定日期会话总数 + * + * @param statDate 日期 + * @return 会话总数 + */ + Long countDistinctSessions(@Param("statDate") LocalDate statDate); + + /** + * 查询仅访问一个页面的会话总数 + * + * @param statDate 日期 + * @return 会话总数 + */ + Long countSinglePageSessions(@Param("statDate") LocalDate statDate); + + /** + * 查询入口页排行 + * + * @param statDate 日期 + * @param limit 结果数量 + * @return 排行 + */ + List selectTopEntryPages(@Param("statDate") LocalDate statDate, @Param("limit") int limit); + + /** + * 查询热门页面排行 + * + * @param statDate 日期 + * @param limit 结果数量 + * @return 排行 + */ + List selectTopHotPages(@Param("statDate") LocalDate statDate, @Param("limit") int limit); + + /** + * 查询热门关键词排行 + * + * @param statDate 日期 + * @param limit 结果数量 + * @return 排行 + */ + List selectTopKeywords(@Param("statDate") LocalDate statDate, @Param("limit") int limit); + + /** + * 新增或更新日汇总 + * + * @param daily 日汇总 + * @return 结果 + */ + int upsertDaily(HwWebVisitDaily daily); + + /** + * 查询日汇总 + * + * @param statDate 日期 + * @return 日汇总 + */ + HwWebVisitDaily selectDailyByDate(@Param("statDate") LocalDate statDate); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwContactUsInfoMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwContactUsInfo; + +import java.util.List; + +/** + * 联系我们信息Mapper接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface HwContactUsInfoMapper +{ + /** + * 查询联系我们信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 联系我们信息 + */ + public HwContactUsInfo selectHwContactUsInfoByContactUsInfoId(Long contactUsInfoId); + + /** + * 查询联系我们信息列表 + * + * @param hwContactUsInfo 联系我们信息 + * @return 联系我们信息集合 + */ + public List selectHwContactUsInfoList(HwContactUsInfo hwContactUsInfo); + + /** + * 新增联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + public int insertHwContactUsInfo(HwContactUsInfo hwContactUsInfo); + + /** + * 修改联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + public int updateHwContactUsInfo(HwContactUsInfo hwContactUsInfo); + + /** + * 删除联系我们信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 结果 + */ + public int deleteHwContactUsInfoByContactUsInfoId(Long contactUsInfoId); + + /** + * 批量删除联系我们信息 + * + * @param contactUsInfoIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwContactUsInfoByContactUsInfoIds(Long[] contactUsInfoIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwPortalConfigMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwPortalConfig; + +import java.util.List; + +/** + * 门户网站配置Mapper接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface HwPortalConfigMapper +{ + /** + * 查询门户网站配置 + * + * @param portalConfigId 门户网站配置主键 + * @return 门户网站配置 + */ + public HwPortalConfig selectHwPortalConfigByPortalConfigId(Long portalConfigId); + + /** + * 查询门户网站配置列表 + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置集合 + */ + public List selectHwPortalConfigList(HwPortalConfig hwPortalConfig); + + /** + * 新增门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + public int insertHwPortalConfig(HwPortalConfig hwPortalConfig); + + /** + * 修改门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + public int updateHwPortalConfig(HwPortalConfig hwPortalConfig); + + /** + * 删除门户网站配置 + * + * @param portalConfigId 门户网站配置主键 + * @return 结果 + */ + public int deleteHwPortalConfigByPortalConfigId(Long portalConfigId); + + /** + * 批量删除门户网站配置 + * + * @param portalConfigIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwPortalConfigByPortalConfigIds(Long[] portalConfigIds); + + /** + * 查询门户网站配置列表,join hw_portal_config_type + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置集合 + */ + public List selectHwPortalConfigJoinList(HwPortalConfig hwPortalConfig); + + + /** + * 查询门户网站配置列表 + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置集合 + */ + public List selectHwPortalConfigList2(HwPortalConfig hwPortalConfig); + + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwPortalConfigTypeMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwPortalConfigType; + +import java.util.List; + +/** + * 门户网站配置类型Mapper接口 + * + * @author xins + * @date 2024-12-11 + */ +public interface HwPortalConfigTypeMapper +{ + /** + * 查询门户网站配置类型 + * + * @param configTypeId 门户网站配置类型主键 + * @return 门户网站配置类型 + */ + public HwPortalConfigType selectHwPortalConfigTypeByConfigTypeId(Long configTypeId); + + /** + * 查询门户网站配置类型列表 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 门户网站配置类型集合 + */ + public List selectHwPortalConfigTypeList(HwPortalConfigType hwPortalConfigType); + + /** + * 新增门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + public int insertHwPortalConfigType(HwPortalConfigType hwPortalConfigType); + + /** + * 修改门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + public int updateHwPortalConfigType(HwPortalConfigType hwPortalConfigType); + + /** + * 删除门户网站配置类型 + * + * @param configTypeId 门户网站配置类型主键 + * @return 结果 + */ + public int deleteHwPortalConfigTypeByConfigTypeId(Long configTypeId); + + /** + * 批量删除门户网站配置类型 + * + * @param configTypeIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwPortalConfigTypeByConfigTypeIds(Long[] configTypeIds); + + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwProductCaseInfoMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwProductCaseInfo; + +import java.util.List; + +/** + * 案例内容Mapper接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface HwProductCaseInfoMapper +{ + /** + * 查询案例内容 + * + * @param caseInfoId 案例内容主键 + * @return 案例内容 + */ + public HwProductCaseInfo selectHwProductCaseInfoByCaseInfoId(Long caseInfoId); + + /** + * 查询案例内容列表 + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容集合 + */ + public List selectHwProductCaseInfoList(HwProductCaseInfo hwProductCaseInfo); + + /** + * 新增案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + public int insertHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo); + + /** + * 修改案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + public int updateHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo); + + /** + * 删除案例内容 + * + * @param caseInfoId 案例内容主键 + * @return 结果 + */ + public int deleteHwProductCaseInfoByCaseInfoId(Long caseInfoId); + + /** + * 批量删除案例内容 + * + * @param caseInfoIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwProductCaseInfoByCaseInfoIds(Long[] caseInfoIds); + + /** + * 查询案例内容列表,Join portalConfigType + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容集合 + */ + public List selectHwProductCaseInfoJoinList(HwProductCaseInfo hwProductCaseInfo); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwProductInfoDetailMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwProductInfoDetail; + +import java.util.List; + +/** + * 产品信息明细配置Mapper接口 + * + * @author xins + * @date 2024-12-11 + */ +public interface HwProductInfoDetailMapper +{ + /** + * 查询产品信息明细配置 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 产品信息明细配置 + */ + public HwProductInfoDetail selectHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId); + + /** + * 查询产品信息明细配置列表 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 产品信息明细配置集合 + */ + public List selectHwProductInfoDetailList(HwProductInfoDetail hwProductInfoDetail); + + /** + * 新增产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + public int insertHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail); + + /** + * 修改产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + public int updateHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail); + + /** + * 删除产品信息明细配置 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 结果 + */ + public int deleteHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId); + + /** + * 批量删除产品信息明细配置 + * + * @param productInfoDetailIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwProductInfoDetailByProductInfoDetailIds(Long[] productInfoDetailIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwProductInfoMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwProductInfo; + +import java.util.List; + +/** + * 产品信息配置Mapper接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface HwProductInfoMapper +{ + /** + * 查询产品信息配置 + * + * @param productInfoId 产品信息配置主键 + * @return 产品信息配置 + */ + public HwProductInfo selectHwProductInfoByProductInfoId(Long productInfoId); + + /** + * 查询产品信息配置列表 + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置集合 + */ + public List selectHwProductInfoList(HwProductInfo hwProductInfo); + + /** + * 新增产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + public int insertHwProductInfo(HwProductInfo hwProductInfo); + + /** + * 修改产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + public int updateHwProductInfo(HwProductInfo hwProductInfo); + + /** + * 删除产品信息配置 + * + * @param productInfoId 产品信息配置主键 + * @return 结果 + */ + public int deleteHwProductInfoByProductInfoId(Long productInfoId); + + /** + * 批量删除产品信息配置 + * + * @param productInfoIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwProductInfoByProductInfoIds(Long[] productInfoIds); + + /** + * 查询产品信息配置列表,join product info detail + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置集合 + */ + public List selectHwProductInfoJoinDetailList(HwProductInfo hwProductInfo); + + /** + * 查询产品信息配置列表,join portalConfigType门户网站配置类型 + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置集合 + */ + public List selectHwProductInfoJoinList(HwProductInfo hwProductInfo); + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwSearchMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.dto.SearchRawRecord; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 门户搜索 Mapper + * + * @author ruoyi + */ +public interface HwSearchMapper +{ + /** + * 关键词搜索原始数据 + * + * @param keyword 关键词 + * @return 命中记录 + */ + List searchByKeyword(@Param("keyword") String keyword); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwWebDocumentMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import java.util.List; +import com.ruoyi.portal.domain.HwWebDocument; + +/** + * Hw资料文件Mapper接口 + * + * @author zch + * @date 2025-09-22 + */ +public interface HwWebDocumentMapper +{ + /** + * 查询Hw资料文件 + * + * @param documentId Hw资料文件主键 + * @return Hw资料文件 + */ + public HwWebDocument selectHwWebDocumentByDocumentId(String documentId); + + /** + * 查询Hw资料文件列表 + * + * @param hwWebDocument Hw资料文件 + * @return Hw资料文件集合 + */ + public List selectHwWebDocumentList(HwWebDocument hwWebDocument); + + /** + * 新增Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + public int insertHwWebDocument(HwWebDocument hwWebDocument); + + /** + * 修改Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + public int updateHwWebDocument(HwWebDocument hwWebDocument); + + /** + * 删除Hw资料文件 + * + * @param documentId Hw资料文件主键 + * @return 结果 + */ + public int deleteHwWebDocumentByDocumentId(String documentId); + + /** + * 批量删除Hw资料文件 + * + * @param documentIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwWebDocumentByDocumentIds(String[] documentIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwWebMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import java.util.List; +import com.ruoyi.portal.domain.HwWeb; + +/** + * haiwei官网jsonMapper接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface HwWebMapper +{ + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + public HwWeb selectHwWebByWebcode(Long webCode); + + /** + * 查询haiwei官网json列表 + * + * @param hwWeb haiwei官网json + * @return haiwei官网json集合 + */ + public List selectHwWebList(HwWeb hwWeb); + + /** + * 新增haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + public int insertHwWeb(HwWeb hwWeb); + + /** + * 修改haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + public int updateHwWeb(HwWeb hwWeb); + + /** + * 删除haiwei官网json + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + public int deleteHwWebByWebId(Long webId); + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwWebByWebIds(Long[] webIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwWebMapper1.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwWeb1; + +import java.util.List; + +/** + * haiwei官网jsonMapper接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface HwWebMapper1 +{ + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + public HwWeb1 selectHwWebByWebcode(Long webCode); + + /** + * 查询haiwei官网json列表 + * + * @param HwWeb1 haiwei官网json + * @return haiwei官网json集合 + */ + public HwWeb1 selectHwWebOne(HwWeb1 hwWeb1); + + /** + * 查询haiwei官网json列表 + * + * @param HwWeb1 haiwei官网json + * @return haiwei官网json集合 + */ + public List selectHwWebList(HwWeb1 hwWeb1); + + /** + * 新增haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + public int insertHwWeb(HwWeb1 hwWeb1); + + /** + * 修改haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + public int updateHwWeb(HwWeb1 hwWeb1); + + /** + * 删除haiwei官网json + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + public int deleteHwWebByWebId(Long webId); + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwWebByWebIds(Long[] webIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper.java` + +```java +package com.ruoyi.portal.mapper; + +import java.util.List; +import com.ruoyi.portal.domain.HwWebMenu; + +/** + * haiwei官网菜单Mapper接口 + * + * @author zch + * @date 2025-08-18 + */ +public interface HwWebMenuMapper +{ + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + public HwWebMenu selectHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 查询haiwei官网菜单列表 + * + * @param hwWebMenu haiwei官网菜单 + * @return haiwei官网菜单集合 + */ + public List selectHwWebMenuList(HwWebMenu hwWebMenu); + + /** + * 新增haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + public int insertHwWebMenu(HwWebMenu hwWebMenu); + + /** + * 修改haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + public int updateHwWebMenu(HwWebMenu hwWebMenu); + + /** + * 删除haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/mapper/HwWebMenuMapper1.java` + +```java +package com.ruoyi.portal.mapper; + +import com.ruoyi.portal.domain.HwWebMenu; +import com.ruoyi.portal.domain.HwWebMenu1; +import com.ruoyi.portal.domain.HwWebMenu1; + +import java.util.List; + +/** + * haiwei官网菜单Mapper接口 + * + * @author zch + * @date 2025-08-18 + */ +public interface HwWebMenuMapper1 +{ + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + public HwWebMenu1 selectHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 查询haiwei官网菜单列表 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return haiwei官网菜单集合 + */ + public List selectHwWebMenuList(HwWebMenu1 HwWebMenu1); + + /** + * 新增haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + public int insertHwWebMenu(HwWebMenu1 HwWebMenu1); + + /** + * 修改haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + public int updateHwWebMenu(HwWebMenu1 HwWebMenu1); + + /** + * 删除haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds); + + + /** + * 获取菜单树列表 + */ + public List selectMenuTree(HwWebMenu1 hwWebMenu); + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/search/convert/PortalSearchDocConverter.java` + +```java +package com.ruoyi.portal.search.convert; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.HwWeb; +import com.ruoyi.portal.domain.HwWeb1; +import com.ruoyi.portal.domain.HwWebDocument; +import com.ruoyi.portal.domain.HwWebMenu; +import com.ruoyi.portal.search.es.entity.PortalSearchDoc; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 搜索文档转换器 + * + * @author ruoyi + */ +@Component +public class PortalSearchDocConverter +{ + public static final String SOURCE_MENU = "menu"; + public static final String SOURCE_WEB = "web"; + public static final String SOURCE_WEB1 = "web1"; + public static final String SOURCE_DOCUMENT = "document"; + public static final String SOURCE_CONFIG_TYPE = "configType"; + + private static final Set SKIP_JSON_KEYS = new HashSet<>(Arrays.asList( + "icon", "url", "banner", "banner1", "img", "imglist", "type", "uuid", "filename", + "documentaddress", "secretkey", "route", "routequery", "webcode", "deviceid", "typeid", + "configtypeid", "id" + )); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public PortalSearchDoc fromWebMenu(HwWebMenu menu) + { + PortalSearchDoc doc = initBase(); + String id = "menu:" + menu.getWebMenuId(); + doc.setId(id); + doc.setDocId(id); + doc.setSourceType(SOURCE_MENU); + doc.setTitle(menu.getWebMenuName()); + doc.setContent(joinText(menu.getWebMenuName(), menu.getAncestors())); + doc.setMenuId(String.valueOf(menu.getWebMenuId())); + doc.setRoute("/test"); + doc.setRouteQueryJson(toJson(Map.of("id", menu.getWebMenuId()))); + doc.setUpdatedAt(new Date()); + return doc; + } + + public PortalSearchDoc fromWeb(HwWeb web) + { + PortalSearchDoc doc = initBase(); + String webCode = toString(web.getWebCode()); + String id = "web:" + webCode; + doc.setId(id); + doc.setDocId(id); + doc.setSourceType(SOURCE_WEB); + doc.setTitle("页面#" + webCode); + doc.setContent(extractSearchableText(web.getWebJsonString())); + doc.setWebCode(webCode); + if ("-1".equals(webCode)) + { + doc.setRoute("/index"); + doc.setRouteQueryJson("{}"); + } + else if ("7".equals(webCode)) + { + doc.setRoute("/productCenter"); + doc.setRouteQueryJson("{}"); + } + else + { + doc.setRoute("/test"); + doc.setRouteQueryJson(toJson(Map.of("id", webCode))); + } + doc.setUpdatedAt(new Date()); + return doc; + } + + public PortalSearchDoc fromWeb1(HwWeb1 web1) + { + PortalSearchDoc doc = initBase(); + String webCode = toString(web1.getWebCode()); + String typeId = toString(web1.getTypeId()); + String deviceId = toString(web1.getDeviceId()); + String id = String.format("web1:%s:%s:%s", webCode, typeId, deviceId); + doc.setId(id); + doc.setDocId(id); + doc.setSourceType(SOURCE_WEB1); + doc.setTitle("详情#" + webCode + "-" + typeId + "-" + deviceId); + doc.setContent(extractSearchableText(web1.getWebJsonString())); + doc.setWebCode(webCode); + doc.setTypeId(typeId); + doc.setDeviceId(deviceId); + doc.setRoute("/productCenter/detail"); + doc.setRouteQueryJson(toJson(Map.of("webCode", webCode, "typeId", typeId, "deviceId", deviceId))); + doc.setUpdatedAt(new Date()); + return doc; + } + + public PortalSearchDoc fromDocument(HwWebDocument document) + { + PortalSearchDoc doc = initBase(); + String id = "doc:" + document.getDocumentId(); + doc.setId(id); + doc.setDocId(id); + doc.setSourceType(SOURCE_DOCUMENT); + String title = StringUtils.isNotBlank(document.getJson()) ? document.getJson() : document.getDocumentId(); + doc.setTitle(title); + doc.setContent(StringUtils.defaultString(document.getJson())); + doc.setDocumentId(document.getDocumentId()); + doc.setWebCode(document.getWebCode()); + doc.setTypeId(document.getType()); + doc.setRoute("/serviceSupport"); + doc.setRouteQueryJson(toJson(Map.of("documentId", document.getDocumentId()))); + doc.setUpdatedAt(new Date()); + return doc; + } + + public PortalSearchDoc fromConfigType(HwPortalConfigType configType) + { + PortalSearchDoc doc = initBase(); + String id = "configType:" + configType.getConfigTypeId(); + doc.setId(id); + doc.setDocId(id); + doc.setSourceType(SOURCE_CONFIG_TYPE); + doc.setTitle(configType.getConfigTypeName()); + doc.setContent(joinText(configType.getConfigTypeName(), configType.getHomeConfigTypeName(), configType.getConfigTypeDesc())); + doc.setTypeId(toString(configType.getConfigTypeId())); + doc.setRoute("/productCenter"); + doc.setRouteQueryJson(toJson(Map.of("configTypeId", configType.getConfigTypeId()))); + doc.setUpdatedAt(new Date()); + return doc; + } + + public String extractSearchableText(String text) + { + if (StringUtils.isBlank(text)) + { + return StringUtils.EMPTY; + } + String stripped = stripHtml(text); + try + { + JsonNode root = objectMapper.readTree(text); + StringBuilder builder = new StringBuilder(); + collectNodeText(root, null, builder); + String extracted = normalizeWhitespace(builder.toString()); + return StringUtils.isBlank(extracted) ? stripped : extracted; + } + catch (Exception ignored) + { + return stripped; + } + } + + private void collectNodeText(JsonNode node, String fieldName, StringBuilder out) + { + if (node == null || node.isNull()) + { + return; + } + if (node.isObject()) + { + node.fields().forEachRemaining(entry -> + { + if (!shouldSkip(entry.getKey())) + { + collectNodeText(entry.getValue(), entry.getKey(), out); + } + }); + return; + } + if (node.isArray()) + { + for (JsonNode child : node) + { + collectNodeText(child, fieldName, out); + } + return; + } + if (node.isTextual()) + { + if (shouldSkip(fieldName)) + { + return; + } + String value = normalizeWhitespace(stripHtml(node.asText())); + if (StringUtils.isBlank(value)) + { + return; + } + if (value.startsWith("http://") || value.startsWith("https://")) + { + return; + } + out.append(value).append(' '); + } + } + + private boolean shouldSkip(String fieldName) + { + if (StringUtils.isBlank(fieldName)) + { + return false; + } + String normalized = fieldName.toLowerCase(); + return SKIP_JSON_KEYS.contains(normalized) || normalized.endsWith("url") || normalized.endsWith("icon"); + } + + private PortalSearchDoc initBase() + { + PortalSearchDoc doc = new PortalSearchDoc(); + doc.setIsDelete("0"); + return doc; + } + + private String joinText(String... parts) + { + return Arrays.stream(parts) + .filter(StringUtils::isNotBlank) + .map(this::normalizeWhitespace) + .collect(Collectors.joining(" ")); + } + + private String stripHtml(String text) + { + if (StringUtils.isBlank(text)) + { + return StringUtils.EMPTY; + } + String noTags = text.replaceAll("<[^>]+>", " "); + return normalizeWhitespace(noTags); + } + + private String normalizeWhitespace(String value) + { + if (value == null) + { + return StringUtils.EMPTY; + } + return value.replaceAll("\\s+", " ").trim(); + } + + private String toString(Object value) + { + return value == null ? null : String.valueOf(value); + } + + private String toJson(Map routeQuery) + { + try + { + return objectMapper.writeValueAsString(routeQuery); + } + catch (Exception e) + { + return "{}"; + } + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/search/es/entity/PortalSearchDoc.java` + +```java +package com.ruoyi.portal.search.es.entity; + +import org.dromara.easyes.annotation.HighLight; +import org.dromara.easyes.annotation.IndexField; +import org.dromara.easyes.annotation.IndexId; +import org.dromara.easyes.annotation.IndexName; +import org.dromara.easyes.annotation.rely.Analyzer; +import org.dromara.easyes.annotation.rely.FieldType; +import org.dromara.easyes.annotation.rely.IdType; + +import java.util.Date; + +/** + * 门户搜索文档 + * + * @author ruoyi + */ +@IndexName("hw_portal_content") +public class PortalSearchDoc +{ + @IndexId(type = IdType.NONE) + private String id; + + @IndexField(fieldType = FieldType.KEYWORD) + private String docId; + + @IndexField(fieldType = FieldType.KEYWORD) + private String sourceType; + + @HighLight(preTag = "", postTag = "") + @IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_MAX_WORD, searchAnalyzer = Analyzer.IK_SMART) + private String title; + + @HighLight(preTag = "", postTag = "") + @IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_MAX_WORD, searchAnalyzer = Analyzer.IK_SMART) + private String content; + + @IndexField(fieldType = FieldType.KEYWORD) + private String webCode; + + @IndexField(fieldType = FieldType.KEYWORD) + private String typeId; + + @IndexField(fieldType = FieldType.KEYWORD) + private String deviceId; + + @IndexField(fieldType = FieldType.KEYWORD) + private String menuId; + + @IndexField(fieldType = FieldType.KEYWORD) + private String documentId; + + @IndexField(fieldType = FieldType.KEYWORD) + private String route; + + private String routeQueryJson; + + @IndexField(fieldType = FieldType.KEYWORD) + private String isDelete; + + private Date updatedAt; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getDocId() + { + return docId; + } + + public void setDocId(String docId) + { + this.docId = docId; + } + + public String getSourceType() + { + return sourceType; + } + + public void setSourceType(String sourceType) + { + this.sourceType = sourceType; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public String getContent() + { + return content; + } + + public void setContent(String content) + { + this.content = content; + } + + public String getWebCode() + { + return webCode; + } + + public void setWebCode(String webCode) + { + this.webCode = webCode; + } + + public String getTypeId() + { + return typeId; + } + + public void setTypeId(String typeId) + { + this.typeId = typeId; + } + + public String getDeviceId() + { + return deviceId; + } + + public void setDeviceId(String deviceId) + { + this.deviceId = deviceId; + } + + public String getMenuId() + { + return menuId; + } + + public void setMenuId(String menuId) + { + this.menuId = menuId; + } + + public String getDocumentId() + { + return documentId; + } + + public void setDocumentId(String documentId) + { + this.documentId = documentId; + } + + public String getRoute() + { + return route; + } + + public void setRoute(String route) + { + this.route = route; + } + + public String getRouteQueryJson() + { + return routeQueryJson; + } + + public void setRouteQueryJson(String routeQueryJson) + { + this.routeQueryJson = routeQueryJson; + } + + public String getIsDelete() + { + return isDelete; + } + + public void setIsDelete(String isDelete) + { + this.isDelete = isDelete; + } + + public Date getUpdatedAt() + { + return updatedAt; + } + + public void setUpdatedAt(Date updatedAt) + { + this.updatedAt = updatedAt; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/search/es/mapper/PortalSearchMapper.java` + +```java +package com.ruoyi.portal.search.es.mapper; + +import com.ruoyi.portal.search.es.entity.PortalSearchDoc; +import org.dromara.easyes.core.kernel.BaseEsMapper; + +/** + * 门户搜索 ES Mapper + * + * @author ruoyi + */ +public interface PortalSearchMapper extends BaseEsMapper +{ +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/search/service/impl/PortalSearchEsServiceImpl.java` + +```java +package com.ruoyi.portal.search.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.dto.SearchPageDTO; +import com.ruoyi.portal.domain.dto.SearchResultDTO; +import com.ruoyi.portal.search.convert.PortalSearchDocConverter; +import com.ruoyi.portal.search.es.entity.PortalSearchDoc; +import com.ruoyi.portal.search.es.mapper.PortalSearchMapper; +import com.ruoyi.portal.search.service.PortalSearchEsService; +import org.dromara.easyes.core.biz.EsPageInfo; +import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * ES 搜索服务实现 + * + * @author ruoyi + */ +@Service +public class PortalSearchEsServiceImpl implements PortalSearchEsService +{ + private static final Logger log = LoggerFactory.getLogger(PortalSearchEsServiceImpl.class); + + private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\.*+\\[\\](){}^$?|])"); + + private final PortalSearchMapper portalSearchMapper; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public PortalSearchEsServiceImpl(PortalSearchMapper portalSearchMapper) + { + this.portalSearchMapper = portalSearchMapper; + } + + @Override + public SearchPageDTO search(String keyword, Integer pageNum, Integer pageSize, boolean editMode) + { + LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); + wrapper.and(q -> q.match(PortalSearchDoc::getTitle, keyword, 5.0f).or().match(PortalSearchDoc::getContent, keyword, 2.0f)); + wrapper.eq(PortalSearchDoc::getIsDelete, "0"); + + EsPageInfo pageInfo = portalSearchMapper.pageQuery(wrapper, pageNum, pageSize); + + List rows = pageInfo.getList().stream().map(doc -> toResult(doc, keyword, editMode)).collect(Collectors.toList()); + SearchPageDTO page = new SearchPageDTO(); + page.setRows(rows); + page.setTotal(pageInfo.getTotal()); + return page; + } + + private SearchResultDTO toResult(PortalSearchDoc doc, String keyword, boolean editMode) + { + SearchResultDTO result = new SearchResultDTO(); + result.setSourceType(doc.getSourceType()); + result.setTitle(doc.getTitle()); + result.setSnippet(buildSnippet(doc.getTitle(), doc.getContent(), keyword)); + result.setScore(0); + result.setRoute(doc.getRoute()); + result.setRouteQuery(parseRouteQuery(doc.getRouteQueryJson())); + if (editMode) + { + result.setEditRoute(buildEditRoute(doc.getSourceType(), doc.getWebCode(), doc.getTypeId(), doc.getDeviceId(), doc.getMenuId(), doc.getDocumentId())); + } + return result; + } + + private Map parseRouteQuery(String json) + { + if (StringUtils.isBlank(json)) + { + return new HashMap<>(); + } + try + { + return objectMapper.readValue(json, new TypeReference>() + { + }); + } + catch (Exception e) + { + log.warn("parse routeQueryJson failed: {}", json); + return new HashMap<>(); + } + } + + private String buildSnippet(String title, String content, String keyword) + { + if (StringUtils.isNotBlank(title) && StringUtils.containsIgnoreCase(title, keyword)) + { + return highlight(title, keyword); + } + String normalized = StringUtils.defaultString(content); + if (StringUtils.isBlank(normalized)) + { + return StringUtils.EMPTY; + } + int idx = StringUtils.indexOfIgnoreCase(normalized, keyword); + if (idx < 0) + { + return StringUtils.substring(normalized, 0, Math.min(120, normalized.length())); + } + int start = Math.max(0, idx - 60); + int end = Math.min(normalized.length(), idx + keyword.length() + 60); + String snippet = normalized.substring(start, end); + if (start > 0) + { + snippet = "..." + snippet; + } + if (end < normalized.length()) + { + snippet = snippet + "..."; + } + return highlight(snippet, keyword); + } + + private String highlight(String text, String keyword) + { + if (StringUtils.isBlank(text) || StringUtils.isBlank(keyword)) + { + return StringUtils.defaultString(text); + } + String escaped = ESCAPE_PATTERN.matcher(keyword).replaceAll("\\\\$1"); + Matcher matcher = Pattern.compile("(?i)" + escaped).matcher(text); + if (!matcher.find()) + { + return text; + } + return matcher.replaceAll("$0"); + } + + private String buildEditRoute(String sourceType, String webCode, String typeId, String deviceId, String menuId, String documentId) + { + if (PortalSearchDocConverter.SOURCE_MENU.equals(sourceType)) + { + return "/editor?type=1&id=" + menuId; + } + if (PortalSearchDocConverter.SOURCE_WEB.equals(sourceType)) + { + if ("7".equals(webCode)) + { + return "/productCenter/edit"; + } + if ("-1".equals(webCode)) + { + return "/editor?type=3&id=-1"; + } + return "/editor?type=1&id=" + webCode; + } + if (PortalSearchDocConverter.SOURCE_WEB1.equals(sourceType)) + { + return "/editor?type=2&id=" + webCode + "," + typeId + "," + deviceId; + } + if (PortalSearchDocConverter.SOURCE_DOCUMENT.equals(sourceType)) + { + if (StringUtils.isNotBlank(webCode) && StringUtils.isNotBlank(typeId) && StringUtils.isNotBlank(deviceId)) + { + return "/editor?type=2&id=" + webCode + "," + typeId + "," + deviceId + "&documentId=" + documentId; + } + return "/editor?type=2&documentId=" + documentId; + } + if (PortalSearchDocConverter.SOURCE_CONFIG_TYPE.equals(sourceType)) + { + return "/productCenter/edit"; + } + return "/editor"; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/search/service/PortalSearchEsService.java` + +```java +package com.ruoyi.portal.search.service; + +import com.ruoyi.portal.domain.dto.SearchPageDTO; + +/** + * ES 搜索服务 + * + * @author ruoyi + */ +public interface PortalSearchEsService +{ + /** + * ES 查询 + * + * @param keyword 关键词 + * @param pageNum 页码 + * @param pageSize 每页条数 + * @param editMode 是否编辑端模式 + * @return 搜索结果 + */ + SearchPageDTO search(String keyword, Integer pageNum, Integer pageSize, boolean editMode); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoDetailService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwAboutUsInfoDetail; + +import java.util.List; + +/** + * 关于我们信息明细Service接口 + * + * @author ruoyi + * @date 2024-12-01 + */ +public interface IHwAboutUsInfoDetailService +{ + /** + * 查询关于我们信息明细 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 关于我们信息明细 + */ + public HwAboutUsInfoDetail selectHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId); + + /** + * 查询关于我们信息明细列表 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 关于我们信息明细集合 + */ + public List selectHwAboutUsInfoDetailList(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 新增关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + public int insertHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 修改关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + public int updateHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail); + + /** + * 批量删除关于我们信息明细 + * + * @param usInfoDetailIds 需要删除的关于我们信息明细主键集合 + * @return 结果 + */ + public int deleteHwAboutUsInfoDetailByUsInfoDetailIds(Long[] usInfoDetailIds); + + /** + * 删除关于我们信息明细信息 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 结果 + */ + public int deleteHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwAboutUsInfoService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwAboutUsInfo; + +import java.util.List; + +/** + * 关于我们信息Service接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface IHwAboutUsInfoService +{ + /** + * 查询关于我们信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 关于我们信息 + */ + public HwAboutUsInfo selectHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId); + + /** + * 查询关于我们信息列表 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 关于我们信息集合 + */ + public List selectHwAboutUsInfoList(HwAboutUsInfo hwAboutUsInfo); + + /** + * 新增关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + public int insertHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo); + + /** + * 修改关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + public int updateHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo); + + /** + * 批量删除关于我们信息 + * + * @param aboutUsInfoIds 需要删除的关于我们信息主键集合 + * @return 结果 + */ + public int deleteHwAboutUsInfoByAboutUsInfoIds(Long[] aboutUsInfoIds); + + /** + * 删除关于我们信息信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 结果 + */ + public int deleteHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwAnalyticsService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.dto.AnalyticsCollectRequest; +import com.ruoyi.portal.domain.dto.AnalyticsDashboardDTO; + +import java.time.LocalDate; + +/** + * 官网访问监控服务 + * + * @author ruoyi + */ +public interface IHwAnalyticsService +{ + /** + * 采集匿名访问事件 + * + * @param request 采集请求 + * @param requestIp 请求 IP + * @param requestUserAgent 请求 UA + */ + void collect(AnalyticsCollectRequest request, String requestIp, String requestUserAgent); + + /** + * 查询日看板 + * + * @param statDate 统计日期 + * @param rankLimit 排行数量 + * @return 看板数据 + */ + AnalyticsDashboardDTO getDashboard(LocalDate statDate, Integer rankLimit); + + /** + * 刷新日汇总 + * + * @param statDate 日期 + */ + void refreshDailyStat(LocalDate statDate); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwContactUsInfoService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwContactUsInfo; + +import java.util.List; + +/** + * 联系我们信息Service接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface IHwContactUsInfoService +{ + /** + * 查询联系我们信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 联系我们信息 + */ + public HwContactUsInfo selectHwContactUsInfoByContactUsInfoId(Long contactUsInfoId); + + /** + * 查询联系我们信息列表 + * + * @param hwContactUsInfo 联系我们信息 + * @return 联系我们信息集合 + */ + public List selectHwContactUsInfoList(HwContactUsInfo hwContactUsInfo); + + /** + * 新增联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + public int insertHwContactUsInfo(HwContactUsInfo hwContactUsInfo); + + /** + * 修改联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + public int updateHwContactUsInfo(HwContactUsInfo hwContactUsInfo); + + /** + * 批量删除联系我们信息 + * + * @param contactUsInfoIds 需要删除的联系我们信息主键集合 + * @return 结果 + */ + public int deleteHwContactUsInfoByContactUsInfoIds(Long[] contactUsInfoIds); + + /** + * 删除联系我们信息信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 结果 + */ + public int deleteHwContactUsInfoByContactUsInfoId(Long contactUsInfoId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwPortalConfigService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwPortalConfig; + +import java.util.List; + +/** + * 门户网站配置Service接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface IHwPortalConfigService +{ + /** + * 查询门户网站配置 + * + * @param portalConfigId 门户网站配置主键 + * @return 门户网站配置 + */ + public HwPortalConfig selectHwPortalConfigByPortalConfigId(Long portalConfigId); + + /** + * 查询门户网站配置列表 + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置集合 + */ + public List selectHwPortalConfigList(HwPortalConfig hwPortalConfig); + + /** + * 新增门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + public int insertHwPortalConfig(HwPortalConfig hwPortalConfig); + + /** + * 修改门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + public int updateHwPortalConfig(HwPortalConfig hwPortalConfig); + + /** + * 批量删除门户网站配置 + * + * @param portalConfigIds 需要删除的门户网站配置主键集合 + * @return 结果 + */ + public int deleteHwPortalConfigByPortalConfigIds(Long[] portalConfigIds); + + /** + * 删除门户网站配置信息 + * + * @param portalConfigId 门户网站配置主键 + * @return 结果 + */ + public int deleteHwPortalConfigByPortalConfigId(Long portalConfigId); + + /** + * 查询门户网站配置列表,Join hw_portal_config_type + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置 + */ + public List selectHwPortalConfigJoinList(HwPortalConfig hwPortalConfig); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwPortalConfigTypeService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.vo.TreeSelect; + +import java.util.List; + +/** + * 门户网站配置类型Service接口 + * + * @author xins + * @date 2024-12-11 + */ +public interface IHwPortalConfigTypeService +{ + /** + * 查询门户网站配置类型 + * + * @param configTypeId 门户网站配置类型主键 + * @return 门户网站配置类型 + */ + public HwPortalConfigType selectHwPortalConfigTypeByConfigTypeId(Long configTypeId); + + /** + * 查询门户网站配置类型列表 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 门户网站配置类型集合 + */ + public List selectHwPortalConfigTypeList(HwPortalConfigType hwPortalConfigType); + + + /** + * 查询门户网站配置类型列表 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 门户网站配置类型集合 + */ + public List selectConfigTypeList(HwPortalConfigType hwPortalConfigType); + + /** + * 新增门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + public int insertHwPortalConfigType(HwPortalConfigType hwPortalConfigType); + + /** + * 修改门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + public int updateHwPortalConfigType(HwPortalConfigType hwPortalConfigType); + + /** + * 批量删除门户网站配置类型 + * + * @param configTypeIds 需要删除的门户网站配置类型主键集合 + * @return 结果 + */ + public int deleteHwPortalConfigTypeByConfigTypeIds(Long[] configTypeIds); + + /** + * 删除门户网站配置类型信息 + * + * @param configTypeId 门户网站配置类型主键 + * @return 结果 + */ + public int deleteHwPortalConfigTypeByConfigTypeId(Long configTypeId); + + /** + * 查询门户网站配置类型树结构信息 + * + * @param portalConfigType 门户网站配置类型信息 + * @return 门户网站配置类型树信息集合 + */ + public List selectPortalConfigTypeTreeList(HwPortalConfigType portalConfigType); + + /** + * 构建前端所需要下拉树结构 + * + * @param portalConfigTypes 门户网站配置类型列表 + * @return 下拉树结构列表 + */ + public List buildPortalConfigTypeTreeSelect(List portalConfigTypes); + /** + * 构建前端所需要树结构 + * + * @param portalConfigTypes 门户网站配置类型列表 + * @return 树结构列表 + */ + public List buildPortalConfigTypeTree(List portalConfigTypes); + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwProductCaseInfoService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwProductCaseInfo; + +import java.util.List; + +/** + * 案例内容Service接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface IHwProductCaseInfoService +{ + /** + * 查询案例内容 + * + * @param caseInfoId 案例内容主键 + * @return 案例内容 + */ + public HwProductCaseInfo selectHwProductCaseInfoByCaseInfoId(Long caseInfoId); + + /** + * 查询案例内容列表 + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容集合 + */ + public List selectHwProductCaseInfoList(HwProductCaseInfo hwProductCaseInfo); + + /** + * 新增案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + public int insertHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo); + + /** + * 修改案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + public int updateHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo); + + /** + * 批量删除案例内容 + * + * @param caseInfoIds 需要删除的案例内容主键集合 + * @return 结果 + */ + public int deleteHwProductCaseInfoByCaseInfoIds(Long[] caseInfoIds); + + /** + * 删除案例内容信息 + * + * @param caseInfoId 案例内容主键 + * @return 结果 + */ + public int deleteHwProductCaseInfoByCaseInfoId(Long caseInfoId); + + /** + * 根据configTypeId获取首页典型案例 + * @param hwProductCaseInfo + * @return + */ + public HwProductCaseInfo getTypicalHomeCaseInfo(HwProductCaseInfo hwProductCaseInfo); + + /** + * 查询案例内容列表,join portalConfigType + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容 + */ + public List selectHwProductCaseInfoJoinList(HwProductCaseInfo hwProductCaseInfo); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwProductInfoDetailService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwProductInfoDetail; + +import java.util.List; + +/** + * 产品信息明细配置Service接口 + * + * @author xins + * @date 2024-12-11 + */ +public interface IHwProductInfoDetailService +{ + /** + * 查询产品信息明细配置 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 产品信息明细配置 + */ + public HwProductInfoDetail selectHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId); + + /** + * 查询产品信息明细配置列表 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 产品信息明细配置集合 + */ + public List selectHwProductInfoDetailList(HwProductInfoDetail hwProductInfoDetail); + + /** + * 新增产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + public int insertHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail); + + /** + * 修改产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + public int updateHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail); + + /** + * 批量删除产品信息明细配置 + * + * @param productInfoDetailIds 需要删除的产品信息明细配置主键集合 + * @return 结果 + */ + public int deleteHwProductInfoDetailByProductInfoDetailIds(Long[] productInfoDetailIds); + + /** + * 删除产品信息明细配置信息 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 结果 + */ + public int deleteHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwProductInfoService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwProductInfo; + +import java.util.List; + +/** + * 产品信息配置Service接口 + * + * @author xins + * @date 2024-12-01 + */ +public interface IHwProductInfoService +{ + /** + * 查询产品信息配置 + * + * @param productInfoId 产品信息配置主键 + * @return 产品信息配置 + */ + public HwProductInfo selectHwProductInfoByProductInfoId(Long productInfoId); + + /** + * 查询产品信息配置列表 + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置集合 + */ + public List selectHwProductInfoList(HwProductInfo hwProductInfo); + + /** + * 新增产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + public int insertHwProductInfo(HwProductInfo hwProductInfo); + + /** + * 修改产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + public int updateHwProductInfo(HwProductInfo hwProductInfo); + + /** + * 批量删除产品信息配置 + * + * @param productInfoIds 需要删除的产品信息配置主键集合 + * @return 结果 + */ + public int deleteHwProductInfoByProductInfoIds(Long[] productInfoIds); + + /** + * 删除产品信息配置信息 + * + * @param productInfoId 产品信息配置主键 + * @return 结果 + */ + public int deleteHwProductInfoByProductInfoId(Long productInfoId); + + /** + * 获取产品中心产品信息(平台简介,hw_product_info获取,(配置模式2左标题+内容,右图片)读取中文标题和英文标题,下面内容从hw_product_info_detail获取,读取标题,内容和图片) + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置 + */ + public List selectHwProductInfoJoinDetailList(HwProductInfo hwProductInfo); + + /** + * 查询产品信息配置列表,join portalConfigType + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置 + */ + public List selectHwProductInfoJoinList(HwProductInfo hwProductInfo); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwSearchRebuildService.java` + +```java +package com.ruoyi.portal.service; + +/** + * 门户搜索索引重建服务 + * + * @author ruoyi + */ +public interface IHwSearchRebuildService +{ + /** + * 全量重建搜索索引 + */ + void rebuildAll(); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwSearchService.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.dto.SearchPageDTO; + +/** + * 门户搜索服务 + * + * @author ruoyi + */ +public interface IHwSearchService +{ + /** + * 门户搜索 + * + * @param keyword 关键词 + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 结果 + */ + SearchPageDTO search(String keyword, Integer pageNum, Integer pageSize); + + /** + * 编辑端搜索 + * + * @param keyword 关键词 + * @param pageNum 页码 + * @param pageSize 每页条数 + * @return 结果 + */ + SearchPageDTO searchForEdit(String keyword, Integer pageNum, Integer pageSize); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwWebDocumentService.java` + +```java +package com.ruoyi.portal.service; + +import java.util.List; +import com.ruoyi.portal.domain.HwWebDocument; + +/** + * Hw资料文件Service接口 + * + * @author zch + * @date 2025-09-22 + */ +public interface IHwWebDocumentService +{ + /** + * 查询Hw资料文件 + * + * @param documentId Hw资料文件主键 + * @return Hw资料文件 + */ + public HwWebDocument selectHwWebDocumentByDocumentId(String documentId); + + /** + * 查询Hw资料文件列表 + * + * @param hwWebDocument Hw资料文件 + * @return Hw资料文件集合 + */ + public List selectHwWebDocumentList(HwWebDocument hwWebDocument); + + /** + * 新增Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + public int insertHwWebDocument(HwWebDocument hwWebDocument); + + /** + * 修改Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + public int updateHwWebDocument(HwWebDocument hwWebDocument); + + /** + * 批量删除Hw资料文件 + * + * @param documentIds 需要删除的Hw资料文件主键集合 + * @return 结果 + */ + public int deleteHwWebDocumentByDocumentIds(String[] documentIds); + + /** + * 删除Hw资料文件信息 + * + * @param documentId Hw资料文件主键 + * @return 结果 + */ + public int deleteHwWebDocumentByDocumentId(String documentId); + + /** + * 验证密钥并获取文件地址 + * @param documentId 文件ID + * @param providedKey 提供的密钥 + * @return 文件地址 + * @throws Exception 如果密钥不匹配 + */ + String verifyAndGetDocumentAddress(String documentId, String providedKey) throws Exception; + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwWebMenuService.java` + +```java +package com.ruoyi.portal.service; + +import java.util.List; +import com.ruoyi.portal.domain.HwWebMenu; + +/** + * haiwei官网菜单Service接口 + * + * @author zch + * @date 2025-08-18 + */ +public interface IHwWebMenuService +{ + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + public HwWebMenu selectHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 查询haiwei官网菜单列表 + * + * @param hwWebMenu haiwei官网菜单 + * @return haiwei官网菜单集合 + */ + public List selectHwWebMenuList(HwWebMenu hwWebMenu); + + /** + * 新增haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + public int insertHwWebMenu(HwWebMenu hwWebMenu); + + /** + * 修改haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + public int updateHwWebMenu(HwWebMenu hwWebMenu); + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的haiwei官网菜单主键集合 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds); + + /** + * 删除haiwei官网菜单信息 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuId(Long webMenuId); + + + /** + * 获取菜单树列表 + */ + public List selectMenuTree(HwWebMenu hwWebMenu); + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwWebMenuService1.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwWebMenu1; +import com.ruoyi.portal.domain.HwWebMenu1; + +import java.util.List; + +/** + * haiwei官网菜单Service接口 + * + * @author zch + * @date 2025-08-18 + */ +public interface IHwWebMenuService1 +{ + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + public HwWebMenu1 selectHwWebMenuByWebMenuId(Long webMenuId); + + /** + * 查询haiwei官网菜单列表 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return haiwei官网菜单集合 + */ + public List selectHwWebMenuList(HwWebMenu1 HwWebMenu1); + + /** + * 新增haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + public int insertHwWebMenu(HwWebMenu1 HwWebMenu1); + + /** + * 修改haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + public int updateHwWebMenu(HwWebMenu1 HwWebMenu1); + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的haiwei官网菜单主键集合 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds); + + /** + * 删除haiwei官网菜单信息 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + public int deleteHwWebMenuByWebMenuId(Long webMenuId); + + + /** + * 获取菜单树列表 + */ + public List selectMenuTree(HwWebMenu1 HwWebMenu1); + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwWebService.java` + +```java +package com.ruoyi.portal.service; + +import java.util.List; +import com.ruoyi.portal.domain.HwWeb; + +/** + * haiwei官网jsonService接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface IHwWebService +{ + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + public HwWeb selectHwWebByWebcode(Long webCode); + + /** + * 查询haiwei官网json列表 + * + * @param hwWeb haiwei官网json + * @return haiwei官网json集合 + */ + public List selectHwWebList(HwWeb hwWeb); + + /** + * 新增haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + public int insertHwWeb(HwWeb hwWeb); + + /** + * 修改haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + public int updateHwWeb(HwWeb hwWeb); + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的haiwei官网json主键集合 + * @return 结果 + */ + public int deleteHwWebByWebIds(Long[] webIds); + + /** + * 删除haiwei官网json信息 + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + public int deleteHwWebByWebId(Long webId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/IHwWebService1.java` + +```java +package com.ruoyi.portal.service; + +import com.ruoyi.portal.domain.HwWeb1; +import java.util.List; + +/** + * haiwei官网jsonService接口 + * + * @author ruoyi + * @date 2025-08-18 + */ +public interface IHwWebService1 +{ + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + public HwWeb1 selectHwWebByWebcode(Long webCode); + + public HwWeb1 selectHwWebOne(HwWeb1 hwWeb1); + + /** + * 查询haiwei官网json列表 + * + * @param HwWeb1 haiwei官网json + * @return haiwei官网json集合 + */ + public List selectHwWebList(HwWeb1 hwWeb1); + + /** + * 新增haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + public int insertHwWeb(HwWeb1 hwWeb1); + + /** + * 修改haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + public int updateHwWeb(HwWeb1 hwWeb1); + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的haiwei官网json主键集合 + * @return 结果 + */ + public int deleteHwWebByWebIds(Long[] webIds); + + /** + * 删除haiwei官网json信息 + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + public int deleteHwWebByWebId(Long webId); +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwAboutUsInfoDetailServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.portal.domain.HwAboutUsInfoDetail; +import com.ruoyi.portal.mapper.HwAboutUsInfoDetailMapper; +import com.ruoyi.portal.service.IHwAboutUsInfoDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 关于我们信息明细Service业务层处理 + * + * @author ruoyi + * @date 2024-12-01 + */ +@Service +public class HwAboutUsInfoDetailServiceImpl implements IHwAboutUsInfoDetailService +{ + @Autowired + private HwAboutUsInfoDetailMapper hwAboutUsInfoDetailMapper; + + /** + * 查询关于我们信息明细 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 关于我们信息明细 + */ + @Override + public HwAboutUsInfoDetail selectHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId) + { + return hwAboutUsInfoDetailMapper.selectHwAboutUsInfoDetailByUsInfoDetailId(usInfoDetailId); + } + + /** + * 查询关于我们信息明细列表 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 关于我们信息明细 + */ + @Override + public List selectHwAboutUsInfoDetailList(HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + return hwAboutUsInfoDetailMapper.selectHwAboutUsInfoDetailList(hwAboutUsInfoDetail); + } + + /** + * 新增关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + @Override + public int insertHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + hwAboutUsInfoDetail.setCreateTime(DateUtils.getNowDate()); + return hwAboutUsInfoDetailMapper.insertHwAboutUsInfoDetail(hwAboutUsInfoDetail); + } + + /** + * 修改关于我们信息明细 + * + * @param hwAboutUsInfoDetail 关于我们信息明细 + * @return 结果 + */ + @Override + public int updateHwAboutUsInfoDetail(HwAboutUsInfoDetail hwAboutUsInfoDetail) + { + hwAboutUsInfoDetail.setUpdateTime(DateUtils.getNowDate()); + return hwAboutUsInfoDetailMapper.updateHwAboutUsInfoDetail(hwAboutUsInfoDetail); + } + + /** + * 批量删除关于我们信息明细 + * + * @param usInfoDetailIds 需要删除的关于我们信息明细主键 + * @return 结果 + */ + @Override + public int deleteHwAboutUsInfoDetailByUsInfoDetailIds(Long[] usInfoDetailIds) + { + return hwAboutUsInfoDetailMapper.deleteHwAboutUsInfoDetailByUsInfoDetailIds(usInfoDetailIds); + } + + /** + * 删除关于我们信息明细信息 + * + * @param usInfoDetailId 关于我们信息明细主键 + * @return 结果 + */ + @Override + public int deleteHwAboutUsInfoDetailByUsInfoDetailId(Long usInfoDetailId) + { + return hwAboutUsInfoDetailMapper.deleteHwAboutUsInfoDetailByUsInfoDetailId(usInfoDetailId); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwAboutUsInfoServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.portal.domain.HwAboutUsInfo; +import com.ruoyi.portal.mapper.HwAboutUsInfoMapper; +import com.ruoyi.portal.service.IHwAboutUsInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 关于我们信息Service业务层处理 + * + * @author xins + * @date 2024-12-01 + */ +@Service +public class HwAboutUsInfoServiceImpl implements IHwAboutUsInfoService +{ + @Autowired + private HwAboutUsInfoMapper hwAboutUsInfoMapper; + + /** + * 查询关于我们信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 关于我们信息 + */ + @Override + public HwAboutUsInfo selectHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId) + { + return hwAboutUsInfoMapper.selectHwAboutUsInfoByAboutUsInfoId(aboutUsInfoId); + } + + /** + * 查询关于我们信息列表 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 关于我们信息 + */ + @Override + public List selectHwAboutUsInfoList(HwAboutUsInfo hwAboutUsInfo) + { + return hwAboutUsInfoMapper.selectHwAboutUsInfoList(hwAboutUsInfo); + } + + /** + * 新增关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + @Override + public int insertHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo) + { + hwAboutUsInfo.setCreateTime(DateUtils.getNowDate()); + return hwAboutUsInfoMapper.insertHwAboutUsInfo(hwAboutUsInfo); + } + + /** + * 修改关于我们信息 + * + * @param hwAboutUsInfo 关于我们信息 + * @return 结果 + */ + @Override + public int updateHwAboutUsInfo(HwAboutUsInfo hwAboutUsInfo) + { + hwAboutUsInfo.setUpdateTime(DateUtils.getNowDate()); + return hwAboutUsInfoMapper.updateHwAboutUsInfo(hwAboutUsInfo); + } + + /** + * 批量删除关于我们信息 + * + * @param aboutUsInfoIds 需要删除的关于我们信息主键 + * @return 结果 + */ + @Override + public int deleteHwAboutUsInfoByAboutUsInfoIds(Long[] aboutUsInfoIds) + { + return hwAboutUsInfoMapper.deleteHwAboutUsInfoByAboutUsInfoIds(aboutUsInfoIds); + } + + /** + * 删除关于我们信息信息 + * + * @param aboutUsInfoId 关于我们信息主键 + * @return 结果 + */ + @Override + public int deleteHwAboutUsInfoByAboutUsInfoId(Long aboutUsInfoId) + { + return hwAboutUsInfoMapper.deleteHwAboutUsInfoByAboutUsInfoId(aboutUsInfoId); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwAnalyticsServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.http.UserAgentUtils; +import com.ruoyi.portal.domain.HwWebVisitDaily; +import com.ruoyi.portal.domain.HwWebVisitEvent; +import com.ruoyi.portal.domain.dto.AnalyticsCollectRequest; +import com.ruoyi.portal.domain.dto.AnalyticsDashboardDTO; +import com.ruoyi.portal.domain.dto.AnalyticsRankItemDTO; +import com.ruoyi.portal.mapper.HwAnalyticsMapper; +import com.ruoyi.portal.service.IHwAnalyticsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 官网访问监控服务实现 + * + * @author ruoyi + */ +@Service +public class HwAnalyticsServiceImpl implements IHwAnalyticsService +{ + private static final Logger log = LoggerFactory.getLogger(HwAnalyticsServiceImpl.class); + + private static final Set ALLOWED_EVENTS = new HashSet<>(Arrays.asList( + "page_view", "page_leave", "search_submit", "download_click", "contact_submit" + )); + + private final HwAnalyticsMapper hwAnalyticsMapper; + + public HwAnalyticsServiceImpl(HwAnalyticsMapper hwAnalyticsMapper) + { + this.hwAnalyticsMapper = hwAnalyticsMapper; + } + + @Override + public void collect(AnalyticsCollectRequest request, String requestIp, String requestUserAgent) + { + if (request == null) + { + throw new ServiceException("请求体不能为空"); + } + String eventType = normalizeText(request.getEventType(), 32); + if (!ALLOWED_EVENTS.contains(eventType)) + { + throw new ServiceException("不支持的事件类型"); + } + + HwWebVisitEvent event = new HwWebVisitEvent(); + event.setEventType(eventType); + event.setVisitorId(requireText(request.getVisitorId(), 64, "visitorId不能为空")); + event.setSessionId(requireText(request.getSessionId(), 64, "sessionId不能为空")); + event.setPath(requireText(request.getPath(), 500, "path不能为空")); + event.setReferrer(normalizeText(request.getReferrer(), 500)); + event.setUtmSource(normalizeText(request.getUtmSource(), 128)); + event.setUtmMedium(normalizeText(request.getUtmMedium(), 128)); + event.setUtmCampaign(normalizeText(request.getUtmCampaign(), 128)); + event.setKeyword(normalizeText(request.getKeyword(), 128)); + event.setStayMs(normalizeStayMs(request.getStayMs())); + event.setEventTime(resolveEventTime(request.getEventTime())); + + String ua = normalizeText(StringUtils.defaultIfBlank(request.getUa(), requestUserAgent), 500); + event.setUa(ua); + event.setBrowser(normalizeText(StringUtils.defaultIfBlank(request.getBrowser(), UserAgentUtils.getBrowser(ua)), 64)); + event.setOs(normalizeText(StringUtils.defaultIfBlank(request.getOs(), UserAgentUtils.getOperatingSystem(ua)), 64)); + event.setDevice(normalizeText(StringUtils.defaultIfBlank(request.getDevice(), detectDevice(ua)), 64)); + + String ipHash = buildIpHash(requestIp); + event.setIpHash(ipHash); + hwAnalyticsMapper.insertVisitEvent(event); + + LocalDate statDate = event.getEventTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + refreshDailyStat(statDate); + } + + @Override + public AnalyticsDashboardDTO getDashboard(LocalDate statDate, Integer rankLimit) + { + LocalDate targetDate = statDate == null ? LocalDate.now() : statDate; + int topN = normalizeRankLimit(rankLimit); + refreshDailyStat(targetDate); + + HwWebVisitDaily daily = hwAnalyticsMapper.selectDailyByDate(targetDate); + AnalyticsDashboardDTO dashboard = new AnalyticsDashboardDTO(); + dashboard.setStatDate(targetDate.toString()); + dashboard.setPv(daily == null ? 0L : safeLong(daily.getPv())); + dashboard.setUv(daily == null ? 0L : safeLong(daily.getUv())); + dashboard.setIpUv(daily == null ? 0L : safeLong(daily.getIpUv())); + dashboard.setAvgStayMs(daily == null ? 0L : safeLong(daily.getAvgStayMs())); + dashboard.setBounceRate(daily == null ? 0D : safeDouble(daily.getBounceRate())); + dashboard.setSearchCount(daily == null ? 0L : safeLong(daily.getSearchCount())); + dashboard.setDownloadCount(daily == null ? 0L : safeLong(daily.getDownloadCount())); + + List entryPages = hwAnalyticsMapper.selectTopEntryPages(targetDate, topN); + List hotPages = hwAnalyticsMapper.selectTopHotPages(targetDate, topN); + List hotKeywords = hwAnalyticsMapper.selectTopKeywords(targetDate, topN); + dashboard.setEntryPages(entryPages); + dashboard.setHotPages(hotPages); + dashboard.setHotKeywords(hotKeywords); + return dashboard; + } + + @Override + public void refreshDailyStat(LocalDate statDate) + { + LocalDate targetDate = statDate == null ? LocalDate.now() : statDate; + Long pv = safeLong(hwAnalyticsMapper.countEventByType(targetDate, "page_view")); + Long uv = safeLong(hwAnalyticsMapper.countDistinctVisitor(targetDate)); + Long ipUv = safeLong(hwAnalyticsMapper.countDistinctIp(targetDate)); + Long avgStay = safeLong(hwAnalyticsMapper.avgStayMs(targetDate)); + Long searchCount = safeLong(hwAnalyticsMapper.countEventByType(targetDate, "search_submit")); + Long downloadCount = safeLong(hwAnalyticsMapper.countEventByType(targetDate, "download_click")); + Long sessionCount = safeLong(hwAnalyticsMapper.countDistinctSessions(targetDate)); + Long singlePageSessionCount = safeLong(hwAnalyticsMapper.countSinglePageSessions(targetDate)); + + double bounceRate = 0D; + if (sessionCount > 0) + { + bounceRate = (singlePageSessionCount * 100.0D) / sessionCount; + bounceRate = Math.round(bounceRate * 100) / 100.0D; + } + + HwWebVisitDaily daily = new HwWebVisitDaily(); + daily.setStatDate(java.sql.Date.valueOf(targetDate)); + daily.setPv(pv); + daily.setUv(uv); + daily.setIpUv(ipUv); + daily.setAvgStayMs(avgStay); + daily.setBounceRate(bounceRate); + daily.setSearchCount(searchCount); + daily.setDownloadCount(downloadCount); + hwAnalyticsMapper.upsertDaily(daily); + } + + private String requireText(String text, int maxLen, String emptyMessage) + { + String normalized = normalizeText(text, maxLen); + if (StringUtils.isBlank(normalized)) + { + throw new ServiceException(emptyMessage); + } + return normalized; + } + + private String normalizeText(String text, int maxLen) + { + String normalized = StringUtils.trim(text); + if (StringUtils.isBlank(normalized)) + { + return null; + } + if (normalized.length() > maxLen) + { + normalized = normalized.substring(0, maxLen); + } + return normalized; + } + + private Long normalizeStayMs(Long stayMs) + { + if (stayMs == null) + { + return null; + } + if (stayMs < 0) + { + return 0L; + } + return Math.min(stayMs, 7L * 24 * 3600 * 1000); + } + + private Date resolveEventTime(Long timestamp) + { + if (timestamp == null || timestamp <= 0) + { + return new Date(); + } + try + { + return Date.from(Instant.ofEpochMilli(timestamp)); + } + catch (Exception e) + { + log.warn("invalid eventTime: {}", timestamp); + return new Date(); + } + } + + private String detectDevice(String ua) + { + String text = StringUtils.defaultString(ua).toLowerCase(); + if (text.contains("ipad") || text.contains("tablet")) + { + return "Tablet"; + } + if (text.contains("mobile") || text.contains("android") || text.contains("iphone")) + { + return "Mobile"; + } + return "Desktop"; + } + + private String buildIpHash(String ip) + { + String safeIp = StringUtils.defaultIfBlank(ip, "unknown"); + try + { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest((safeIp + "|hw-portal-analytics").getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : hash) + { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + catch (Exception e) + { + return Integer.toHexString(safeIp.hashCode()); + } + } + + private int normalizeRankLimit(Integer rankLimit) + { + if (rankLimit == null || rankLimit <= 0) + { + return 10; + } + return Math.min(rankLimit, 50); + } + + private long safeLong(Long value) + { + return value == null ? 0L : value; + } + + private double safeDouble(Double value) + { + return value == null ? 0D : value; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwContactUsInfoServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.portal.domain.HwContactUsInfo; +import com.ruoyi.portal.mapper.HwContactUsInfoMapper; +import com.ruoyi.portal.service.IHwContactUsInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 联系我们信息Service业务层处理 + * + * @author xins + * @date 2024-12-01 + */ +@Service +public class HwContactUsInfoServiceImpl implements IHwContactUsInfoService +{ + @Autowired + private HwContactUsInfoMapper hwContactUsInfoMapper; + + /** + * 查询联系我们信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 联系我们信息 + */ + @Override + public HwContactUsInfo selectHwContactUsInfoByContactUsInfoId(Long contactUsInfoId) + { + return hwContactUsInfoMapper.selectHwContactUsInfoByContactUsInfoId(contactUsInfoId); + } + + /** + * 查询联系我们信息列表 + * + * @param hwContactUsInfo 联系我们信息 + * @return 联系我们信息 + */ + @Override + public List selectHwContactUsInfoList(HwContactUsInfo hwContactUsInfo) + { + return hwContactUsInfoMapper.selectHwContactUsInfoList(hwContactUsInfo); + } + + /** + * 新增联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + @Override + public int insertHwContactUsInfo(HwContactUsInfo hwContactUsInfo) + { + + hwContactUsInfo.setCreateTime(DateUtils.getNowDate()); + return hwContactUsInfoMapper.insertHwContactUsInfo(hwContactUsInfo); + } + + /** + * 修改联系我们信息 + * + * @param hwContactUsInfo 联系我们信息 + * @return 结果 + */ + @Override + public int updateHwContactUsInfo(HwContactUsInfo hwContactUsInfo) + { + hwContactUsInfo.setUpdateTime(DateUtils.getNowDate()); + return hwContactUsInfoMapper.updateHwContactUsInfo(hwContactUsInfo); + } + + /** + * 批量删除联系我们信息 + * + * @param contactUsInfoIds 需要删除的联系我们信息主键 + * @return 结果 + */ + @Override + public int deleteHwContactUsInfoByContactUsInfoIds(Long[] contactUsInfoIds) + { + return hwContactUsInfoMapper.deleteHwContactUsInfoByContactUsInfoIds(contactUsInfoIds); + } + + /** + * 删除联系我们信息信息 + * + * @param contactUsInfoId 联系我们信息主键 + * @return 结果 + */ + @Override + public int deleteHwContactUsInfoByContactUsInfoId(Long contactUsInfoId) + { + return hwContactUsInfoMapper.deleteHwContactUsInfoByContactUsInfoId(contactUsInfoId); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwPortalConfigServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.portal.domain.HwPortalConfig; +import com.ruoyi.portal.mapper.HwPortalConfigMapper; +import com.ruoyi.portal.service.IHwPortalConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 门户网站配置Service业务层处理 + * + * @author xins + * @date 2024-12-01 + */ +@Service +public class HwPortalConfigServiceImpl implements IHwPortalConfigService +{ + @Autowired + private HwPortalConfigMapper hwPortalConfigMapper; + + /** + * 查询门户网站配置 + * + * @param portalConfigId 门户网站配置主键 + * @return 门户网站配置 + */ + @Override + public HwPortalConfig selectHwPortalConfigByPortalConfigId(Long portalConfigId) + { + return hwPortalConfigMapper.selectHwPortalConfigByPortalConfigId(portalConfigId); + } + + /** + * 查询门户网站配置列表 + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置 + */ + @Override + public List selectHwPortalConfigList(HwPortalConfig hwPortalConfig) + { + if("2".equals(hwPortalConfig.getPortalConfigType())){ + List hwPortalConfigs = hwPortalConfigMapper.selectHwPortalConfigList2(hwPortalConfig); + return hwPortalConfigs; + } + return hwPortalConfigMapper.selectHwPortalConfigList(hwPortalConfig); + } + + /** + * 新增门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + @Override + public int insertHwPortalConfig(HwPortalConfig hwPortalConfig) + { + hwPortalConfig.setCreateTime(DateUtils.getNowDate()); + return hwPortalConfigMapper.insertHwPortalConfig(hwPortalConfig); + } + + /** + * 修改门户网站配置 + * + * @param hwPortalConfig 门户网站配置 + * @return 结果 + */ + @Override + public int updateHwPortalConfig(HwPortalConfig hwPortalConfig) + { + hwPortalConfig.setUpdateTime(DateUtils.getNowDate()); + return hwPortalConfigMapper.updateHwPortalConfig(hwPortalConfig); + } + + /** + * 批量删除门户网站配置 + * + * @param portalConfigIds 需要删除的门户网站配置主键 + * @return 结果 + */ + @Override + public int deleteHwPortalConfigByPortalConfigIds(Long[] portalConfigIds) + { + return hwPortalConfigMapper.deleteHwPortalConfigByPortalConfigIds(portalConfigIds); + } + + /** + * 删除门户网站配置信息 + * + * @param portalConfigId 门户网站配置主键 + * @return 结果 + */ + @Override + public int deleteHwPortalConfigByPortalConfigId(Long portalConfigId) + { + return hwPortalConfigMapper.deleteHwPortalConfigByPortalConfigId(portalConfigId); + } + + + + /** + * 查询门户网站配置列表,Join hw_portal_config_type + * + * @param hwPortalConfig 门户网站配置 + * @return 门户网站配置 + */ + @Override + public List selectHwPortalConfigJoinList(HwPortalConfig hwPortalConfig) + { + return hwPortalConfigMapper.selectHwPortalConfigJoinList(hwPortalConfig); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwPortalConfigTypeServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.vo.TreeSelect; +import com.ruoyi.portal.mapper.HwPortalConfigTypeMapper; +import com.ruoyi.portal.service.IHwPortalConfigTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 门户网站配置类型Service业务层处理 + * + * @author xins + * @date 2024-12-11 + */ +@Service +public class HwPortalConfigTypeServiceImpl implements IHwPortalConfigTypeService +{ + @Autowired + private HwPortalConfigTypeMapper hwPortalConfigTypeMapper; + + /** + * 查询门户网站配置类型 + * + * @param configTypeId 门户网站配置类型主键 + * @return 门户网站配置类型 + */ + @Override + public HwPortalConfigType selectHwPortalConfigTypeByConfigTypeId(Long configTypeId) + { + return hwPortalConfigTypeMapper.selectHwPortalConfigTypeByConfigTypeId(configTypeId); + } + + /** + * 查询门户网站配置类型列表 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 门户网站配置类型 + */ + @Override + public List selectHwPortalConfigTypeList(HwPortalConfigType hwPortalConfigType) + { + return hwPortalConfigTypeMapper.selectHwPortalConfigTypeList(hwPortalConfigType); + } + + + /** + * 查询门户网站配置类型列表 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 门户网站配置类型 + */ + @Override + public List selectConfigTypeList(HwPortalConfigType hwPortalConfigType) + { + // 如果有 configTypeClassfication 条件,需要特殊处理以确保树形结构完整 + if (StringUtils.isNotEmpty(hwPortalConfigType.getConfigTypeClassfication())) { + // 查询所有数据 + List allList = hwPortalConfigTypeMapper.selectHwPortalConfigTypeList(new HwPortalConfigType()); + + // 找出指定分类的顶级节点 + List topLevelNodes = allList.stream() + .filter(item -> hwPortalConfigType.getConfigTypeClassfication().equals(item.getConfigTypeClassfication()) + && (item.getParentId() == null || item.getParentId() == 0L)) + .collect(Collectors.toList()); + + // 构建包含所有子孙节点的完整列表 + List completeList = new ArrayList<>(); + for (HwPortalConfigType topNode : topLevelNodes) { + completeList.add(topNode); + addAllDescendants(allList, topNode, completeList); + } + + return buildPortalConfigTypeTree(completeList); + } else { + // 没有特定过滤条件时,直接查询并构建树形结构 + List list = hwPortalConfigTypeMapper.selectHwPortalConfigTypeList(hwPortalConfigType); + return buildPortalConfigTypeTree(list); + } + } + + /** + * 递归添加所有子孙节点 + */ + private void addAllDescendants(List allList, HwPortalConfigType parentNode, List resultList) { + for (HwPortalConfigType item : allList) { + if (item.getParentId() != null && item.getParentId().equals(parentNode.getConfigTypeId())) { + resultList.add(item); + addAllDescendants(allList, item, resultList); // 递归添加子节点的子节点 + } + } + } + + /** + * 新增门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + @Override + public int insertHwPortalConfigType(HwPortalConfigType hwPortalConfigType) + { + if (hwPortalConfigType.getParentId() == null) { + hwPortalConfigType.setParentId(0L); + hwPortalConfigType.setAncestors("0"); + } else { + HwPortalConfigType info = hwPortalConfigTypeMapper.selectHwPortalConfigTypeByConfigTypeId(hwPortalConfigType.getParentId()); + + hwPortalConfigType.setAncestors(info.getAncestors() + "," + hwPortalConfigType.getParentId()); + } + + hwPortalConfigType.setCreateTime(DateUtils.getNowDate()); + hwPortalConfigType.setCreateBy(SecurityUtils.getUsername()); + return hwPortalConfigTypeMapper.insertHwPortalConfigType(hwPortalConfigType); + } + + /** + * 修改门户网站配置类型 + * + * @param hwPortalConfigType 门户网站配置类型 + * @return 结果 + */ + @Override + public int updateHwPortalConfigType(HwPortalConfigType hwPortalConfigType) + { + hwPortalConfigType.setUpdateTime(DateUtils.getNowDate()); + return hwPortalConfigTypeMapper.updateHwPortalConfigType(hwPortalConfigType); + } + + /** + * 批量删除门户网站配置类型 + * + * @param configTypeIds 需要删除的门户网站配置类型主键 + * @return 结果 + */ + @Override + public int deleteHwPortalConfigTypeByConfigTypeIds(Long[] configTypeIds) + { + return hwPortalConfigTypeMapper.deleteHwPortalConfigTypeByConfigTypeIds(configTypeIds); + } + + /** + * 删除门户网站配置类型信息 + * + * @param configTypeId 门户网站配置类型主键 + * @return 结果 + */ + @Override + public int deleteHwPortalConfigTypeByConfigTypeId(Long configTypeId) + { + return hwPortalConfigTypeMapper.deleteHwPortalConfigTypeByConfigTypeId(configTypeId); + } + + + /** + * 查询门户网站配置类型树结构信息 + * + * @param portalConfigType 门户网站配置类型信息 + * @return 门户网站配置类型树信息集合 + */ + @Override + public List selectPortalConfigTypeTreeList(HwPortalConfigType portalConfigType) { + List portalConfigTypes = this.selectHwPortalConfigTypeList(portalConfigType); + return buildPortalConfigTypeTreeSelect(portalConfigTypes); + } + + /** + * 构建前端所需要下拉树结构 + * + * @param portalConfigTypes 门户网站配置类型列表 + * @return 下拉树结构列表 + */ + @Override + public List buildPortalConfigTypeTreeSelect(List portalConfigTypes) { + List deptTrees = buildPortalConfigTypeTree(portalConfigTypes); + return deptTrees.stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + + /** + * 构建前端所需要树结构 + * + * @param portalConfigTypes 门户网站配置类型列表 + * @return 树结构列表 + */ + @Override + public List buildPortalConfigTypeTree(List portalConfigTypes) { + List returnList = new ArrayList(); + List tempList = portalConfigTypes.stream().map(HwPortalConfigType::getConfigTypeId).collect(Collectors.toList()); + for (HwPortalConfigType portalConfigType : portalConfigTypes) { + // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(portalConfigType.getParentId())) { + recursionFn(portalConfigTypes, portalConfigType); + returnList.add(portalConfigType); + } + } + if (returnList.isEmpty()) { + returnList = portalConfigTypes; + } + return returnList; + } + + + /** + * 递归列表 + */ + private void recursionFn(List list, HwPortalConfigType t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (HwPortalConfigType tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, HwPortalConfigType t) { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + HwPortalConfigType n = (HwPortalConfigType) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getConfigTypeId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, HwPortalConfigType t) { + return getChildList(list, t).size() > 0 ? true : false; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwProductCaseInfoServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.portal.domain.HwProductCaseInfo; +import com.ruoyi.portal.mapper.HwProductCaseInfoMapper; +import com.ruoyi.portal.service.IHwProductCaseInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 案例内容Service业务层处理 + * + * @author xins + * @date 2024-12-01 + */ +@Service +public class HwProductCaseInfoServiceImpl implements IHwProductCaseInfoService +{ + @Autowired + private HwProductCaseInfoMapper hwProductCaseInfoMapper; + + /** + * 查询案例内容 + * + * @param caseInfoId 案例内容主键 + * @return 案例内容 + */ + @Override + public HwProductCaseInfo selectHwProductCaseInfoByCaseInfoId(Long caseInfoId) + { + return hwProductCaseInfoMapper.selectHwProductCaseInfoByCaseInfoId(caseInfoId); + } + + /** + * 查询案例内容列表 + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容 + */ + @Override + public List selectHwProductCaseInfoList(HwProductCaseInfo hwProductCaseInfo) + { + return hwProductCaseInfoMapper.selectHwProductCaseInfoList(hwProductCaseInfo); + } + + /** + * 新增案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + @Override + public int insertHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo) + { + hwProductCaseInfo.setCreateTime(DateUtils.getNowDate()); + return hwProductCaseInfoMapper.insertHwProductCaseInfo(hwProductCaseInfo); + } + + /** + * 修改案例内容 + * + * @param hwProductCaseInfo 案例内容 + * @return 结果 + */ + @Override + public int updateHwProductCaseInfo(HwProductCaseInfo hwProductCaseInfo) + { + hwProductCaseInfo.setUpdateTime(DateUtils.getNowDate()); + return hwProductCaseInfoMapper.updateHwProductCaseInfo(hwProductCaseInfo); + } + + /** + * 批量删除案例内容 + * + * @param caseInfoIds 需要删除的案例内容主键 + * @return 结果 + */ + @Override + public int deleteHwProductCaseInfoByCaseInfoIds(Long[] caseInfoIds) + { + return hwProductCaseInfoMapper.deleteHwProductCaseInfoByCaseInfoIds(caseInfoIds); + } + + /** + * 删除案例内容信息 + * + * @param caseInfoId 案例内容主键 + * @return 结果 + */ + @Override + public int deleteHwProductCaseInfoByCaseInfoId(Long caseInfoId) + { + return hwProductCaseInfoMapper.deleteHwProductCaseInfoByCaseInfoId(caseInfoId); + } + + /** + * 根据configTypeId获取首页典型案例 + * @param hwProductCaseInfo + * @return + */ + @Override + public HwProductCaseInfo getTypicalHomeCaseInfo(HwProductCaseInfo hwProductCaseInfo){ + hwProductCaseInfo.setHomeTypicalFlag("1"); + List productCaseInfoList = hwProductCaseInfoMapper.selectHwProductCaseInfoList(hwProductCaseInfo); + List typicalProductCaseInfoList = productCaseInfoList.stream().filter(pci -> pci.getTypicalFlag().equals("1")).collect(Collectors.toList()); + if(typicalProductCaseInfoList!=null && ! typicalProductCaseInfoList.isEmpty()){ + return typicalProductCaseInfoList.get(0); + }else if (productCaseInfoList!=null && !productCaseInfoList.isEmpty()){ + return productCaseInfoList.get(0); + } + return new HwProductCaseInfo(); + } + + + /** + * 查询案例内容列表,join portalConfigType + * + * @param hwProductCaseInfo 案例内容 + * @return 案例内容 + */ + @Override + public List selectHwProductCaseInfoJoinList(HwProductCaseInfo hwProductCaseInfo) + { + return hwProductCaseInfoMapper.selectHwProductCaseInfoJoinList(hwProductCaseInfo); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwProductInfoDetailServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.HwProductInfoDetail; +import com.ruoyi.portal.mapper.HwProductInfoDetailMapper; +import com.ruoyi.portal.service.IHwProductInfoDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 产品信息明细配置Service业务层处理 + * + * @author xins + * @date 2024-12-11 + */ +@Service +public class HwProductInfoDetailServiceImpl implements IHwProductInfoDetailService +{ + @Autowired + private HwProductInfoDetailMapper hwProductInfoDetailMapper; + + /** + * 查询产品信息明细配置 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 产品信息明细配置 + */ + @Override + public HwProductInfoDetail selectHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId) + { + return hwProductInfoDetailMapper.selectHwProductInfoDetailByProductInfoDetailId(productInfoDetailId); + } + + /** + * 查询产品信息明细配置列表 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 产品信息明细配置 + */ + @Override + public List selectHwProductInfoDetailList(HwProductInfoDetail hwProductInfoDetail) + { + return hwProductInfoDetailMapper.selectHwProductInfoDetailList(hwProductInfoDetail); + } + + /** + * 新增产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + @Override + public int insertHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail) + { + if (hwProductInfoDetail.getParentId() == null) { + hwProductInfoDetail.setParentId(0L); + hwProductInfoDetail.setAncestors("0"); + }else if(hwProductInfoDetail.getParentId() == 0L) { + hwProductInfoDetail.setParentId(0L); + hwProductInfoDetail.setAncestors("0"); + } else { + HwProductInfoDetail info = hwProductInfoDetailMapper.selectHwProductInfoDetailByProductInfoDetailId(hwProductInfoDetail.getParentId()); + + hwProductInfoDetail.setAncestors(info.getAncestors() + "," + hwProductInfoDetail.getParentId()); + } + + hwProductInfoDetail.setCreateTime(DateUtils.getNowDate()); + hwProductInfoDetail.setCreateBy(SecurityUtils.getUsername()); + + return hwProductInfoDetailMapper.insertHwProductInfoDetail(hwProductInfoDetail); + } + + /** + * 修改产品信息明细配置 + * + * @param hwProductInfoDetail 产品信息明细配置 + * @return 结果 + */ + @Override + public int updateHwProductInfoDetail(HwProductInfoDetail hwProductInfoDetail) + { + hwProductInfoDetail.setUpdateTime(DateUtils.getNowDate()); + return hwProductInfoDetailMapper.updateHwProductInfoDetail(hwProductInfoDetail); + } + + /** + * 批量删除产品信息明细配置 + * + * @param productInfoDetailIds 需要删除的产品信息明细配置主键 + * @return 结果 + */ + @Override + public int deleteHwProductInfoDetailByProductInfoDetailIds(Long[] productInfoDetailIds) + { + return hwProductInfoDetailMapper.deleteHwProductInfoDetailByProductInfoDetailIds(productInfoDetailIds); + } + + /** + * 删除产品信息明细配置信息 + * + * @param productInfoDetailId 产品信息明细配置主键 + * @return 结果 + */ + @Override + public int deleteHwProductInfoDetailByProductInfoDetailId(Long productInfoDetailId) + { + return hwProductInfoDetailMapper.deleteHwProductInfoDetailByProductInfoDetailId(productInfoDetailId); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwProductInfoServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.constant.HwPortalConstants; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.HwProductInfo; +import com.ruoyi.portal.domain.HwProductInfoDetail; +import com.ruoyi.portal.mapper.HwProductInfoMapper; +import com.ruoyi.portal.service.IHwProductInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 产品信息配置Service业务层处理 + * + * @author xins + * @date 2024-12-01 + */ +@Service +public class HwProductInfoServiceImpl implements IHwProductInfoService +{ + @Autowired + private HwProductInfoMapper hwProductInfoMapper; + + /** + * 查询产品信息配置 + * + * @param productInfoId 产品信息配置主键 + * @return 产品信息配置 + */ + @Override + public HwProductInfo selectHwProductInfoByProductInfoId(Long productInfoId) + { + return hwProductInfoMapper.selectHwProductInfoByProductInfoId(productInfoId); + } + + /** + * 查询产品信息配置列表 + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置 + */ + @Override + public List selectHwProductInfoList(HwProductInfo hwProductInfo) + { + return hwProductInfoMapper.selectHwProductInfoList(hwProductInfo); + } + + /** + * 新增产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + @Override + public int insertHwProductInfo(HwProductInfo hwProductInfo) + { + hwProductInfo.setCreateTime(DateUtils.getNowDate()); + return hwProductInfoMapper.insertHwProductInfo(hwProductInfo); + } + + /** + * 修改产品信息配置 + * + * @param hwProductInfo 产品信息配置 + * @return 结果 + */ + @Override + public int updateHwProductInfo(HwProductInfo hwProductInfo) + { + hwProductInfo.setUpdateTime(DateUtils.getNowDate()); + return hwProductInfoMapper.updateHwProductInfo(hwProductInfo); + } + + /** + * 批量删除产品信息配置 + * + * @param productInfoIds 需要删除的产品信息配置主键 + * @return 结果 + */ + @Override + public int deleteHwProductInfoByProductInfoIds(Long[] productInfoIds) + { + return hwProductInfoMapper.deleteHwProductInfoByProductInfoIds(productInfoIds); + } + + /** + * 删除产品信息配置信息 + * + * @param productInfoId 产品信息配置主键 + * @return 结果 + */ + @Override + public int deleteHwProductInfoByProductInfoId(Long productInfoId) + { + return hwProductInfoMapper.deleteHwProductInfoByProductInfoId(productInfoId); + } + + + /** + * 获取产品中心产品信息(平台简介,hw_product_info获取,(配置模式2左标题+内容,右图片)读取中文标题和英文标题,下面内容从hw_product_info_detail获取,读取标题,内容和图片) + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置 + */ + @Override + public List selectHwProductInfoJoinDetailList(HwProductInfo hwProductInfo) + { + List hwProductInfoJoinDetailList = hwProductInfoMapper.selectHwProductInfoJoinDetailList(hwProductInfo); + + // 若配置模式configModal为13,hwProductInfoDetailList应该变为树形结构 + if ("13".equals(hwProductInfo.getConfigModal())) { + for (HwProductInfo productInfo : hwProductInfoJoinDetailList) { + if (productInfo.getHwProductInfoDetailList() != null && !productInfo.getHwProductInfoDetailList().isEmpty()) { + // 将每个产品信息的明细列表转换为树形结构 + List treeStructureList = buildProductInfoDetailTree(productInfo.getHwProductInfoDetailList()); + productInfo.setHwProductInfoDetailList(treeStructureList); + } + } + } + + for (HwProductInfo productInfo : hwProductInfoJoinDetailList) { + List hwProductInfoDetailList = productInfo.getHwProductInfoDetailList(); + for (HwProductInfoDetail hwProductInfoDetail : hwProductInfoDetailList) { + if ("13".equals(hwProductInfoDetail.getConfigModel())){ + // 将每个产品信息的明细列表转换为树形结构 + List treeStructureList = buildProductInfoDetailTree(productInfo.getHwProductInfoDetailList()); + productInfo.setHwProductInfoDetailList(treeStructureList); + } + } + } + + return hwProductInfoJoinDetailList; + } + + + + /** + * 构建前端所需要树结构 + * + * @param productInfoDetails 产品明细列表 + * @return 树结构列表 + */ + public List buildProductInfoDetailTree(List productInfoDetails) { + List returnList = new ArrayList<>(); + List tempList = productInfoDetails.stream().map(HwProductInfoDetail::getProductInfoDetailId).collect(Collectors.toList()); + for (HwProductInfoDetail hwProductInfoDetail : productInfoDetails) { +/* // 如果是顶级节点, 遍历该父节点的所有子节点 + if (!tempList.contains(hwProductInfoDetail.getParentId())) {*/ + // 如果是顶级节点(parentId为null、0或者不在当前列表中), 遍历该父节点的所有子节点 + if (hwProductInfoDetail.getParentId() == null || hwProductInfoDetail.getParentId() == 0L || !tempList.contains(hwProductInfoDetail.getParentId())) { + recursionFn(productInfoDetails, hwProductInfoDetail); + returnList.add(hwProductInfoDetail); + } + } + if (returnList.isEmpty()) { + returnList = productInfoDetails; + } + return returnList; + } + + /** + * 递归列表 + */ + private void recursionFn(List list, HwProductInfoDetail t) { + // 得到子节点列表 + List childList = getChildList(list, t); + // 设置TreeEntity的children字段 + t.setChildren(childList); + // 设置HwProductInfoDetail的hwProductInfoDetailList字段 + t.setHwProductInfoDetailList(childList); + + for (HwProductInfoDetail tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + + /** + * 得到子节点列表 + */ + private List getChildList(List list, HwProductInfoDetail t) { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + HwProductInfoDetail n = (HwProductInfoDetail) it.next(); + if (StringUtils.isNotNull(n.getParentId()) && n.getParentId().longValue() == t.getProductInfoDetailId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, HwProductInfoDetail t) { + return getChildList(list, t).size() > 0 ? true : false; + } + + + /** + * 查询产品信息配置列表,join portalConfigType + * + * @param hwProductInfo 产品信息配置 + * @return 产品信息配置 + */ + @Override + public List selectHwProductInfoJoinList(HwProductInfo hwProductInfo) + { + return hwProductInfoMapper.selectHwProductInfoJoinList(hwProductInfo); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwSearchRebuildServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.portal.domain.HwPortalConfigType; +import com.ruoyi.portal.domain.HwWeb; +import com.ruoyi.portal.domain.HwWeb1; +import com.ruoyi.portal.domain.HwWebDocument; +import com.ruoyi.portal.domain.HwWebMenu; +import com.ruoyi.portal.mapper.HwPortalConfigTypeMapper; +import com.ruoyi.portal.mapper.HwWebDocumentMapper; +import com.ruoyi.portal.mapper.HwWebMapper; +import com.ruoyi.portal.mapper.HwWebMapper1; +import com.ruoyi.portal.mapper.HwWebMenuMapper; +import com.ruoyi.portal.search.convert.PortalSearchDocConverter; +import com.ruoyi.portal.search.es.entity.PortalSearchDoc; +import com.ruoyi.portal.search.es.mapper.PortalSearchMapper; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 搜索索引重建实现 + * + * @author ruoyi + */ +@Service +public class HwSearchRebuildServiceImpl implements IHwSearchRebuildService +{ + private static final Logger log = LoggerFactory.getLogger(HwSearchRebuildServiceImpl.class); + + private static final String INDEX_NAME = "hw_portal_content"; + + private final PortalSearchMapper portalSearchMapper; + + private final PortalSearchDocConverter converter; + + private final HwWebMenuMapper hwWebMenuMapper; + + private final HwWebMapper hwWebMapper; + + private final HwWebMapper1 hwWebMapper1; + + private final HwWebDocumentMapper hwWebDocumentMapper; + + private final HwPortalConfigTypeMapper hwPortalConfigTypeMapper; + + public HwSearchRebuildServiceImpl(PortalSearchMapper portalSearchMapper, PortalSearchDocConverter converter, + HwWebMenuMapper hwWebMenuMapper, HwWebMapper hwWebMapper, HwWebMapper1 hwWebMapper1, + HwWebDocumentMapper hwWebDocumentMapper, HwPortalConfigTypeMapper hwPortalConfigTypeMapper) + { + this.portalSearchMapper = portalSearchMapper; + this.converter = converter; + this.hwWebMenuMapper = hwWebMenuMapper; + this.hwWebMapper = hwWebMapper; + this.hwWebMapper1 = hwWebMapper1; + this.hwWebDocumentMapper = hwWebDocumentMapper; + this.hwPortalConfigTypeMapper = hwPortalConfigTypeMapper; + } + + @Override + public void rebuildAll() + { + try + { + if (portalSearchMapper.existsIndex(INDEX_NAME)) + { + portalSearchMapper.deleteIndex(INDEX_NAME); + } + portalSearchMapper.createIndex(); + + List docs = collectDocs(); + if (!docs.isEmpty()) + { + portalSearchMapper.insertBatch(docs); + } + log.info("rebuild search index finished. docs={}", docs.size()); + } + catch (Exception e) + { + throw new ServiceException("重建搜索索引失败: " + e.getMessage()); + } + } + + private List collectDocs() + { + List docs = new ArrayList<>(); + List menus = hwWebMenuMapper.selectHwWebMenuList(new HwWebMenu()); + menus.forEach(item -> docs.add(converter.fromWebMenu(item))); + + List webs = hwWebMapper.selectHwWebList(new HwWeb()); + webs.forEach(item -> docs.add(converter.fromWeb(item))); + + List web1List = hwWebMapper1.selectHwWebList(new HwWeb1()); + web1List.forEach(item -> docs.add(converter.fromWeb1(item))); + + List documents = hwWebDocumentMapper.selectHwWebDocumentList(new HwWebDocument()); + documents.forEach(item -> docs.add(converter.fromDocument(item))); + + List configTypes = hwPortalConfigTypeMapper.selectHwPortalConfigTypeList(new HwPortalConfigType()); + configTypes.forEach(item -> docs.add(converter.fromConfigType(item))); + return docs; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwSearchServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.dto.SearchPageDTO; +import com.ruoyi.portal.domain.dto.SearchRawRecord; +import com.ruoyi.portal.domain.dto.SearchResultDTO; +import com.ruoyi.portal.mapper.HwSearchMapper; +import com.ruoyi.portal.search.convert.PortalSearchDocConverter; +import com.ruoyi.portal.search.service.PortalSearchEsService; +import com.ruoyi.portal.service.IHwSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 门户搜索服务实现 + * + * @author ruoyi + */ +@Service +public class HwSearchServiceImpl implements IHwSearchService +{ + private static final Logger log = LoggerFactory.getLogger(HwSearchServiceImpl.class); + + private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\.*+\\[\\](){}^$?|])"); + + private final HwSearchMapper hwSearchMapper; + + private final PortalSearchDocConverter converter; + + private final PortalSearchEsService portalSearchEsService; + + @Value("${search.engine:mysql}") + private String searchEngine; + + @Value("${search.es.enabled:false}") + private boolean esEnabled; + + public HwSearchServiceImpl(HwSearchMapper hwSearchMapper, PortalSearchDocConverter converter, + @Autowired(required = false) PortalSearchEsService portalSearchEsService) + { + this.hwSearchMapper = hwSearchMapper; + this.converter = converter; + this.portalSearchEsService = portalSearchEsService; + } + + @Override + public SearchPageDTO search(String keyword, Integer pageNum, Integer pageSize) + { + return doSearch(keyword, pageNum, pageSize, false); + } + + @Override + public SearchPageDTO searchForEdit(String keyword, Integer pageNum, Integer pageSize) + { + return doSearch(keyword, pageNum, pageSize, true); + } + + private SearchPageDTO doSearch(String keyword, Integer pageNum, Integer pageSize, boolean editMode) + { + String normalizedKeyword = validateKeyword(keyword); + int normalizedPageNum = normalizePageNum(pageNum); + int normalizedPageSize = normalizePageSize(pageSize); + + if (useEsEngine()) + { + try + { + return portalSearchEsService.search(normalizedKeyword, normalizedPageNum, normalizedPageSize, editMode); + } + catch (Exception e) + { + log.error("ES search failed, fallback to mysql. keyword={}", normalizedKeyword, e); + } + } + return searchByMysql(normalizedKeyword, normalizedPageNum, normalizedPageSize, editMode); + } + + private boolean useEsEngine() + { + return esEnabled && portalSearchEsService != null && "es".equalsIgnoreCase(searchEngine); + } + + private SearchPageDTO searchByMysql(String keyword, int pageNum, int pageSize, boolean editMode) + { + List rawRecords = hwSearchMapper.searchByKeyword(keyword); + if (rawRecords == null || rawRecords.isEmpty()) + { + return new SearchPageDTO(); + } + + List all = new ArrayList<>(); + for (SearchRawRecord raw : rawRecords) + { + SearchResultDTO dto = toResult(raw, keyword, editMode); + if (dto != null) + { + all.add(dto); + } + } + all = all.stream().sorted(Comparator.comparing(SearchResultDTO::getScore).reversed()).collect(Collectors.toList()); + + SearchPageDTO page = new SearchPageDTO(); + page.setTotal(all.size()); + int from = Math.max(0, (pageNum - 1) * pageSize); + if (from >= all.size()) + { + page.setRows(new ArrayList<>()); + return page; + } + int to = Math.min(all.size(), from + pageSize); + page.setRows(all.subList(from, to)); + return page; + } + + private SearchResultDTO toResult(SearchRawRecord raw, String keyword, boolean editMode) + { + String sourceType = raw.getSourceType(); + String content = StringUtils.defaultString(raw.getContent()); + if (PortalSearchDocConverter.SOURCE_WEB.equals(sourceType) || PortalSearchDocConverter.SOURCE_WEB1.equals(sourceType)) + { + content = converter.extractSearchableText(content); + if (!containsIgnoreCase(raw.getTitle(), keyword) && !containsIgnoreCase(content, keyword)) + { + return null; + } + } + else if (!containsIgnoreCase(raw.getTitle(), keyword) && !containsIgnoreCase(content, keyword)) + { + return null; + } + + SearchResultDTO dto = new SearchResultDTO(); + dto.setSourceType(sourceType); + dto.setTitle(raw.getTitle()); + dto.setSnippet(buildSnippet(raw.getTitle(), content, keyword)); + dto.setScore(calculateScore(raw, keyword)); + dto.setRoute(buildRoute(sourceType, raw.getWebCode())); + dto.setRouteQuery(buildRouteQuery(raw)); + if (editMode) + { + dto.setEditRoute(buildEditRoute(raw)); + } + return dto; + } + + private int calculateScore(SearchRawRecord raw, String keyword) + { + int base = raw.getScore() == null ? 0 : raw.getScore(); + String title = StringUtils.defaultString(raw.getTitle()); + String content = StringUtils.defaultString(raw.getContent()); + if (containsIgnoreCase(title, keyword)) + { + base += 20; + } + if (containsIgnoreCase(content, keyword)) + { + base += 10; + } + return base; + } + + private String buildRoute(String sourceType, String webCode) + { + if (PortalSearchDocConverter.SOURCE_MENU.equals(sourceType)) + { + return "/test"; + } + if (PortalSearchDocConverter.SOURCE_WEB.equals(sourceType)) + { + if ("-1".equals(webCode)) + { + return "/index"; + } + if ("7".equals(webCode)) + { + return "/productCenter"; + } + return "/test"; + } + if (PortalSearchDocConverter.SOURCE_WEB1.equals(sourceType)) + { + return "/productCenter/detail"; + } + if (PortalSearchDocConverter.SOURCE_DOCUMENT.equals(sourceType)) + { + return "/serviceSupport"; + } + if (PortalSearchDocConverter.SOURCE_CONFIG_TYPE.equals(sourceType)) + { + return "/productCenter"; + } + return "/index"; + } + + private Map buildRouteQuery(SearchRawRecord raw) + { + Map query = new HashMap<>(); + String sourceType = raw.getSourceType(); + if (PortalSearchDocConverter.SOURCE_MENU.equals(sourceType)) + { + query.put("id", raw.getMenuId()); + return query; + } + if (PortalSearchDocConverter.SOURCE_WEB.equals(sourceType) && !"-1".equals(raw.getWebCode()) && !"7".equals(raw.getWebCode())) + { + query.put("id", raw.getWebCode()); + return query; + } + if (PortalSearchDocConverter.SOURCE_WEB1.equals(sourceType)) + { + query.put("webCode", raw.getWebCode()); + query.put("typeId", raw.getTypeId()); + query.put("deviceId", raw.getDeviceId()); + return query; + } + if (PortalSearchDocConverter.SOURCE_DOCUMENT.equals(sourceType)) + { + query.put("documentId", raw.getDocumentId()); + query.put("webCode", raw.getWebCode()); + query.put("typeId", raw.getTypeId()); + return query; + } + if (PortalSearchDocConverter.SOURCE_CONFIG_TYPE.equals(sourceType)) + { + query.put("configTypeId", raw.getTypeId()); + return query; + } + return query; + } + + private String buildEditRoute(SearchRawRecord raw) + { + String sourceType = raw.getSourceType(); + if (PortalSearchDocConverter.SOURCE_MENU.equals(sourceType)) + { + return "/editor?type=1&id=" + raw.getMenuId(); + } + if (PortalSearchDocConverter.SOURCE_WEB.equals(sourceType)) + { + if ("7".equals(raw.getWebCode())) + { + return "/productCenter/edit"; + } + if ("-1".equals(raw.getWebCode())) + { + return "/editor?type=3&id=-1"; + } + return "/editor?type=1&id=" + raw.getWebCode(); + } + if (PortalSearchDocConverter.SOURCE_WEB1.equals(sourceType)) + { + return "/editor?type=2&id=" + raw.getWebCode() + "," + raw.getTypeId() + "," + raw.getDeviceId(); + } + if (PortalSearchDocConverter.SOURCE_DOCUMENT.equals(sourceType)) + { + if (StringUtils.isNotBlank(raw.getWebCode()) && StringUtils.isNotBlank(raw.getTypeId()) && StringUtils.isNotBlank(raw.getDeviceId())) + { + return "/editor?type=2&id=" + raw.getWebCode() + "," + raw.getTypeId() + "," + raw.getDeviceId() + "&documentId=" + raw.getDocumentId(); + } + return "/editor?type=2&documentId=" + raw.getDocumentId(); + } + if (PortalSearchDocConverter.SOURCE_CONFIG_TYPE.equals(sourceType)) + { + return "/productCenter/edit"; + } + return "/editor"; + } + + private String buildSnippet(String title, String content, String keyword) + { + if (StringUtils.isNotBlank(title) && containsIgnoreCase(title, keyword)) + { + return highlight(title, keyword); + } + if (StringUtils.isBlank(content)) + { + return StringUtils.EMPTY; + } + String normalized = content.replaceAll("\\s+", " ").trim(); + int index = StringUtils.indexOfIgnoreCase(normalized, keyword); + if (index < 0) + { + return StringUtils.substring(normalized, 0, Math.min(120, normalized.length())); + } + int start = Math.max(0, index - 60); + int end = Math.min(normalized.length(), index + keyword.length() + 60); + String snippet = normalized.substring(start, end); + if (start > 0) + { + snippet = "..." + snippet; + } + if (end < normalized.length()) + { + snippet = snippet + "..."; + } + return highlight(snippet, keyword); + } + + private String highlight(String text, String keyword) + { + if (StringUtils.isBlank(text) || StringUtils.isBlank(keyword)) + { + return StringUtils.defaultString(text); + } + String escaped = ESCAPE_PATTERN.matcher(keyword).replaceAll("\\\\$1"); + Matcher matcher = Pattern.compile("(?i)" + escaped).matcher(text); + if (!matcher.find()) + { + return text; + } + return matcher.replaceAll("$0"); + } + + private boolean containsIgnoreCase(String text, String keyword) + { + return StringUtils.isNotBlank(text) && StringUtils.isNotBlank(keyword) && StringUtils.containsIgnoreCase(text, keyword); + } + + private int normalizePageNum(Integer pageNum) + { + if (pageNum == null || pageNum <= 0) + { + return 1; + } + return pageNum; + } + + private int normalizePageSize(Integer pageSize) + { + if (pageSize == null || pageSize <= 0) + { + return 20; + } + return Math.min(pageSize, 50); + } + + private String validateKeyword(String keyword) + { + String normalized = StringUtils.trim(keyword); + if (StringUtils.isBlank(normalized)) + { + throw new ServiceException("关键词不能为空"); + } + if (normalized.length() > 50) + { + throw new ServiceException("关键词长度不能超过50"); + } + return normalized; + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwWebDocumentServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import java.util.List; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.portal.mapper.HwWebDocumentMapper; +import com.ruoyi.portal.domain.HwWebDocument; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import com.ruoyi.portal.service.IHwWebDocumentService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Hw资料文件Service业务层处理 + * + * @author zch + * @date 2025-09-22 + */ +@Service +public class HwWebDocumentServiceImpl implements IHwWebDocumentService +{ + private static final Logger log = LoggerFactory.getLogger(HwWebDocumentServiceImpl.class); + + @Autowired + private HwWebDocumentMapper hwWebDocumentMapper; + + @Autowired(required = false) + private IHwSearchRebuildService hwSearchRebuildService; + + /** + * 查询Hw资料文件 + * + * @param documentId Hw资料文件主键 + * @return Hw资料文件 + */ + @Override + public HwWebDocument selectHwWebDocumentByDocumentId(String documentId) + { + return hwWebDocumentMapper.selectHwWebDocumentByDocumentId(documentId); + } + + /** + * 查询Hw资料文件列表 + * + * @param hwWebDocument Hw资料文件 + * @return Hw资料文件 + */ + @Override + public List selectHwWebDocumentList(HwWebDocument hwWebDocument) + { + return hwWebDocumentMapper.selectHwWebDocumentList(hwWebDocument); + } + + /** + * 新增Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + @Override + public int insertHwWebDocument(HwWebDocument hwWebDocument) + { + hwWebDocument.setCreateTime(DateUtils.getNowDate()); + int rows = hwWebDocumentMapper.insertHwWebDocument(hwWebDocument); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 修改Hw资料文件 + * + * @param hwWebDocument Hw资料文件 + * @return 结果 + */ + @Override + public int updateHwWebDocument(HwWebDocument hwWebDocument) + { + // 特殊处理 secretKey:前端不传或传 null 时清空数据库密钥 + // 将 null 转换为空字符串,触发 Mapper 更新条件 + if (hwWebDocument.getSecretKey() == null) { + hwWebDocument.setSecretKey(""); + } + + int rows = hwWebDocumentMapper.updateHwWebDocument(hwWebDocument); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 批量删除Hw资料文件 + * + * @param documentIds 需要删除的Hw资料文件主键 + * @return 结果 + */ + @Override + public int deleteHwWebDocumentByDocumentIds(String[] documentIds) + { + int rows = hwWebDocumentMapper.deleteHwWebDocumentByDocumentIds(documentIds); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 删除Hw资料文件信息 + * + * @param documentId Hw资料文件主键 + * @return 结果 + */ + @Override + public int deleteHwWebDocumentByDocumentId(String documentId) + { + int rows = hwWebDocumentMapper.deleteHwWebDocumentByDocumentId(documentId); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + @Override + public String verifyAndGetDocumentAddress(String documentId, String providedKey) throws Exception { + HwWebDocument document = selectHwWebDocumentByDocumentId(documentId); + if (document == null) { + throw new ServiceException("文件不存在"); + } + String secretKey = document.getSecretKey(); + String address = document.getDocumentAddress(); + + // 若数据库密钥为空,则直接返回文件地址 + if (secretKey == null || secretKey.trim().isEmpty()) { + return address; + } + + // 若密钥不为空,则需要验证提供的密钥是否相等 + String trimmedProvided = providedKey == null ? null : providedKey.trim(); + if (trimmedProvided == null || trimmedProvided.isEmpty()) { + throw new ServiceException("密钥不能为空"); + } + + if (secretKey.trim().equals(trimmedProvided)) { + return address; + } else { + throw new ServiceException("密钥错误"); + } + } + + private void rebuildSearchIndexQuietly() + { + if (hwSearchRebuildService == null) + { + return; + } + try + { + hwSearchRebuildService.rebuildAll(); + } + catch (Exception e) + { + log.error("rebuild portal search index failed after hw_web_document changed", e); + } + } + + +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwWebMenuServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.HwProductInfoDetail; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.portal.mapper.HwWebMenuMapper; +import com.ruoyi.portal.domain.HwWebMenu; +import com.ruoyi.portal.service.IHwWebMenuService; + +/** + * haiwei官网菜单Service业务层处理 + * + * @author zch + * @date 2025-08-18 + */ +@Service +public class HwWebMenuServiceImpl implements IHwWebMenuService +{ + private static final Logger log = LoggerFactory.getLogger(HwWebMenuServiceImpl.class); + + @Autowired + private HwWebMenuMapper hwWebMenuMapper; + + @Autowired(required = false) + private IHwSearchRebuildService hwSearchRebuildService; + + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + @Override + public HwWebMenu selectHwWebMenuByWebMenuId(Long webMenuId) + { + return hwWebMenuMapper.selectHwWebMenuByWebMenuId(webMenuId); + } + + /** + * 查询haiwei官网菜单列表 + * + * @param hwWebMenu haiwei官网菜单 + * @return haiwei官网菜单 + */ + @Override + public List selectHwWebMenuList(HwWebMenu hwWebMenu) + { + List hwWebMenus = hwWebMenuMapper.selectHwWebMenuList(hwWebMenu); + return hwWebMenus; + } + + /** + * 获取菜单树列表 + */ + @Override + public List selectMenuTree(HwWebMenu hwWebMenu) + { + List hwWebMenus = hwWebMenuMapper.selectHwWebMenuList(hwWebMenu); + return buildWebMenuTree(hwWebMenus); + } + + /** + * 新增haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + @Override + public int insertHwWebMenu(HwWebMenu hwWebMenu) + { + int rows = hwWebMenuMapper.insertHwWebMenu(hwWebMenu); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 修改haiwei官网菜单 + * + * @param hwWebMenu haiwei官网菜单 + * @return 结果 + */ + @Override + public int updateHwWebMenu(HwWebMenu hwWebMenu) + { + int rows = hwWebMenuMapper.updateHwWebMenu(hwWebMenu); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的haiwei官网菜单主键 + * @return 结果 + */ + @Override + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds) + { + int rows = hwWebMenuMapper.deleteHwWebMenuByWebMenuIds(webMenuIds); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 删除haiwei官网菜单信息 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + @Override + public int deleteHwWebMenuByWebMenuId(Long webMenuId) + { + int rows = hwWebMenuMapper.deleteHwWebMenuByWebMenuId(webMenuId); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + +/** + * 构建前端所需要树结构(根据传入的平铺菜单列表构造树) + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildWebMenuTree(List menus) { + List returnList = new ArrayList<>(); + List tempList = menus.stream().map(HwWebMenu::getWebMenuId).collect(Collectors.toList()); + for (HwWebMenu menu : menus) { + // 如果是顶级节点(parent为null、0或者不在当前列表中), 遍历该父节点的所有子节点 + if (menu.getParent() == null || menu.getParent() == 0L || !tempList.contains(menu.getParent())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) { + returnList = menus; + } + return returnList; + } + + /** + * 递归设置子节点 + */ + private void recursionFn(List list, HwWebMenu t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (HwWebMenu tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, HwWebMenu t) { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + HwWebMenu n = it.next(); + if (StringUtils.isNotNull(n.getParent()) && n.getParent().longValue() == t.getWebMenuId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, HwWebMenu t) { + return !getChildList(list, t).isEmpty(); + } + + private void rebuildSearchIndexQuietly() + { + if (hwSearchRebuildService == null) + { + return; + } + try + { + hwSearchRebuildService.rebuildAll(); + } + catch (Exception e) + { + log.error("rebuild portal search index failed after hw_web_menu changed", e); + } + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwWebMenuServiceImpl1.java` + +```java +package com.ruoyi.portal.service.impl; + +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.portal.domain.HwWebMenu1; +import com.ruoyi.portal.mapper.HwWebMenuMapper1; +import com.ruoyi.portal.service.IHwWebMenuService1; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * haiwei官网菜单Service业务层处理 + * + * @author zch + * @date 2025-08-18 + */ +@Service +public class HwWebMenuServiceImpl1 implements IHwWebMenuService1 +{ + @Autowired + private HwWebMenuMapper1 HwWebMenuMapper1; + + /** + * 查询haiwei官网菜单 + * + * @param webMenuId haiwei官网菜单主键 + * @return haiwei官网菜单 + */ + @Override + public HwWebMenu1 selectHwWebMenuByWebMenuId(Long webMenuId) + { + return HwWebMenuMapper1.selectHwWebMenuByWebMenuId(webMenuId); + } + + /** + * 查询haiwei官网菜单列表 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return haiwei官网菜单 + */ + @Override + public List selectHwWebMenuList(HwWebMenu1 HwWebMenu1) + { + List hwWebMenus = HwWebMenuMapper1.selectHwWebMenuList(HwWebMenu1); + return hwWebMenus; + } + + /** + * 获取菜单树列表 + */ + @Override + public List selectMenuTree(HwWebMenu1 HwWebMenu1) + { + List hwWebMenus = HwWebMenuMapper1.selectHwWebMenuList(HwWebMenu1); + return buildWebMenuTree(hwWebMenus); + } + + /** + * 新增haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + @Override + public int insertHwWebMenu(HwWebMenu1 HwWebMenu1) + { + return HwWebMenuMapper1.insertHwWebMenu(HwWebMenu1); + } + + /** + * 修改haiwei官网菜单 + * + * @param HwWebMenu1 haiwei官网菜单 + * @return 结果 + */ + @Override + public int updateHwWebMenu(HwWebMenu1 HwWebMenu1) + { + return HwWebMenuMapper1.updateHwWebMenu(HwWebMenu1); + } + + /** + * 批量删除haiwei官网菜单 + * + * @param webMenuIds 需要删除的haiwei官网菜单主键 + * @return 结果 + */ + @Override + public int deleteHwWebMenuByWebMenuIds(Long[] webMenuIds) + { + return HwWebMenuMapper1.deleteHwWebMenuByWebMenuIds(webMenuIds); + } + + /** + * 删除haiwei官网菜单信息 + * + * @param webMenuId haiwei官网菜单主键 + * @return 结果 + */ + @Override + public int deleteHwWebMenuByWebMenuId(Long webMenuId) + { + return HwWebMenuMapper1.deleteHwWebMenuByWebMenuId(webMenuId); + } + +/** + * 构建前端所需要树结构(根据传入的平铺菜单列表构造树) + * + * @param menus 菜单列表 + * @return 树结构列表 + */ + public List buildWebMenuTree(List menus) { + List returnList = new ArrayList<>(); + List tempList = menus.stream().map(HwWebMenu1::getWebMenuId).collect(Collectors.toList()); + for (HwWebMenu1 menu : menus) { + // 如果是顶级节点(parent为null、0或者不在当前列表中), 遍历该父节点的所有子节点 + if (menu.getParent() == null || menu.getParent() == 0L || !tempList.contains(menu.getParent())) { + recursionFn(menus, menu); + returnList.add(menu); + } + } + if (returnList.isEmpty()) { + returnList = menus; + } + return returnList; + } + + /** + * 递归设置子节点 + */ + private void recursionFn(List list, HwWebMenu1 t) { + // 得到子节点列表 + List childList = getChildList(list, t); + t.setChildren(childList); + for (HwWebMenu1 tChild : childList) { + if (hasChild(list, tChild)) { + recursionFn(list, tChild); + } + } + } + + /** + * 得到子节点列表 + */ + private List getChildList(List list, HwWebMenu1 t) { + List tlist = new ArrayList(); + Iterator it = list.iterator(); + while (it.hasNext()) { + HwWebMenu1 n = it.next(); + if (StringUtils.isNotNull(n.getParent()) && n.getParent().longValue() == t.getWebMenuId().longValue()) { + tlist.add(n); + } + } + return tlist; + } + + /** + * 判断是否有子节点 + */ + private boolean hasChild(List list, HwWebMenu1 t) { + return !getChildList(list, t).isEmpty(); + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwWebServiceImpl.java` + +```java +package com.ruoyi.portal.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.portal.mapper.HwWebMapper; +import com.ruoyi.portal.domain.HwWeb; +import com.ruoyi.portal.service.IHwWebService; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.annotation.Transactional; + +/** + * haiwei官网jsonService业务层处理 + * + * @author ruoyi + * @date 2025-08-18 + */ +@Service +public class HwWebServiceImpl implements IHwWebService +{ + private static final Logger log = LoggerFactory.getLogger(HwWebServiceImpl.class); + + @Autowired + private HwWebMapper hwWebMapper; + + @Autowired(required = false) + private IHwSearchRebuildService hwSearchRebuildService; + + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + @Override + public HwWeb selectHwWebByWebcode(Long webCode) + { + HwWeb hwWeb = hwWebMapper.selectHwWebByWebcode(webCode); + return hwWeb; + } + + + /** + * 查询haiwei官网json列表 + * + * @param hwWeb haiwei官网json + * @return haiwei官网json + */ + @Override + public List selectHwWebList(HwWeb hwWeb) + { + return hwWebMapper.selectHwWebList(hwWeb); + } + + /** + * 新增haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + @Override + public int insertHwWeb(HwWeb hwWeb) + { + int rows = hwWebMapper.insertHwWeb(hwWeb); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 修改haiwei官网json + * + * @param hwWeb haiwei官网json + * @return 结果 + */ + @Override + @Transactional( rollbackFor = Exception.class ) + public int updateHwWeb(HwWeb hwWeb) + { + HwWeb codeWeb = new HwWeb(); + //编号唯一 + codeWeb.setWebCode(hwWeb.getWebCode()); + List exists = hwWebMapper.selectHwWebList(codeWeb); + if (!exists.isEmpty()) { + Long[] webIds = exists.stream().map(HwWeb::getWebId).toArray(Long[]::new); + //逻辑删除旧纪录 + hwWebMapper.deleteHwWebByWebIds(webIds); + } + // 插入新记录,避免复用旧主键 + // hwWeb.setWebId(null); + hwWeb.setIsDelete("0"); + int rows = hwWebMapper.insertHwWeb(hwWeb); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的haiwei官网json主键 + * @return 结果 + */ + @Override + public int deleteHwWebByWebIds(Long[] webIds) + { + int rows = hwWebMapper.deleteHwWebByWebIds(webIds); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 删除haiwei官网json信息 + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + @Override + public int deleteHwWebByWebId(Long webId) + { + int rows = hwWebMapper.deleteHwWebByWebId(webId); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + private void rebuildSearchIndexQuietly() + { + if (hwSearchRebuildService == null) + { + return; + } + try + { + hwSearchRebuildService.rebuildAll(); + } + catch (Exception e) + { + log.error("rebuild portal search index failed after hw_web changed", e); + } + } +} +``` + +### `ruoyi-portal/src/main/java/com/ruoyi/portal/service/impl/HwWebServiceImpl1.java` + +```java +package com.ruoyi.portal.service.impl; + + +import com.ruoyi.portal.mapper.HwWebMapper1; +import com.ruoyi.portal.service.IHwSearchRebuildService; +import com.ruoyi.portal.service.IHwWebService1; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; +import com.ruoyi.portal.domain.HwWeb1; +import com.ruoyi.portal.domain.HwWeb; +import org.springframework.transaction.annotation.Transactional; + +/** + * haiwei官网jsonService业务层处理 + * + * @author ruoyi + * @date 2025-08-18 + */ +@Service +public class HwWebServiceImpl1 implements IHwWebService1 +{ + private static final Logger log = LoggerFactory.getLogger(HwWebServiceImpl1.class); + + @Autowired + private HwWebMapper1 hwWebMapper1; + + @Autowired(required = false) + private IHwSearchRebuildService hwSearchRebuildService; + + /** + * 查询haiwei官网json + * + * @param webId haiwei官网json主键 + * @return haiwei官网json + */ + @Override + public HwWeb1 selectHwWebByWebcode(Long webCode) + { + return hwWebMapper1.selectHwWebByWebcode(webCode); + } + + @Override + public HwWeb1 selectHwWebOne(HwWeb1 hwWeb1) + { + return hwWebMapper1.selectHwWebOne(hwWeb1); + } + + + /** + * 查询haiwei官网json列表 + * + * @param HwWeb1 haiwei官网json + * @return haiwei官网json + */ + @Override + public List selectHwWebList(HwWeb1 hwWeb1) + { + return hwWebMapper1.selectHwWebList(hwWeb1); + } + + /** + * 新增haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + @Override + public int insertHwWeb(HwWeb1 hwWeb1) + { + int rows = hwWebMapper1.insertHwWeb(hwWeb1); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 修改haiwei官网json + * + * @param HwWeb1 haiwei官网json + * @return 结果 + */ + @Override + @Transactional( rollbackFor = Exception.class ) + public int updateHwWeb(HwWeb1 hwWeb1) + { + HwWeb1 codeWeb = new HwWeb1(); + // 编号、typeid、deviceID保证唯一 + codeWeb.setWebCode(hwWeb1.getWebCode()); + codeWeb.setTypeId(hwWeb1.getTypeId()); + codeWeb.setDeviceId(hwWeb1.getDeviceId()); + List exists = hwWebMapper1.selectHwWebList(codeWeb); + if (!exists.isEmpty()) { + Long[] webIds = exists.stream().map(HwWeb1::getWebId).toArray(Long[]::new); + //逻辑删除旧纪录 + hwWebMapper1.deleteHwWebByWebIds(webIds); + } + // 插入新记录,避免复用旧主键 + // hwWeb1.setWebId(null); + hwWeb1.setIsDelete("0"); + int rows = hwWebMapper1.insertHwWeb(hwWeb1); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 批量删除haiwei官网json + * + * @param webIds 需要删除的haiwei官网json主键 + * @return 结果 + */ + @Override + public int deleteHwWebByWebIds(Long[] webIds) + { + int rows = hwWebMapper1.deleteHwWebByWebIds(webIds); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + /** + * 删除haiwei官网json信息 + * + * @param webId haiwei官网json主键 + * @return 结果 + */ + @Override + public int deleteHwWebByWebId(Long webId) + { + int rows = hwWebMapper1.deleteHwWebByWebId(webId); + if (rows > 0) + { + rebuildSearchIndexQuietly(); + } + return rows; + } + + private void rebuildSearchIndexQuietly() + { + if (hwSearchRebuildService == null) + { + return; + } + try + { + hwSearchRebuildService.rebuildAll(); + } + catch (Exception e) + { + log.error("rebuild portal search index failed after hw_web1 changed", e); + } + } +} +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwAboutUsInfoDetailMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + select us_info_detail_id, about_us_info_id, us_info_detail_title, us_info_detail_desc, us_info_detail_order, us_info_detail_pic, create_time, create_by, update_time, update_by from hw_about_us_info_detail + + + + + + + + insert into hw_about_us_info_detail + + about_us_info_id, + us_info_detail_title, + us_info_detail_desc, + us_info_detail_order, + us_info_detail_pic, + create_time, + create_by, + update_time, + update_by, + + + #{aboutUsInfoId}, + #{usInfoDetailTitle}, + #{usInfoDetailDesc}, + #{usInfoDetailOrder}, + #{usInfoDetailPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_about_us_info_detail + + about_us_info_id = #{aboutUsInfoId}, + us_info_detail_title = #{usInfoDetailTitle}, + us_info_detail_desc = #{usInfoDetailDesc}, + us_info_detail_order = #{usInfoDetailOrder}, + us_info_detail_pic = #{usInfoDetailPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where us_info_detail_id = #{usInfoDetailId} + + + + delete from hw_about_us_info_detail where us_info_detail_id = #{usInfoDetailId} + + + + delete from hw_about_us_info_detail where us_info_detail_id in + + #{usInfoDetailId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwAboutUsInfoMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + + select about_us_info_id, about_us_info_type, about_us_info_title, about_us_info_etitle,about_us_info_desc, about_us_info_order,display_modal, about_us_info_pic, create_time, create_by, update_time, update_by from hw_about_us_info + + + + + + + + insert into hw_about_us_info + + about_us_info_type, + about_us_info_etitle, + about_us_info_title, + about_us_info_desc, + about_us_info_order, + display_modal, + about_us_info_pic, + create_time, + create_by, + update_time, + update_by, + + + #{aboutUsInfoType}, + #{aboutUsInfoEtitle}, + #{aboutUsInfoTitle}, + #{aboutUsInfoDesc}, + #{aboutUsInfoOrder}, + #{displayModal}, + #{aboutUsInfoPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_about_us_info + + about_us_info_type = #{aboutUsInfoType}, + about_us_info_etitle = #{aboutUsInfoEtitle}, + about_us_info_title = #{aboutUsInfoTitle}, + about_us_info_desc = #{aboutUsInfoDesc}, + about_us_info_order = #{aboutUsInfoOrder}, + display_modal = #{displayModal}, + about_us_info_pic = #{aboutUsInfoPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where about_us_info_id = #{aboutUsInfoId} + + + + delete from hw_about_us_info where about_us_info_id = #{aboutUsInfoId} + + + + delete from hw_about_us_info where about_us_info_id in + + #{aboutUsInfoId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwAnalyticsMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + INSERT INTO hw_web_visit_event ( + event_type, visitor_id, session_id, path, referrer, utm_source, utm_medium, utm_campaign, + keyword, ip_hash, ua, device, browser, os, stay_ms, event_time, created_at + ) VALUES ( + #{eventType}, #{visitorId}, #{sessionId}, #{path}, #{referrer}, #{utmSource}, #{utmMedium}, #{utmCampaign}, + #{keyword}, #{ipHash}, #{ua}, #{device}, #{browser}, #{os}, #{stayMs}, #{eventTime}, NOW() + ) + + + + + + + + + + + + + + + + + + + + + + INSERT INTO hw_web_visit_daily ( + stat_date, pv, uv, ip_uv, avg_stay_ms, bounce_rate, search_count, download_count, created_at, updated_at + ) VALUES ( + #{statDate}, #{pv}, #{uv}, #{ipUv}, #{avgStayMs}, #{bounceRate}, #{searchCount}, #{downloadCount}, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + pv = VALUES(pv), + uv = VALUES(uv), + ip_uv = VALUES(ip_uv), + avg_stay_ms = VALUES(avg_stay_ms), + bounce_rate = VALUES(bounce_rate), + search_count = VALUES(search_count), + download_count = VALUES(download_count), + updated_at = NOW() + + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwContactUsInfoMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + select contact_us_info_id, user_name, user_email, user_phone, user_ip, remark,create_time, create_by, update_time, update_by from hw_contact_us_info + + + + + + + + insert into hw_contact_us_info + + user_name, + user_email, + user_phone, + user_ip, + remark, + create_time, + create_by, + update_time, + update_by, + + + #{userName}, + #{userEmail}, + #{userPhone}, + #{userIp}, + #{remark}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_contact_us_info + + user_name = #{userName}, + user_email = #{userEmail}, + user_phone = #{userPhone}, + user_ip = #{userIp}, + remark = #{remark}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where contact_us_info_id = #{contactUsInfoId} + + + + delete from hw_contact_us_info where contact_us_info_id = #{contactUsInfoId} + + + + delete from hw_contact_us_info where contact_us_info_id in + + #{contactUsInfoId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwPortalConfigMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select portal_config_id, portal_config_type,portal_config_type_id, portal_config_title, portal_config_order, + portal_config_desc, button_name, router_address, portal_config_pic, create_time, create_by, update_time, + update_by + from hw_portal_config + + + + select hpc.portal_config_id, hpc.portal_config_type,hpc.portal_config_type_id, hpc.portal_config_title, hpc.portal_config_order, + hpc.portal_config_desc, hpc.button_name, hpc.router_address, hpc.portal_config_pic, hpc.create_time, hpc.create_by, hpc.update_time, + hpc.update_by, + hpct.config_type_name, + hpct.home_config_type_pic, + hpct.config_type_icon, + hpct.home_config_type_name, + hpct.config_type_classfication, + hpct.parent_id, + hpct.ancestors + from hw_portal_config hpc + left join hw_portal_config_type hpct on hpc.portal_config_type_id = hpct.config_type_id + + + + + + + + + + insert into hw_portal_config + + portal_config_type, + portal_config_type_id, + portal_config_title, + portal_config_order, + portal_config_desc, + button_name, + router_address, + portal_config_pic, + create_time, + create_by, + update_time, + update_by, + + + #{portalConfigType}, + #{portalConfigTypeId}, + #{portalConfigTitle}, + #{portalConfigOrder}, + #{portalConfigDesc}, + #{buttonName}, + #{routerAddress}, + #{portalConfigPic}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_portal_config + + portal_config_type = #{portalConfigType}, + portal_config_type_id = #{portalConfigTypeId}, + portal_config_title = #{portalConfigTitle}, + portal_config_order = #{portalConfigOrder}, + portal_config_desc = #{portalConfigDesc}, + button_name = #{buttonName}, + router_address = #{routerAddress}, + portal_config_pic = #{portalConfigPic}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where portal_config_id = #{portalConfigId} + + + + delete from hw_portal_config where portal_config_id = #{portalConfigId} + + + + delete from hw_portal_config where portal_config_id in + + #{portalConfigId} + + + + + + + select hpc.portal_config_id, hpc.portal_config_type,hpc.portal_config_type_id, + hpc.portal_config_title, hpc.portal_config_order, hpc.portal_config_desc, + hpc.button_name, hpc.router_address, hpc.portal_config_pic, + hpc.create_time, hpc.create_by, hpc.update_time, hpc.update_by,hpct.config_type_name from hw_portal_config hpc + left join hw_portal_config_type hpct on hpc.portal_config_type_id = hpct.config_type_id + + + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwPortalConfigTypeMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + + + select config_type_id, config_type_classfication, config_type_name, home_config_type_name, config_type_desc, config_type_icon, home_config_type_pic, parent_id, ancestors, create_time, create_by, update_time, update_by + from hw_portal_config_type + + + + + + + + insert into hw_portal_config_type + + config_type_classfication, + config_type_name, + home_config_type_name, + config_type_desc, + config_type_icon, + home_config_type_pic, + parent_id, + ancestors, + create_time, + create_by, + update_time, + update_by, + + + #{configTypeClassfication}, + #{configTypeName}, + #{homeConfigTypeName}, + #{configTypeDesc}, + #{configTypeIcon}, + #{homeConfigTypePic}, + #{parentId}, + #{ancestors}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_portal_config_type + + config_type_classfication = #{configTypeClassfication}, + config_type_name = #{configTypeName}, + home_config_type_name = #{homeConfigTypeName}, + config_type_desc = #{configTypeDesc}, + config_type_icon = #{configTypeIcon}, + home_config_type_pic = #{homeConfigTypePic}, + parent_id = #{parentId}, + ancestors = #{ancestors}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where config_type_id = #{configTypeId} + + + + delete from hw_portal_config_type where config_type_id = #{configTypeId} + + + + delete from hw_portal_config_type where config_type_id in + + #{configTypeId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwProductCaseInfoMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + select case_info_id, case_info_title, config_type_id, typical_flag, case_info_desc, case_info_pic, case_info_html, create_time, create_by, update_time, update_by from hw_product_case_info hpci + + + + select case_info_id, case_info_title, config_type_id, typical_flag, case_info_desc, case_info_pic, create_time, create_by, update_time, update_by from hw_product_case_info hpci + + + + + + + + + insert into hw_product_case_info + + case_info_title, + config_type_id, + typical_flag, + case_info_desc, + case_info_pic, + case_info_html, + create_time, + create_by, + update_time, + update_by, + + + #{caseInfoTitle}, + #{configTypeId}, + #{typicalFlag}, + #{caseInfoDesc}, + #{caseInfoPic}, + #{caseInfoHtml}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_case_info + + case_info_title = #{caseInfoTitle}, + config_type_id = #{configTypeId}, + typical_flag = #{typicalFlag}, + case_info_desc = #{caseInfoDesc}, + case_info_pic = #{caseInfoPic}, + case_info_html = #{caseInfoHtml}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where case_info_id = #{caseInfoId} + + + + delete from hw_product_case_info where case_info_id = #{caseInfoId} + + + + delete from hw_product_case_info where case_info_id in + + #{caseInfoId} + + + + + + + select hpci.case_info_id, hpci.case_info_title, hpci.config_type_id, hpci.typical_flag, hpci.case_info_desc, hpci.case_info_pic, hpci.create_time, + hpci.create_by, hpci.update_time, hpci.update_by,hpct.config_type_name from hw_product_case_info hpci left join hw_portal_config_type hpct on hpci.config_type_id=hpct.config_type_id + + + + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwProductInfoDetailMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + + + select product_info_detail_id, parent_id, product_info_id, config_modal, product_info_detail_title, product_info_detail_desc, product_info_detail_order, product_info_detail_pic, ancestors, create_time, create_by, update_time, update_by + from hw_product_info_detail + + + + + + + + insert into hw_product_info_detail + + parent_id, + product_info_id, + config_modal, + product_info_detail_title, + product_info_detail_desc, + product_info_detail_order, + product_info_detail_pic, + ancestors, + create_time, + create_by, + update_time, + update_by, + + + #{parentId}, + #{productInfoId}, + #{configModal}, + #{productInfoDetailTitle}, + #{productInfoDetailDesc}, + #{productInfoDetailOrder}, + #{productInfoDetailPic}, + #{ancestors}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_info_detail + + parent_id = #{parentId}, + product_info_id = #{productInfoId}, + config_modal = #{configModal}, + product_info_detail_title = #{productInfoDetailTitle}, + product_info_detail_desc = #{productInfoDetailDesc}, + product_info_detail_order = #{productInfoDetailOrder}, + product_info_detail_pic = #{productInfoDetailPic}, + ancestors = #{ancestors}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where product_info_detail_id = #{productInfoDetailId} + + + + delete from hw_product_info_detail where product_info_detail_id = #{productInfoDetailId} + + + + delete from hw_product_info_detail where product_info_detail_id in + + #{productInfoDetailId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwProductInfoMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select product_info_id, config_type_id, tab_flag, config_modal, product_info_etitle, product_info_ctitle, product_info_order, create_time, create_by, update_time, update_by from hw_product_info + + + + + + + + insert into hw_product_info + + config_type_id, + tab_flag, + config_modal, + product_info_etitle, + product_info_ctitle, + product_info_order, + create_time, + create_by, + update_time, + update_by, + + + #{configTypeId}, + #{tabFlag}, + #{configModal}, + #{productInfoEtitle}, + #{productInfoCtitle}, + #{productInfoOrder}, + #{createTime}, + #{createBy}, + #{updateTime}, + #{updateBy}, + + + + + update hw_product_info + + config_type_id = #{configTypeId}, + tab_flag = #{tabFlag}, + config_modal = #{configModal}, + product_info_etitle = #{productInfoEtitle}, + product_info_ctitle = #{productInfoCtitle}, + product_info_order = #{productInfoOrder}, + create_time = #{createTime}, + create_by = #{createBy}, + update_time = #{updateTime}, + update_by = #{updateBy}, + + where product_info_id = #{productInfoId} + + + + delete from hw_product_info where product_info_id = #{productInfoId} + + + + delete from hw_product_info where product_info_id in + + #{productInfoId} + + + + + + + + + + + + + select hpi.product_info_id, hpi.config_type_id, hpi.tab_flag, hpi.config_modal, hpi.product_info_etitle, hpi.product_info_ctitle, hpi.product_info_order, + hpi.create_time, hpi.create_by, hpi.update_time, hpi.update_by,hpct.config_type_name from hw_product_info hpi left join hw_portal_config_type hpct on hpi.config_type_id=hpct.config_type_id + + + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwSearchMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwWebDocumentMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + select document_id, tenant_id, document_address, create_time, web_code, secretKey , + json, type, + is_delete + from hw_web_document + + + + + + + + insert into hw_web_document + + document_id, + tenant_id, + document_address, + create_time, + web_code, + secretKey, + json, + type, + is_delete, + + + #{documentId}, + #{tenantId}, + #{documentAddress}, + #{createTime}, + #{webCode}, + #{secretKey}, + #{json}, + #{type}, + + #{isDelete}, + '0', + + + + + + update hw_web_document + + document_id = #{documentId}, + tenant_id = #{tenantId}, + document_address = #{documentAddress}, + create_time = #{createTime}, + web_code = #{webCode}, + + secretKey = + NULL + #{secretKey} + , + + json = #{json}, + type = #{type}, + + where document_id = #{documentId} + + + + update hw_web_document set is_delete = '1' where document_id = #{documentId} + + + + update hw_web_document set is_delete = '1' where document_id in + + #{documentId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwWebMapper.xml` + +```xml + + + + + + + + + + + + + + + select web_id, web_json, web_json_string, web_code, + web_json_english, + is_delete + from hw_web + + + + + + + + insert into hw_web + + web_json, + web_json_string, + web_code, + web_json_english, + is_delete, + + + #{webJson}, + #{webJsonString}, + #{webCode}, + #{webJsonEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web + + web_json = #{webJson}, + web_json_string = #{webJsonString}, + + web_json_english = #{webJsonEnglish}, + + where web_code = #{webCode} + + + + update hw_web set is_delete = '1' where web_id = #{webId} + + + + update hw_web set is_delete = '1' where web_id in + + #{webId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwWebMapper1.xml` + +```xml + + + + + + + + + + + + + + + + + select web_id, web_json, web_json_string, web_code, + device_id, typeId, web_json_english, + is_delete + from hw_web1 + + + + + + + + + + insert into hw_web1 + + web_json, + web_json_string, + web_code, + device_id, + typeId, + web_json_english, + is_delete, + + + #{webJson}, + #{webJsonString}, + #{webCode}, + #{deviceId}, + #{typeId}, + #{webJsonEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web1 + + web_json = #{webJson}, + web_json_string = #{webJsonString}, + + + + web_json_english = #{webJsonEnglish}, + + where web_code = #{webCode} + and device_id = #{deviceId} + and typeId = #{typeId} + + + + update hw_web1 set is_delete = '1' where web_id = #{webId} + + + + update hw_web1 set is_delete = '1' where web_id in + + #{webId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwWebMenuMapper.xml` + +```xml + + + + + + + + + + + + + + + + + + + + select web_menu_id, parent, ancestors, status, web_menu_name, tenant_id, web_menu__pic, web_menu_type, + order, + web_menu_name_english, + is_delete + from hw_web_menu + + + + + + + + insert into hw_web_menu + + web_menu_id, + parent, + ancestors, + status, + web_menu_name, + tenant_id, + web_menu__pic, + web_menu_type, + `order`, + web_menu_name_english, + is_delete, + + + #{webMenuId}, + #{parent}, + #{ancestors}, + #{status}, + #{webMenuName}, + #{tenantId}, + #{webMenuPic}, + #{webMenuType}, + #{order}, + #{webMenuNameEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web_menu + + parent = #{parent}, + ancestors = #{ancestors}, + status = #{status}, + web_menu_name = #{webMenuName}, + tenant_id = #{tenantId}, + web_menu__pic = #{webMenuPic}, + web_menu_type = #{webMenuType}, + `order` = #{order}, + web_menu_name_english = #{webMenuNameEnglish}, + + where web_menu_id = #{webMenuId} + + + + update hw_web_menu set is_delete = '1' where web_menu_id = #{webMenuId} + + + + update hw_web_menu set is_delete = '1' where web_menu_id in + + #{webMenuId} + + + +``` + +### `ruoyi-portal/src/main/resources/mapper/portal/HwWebMenuMapper1.xml` + +```xml + + + + + + + + + + + + + + + + + + + + select web_menu_id, parent, ancestors, status, web_menu_name, tenant_id, web_menu__pic, + value, + web_menu_type, + web_menu_name_english, + is_delete + from hw_web_menu1 + + + + + + + + insert into hw_web_menu1 + + web_menu_id, + parent, + ancestors, + status, + web_menu_name, + tenant_id, + web_menu__pic, + web_menu_type, + value, + web_menu_name_english, + is_delete, + + + #{webMenuId}, + #{parent}, + #{ancestors}, + #{status}, + #{webMenuName}, + #{tenantId}, + #{webMenuPic}, + #{webMenuType}, + #{value}, + #{webMenuNameEnglish}, + + #{isDelete}, + '0', + + + + + + update hw_web_menu1 + + parent = #{parent}, + ancestors = #{ancestors}, + status = #{status}, + web_menu_name = #{webMenuName}, + tenant_id = #{tenantId}, + web_menu__pic = #{webMenuPic}, + web_menu_type = #{webMenuType}, + value = #{value}, + web_menu_name_english = #{webMenuNameEnglish}, + + where web_menu_id = #{webMenuId} + + + + update hw_web_menu1 set is_delete = '1' where web_menu_id = #{webMenuId} + + + + update hw_web_menu1 set is_delete = '1' where web_menu_id in + + #{webMenuId} + + + +``` diff --git a/Admin.NET-v2/HW_PORTAL_搜索改造方案.md b/Admin.NET-v2/HW_PORTAL_搜索改造方案.md new file mode 100644 index 0000000..635bb73 --- /dev/null +++ b/Admin.NET-v2/HW_PORTAL_搜索改造方案.md @@ -0,0 +1,996 @@ +# hw-portal 搜索改造方案 + +## 1. 文档目的 + +- 文档范围:给 `Admin.NET-v2` 中的 `hw-portal` 插件模块设计一套新的搜索方案。 +- 目标结果:使用 `EF Core 10 + MySQL 普通搜索引擎(全文检索 + 兜底模糊搜索)`,替换当前基于 `UNION ALL + LIKE` 的关键词搜索。 +- 约束来源:基于当前对话已确认,不做向量搜索,不接大模型,不接本地 embedding 模型,保持现有搜索接口不变,允许新增搜索索引表与数据库索引。 +- 文档用途:供后续开发、代码评审、数据库设计、前后端联调、测试验收使用。 + +--- + +## 2. 当前理解 + +## 2.1 当前项目结构理解 + +- 后端主框架:`Furion + SqlSugar` +- 当前新增的门户模块:`Admin.NET\Plugins\Admin.NET.Plugin.HwPortal` +- 当前 `hw-portal` 已经是独立插件项目,但搜索仍是“兼容 MyBatis XML”的方案,不是 EF Core 方案。 +- 当前仓库里没有发现 `EF Core`、`DbContext`、`Npgsql/Pomelo/MySql EF Provider` 等依赖,说明 EF Core 搜索子系统需要从零引入。 + +## 2.2 当前搜索实现理解 + +当前搜索主入口: + +- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs) +- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs) + +当前搜索核心实现: + +- [HwSearchService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs) + +当前搜索 SQL: + +- [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml) + +当前索引重建实现: + +- [IHwSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/IHwSearchRebuildService.cs) +- [HwNoopSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs) + +当前文本抽取器: + +- [PortalSearchDocConverter.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs) + +## 2.3 当前搜索的实际问题 + +1. 搜索结果直接查业务表,`UNION ALL + LIKE` 性能一般。 +2. 搜索和业务表强耦合,新增来源或调整评分成本高。 +3. 重建接口当前是空实现,没有真正索引重建能力。 +4. 关键词搜索只能做字符匹配,不是“搜索引擎式”的独立索引架构。 +5. 当前不是 EF Core 方案,也没有独立搜索库/搜索表/全文索引。 + +--- + +## 3. 对话中的选择结论 + +本方案不是拍脑袋决定,而是基于当前对话确认的选择: + +### 3.1 已确认选择 + +- 不做 `Vector Search` +- 不使用大模型 +- 可以做“普通搜索引擎” +- 保持原接口不变 +- 允许改数据库结构 +- 搜索相关改造切到 `EF Core 10` + +### 3.2 因此得出的最终方案 + +不是: + +- `EF Core 10 + Vector Search` +- `Elasticsearch` +- `Easy-ES` +- 继续使用 `MyBatis XML + LIKE` + +而是: + +- `EF Core 10 + MySQL 全文检索(FULLTEXT)` +- 辅助 `LIKE` 兜底 +- 独立搜索索引表 +- 独立搜索 `DbContext` +- 新旧搜索逻辑保留过渡接口,但默认切新方案 + +--- + +## 4. 具体需求 + +## 4.1 业务需求 + +1. 前台搜索继续支持: + - 关键词搜索 + - 分页 + - 命中摘要 + - 高亮 + - 路由跳转 +2. 编辑端搜索继续支持: + - 编辑路由 `editRoute` +3. 搜索来源继续覆盖: + - 菜单 `hw_web_menu` + - 页面 `hw_web` + - 页面详情 `hw_web1` + - 资料文档 `hw_web_document` + - 配置类型 `hw_portal_config_type` +4. 后台继续保留“重建索引”接口。 + +## 4.2 技术需求 + +1. 新搜索查询必须使用 `EF Core 10` +2. 新搜索必须有独立搜索索引表,而不是每次直查原业务表 +3. 新搜索必须兼容 MySQL +4. 现有公开接口路径与返回 DTO 不变 +5. 要支持增量同步索引,不允许每次保存都全量扫描业务表 + +--- + +## 5. 应用场景分析 + +## 5.1 用户搜索官网内容 + +典型场景: + +- 用户输入“工业物联网” +- 需要搜到: + - 产品中心配置类型 + - 产品页面 JSON 文本 + - 页面详情 JSON 文本 + - 相关文档 + - 菜单项 + +要求: + +- 响应时间稳定 +- 标题命中优先 +- 摘要可读 +- 可跳前台页面 + +## 5.2 编辑端搜索 + +典型场景: + +- 运营同事在后台搜索某个页面或某个资料文档 +- 需要直接跳到编辑页 + +要求: + +- 复用同一套索引 +- 只是在结果里补 `editRoute` + +## 5.3 数据更新后的搜索可见性 + +典型场景: + +- 新增/修改页面 JSON +- 更新资料文档 +- 新增菜单 + +要求: + +- 保存成功后索引同步更新 +- 不依赖手工全量重建 + +--- + +## 6. 技术选型分析 + +## 6.1 为什么不继续用当前 SQL 搜索 + +当前 [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml) 的特点: + +- 多张表 `UNION ALL` +- 每张表都做 `%keyword%` +- 排序靠写死分数和更新时间 + +优点: + +- 简单 +- 快速可用 + +问题: + +- 性能扩展性差 +- 搜索能力依赖业务表结构 +- 不像真正搜索引擎 +- 不利于后续继续增加来源 + +## 6.2 为什么不选向量搜索 + +向量搜索至少需要: + +1. 文本 embedding 模型 +2. 向量字段 +3. 相似度检索能力 + +当前对话里已明确: + +- 不做向量搜索 +- 不用大模型 + +所以本次方案不能再写成 `Vector Search`。 + +## 6.3 为什么选 MySQL FULLTEXT + 独立搜索表 + +优点: + +1. 不依赖外部搜索引擎 +2. 不依赖模型 +3. 能明显优于当前 `LIKE` 方案 +4. 可以保留原有 DTO 和接口 +5. 可先做成普通搜索引擎,后续再升级成 ES/向量检索 + +## 6.4 为什么引入 EF Core 10 + +虽然主项目主链路是 `SqlSugar + Furion`,但搜索子系统可以独立引入 EF Core,理由如下: + +1. 搜索索引表是新增子系统,不必受当前 MyBatis 兼容层约束 +2. EF Core 对“独立读写模型 + 独立迁移 + 查询表达式”很合适 +3. 后续若要演进到更多数据库特性,EF Core 子系统维护更清晰 + +--- + +## 7. 改造后的目标架构 + +```text +用户请求 /portal/search + ↓ +HwSearchController + ↓ +HwSearchService(新) + ↓ +HwPortalSearchDbContext + ↓ +hw_portal_search_doc(搜索索引表) + ↓ +MySQL FULLTEXT / LIKE 兜底 +``` + +业务数据更新流程: + +```text +业务保存成功(页面/菜单/文档/配置类型) + ↓ +对应业务 Service + ↓ +IHwSearchIndexService.UpsertAsync(...) + ↓ +PortalSearchDocConverter + ↓ +hw_portal_search_doc +``` + +--- + +## 8. 数据库设计方案 + +## 8.1 新增搜索索引表 + +建议表名: + +```sql +hw_portal_search_doc +``` + +建议字段: + +```sql +CREATE TABLE `hw_portal_search_doc` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', + `doc_id` VARCHAR(128) NOT NULL COMMENT '文档唯一标识,例如 menu:1、web:7', + `source_type` VARCHAR(32) NOT NULL COMMENT '来源类型:menu/web/web1/document/configType', + `biz_id` VARCHAR(64) NULL COMMENT '业务主键字符串', + `title` VARCHAR(500) NULL COMMENT '搜索标题', + `content` LONGTEXT NULL COMMENT '搜索正文', + `web_code` VARCHAR(64) NULL COMMENT '页面编码', + `type_id` VARCHAR(64) NULL COMMENT '类型ID', + `device_id` VARCHAR(64) NULL COMMENT '设备ID', + `menu_id` VARCHAR(64) NULL COMMENT '菜单ID', + `document_id` VARCHAR(64) NULL COMMENT '文档ID', + `base_score` INT NOT NULL DEFAULT 0 COMMENT '基础分', + `route` VARCHAR(255) NULL COMMENT '展示路由', + `route_query_json` JSON NULL COMMENT '展示路由参数', + `edit_route` VARCHAR(255) NULL COMMENT '编辑路由', + `is_delete` CHAR(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除', + `updated_at` DATETIME NULL COMMENT '业务更新时间', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '索引创建时间', + `modified_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '索引更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_hw_portal_search_doc_doc_id` (`doc_id`), + KEY `idx_hw_portal_search_doc_source_type` (`source_type`), + KEY `idx_hw_portal_search_doc_updated_at` (`updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='hw-portal 搜索索引表'; +``` + +全文索引: + +```sql +ALTER TABLE `hw_portal_search_doc` +ADD FULLTEXT KEY `ft_hw_portal_search_doc_title_content` (`title`, `content`); +``` + +## 8.2 设计理由 + +- `doc_id`:保证一个业务对象只对应一条搜索文档 +- `source_type`:保留来源分类 +- `title/content`:统一搜索输入 +- `route/edit_route`:避免查询后再做过多二次拼装 +- `updated_at`:用于排序 +- `base_score`:保留当前旧搜索里“不同来源不同基础权重”的思路 + +--- + +## 9. 代码改造总览 + +## 9.1 现有代码保留 + +保留但用途调整: + +- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs) +- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs) +- [SearchPageDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs) +- [SearchResultDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs) +- [PortalSearchDocConverter.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/PortalSearchDocConverter.cs) + +## 9.2 现有代码废弃或降级为兼容模式 + +- [HwSearchMapper.xml](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Sql/HwSearchMapper.xml) +- [HwSearchService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwSearchService.cs) 当前 XML 方案 +- [HwNoopSearchRebuildService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/Search/HwNoopSearchRebuildService.cs) + +这些文件可以: + +- 保留一版作为 `legacy` 回退 +- 或重命名为 `LegacyHwSearchService` + +## 9.3 建议新增文件位置 + +推荐新增目录: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf +``` + +建议文件: + +- `SearchEf/HwPortalSearchDbContext.cs` +- `SearchEf/Entity/HwPortalSearchDoc.cs` +- `SearchEf/Option/HwPortalSearchOptions.cs` +- `SearchEf/Service/IHwSearchIndexService.cs` +- `SearchEf/Service/HwSearchIndexService.cs` +- `SearchEf/Service/HwSearchQueryService.cs` +- `SearchEf/Service/HwSearchRebuildService.cs` +- `SearchEf/Extensions/HwPortalSearchServiceCollectionExtensions.cs` + +如希望保持插件结构更规整,也可以落在: + +- `Entity/Search` +- `Service/SearchEf` +- `Option` + +--- + +## 10. 具体代码设计 + +## 10.1 搜索索引实体 + +建议文件: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Entity\HwPortalSearchDoc.cs +``` + +建议代码: + +```csharp +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Admin.NET.Plugin.HwPortal.SearchEf.Entity; + +/// +/// hw-portal 搜索索引实体。 +/// 这不是业务原表,而是专门为搜索准备的索引表。 +/// +[Table("hw_portal_search_doc")] +[Index(nameof(DocId), IsUnique = true)] +[Index(nameof(SourceType))] +[Index(nameof(UpdatedAt))] +public class HwPortalSearchDoc +{ + [Key] + public long Id { get; set; } + + /// + /// 文档唯一键。 + /// 例如:menu:12、web:7、document:abc001。 + /// 用它实现幂等更新。 + /// + [Required] + [MaxLength(128)] + public string DocId { get; set; } = string.Empty; + + /// + /// 来源类型。 + /// 值域与当前 PortalSearchDocConverter 里的常量保持一致。 + /// + [Required] + [MaxLength(32)] + public string SourceType { get; set; } = string.Empty; + + [MaxLength(64)] + public string? BizId { get; set; } + + [MaxLength(500)] + public string? Title { get; set; } + + /// + /// 正文。 + /// 这里通常会存“已抽取、已清洗”的文本,而不是原始 JSON。 + /// + public string? Content { get; set; } + + [MaxLength(64)] + public string? WebCode { get; set; } + + [MaxLength(64)] + public string? TypeId { get; set; } + + [MaxLength(64)] + public string? DeviceId { get; set; } + + [MaxLength(64)] + public string? MenuId { get; set; } + + [MaxLength(64)] + public string? DocumentId { get; set; } + + /// + /// 基础分。 + /// 保留旧系统“菜单比正文更高”的评分思想。 + /// + public int BaseScore { get; set; } + + [MaxLength(255)] + public string? Route { get; set; } + + public string? RouteQueryJson { get; set; } + + [MaxLength(255)] + public string? EditRoute { get; set; } + + [MaxLength(1)] + public string IsDelete { get; set; } = "0"; + + public DateTime? UpdatedAt { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime ModifiedAt { get; set; } +} +``` + +## 10.2 搜索 DbContext + +建议文件: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\HwPortalSearchDbContext.cs +``` + +建议代码: + +```csharp +using Admin.NET.Plugin.HwPortal.SearchEf.Entity; +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal.SearchEf; + +/// +/// 搜索子系统专用 DbContext。 +/// 只管理搜索相关表,不把整个 hw-portal 都切到 EF Core。 +/// +public class HwPortalSearchDbContext : DbContext +{ + public HwPortalSearchDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet SearchDocs => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // Why: + // 这里做 EF 层的结构约束声明。 + // FULLTEXT 索引建议仍通过 SQL 脚本或迁移脚本手工加,避免 provider 差异。 + modelBuilder.Entity(entity => + { + entity.Property(x => x.Content).HasColumnType("longtext"); + entity.Property(x => x.RouteQueryJson).HasColumnType("json"); + }); + } +} +``` + +## 10.3 搜索索引服务接口 + +建议文件: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\IHwSearchIndexService.cs +``` + +建议代码: + +```csharp +namespace Admin.NET.Plugin.HwPortal.SearchEf.Service; + +/// +/// 搜索索引服务接口。 +/// 负责把业务对象同步成搜索文档。 +/// +public interface IHwSearchIndexService +{ + Task UpsertMenuAsync(HwWebMenu menu, CancellationToken cancellationToken = default); + + Task UpsertWebAsync(HwWeb web, CancellationToken cancellationToken = default); + + Task UpsertWeb1Async(HwWeb1 web1, CancellationToken cancellationToken = default); + + Task UpsertDocumentAsync(HwWebDocument document, CancellationToken cancellationToken = default); + + Task UpsertConfigTypeAsync(HwPortalConfigType configType, CancellationToken cancellationToken = default); + + Task DeleteByDocIdAsync(string docId, CancellationToken cancellationToken = default); + + Task RebuildAllAsync(CancellationToken cancellationToken = default); +} +``` + +## 10.4 搜索查询服务 + +建议文件: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchQueryService.cs +``` + +建议代码片段: + +```csharp +using Admin.NET.Plugin.HwPortal.SearchEf.Entity; +using Microsoft.EntityFrameworkCore; + +namespace Admin.NET.Plugin.HwPortal.SearchEf.Service; + +/// +/// 搜索查询服务。 +/// 负责对索引表做查询,而不是直接查业务表。 +/// +public class HwSearchQueryService +{ + private readonly HwPortalSearchDbContext _db; + + public HwSearchQueryService(HwPortalSearchDbContext db) + { + _db = db; + } + + public async Task> SearchAsync(string keyword, int take, CancellationToken cancellationToken = default) + { + keyword = keyword.Trim(); + + // 第一阶段:先走普通 LIKE 版本,确保功能先跑通。 + // 第二阶段:再补 FromSqlRaw + MATCH AGAINST 做 FULLTEXT 优化。 + IQueryable query = _db.SearchDocs + .AsNoTracking() + .Where(x => x.IsDelete == "0") + .Where(x => + (x.Title != null && EF.Functions.Like(x.Title, $"%{keyword}%")) || + (x.Content != null && EF.Functions.Like(x.Content, $"%{keyword}%"))) + .OrderByDescending(x => x.BaseScore) + .ThenByDescending(x => x.UpdatedAt); + + return await query.Take(take).ToListAsync(cancellationToken); + } +} +``` + +说明: + +- 第一阶段先保证 EF Core 路径可用。 +- 第二阶段建议改成: + - `FromSqlInterpolated` + - `MATCH(title, content) AGAINST (...)` +- 这样能兼顾“先实现”和“后优化”。 + +## 10.5 搜索重建服务 + +建议文件: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchRebuildService.cs +``` + +建议代码片段: + +```csharp +namespace Admin.NET.Plugin.HwPortal.SearchEf.Service; + +/// +/// 全量重建搜索索引。 +/// 当前管理后台的 /portal/search/admin/rebuild 最终会调用这里。 +/// +public class HwSearchRebuildService : IHwSearchRebuildService +{ + private readonly HwPortalSearchDbContext _db; + private readonly IHwSearchIndexService _indexService; + private readonly HwWebMenuService _menuService; + private readonly HwWebService _webService; + private readonly HwWeb1Service _web1Service; + private readonly HwWebDocumentService _documentService; + private readonly HwPortalConfigTypeService _configTypeService; + + public HwSearchRebuildService( + HwPortalSearchDbContext db, + IHwSearchIndexService indexService, + HwWebMenuService menuService, + HwWebService webService, + HwWeb1Service web1Service, + HwWebDocumentService documentService, + HwPortalConfigTypeService configTypeService) + { + _db = db; + _indexService = indexService; + _menuService = menuService; + _webService = webService; + _web1Service = web1Service; + _documentService = documentService; + _configTypeService = configTypeService; + } + + public async Task RebuildAllAsync() + { + // Why: + // 全量重建时,先清空索引表,再重新从业务表扫描一次。 + await _db.Database.ExecuteSqlRawAsync("TRUNCATE TABLE hw_portal_search_doc;"); + + List menus = await _menuService.SelectHwWebMenuList(new HwWebMenu()); + foreach (HwWebMenu item in menus) + { + await _indexService.UpsertMenuAsync(item); + } + + List webs = await _webService.SelectHwWebList(new HwWeb()); + foreach (HwWeb item in webs) + { + await _indexService.UpsertWebAsync(item); + } + + // 其余来源同理继续补齐。 + } +} +``` + +## 10.6 与现有控制器对接 + +保持现有文件位置不变: + +- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs) +- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs) + +建议改造方式: + +- 控制器路径不改 +- 入参不改 +- 返回 DTO 不改 +- 内部调用从旧 `HwSearchService` 切到新 `HwSearchQueryService` + +--- + +## 11. 关键代码改造位置 + +## 11.1 插件启动注册 + +当前文件: + +- [Startup.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs) + +当前问题: + +- 只注册了 XML 执行器 +- `IHwSearchRebuildService` 绑定的是空实现 + +建议新增注册: + +```csharp +using Microsoft.EntityFrameworkCore; + +public void ConfigureServices(IServiceCollection services) +{ + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + + // 新增:注册搜索专用 DbContext。 + // Why: + // 搜索子系统是独立读写模型,不和 SqlSugar 主链路抢职责。 + services.AddDbContext(options => + { + options.UseMySql( + "这里读取配置中的搜索连接串", + ServerVersion.AutoDetect("这里读取配置中的搜索连接串")); + }); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); +} +``` + +## 11.2 业务服务改造点 + +需要接入索引增量同步的文件: + +- [HwWebService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs) +- [HwWeb1Service.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs) +- [HwWebMenuService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs) +- [HwWebDocumentService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs) +- [HwPortalConfigTypeService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs) + +建议改造模式: + +```csharp +public class HwWebService : ITransient +{ + private readonly IHwSearchIndexService _searchIndexService; + + public async Task InsertHwWeb(HwWeb input) + { + long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input); + input.WebId = identity; + + if (identity > 0) + { + // Why: + // 不再做全量重建,而是只更新这一个文档的搜索索引。 + await _searchIndexService.UpsertWebAsync(input); + } + + return identity > 0 ? 1 : 0; + } +} +``` + +--- + +## 12. 配置设计 + +建议新增配置文件: + +```text +Admin.NET\Admin.NET.Application\Configuration\Search.json +``` + +建议内容: + +```json +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + "HwPortalSearch": { + "Engine": "mysql_fulltext", + "EnableLegacyFallback": true, + "ConnectionString": "Server=localhost;Database=hw_portal;Uid=root;Pwd=123456;CharSet=utf8mb4;", + "BatchSize": 500, + "TakeLimit": 500 + } +} +``` + +建议新增配置类: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Option\HwPortalSearchOptions.cs +``` + +--- + +## 13. 返回结构兼容要求 + +现有返回 DTO 不变: + +- [SearchPageDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchPageDTO.cs) +- [SearchResultDTO.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Dto/SearchResultDTO.cs) + +必须保持: + +### SearchPageDTO + +- `total` +- `rows` + +### SearchResultDTO + +- `sourceType` +- `title` +- `snippet` +- `score` +- `route` +- `routeQuery` +- `editRoute` + +这样前端调用逻辑不用改。 + +--- + +## 14. 评分策略建议 + +建议沿用当前评分思路并做增强: + +### 14.1 基础分 + +- menu: 110 +- web1: 90 +- web: 80 +- document: 70 +- configType: 65 + +### 14.2 二次加分 + +- 标题命中:+20 +- 正文命中:+10 +- 最近更新时间更近:排序优先 + +### 14.3 后续可扩展 + +- 标题完整命中额外加分 +- 命中次数加权 +- 前台搜索与编辑搜索使用不同的加权策略 + +--- + +## 15. 方案实施步骤 + +## 15.1 第一步:搭建 EF Core 搜索基础设施 + +1. 引入 EF Core 10 MySQL Provider +2. 新建 `HwPortalSearchDbContext` +3. 新建 `HwPortalSearchDoc` 实体 +4. 新建 `Search.json` 与配置对象 +5. 在插件 `Startup` 中注册 + +## 15.2 第二步:建搜索索引表 + +1. 建表 `hw_portal_search_doc` +2. 建唯一索引 +3. 建普通索引 +4. 建 FULLTEXT 索引 + +## 15.3 第三步:实现索引同步 + +1. 补 `IHwSearchIndexService` +2. 实现 `UpsertMenuAsync` +3. 实现 `UpsertWebAsync` +4. 实现 `UpsertWeb1Async` +5. 实现 `UpsertDocumentAsync` +6. 实现 `UpsertConfigTypeAsync` + +## 15.4 第四步:实现查询服务 + +1. 新建 `HwSearchQueryService` +2. 先实现 `LIKE` 版 EF 查询 +3. 再补 `MATCH AGAINST` +4. 复用现有摘要、高亮、路由逻辑 + +## 15.5 第五步:替换控制器调用 + +1. 保持控制器不变 +2. 改内部服务依赖 +3. 管理端重建接口切到真正重建服务 + +## 15.6 第六步:移除空实现 + +1. 去掉 `HwNoopSearchRebuildService` 默认绑定 +2. 保留旧 XML 搜索作为 `legacy` 回退实现 + +--- + +## 16. 测试方案 + +## 16.1 功能测试 + +1. 搜“工业物联网”,能返回多来源结果 +2. 前台搜索返回 `route` +3. 编辑搜索返回 `editRoute` +4. 标题命中排在正文命中前面 +5. 空结果时返回空分页对象 + +## 16.2 增量同步测试 + +1. 新增页面后立即可搜 +2. 修改文档标题后立即可搜到新标题 +3. 删除菜单后搜索不再命中 + +## 16.3 重建测试 + +1. 执行 `/portal/search/admin/rebuild` +2. 清空索引表后重新生成 +3. 重建后结果数与原业务数据量匹配 + +## 16.4 性能测试 + +1. 对比旧版 `UNION ALL + LIKE` +2. 高频关键词响应时间明显下降 +3. 数据量增长后查询性能仍可接受 + +--- + +## 17. 风险与注意事项 + +## 17.1 风险点 + +1. 当前仓库还未引入 EF Core 依赖 +2. 当前本机没有 .NET SDK,暂时不能做真实编译验证 +3. MySQL FULLTEXT 对中文分词能力有限 +4. 如果业务文本大量是 JSON,必须先清洗文本,否则全文检索效果很差 + +## 17.2 风险应对 + +1. 先保留旧搜索实现作为 `legacy` 回退 +2. 搜索文档始终存“清洗后的文本” +3. 先做功能正确,再做 MATCH AGAINST 优化 +4. 若后续中文搜索质量不够,再评估: + - ngram parser + - ES + - PostgreSQL + 中文分词 + +--- + +## 18. 当前不做的内容 + +本次方案明确不做: + +1. 向量搜索 +2. embedding +3. 大模型召回 +4. Elasticsearch +5. 把整个 `hw-portal` 全量迁到 EF Core + +--- + +## 19. 当前建议的实际落地文件清单 + +### 19.1 必改 + +- [Startup.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Startup.cs) +- [HwSearchController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchController.cs) +- [HwSearchAdminController.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Controllers/HwSearchAdminController.cs) +- [HwWebService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebService.cs) +- [HwWeb1Service.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWeb1Service.cs) +- [HwWebMenuService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebMenuService.cs) +- [HwWebDocumentService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwWebDocumentService.cs) +- [HwPortalConfigTypeService.cs](/C:/D/WORK/NewP/Admin.NET-v2/Admin.NET/Plugins/Admin.NET.Plugin.HwPortal/Service/HwPortalConfigTypeService.cs) + +### 19.2 建议新增 + +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\HwPortalSearchDbContext.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Entity\HwPortalSearchDoc.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Option\HwPortalSearchOptions.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\IHwSearchIndexService.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchIndexService.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchQueryService.cs` +- `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\SearchEf\Service\HwSearchRebuildService.cs` +- `Admin.NET\Admin.NET.Application\Configuration\Search.json` + +--- + +## 20. 总结 + +当前 `hw-portal` 的搜索还不是“搜索引擎”,本质上仍是业务表上的 SQL 模糊匹配。 +本次方案的核心不是“把 LIKE 换成 EF”,而是: + +1. 建独立搜索索引表 +2. 用 EF Core 10 管理搜索索引子系统 +3. 用 MySQL FULLTEXT + LIKE 兜底做普通搜索引擎 +4. 保持现有接口和返回结构不变 +5. 把空的索引重建能力变成真正可用的全量/增量索引能力 + +如果后续你要继续深化,我建议下一份文档再单独写: + +- 《hw-portal 搜索表 SQL 脚本与迁移方案》 +- 《hw-portal 搜索改造详细代码实施清单》 +- 《hw-portal 搜索测试用例与验收清单》 + +最自律帅气聪明的臧辰浩 diff --git a/Admin.NET-v2/HW_PORTAL_本机环境配置与启动教程.md b/Admin.NET-v2/HW_PORTAL_本机环境配置与启动教程.md new file mode 100644 index 0000000..bcec486 --- /dev/null +++ b/Admin.NET-v2/HW_PORTAL_本机环境配置与启动教程.md @@ -0,0 +1,423 @@ +# HW Portal 本机环境配置与启动教程 + +## 1. 文档目的 + +- 适用场景:当前电脑没有任何 C#/.NET 环境,也没有 Visual Studio。 +- 目标:让你可以在当前仓库 `C:\D\WORK\NewP\Admin.NET-v2` 上完成本地环境准备、后端启动、前端启动、`hw-portal` 插件识别,以及数据库和其他配置修改。 +- 说明:本仓库当前后端为 `net8.0;net10.0` 双目标,前端实际依赖以 `Web/package.json` 为准,当前为 `Node >= 18`、`pnpm 10.28.2`、`vite ^7.3.1`、`vue ^3.5.28`。 + +## 2. 必需环境 + +### 2.1 后端必需 + +1. 安装 `.NET SDK 10.x` + - 这是当前项目的主目标框架之一,后续新增的 `hw-portal` 插件也已按 `net8.0;net10.0` 配置。 + - 推荐直接安装最新稳定版 `.NET 10 SDK`。 +2. 建议同时安装 `.NET SDK 8.x` + - 虽然通常高版本 SDK 可以处理低版本目标框架,但本项目是双目标,装上 8.x 能减少还原/构建时的目标框架兼容风险。 +3. 安装 `ASP.NET Core Runtime` + - 若已安装 SDK,通常已包含开发所需运行时。 +4. 数据库 + - 当前默认配置是 `Sqlite`,不需要额外安装数据库服务。 + - 如果后续切换到 `MySql`、`PostgreSQL`、`SqlServer`,再安装对应数据库即可。 + +### 2.2 前端必需 + +1. 安装 `Node.js 18+` +2. 安装 `pnpm` + - 推荐:`npm install -g pnpm` +3. 建议安装 `Git` + +### 2.3 编辑器建议 + +- 不装 Visual Studio 也可以开发。 +- 推荐最小组合: + - `VS Code` + - `C# Dev Kit` + - `C#` + - `Vue - Official` + - `TypeScript Vue Plugin (Volar)` + - `ESLint` + +### 2.4 PowerShell + +- 当前仓库部分前端脚本使用 `pwsh`,建议安装 `PowerShell 7`。 +- 如果只跑最基本的前后端启动,Windows 自带 PowerShell 也基本够用。 + +## 3. Windows 新机器安装顺序 + +### 3.1 推荐顺序 + +1. 安装 `Git` +2. 安装 `Node.js 18+` +3. 执行 `npm install -g pnpm` +4. 安装 `.NET SDK 10.x` +5. 建议再安装 `.NET SDK 8.x` +6. 安装 `VS Code` +7. 安装 VS Code 扩展:`C# Dev Kit`、`C#`、`Vue - Official` + +### 3.2 安装后自检命令 + +在 PowerShell 里执行: + +```powershell +git --version +node -v +pnpm -v +dotnet --list-sdks +dotnet --info +``` + +至少应满足: + +- `node` 版本 `>= 18` +- `pnpm` 可用 +- `dotnet --list-sdks` 能看到 `8.x` 和/或 `10.x` + +## 4. 项目启动教程 + +## 4.1 打开仓库 + +仓库根目录: + +```text +C:\D\WORK\NewP\Admin.NET-v2 +``` + +## 4.2 后端启动 + +后端解决方案目录: + +```text +C:\D\WORK\NewP\Admin.NET-v2\Admin.NET +``` + +首次执行: + +```powershell +cd C:\D\WORK\NewP\Admin.NET-v2 +dotnet restore .\Admin.NET\Admin.NET.sln +dotnet build .\Admin.NET\Admin.NET.sln -c Debug +``` + +启动后端: + +```powershell +dotnet run --project .\Admin.NET\Admin.NET.Web.Entry +``` + +默认监听地址来自: + +```text +Admin.NET\Admin.NET.Application\Configuration\App.json +``` + +当前默认端口: + +```json +"Urls": "http://*:5005" +``` + +也就是本机通常访问: + +```text +http://localhost:5005 +``` + +## 4.3 前端启动 + +前端目录: + +```text +C:\D\WORK\NewP\Admin.NET-v2\Web +``` + +首次执行: + +```powershell +cd C:\D\WORK\NewP\Admin.NET-v2 +pnpm install --dir Web +``` + +启动前端: + +```powershell +pnpm --dir Web dev +``` + +当前前端开发端口来自: + +```text +Web\.env +``` + +默认是: + +```text +VITE_PORT = 8888 +``` + +所以前端开发地址通常是: + +```text +http://localhost:8888 +``` + +## 4.4 前后端联调 + +当前前端开发环境接口地址来自: + +```text +Web\.env.development +``` + +默认值: + +```text +VITE_API_URL = http://localhost:5005 +``` + +因此标准联调顺序是: + +1. 先启动后端 `5005` +2. 再启动前端 `8888` +3. 浏览器访问 `http://localhost:8888` + +## 4.5 默认账号 + +前端开发环境默认登录信息来自: + +```text +Web\.env.development +``` + +当前值: + +```text +VITE_DEFAULT_USER = superAdmin.NET +VITE_DEFAULT_USER_PASSWORD = Admin.NET++010101 +``` + +## 5. 数据库配置修改教程 + +## 5.1 当前默认数据库 + +当前后端默认数据库配置文件: + +```text +Admin.NET\Admin.NET.Application\Configuration\Database.json +``` + +当前默认是: + +- `DbType = Sqlite` +- `ConnectionString = DataSource=./Admin.NET.db` + +这意味着: + +- 默认直接在后端运行目录生成/使用 SQLite 文件 +- 本机第一次启动时,如果初始化开关打开,会自动建库建表和种子 + +## 5.2 改成 MySQL / PostgreSQL / SQL Server + +你主要只改这里: + +```text +Admin.NET\Admin.NET.Application\Configuration\Database.json +``` + +重点字段: + +- `DbConnection.ConnectionConfigs[0].DbType` +- `DbConnection.ConnectionConfigs[0].ConnectionString` +- `DbSettings` +- `TableSettings` +- `SeedSettings` + +示例思路: + +- MySQL:把 `DbType` 改成 `MySql` +- PostgreSQL:把 `DbType` 改成 `PostgreSQL` +- SQL Server:把 `DbType` 改成 `SqlServer` + +首次迁移 `hw-portal` 数据时建议: + +- 先确认连接串可用 +- 首次导入已有业务库时,谨慎检查 `EnableInitDb`、`EnableInitTable`、`EnableInitSeed` +- 如果数据库表结构已经存在且不希望框架自动处理,建议酌情关闭初始化开关 + +## 5.3 与 `hw-portal` 相关的数据库说明 + +- `hw-portal` 当前是插件模块 +- 其 SQL 原文保存在: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\Sql +``` + +- 这些 XML 是从 `ruoyi-portal` 原样拷贝过来的,当前迁移策略是: + - 表结构不变 + - SQL 不变 + - C# 侧通过 XML 执行器读取并执行这些 SQL + +## 6. 其余配置文件地址 + +## 6.1 后端核心启动与宿主 + +- 后端入口: + - `Admin.NET\Admin.NET.Web.Entry\Program.cs` +- 后端核心装配: + - `Admin.NET\Admin.NET.Web.Core\Startup.cs` + +## 6.2 后端主要配置目录 + +统一配置目录: + +```text +Admin.NET\Admin.NET.Application\Configuration +``` + +常用文件如下: + +- 应用地址、端口、动态 API、CORS、异常: + - `Admin.NET\Admin.NET.Application\Configuration\App.json` +- 数据库: + - `Admin.NET\Admin.NET.Application\Configuration\Database.json` +- 缓存: + - `Admin.NET\Admin.NET.Application\Configuration\Cache.json` +- JWT: + - `Admin.NET\Admin.NET.Application\Configuration\JWT.json` +- 限流: + - `Admin.NET\Admin.NET.Application\Configuration\Limit.json` +- 日志: + - `Admin.NET\Admin.NET.Application\Configuration\Logging.json` +- 事件总线: + - `Admin.NET\Admin.NET.Application\Configuration\EventBus.json` +- Swagger: + - `Admin.NET\Admin.NET.Application\Configuration\Swagger.json` +- 上传: + - `Admin.NET\Admin.NET.Application\Configuration\Upload.json` +- OAuth: + - `Admin.NET\Admin.NET.Application\Configuration\OAuth.json` +- 邮件: + - `Admin.NET\Admin.NET.Application\Configuration\Email.json` +- 短信: + - `Admin.NET\Admin.NET.Application\Configuration\SMS.json` +- 验证码: + - `Admin.NET\Admin.NET.Application\Configuration\Captcha.json` +- 微信: + - `Admin.NET\Admin.NET.Application\Configuration\Wechat.json` +- 支付宝: + - `Admin.NET\Admin.NET.Application\Configuration\Alipay.json` +- ElasticSearch: + - `Admin.NET\Admin.NET.Application\Configuration\ElasticSearch.json` + +## 6.3 前端配置入口 + +- 前端依赖与脚本: + - `Web\package.json` +- Vite 配置: + - `Web\vite.config.ts` +- 通用前端环境变量: + - `Web\.env` +- 开发环境变量: + - `Web\.env.development` +- 生产环境变量: + - `Web\.env.production` + +当前最常改的前端配置项: + +- `VITE_PORT` +- `VITE_API_URL` +- `VITE_PUBLIC_PATH` +- `VITE_SM_PUBLIC_KEY` + +## 6.4 `hw-portal` 模块接入文件 + +- 插件项目文件: + - `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\Admin.NET.Plugin.HwPortal.csproj` +- 插件启动注册: + - `Admin.NET\Plugins\Admin.NET.Plugin.HwPortal\Startup.cs` +- 解决方案挂载: + - `Admin.NET\Admin.NET.sln` +- 应用层引用插件: + - `Admin.NET\Admin.NET.Application\Admin.NET.Application.csproj` + +## 7. `hw-portal` 插件模块结构 + +当前模块根目录: + +```text +Admin.NET\Plugins\Admin.NET.Plugin.HwPortal +``` + +结构说明: + +- `Common` + - 放统一返回、基础实体、控制器基类、上下文辅助 +- `Controllers` + - 放与原 Spring Controller 对应的 ASP.NET Controller +- `Dto` + - 放请求/返回 DTO、树结构 DTO、搜索 DTO +- `Entity` + - 放 `hw_*` 实体模型 +- `Infrastructure` + - 放 MyBatis XML 渲染器与执行器 +- `Service` + - 放业务服务 +- `Service\Analytics` + - 放官网访问统计服务 +- `Service\Search` + - 放搜索服务、搜索索引重建接口、文本提取器 +- `Sql` + - 放从 `ruoyi-portal` 原样复制过来的 MyBatis XML + +## 8. 推荐启动顺序 + +## 8.1 第一次启动 + +1. 安装 `.NET SDK` +2. 安装 `Node.js` 和 `pnpm` +3. 执行后端 `restore` +4. 执行后端 `build` +5. 执行后端 `run` +6. 执行前端 `pnpm install` +7. 执行前端 `pnpm --dir Web dev` + +## 8.2 命令清单 + +```powershell +cd C:\D\WORK\NewP\Admin.NET-v2 + +dotnet restore .\Admin.NET\Admin.NET.sln +dotnet build .\Admin.NET\Admin.NET.sln -c Debug +dotnet run --project .\Admin.NET\Admin.NET.Web.Entry +``` + +另开一个终端: + +```powershell +cd C:\D\WORK\NewP\Admin.NET-v2 + +pnpm install --dir Web +pnpm --dir Web dev +``` + +## 9. 当前已知注意事项 + +1. 你这台机器当前没有 .NET 环境,因此现在还不能直接运行后端命令。 +2. 本仓库当前最省事的本地数据库是 `SQLite`,第一次跑通建议先不要切换到外部数据库。 +3. `hw-portal` 目前已经接成插件模块,但后续仍建议在装好 SDK 后做一次完整编译和接口联调。 +4. 若后续你要改端口,后端改 `App.json`,前端改 `.env` / `.env.development`。 + +## 10. 你下一步最小操作建议 + +1. 先安装 `Node.js 18+` +2. 安装 `pnpm` +3. 安装 `.NET SDK 10.x` +4. 建议补装 `.NET SDK 8.x` +5. 用 VS Code 打开仓库 +6. 先跑后端,再跑前端 +7. 能跑通后,再决定是否切换数据库 + +最自律帅气聪明的臧辰浩 diff --git a/Admin.NET-v2/LICENSE-APACHE b/Admin.NET-v2/LICENSE-APACHE new file mode 100644 index 0000000..71093ce --- /dev/null +++ b/Admin.NET-v2/LICENSE-APACHE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright © 2020-2024 zuohuaijun + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Admin.NET-v2/LICENSE-MIT b/Admin.NET-v2/LICENSE-MIT new file mode 100644 index 0000000..8ec5650 --- /dev/null +++ b/Admin.NET-v2/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2020-2024 zuohuaijun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Admin.NET-v2/README.md b/Admin.NET-v2/README.md new file mode 100644 index 0000000..bc35dce --- /dev/null +++ b/Admin.NET-v2/README.md @@ -0,0 +1,64 @@ +
+

+ logo +

+
+

站在巨人肩膀上的 .NET 通用权限开发框架

+ +
+ + +
+ +## 🎁框架介绍 +Admin.NET 是基于 .NET8/10 (Furion/SqlSugar) 实现的通用权限开发框架,前端采用 Vue3+Element-plus+Vite5,整合众多优秀技术和框架,模块插件式开发。集成多租户、缓存、数据校验、鉴权、事件总线、动态API、通讯、远程请求、任务调度、打印等众多黑科技。代码结构简单清晰,注释详尽,易于上手与二次开发,即便是复杂业务逻辑也能迅速实现,真正实现“开箱即用”。 + +面向中小企业快速开发平台框架,框架采用主流技术开发设计,前后端分离架构模式。完美适配国产化软硬件环境,支持国产中间件、国产数据库、麒麟操作系统、Windows、Linux部署使用;集成国密加解密插件,使用SM2、SM3、SM4等国密算法进行签名、数据完整性保护;软件层面全面遵循等级保护测评要求,完全符合等保、密评要求。 + +``` +超高人气的框架(Furion)配合高性能超简单的ORM(SqlSugar)加持,阅历痛点,相见恨晚!让 .NET 开发更简单,更通用,更流行! +``` + +## 🍁说明 +1. 支持各种数据库,后台配置文件自行修改(自动生成数据库及种子数据) +2. 前端运行步骤:1、安装依赖pnpm install 2、运行pnpm run dev 3、打包pnpm run build +4. 在线文档 [https://adminnet.top/](https://adminnet.top/) + +## 📙开发流程 +```bash +1. 建议每个应用系统单独创建一个工程(Admin.NET.Application层只是示例),单独设置各项配置,引用Admin.NET.Core层(非必须不改工程名) + +2. Web层引用新建的应用层工程即可(所有应用系统一个解决方案显示一个后台一套代码搞定,可以自由切换不同应用层) + +# 可以随主仓库升级而升级避免冲突,原则上接口、服务、控制器合并模式不影响自建应用层发挥与使用。若必须修改或补充主框架,也欢迎PR! +``` + + +## 🍖内置功能 + 1. 主控面板:控制台页面,可进行工作台,分析页,统计等功能的展示。 + 2. 用户管理:对企业用户和系统管理员用户的维护,可绑定用户职务,机构,角色,数据权限等。 + 3. 机构管理:公司组织架构维护,支持多层级结构的树形结构。 + 4. 职位管理:用户职务管理,职务可作为用户的一个标签。 + 5. 菜单管理:配置系统菜单,操作权限,按钮权限标识等,包括目录、菜单、按钮。 + 6. 角色管理:角色绑定菜单后,可限制相关角色的人员登录系统的功能范围。角色也可以绑定数据授权范围。 + 7. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 + 8. 访问日志:用户的登录和退出日志的查看和管理。 + 9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 +10. 服务监控:服务器的运行状态,CPU、内存、网络等信息数据的查看。 +11. 在线用户:当前系统在线用户的查看,包括强制下线。基于 SignalR 实现。 +12. 公告管理:系统通知公告信息发布维护,使用 SignalR 实现对用户实时通知。 +13. 文件管理:文件的上传下载查看等操作,文件可使用本地存储,阿里云oss、腾讯cos等接入,支持拓展。 +14. 任务调度:采用 Sundial,.NET 功能齐全的开源分布式作业调度系统。 +15. 系统配置:系统运行的参数的维护,参数的配置与系统运行机制息息相关。 +16. 邮件短信:发送邮件功能、发送短信功能。 +17. 系统接口:使用 Swagger 生成相关 api 接口文档。支持 Knife4jUI 皮肤。 +18. 代码生成:可以一键生成前后端代码,自定义配置前端展示控件,让开发更快捷高效。 +19. 在线构建器:拖动表单元素生成相应的 VUE 代码(支持vue3)。 +20. 对接微信:对接微信小程序开发,包括微信支付。 +21. 导入导出:采用 Magicodes.IE 支持文件导入导出,支持根据H5模板生成PDF等报告文件。 +22. 限流控制:采用 AspNetCoreRateLimit 组件实现对接口访问限制。 +23. ES 日志:通过 NEST 组件实现日志存取到 Elasticsearch 日志系统。 +24. 开放授权:支持OAuth 2.0开放标准授权登录,比如微信。 +25. APIJSON:适配腾讯APIJSON协议,支持后端0代码,[使用文档](https://github.com/liaozb/APIJSON.NET)。 +26. 数据库视图:基于SqlSugar生成查询SQL + 表实体维护视图,可维护性更强。 + diff --git a/Admin.NET-v2/docker/.env.production b/Admin.NET-v2/docker/.env.production new file mode 100644 index 0000000..ceb2de9 --- /dev/null +++ b/Admin.NET-v2/docker/.env.production @@ -0,0 +1,5 @@ +# 线上环境 +ENV = 'production' + +# 线上环境接口地址 +VITE_API_URL = '/prod-api' \ No newline at end of file diff --git a/Admin.NET-v2/docker/README.md b/Admin.NET-v2/docker/README.md new file mode 100644 index 0000000..b0fa730 --- /dev/null +++ b/Admin.NET-v2/docker/README.md @@ -0,0 +1,28 @@ +# 启动前准备 + +* 安装 docker、docker-compose 环境 +* 使用 docker-compose -f docker-compose-builder.yml up 命令编译结果会直接被 docker-compose up使用 发布编译结果跟项目运行全部在linux docker环境 不再需要vs发布编译 +* docker-compose.yml 虽然配置了mysql 跟tdengine环境默认是没启用的,需要的话自行配置数据库链接 + +# 注意事项 + +1. *docker/app/Configuration/Database.json* 文件不需要修改,不要覆盖掉了 +2. *Web/.env.production* 文件配置接口地址配置为 VITE\_API\_URL = '/prod-api' +3. nginx、mysql 配置文件无需修改 +4. redis/redis.conf 中配置密码,如果不设密码 REDIS_PASSWORD 置空,app/Configuration/Cache.json中server=redis:6379,password 置空 + +*** +# 编译命令 +1. *docker-compose -f docker-compose-builder.yml up net-builder* 编译admin.net 项目 +2. *docker-compose -f docker-compose-builder.yml up web-builder* 编译前端 +3. *docker-compose -f docker-compose-builder.yml up down* 移除docker 容器方便下次编译 + + +# 启动命令 + +`docker-compose up -d` + +# 访问入口 + +****** +****** diff --git a/Admin.NET-v2/docker/app/Configuration/App.json b/Admin.NET-v2/docker/app/Configuration/App.json new file mode 100644 index 0000000..c62e2c8 --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/App.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Urls": "http://*:5005", // 配置默认端口 + // "https_port": 44325, + + "AllowedHosts": "*", + + "AppSettings": { + "InjectSpecificationDocument": true // 生产环境是否开启Swagger + }, + "DynamicApiControllerSettings": { + //"DefaultRoutePrefix": "api", // 默认路由前缀 + "CamelCaseSeparator": "", // 驼峰命名分隔符 + "SplitCamelCase": false, // 切割骆驼(驼峰)/帕斯卡命名 + "LowercaseRoute": false, // 小写路由格式 + "AsLowerCamelCase": true, // 小驼峰命名(首字母小写) + "KeepVerb": false, // 保留动作方法请求谓词 + "KeepName": false // 保持原有名称不处理 + }, + "FriendlyExceptionSettings": { + "DefaultErrorMessage": "系统异常,请联系管理员", + "ThrowBah": true, // 是否将 Oops.Oh 默认抛出为业务异常 + "LogError": false // 是否输出异常日志 + }, + "LocalizationSettings": { + "SupportedCultures": [ "zh-CN", "en" ], // 语言列表 + "DefaultCulture": "zh-CN", // 默认语言 + "DateTimeFormatCulture": "zh-CN" // 固定时间区域为特定时区(多语言) + }, + "CorsAccessorSettings": { + "WithExposedHeaders": [ "Content-Disposition", "X-Pagination", "access-token", "x-access-token" ], // 如果前端不代理且是axios请求 + "SignalRSupport": true // 启用 SignalR 跨域支持 + }, + "SnowId": { + "WorkerId": 1, // 机器码 全局唯一 + "WorkerIdBitLength": 2, // 机器码位长 默认值6,取值范围 [1, 19] + "SeqBitLength": 6, // 序列数位长 默认值6,取值范围 [3, 21](建议不小于4,值越大性能越高、Id位数也更长) + "WorkerPrefix": "adminnet_" // 缓存前缀 + }, + "Cryptogram": { + "StrongPassword": false, // 是否开启密码强度验证 + "PasswordStrengthValidation": "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[~@#$%\\*-\\+=:,\\\\?\\[\\]\\{}]).{6,16}$", // 密码强度验证正则表达式,必须须包含大小写字母、数字和特殊字符的组合,长度在6-16之间 + "PasswordStrengthValidationMsg": "密码必须包含大小写字母、数字和特殊字符的组合,长度在6-16之间", // 密码强度验证消息提示 + "CryptoType": "SM2", // 密码加密算法:MD5、SM2、SM4 + "PublicKey": "0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312", // 公钥 + "PrivateKey": "8EDB615B1D48B8BE188FC0F18EC08A41DF50EA731FA28BF409E6552809E3A111" // 私钥 + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/Cache.json b/Admin.NET-v2/docker/app/Configuration/Cache.json new file mode 100644 index 0000000..828d21e --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/Cache.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Cache": { + "Prefix": "adminnet_", // 全局缓存前缀 + "CacheType": "Redis", // Memory、Redis + "Redis": { + "Configuration": "server=redis:6379;password=123456;db=5;", // Redis连接字符串 + "Prefix": "adminnet_", // Redis前缀(目前没用) + "MaxMessageSize": "1048576" // 最大消息大小 默认1024 * 1024 + } + }, + "Cluster": { // 集群配置 + "Enabled": true, // 启用集群:前提开启Redis缓存模式 + "ServerId": "adminnet", // 服务器标识 + "ServerIp": "", // 服务器IP + "SignalR": { + "RedisConfiguration": "redis:6379,ssl=false,password=123456,defaultDatabase=5", + "ChannelPrefix": "signalrPrefix_" + }, + "DataProtecteKey": "AdminNet:DataProtection-Keys", + "IsSentinel": false, // 是否哨兵模式 + "SentinelConfig": { + "DefaultDb": "4", + "EndPoints": [ // 哨兵端口 + // "10.10.0.124:26380" + ], + "MainPrefix": "adminNet:", + "Password": "123456", + "SentinelPassword": "adminNet", + "ServiceName": "adminNet", + "SignalRChannelPrefix": "signalR:" + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/Database.json b/Admin.NET-v2/docker/app/Configuration/Database.json new file mode 100644 index 0000000..1bea92a --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/Database.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + // 详细数据库配置见SqlSugar官网(第一个为默认库) + "DbConnection": { + "EnableConsoleSql": true, // 启用控制台打印SQL + "ConnectionConfigs": [ + { + //"ConfigId": "1300000000001", // 默认库标识-禁止修改 + "DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom + "ConnectionString": "DataSource=./Admin.NET.db", // 库连接字符串 + "DbSettings": { + "EnableInitDb": true, // 启用库初始化 + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": false // 启用驼峰转下划线 + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化 + "EnableIncreTable": false // 启用表增量更新-特性[IncreTable] + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化 + "EnableIncreSeed": false // 启用种子增量更新-特性[IncreSeed] + } + } + + + + //// 日志独立数据库配置 + //{ + // "ConfigId": "1300000000002", // 日志库标识-禁止修改 + // "DbType": "Sqlite", + // "ConnectionString": "DataSource=./Admin.NET.Log.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化 + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化 + // "EnableIncreTable": false // 启用表增量更新-特性[IncreTable] + // }, + // "SeedSettings": { + // "EnableInitSeed": false, // 启用种子初始化 + // "EnableIncreSeed": false // 启用种子增量更新-特性[IncreSeed] + // } + //}, + //// 其他数据库配置(可以配置多个) + //{ + // "ConfigId": "test", // 库标识 + // "DbType": "Sqlite", // 库类型 + // "ConnectionString": "DataSource=./Admin.NET.Test.db", // 库连接字符串 + // "DbSettings": { + // "EnableInitDb": true, // 启用库初始化 + // "EnableDiffLog": false, // 启用库表差异日志 + // "EnableUnderLine": false // 启用驼峰转下划线 + // }, + // "TableSettings": { + // "EnableInitTable": true, // 启用表初始化 + // "EnableIncreTable": false // 启用表增量更新-特性[IncreTable] + // }, + // "SeedSettings": { + // "EnableInitSeed": true, // 启用种子初始化 + // "EnableIncreSeed": false // 启用种子增量更新-特性[IncreSeed] + // } + //} + + + + ] + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/EventBus.json b/Admin.NET-v2/docker/app/Configuration/EventBus.json new file mode 100644 index 0000000..02b5ab2 --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/EventBus.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "EventBus": { + // 事件源存储器类型,默认内存存储(Redis则需要配合缓存相关配置) + "EventSourceType": "Memory", // Memory、Redis、RabbitMQ、Kafka + "RabbitMQ": { + "UserName": "adminnet", + "Password": "adminnet++123456", + "HostName": "127.0.0.1", + "Port": 5672 + }, + "Kafka": { + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/Logging.json b/Admin.NET-v2/docker/app/Configuration/Logging.json new file mode 100644 index 0000000..2f5be95 --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/Logging.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information", + "AspNetCoreRateLimit": "None", + "System.Net.Http.HttpClient": "Warning" + }, + "File": { + "Enabled": true, // 启用文件日志 + "FileName": "logs/{0:yyyyMMdd}_{1}.log", // 日志文件 + "Append": true, // 追加覆盖 + "MinimumLevel": "Error", // 日志级别 + "FileSizeLimitBytes": 10485760, // 10M=10*1024*1024 + "MaxRollingFiles": 30 // 只保留30个文件 + }, + "Database": { + "Enabled": true, // 启用数据库日志 + "MinimumLevel": "Information" // 日志级别 + }, + "ElasticSearch": { + "Enabled": false, // 启用ES日志 + "AuthType": "Basic", // ES认证类型,可选 Basic、ApiKey、Base64ApiKey + "User": "admin", // Basic认证的用户名,使用Basic认证类型时必填 + "Password": "123456", // Basic认证的密码,使用Basic认证类型时必填 + "ApiId": "", // 使用ApiKey认证类型时必填 + "ApiKey": "", // 使用ApiKey认证类型时必填 + "Base64ApiKey": "TmtrOEszNEJuQ0NyaWlydGtROFk6SG1RZ0w3YzBTc2lCanJTYlV3aXNzZw==", // 使用Base64ApiKey认证类型时必填 + "Fingerprint": "37:08:6A:C6:06:CC:9A:43:CF:ED:25:A2:1C:A4:69:57:90:31:2C:06:CA:61:56:39:6A:9C:46:11:BD:22:51:DA", // ES使用Https时的证书指纹 + "ServerUris": [ "http://192.168.1.100:9200" ], // 地址 + "DefaultIndex": "adminnet" // 索引 + }, + "Monitor": { + "GlobalEnabled": true, // 启用全局拦截日志(建议生产环境关闭,否则对性能有影响) + "IncludeOfMethods": [], // 拦截特定方法,当GlobalEnabled=false有效 + "ExcludeOfMethods": [], // 排除特定方法,当GlobalEnabled=true有效 + "BahLogLevel": "Information", // Oops.Oh 和 Oops.Bah 业务日志输出级别 + "WithReturnValue": true, // 是否包含返回值,默认true + "ReturnValueThreshold": 0, // 返回值字符串阈值,默认0全量输出 + "JsonBehavior": "None", // 是否输出Json,默认None(OnlyJson、All) + "JsonIndented": false, // 是否格式化Json + "UseUtcTimestamp": false, // 时间格式UTC、LOCAL + "ConsoleLog": true // 是否显示控制台日志 + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/OAuth.json b/Admin.NET-v2/docker/app/Configuration/OAuth.json new file mode 100644 index 0000000..f2f28aa --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/OAuth.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "OAuth": { + "Weixin": { + "ClientId": "xxx", + "ClientSecret": "xxx" + }, + "Gitee": { + "ClientId": "xxx", + "ClientSecret": "xxx" + } + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/Configuration/Upload.json b/Admin.NET-v2/docker/app/Configuration/Upload.json new file mode 100644 index 0000000..2075742 --- /dev/null +++ b/Admin.NET-v2/docker/app/Configuration/Upload.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json", + + "Upload": { + "Path": "upload/{yyyy}/{MM}/{dd}", // 文件上传目录 + "MaxSize": 51200, // 文件最大限制KB:1024*50 + "ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive" ], + "EnableMd5": false // 启用文件MDF5验证-防止重复上传 + }, + "OSSProvider": { + "Enabled": false, + "Provider": "Minio", // OSS提供者 Invalid/Minio/Aliyun/QCloud/Qiniu/HuaweiCloud + "Endpoint": "xxx.xxx.xxx.xxx:8090", // 节点/API地址(在腾讯云OSS中表示AppId) + "Region": "xxx.xxx.xxx.xxx", // 地域 + "AccessKey": "", + "SecretKey": "", + "IsEnableHttps": false, // 是否启用HTTPS + "IsEnableCache": true, // 是否启用缓存 + "Bucket": "admin.net", + "CustomHost": "" // 自定义Host:拼接外链的Host,若空则使用Endpoint拼接 + }, + "SSHProvider": { + "Enabled": false, + "Host": "127.0.0.1", + "Port": 8222, + "Username": "sshuser", + "Password": "Password.1" + } +} \ No newline at end of file diff --git a/Admin.NET-v2/docker/app/wait-for-it.sh b/Admin.NET-v2/docker/app/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/Admin.NET-v2/docker/app/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/Admin.NET-v2/docker/docker-compose-builder.yml b/Admin.NET-v2/docker/docker-compose-builder.yml new file mode 100644 index 0000000..1caceb1 --- /dev/null +++ b/Admin.NET-v2/docker/docker-compose-builder.yml @@ -0,0 +1,21 @@ +version: "3" + +services: + web-builder: + image: node:22-alpine # 官方Node.js v22镜像(Alpine版仅约180MB):ml-citation{ref="3,4" data="citationList"} + volumes: + - ../Web/:/app # 挂载项目代码目录 + - ./.env.production:/app/.env.production + working_dir: /app + environment: + NODE_ENV: development + command: sh -c "npm install --legacy-peer-deps --registry=https://registry.npmmirror.com && npm run build" + net-builder: + image: mcr.microsoft.com/dotnet/sdk:9.0 + volumes: + - ../Admin.NET/:/app + working_dir: /app + command: dotnet build Admin.NET.sln -c Release + + + diff --git a/Admin.NET-v2/docker/docker-compose.yml b/Admin.NET-v2/docker/docker-compose.yml new file mode 100644 index 0000000..299989b --- /dev/null +++ b/Admin.NET-v2/docker/docker-compose.yml @@ -0,0 +1,110 @@ +version: "3" + +services: + nginx: + image: nginx:1.20.2 + ports: + - "9100:80" + - "9103:443" + environment: + - TZ=Asia/Shanghai + volumes: + - ../Web/dist:/usr/share/nginx/html + - "./nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro" + - "./nginx/key:/etc/nginx/key/" + links: + - adminNet + mysql: + image: mysql:5.7 + ports: + - 9101:3306 + restart: unless-stopped + privileged: true + ulimits: + nproc: 655350 + nofile: + soft: 131072 + hard: 400000 + #healthcheck: + # test: "/usr/bin/mysql --user=root --password=root --execute \"SHOW DATABASES;\"" + # interval: 10s # 间隔时间 + # timeout: 3s # 超时时间 + # retries: 50 # 重试次数 + environment: + MYSQL_ROOT_HOST: "%" + MYSQL_DATABASE: admin + MYSQL_ROOT_PASSWORD: root + TZ: Asia/Shanghai + volumes: + - ./mysql/mysql:/var/lib/mysql + - ./mysql/mysql.cnf:/etc/mysql/conf.d/mysql.cnf + redis: + image: 'redis:latest' # 使用最新版本的 Redis 镜像,也可以指定特定版本如 'redis:6.2.7' + container_name: my-redis # 自定义容器名称 + ports: + - '6379:6379' # 映射宿主机的 6379 端口到容器的 6379 端口 + volumes: # 持久化数据 + - ./redis/redis.conf:/usr/local/etc/redis/redis.conf + - ./redis/data:/data:rw + - ./redis/logs:/logs + #command: ['redis-server', '--appendonly', 'yes'] # 启用AOF持久化 + command: ['redis-server','/usr/local/etc/redis/redis.conf'] + environment: # 设置环境变量,例如密码 + - REDIS_PASSWORD=123456 + minio: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + container_name: minio + restart: always + environment: + - MINIO_ROOT_USER=admin + - MINIO_ROOT_PASSWORD=admin123456 + volumes: + - ./minio/data:/data + - ./minio/config:/root/.minio + ports: + - "9104:9000" # API端口 + - "9105:9001" # 控制台端口 + command: server /data --console-address ":9001" + tdengine: + image: tdengine/tdengine:3.3.6.13 + volumes: + - ./tdengine/taos/dnode/data:/var/lib/taos + - ./tdengine/taos/dnode/log:/var/log/taos + hostname: tdengine + container_name: tdengine + privileged: true + environment: + TAOS_FQDN: "tdengine" + TAOS_FIRST_EP: "tdengine" # 指向首个节点主机名 + TAOS_SECOND_EP: "tdengine" # 可选备用节点 + + + ports: + - 6030:6030 + - 6041:6041 + - 6044-6049:6044-6049 + - 6044-6045:6044-6045/udp + - 6060:6060 + + + + + + + adminNet: + image: mcr.microsoft.com/dotnet/aspnet:9.0 + ports: + - "9102:5050" + environment: + - TZ=Asia/Shanghai + volumes: + - ../Admin.NET/Admin.NET.Web.Entry/bin/Release/net9.0/:/app + - ./app/Configuration/:/app/Configuration/ + - ./app/wait-for-it.sh:/app/wait-for-it.sh + working_dir: /app + command: ["/bin/bash", "-c", "/app/wait-for-it.sh mysql:3306 -t 120 && dotnet Admin.NET.Web.Entry.dll --content-root /app/wwwroot"] + + depends_on: + - mysql + - redis + diff --git a/Admin.NET-v2/docker/mysql/mysql.cnf b/Admin.NET-v2/docker/mysql/mysql.cnf new file mode 100644 index 0000000..cc172bc --- /dev/null +++ b/Admin.NET-v2/docker/mysql/mysql.cnf @@ -0,0 +1,51 @@ +[client] +default-character-set=utf8 + +[mysqld] +character-set-server=utf8 +log-bin=mysql-bin +server-id=1 +pid-file = /var/run/mysqld/mysqld.pid +socket = /var/run/mysqld/mysqld.sock +datadir = /var/lib/mysql +sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION +symbolic-links=0 +secure_file_priv = +wait_timeout=86400 +interactive_timeout=86400 +default-time_zone = '+8:00' +skip-external-locking +skip-name-resolve +open_files_limit = 10240 +max_connections = 1000 +max_connect_errors = 6000 +table_open_cache = 800 +max_allowed_packet = 40m +sort_buffer_size = 2M +join_buffer_size = 1M +thread_cache_size = 32 +query_cache_size = 64M +transaction_isolation = READ-COMMITTED +tmp_table_size = 128M +max_heap_table_size = 128M +log-bin = mysql-bin +sync-binlog = 1 +binlog_format = ROW +binlog_cache_size = 1M +key_buffer_size = 128M +read_buffer_size = 2M +read_rnd_buffer_size = 4M +bulk_insert_buffer_size = 64M +lower_case_table_names = 1 +explicit_defaults_for_timestamp=true +skip_name_resolve = ON +event_scheduler = ON +log_bin_trust_function_creators = 1 +innodb_buffer_pool_size = 512M +innodb_flush_log_at_trx_commit = 1 +innodb_file_per_table = 1 +innodb_log_buffer_size = 4M +innodb_log_file_size = 256M +innodb_max_dirty_pages_pct = 90 +innodb_read_io_threads = 4 +innodb_write_io_threads = 4 \ No newline at end of file diff --git a/Admin.NET-v2/docker/nginx/conf/nginx.conf b/Admin.NET-v2/docker/nginx/conf/nginx.conf new file mode 100644 index 0000000..8484324 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/conf/nginx.conf @@ -0,0 +1,67 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + gzip on; + gzip_min_length 1k; + gzip_buffers 16 64K; + gzip_http_version 1.1; + gzip_comp_level 5; + gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; + gzip_vary on; + gzip_proxied expired no-cache no-store private auth; + gzip_disable "MSIE [1-6]\."; + + server { + listen 443 ssl; + listen 80; + server_name localhost; + charset utf-8; + ssl_certificate /etc/nginx/key/abc.admin.com.pem; + ssl_certificate_key /etc/nginx/key/abc.admin.com.key; + ssl_session_cache shared:SSL:1m; + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + index index.html index.htm; + # 如果缓存了 index.html ,刷新后可能仍会出现更新通知,因此需要禁用 index.html 的缓存。这也是部署 SPA 应用程序的最佳做法。 + if ( $uri = '/index.html' ) { # disabled index.html cache + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + } + + + location /prod-api/hubs { + proxy_pass http://adminNet:5005/hubs; #启用http长连接支持websocket + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + + location /prod-api/ { + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://adminNet:5005/; + } + + + + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } +} diff --git a/Admin.NET-v2/docker/nginx/key/abc.admin.com.crt b/Admin.NET-v2/docker/nginx/key/abc.admin.com.crt new file mode 100644 index 0000000..2bc7a06 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/abc.admin.com.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID6TCCAtGgAwIBAgIUaMxuYItti+c4Rj+p/bT43EdG6mcwDQYJKoZIhvcNAQEL +BQAwZzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDTALBgNVBAcMBExlaGkx +GTAXBgNVBAoMEEFCQyBDb21wYW55LEluYy4xCzAJBgNVBAsMAklUMRIwEAYDVQQD +DAlhZG1pbi5jb20wHhcNMjMwMzI4MDg0NDA0WhcNMzYxMjA0MDg0NDA0WjBrMQsw +CQYDVQQGEwJVUzENMAsGA1UECAwEVXRhaDENMAsGA1UEBwwETGVoaTEZMBcGA1UE +CgwQQUJDIENvbXBhbnksSW5jLjELMAkGA1UECwwCSVQxFjAUBgNVBAMMDWFiYy5h +ZG1pbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCn0L97ITGf +i2KFMBm8lzz9VGfZ0iKMi0ijHH5pVOPqsgA19phQ0DZ6G4Wy1EbdF3wnWZdDEYke +nqxHVykzulFN9W3Tc5Ao67zBZLcBc8Oe9zo88nh/J03ds8nyPhY3zTptAGKsEeoS +Ko0LY7m1Ozf2npLa2E+NH815/+WzMoM3jvi3iCXWJ4bvvhjNPDp+oqY+6wzcU9SM +T+zMvRPoVmVvevVhzSpfH2dhZG44mBcTdkaxnvxuEJm8uAfAA1vFCyM/2n3gDUGO +IxQSv6nNKgVnIqsa9/gWOjR3EoOejw6a+Hlx9hFL9Tv4hsJ0o2AKsjGl5WchNvIt +ONQgeeHAGUMzAgMBAAGjgYgwgYUwQwYDVR0RBDwwOocEfwAAAYcEwKgBAYcEwKgA +/IINYWJjLmFkbWluLmNvbYIMbXguYWRtaW4uY29tgglsb2NhbGhvc3QwHQYDVR0O +BBYEFB0HsgY6EF2yi6+Czg6xUn/25NmzMB8GA1UdIwQYMBaAFJFdp03B0hlqnbc+ +Ko98wa7Wj7EpMA0GCSqGSIb3DQEBCwUAA4IBAQAFAvdmaSGZtyGwbeZtft9R7BHP +dxYDgCYvKZJ0BNtE06E5V6m8b8GilVPLGvDwWftXWPMX5Vq2WSq22fPVqG0pdmPR +bit80G7rlauY5Ub/ws785F9ZoTPM71kO55eHHAFaKqGT7UMGairyk8Je8fYV4SZ/ +YCseDOidSN47Rq1ZRbbc3og5zTtFwrfWuv8uP0acgUayT1cAVg/D3SOmljIp3X+S +5i+OQQK96AC6RbFczL8ErCJwhS0CAwHc1Q3EBTliOBXMeMy/B965DtwQl5rMwCHk +HIFTMIj4006O3tEksR/33DThqENnxj6bKL3Pt0Mu5XkRKVl6k1vf2IgYM29L +-----END CERTIFICATE----- diff --git a/Admin.NET-v2/docker/nginx/key/abc.admin.com.csr b/Admin.NET-v2/docker/nginx/key/abc.admin.com.csr new file mode 100644 index 0000000..c52f366 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/abc.admin.com.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICsDCCAZgCAQAwazELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDTALBgNV +BAcMBExlaGkxGTAXBgNVBAoMEEFCQyBDb21wYW55LEluYy4xCzAJBgNVBAsMAklU +MRYwFAYDVQQDDA1hYmMuYWRtaW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAp9C/eyExn4tihTAZvJc8/VRn2dIijItIoxx+aVTj6rIANfaYUNA2 +ehuFstRG3Rd8J1mXQxGJHp6sR1cpM7pRTfVt03OQKOu8wWS3AXPDnvc6PPJ4fydN +3bPJ8j4WN806bQBirBHqEiqNC2O5tTs39p6S2thPjR/Nef/lszKDN474t4gl1ieG +774YzTw6fqKmPusM3FPUjE/szL0T6FZlb3r1Yc0qXx9nYWRuOJgXE3ZGsZ78bhCZ +vLgHwANbxQsjP9p94A1BjiMUEr+pzSoFZyKrGvf4Fjo0dxKDno8Omvh5cfYRS/U7 ++IbCdKNgCrIxpeVnITbyLTjUIHnhwBlDMwIDAQABoAAwDQYJKoZIhvcNAQELBQAD +ggEBACLBJSKDMGJd97ruygNmlzboOi+1dTbxvYM9dkB5EEbz9ixif5M81fE+/oMk +IbuBiqiKATjBlwXnU5nTBK+llw9r+Ro2pa4pSjEcpfD1cZS9r/ap1EAOxy++x215 +Jt87U2/xFMnYdsmSZgb+LtkaVJyiaK1tW/dbPbqhu0/wWaB394XxnfkJre4u207o +TRMHLaVzU1t8OJ/aFkisoiWKhAQ32DK1gulLUoujBeFmN7HSQpXmIueV9T1nfHFU +2XMYNQ4o1w35rt4n2oAguUlqZ6w8z90xfBbxI3V0QP4ejw5Z4hIRLajDano5GymP +jcTrbtN5b1dUt52DZ73jXaVXdU0= +-----END CERTIFICATE REQUEST----- diff --git a/Admin.NET-v2/docker/nginx/key/abc.admin.com.key b/Admin.NET-v2/docker/nginx/key/abc.admin.com.key new file mode 100644 index 0000000..b01a3f9 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/abc.admin.com.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCn0L97ITGfi2KF +MBm8lzz9VGfZ0iKMi0ijHH5pVOPqsgA19phQ0DZ6G4Wy1EbdF3wnWZdDEYkenqxH +VykzulFN9W3Tc5Ao67zBZLcBc8Oe9zo88nh/J03ds8nyPhY3zTptAGKsEeoSKo0L +Y7m1Ozf2npLa2E+NH815/+WzMoM3jvi3iCXWJ4bvvhjNPDp+oqY+6wzcU9SMT+zM +vRPoVmVvevVhzSpfH2dhZG44mBcTdkaxnvxuEJm8uAfAA1vFCyM/2n3gDUGOIxQS +v6nNKgVnIqsa9/gWOjR3EoOejw6a+Hlx9hFL9Tv4hsJ0o2AKsjGl5WchNvItONQg +eeHAGUMzAgMBAAECggEAO7Rok7GVGpq4FAOvfGngYI7pndUFxrP9RU7raKUzq3nl +2k0gFsxlPV9SW3PrvFhRKxzUVJ/GBZdVWtJXTdiReaeCifL5DJ4GW1XuSD18ETAL +T8jCdxawPNrs09skA3loOoSjFm4PNaRe5vj1htWJTRxQyjygXOi+LZQOEIm9poOL +UZwlDUDco4K46lrW1znwym6idpEkZ0tYh5c9Szf0KOwmrP6cp63bqRK3Yr7pt7ry +ooKDzhxe/kvdOOlz1RrI+NWiJxYwcG+LEIVWRggqQmC4Aw2EgqOSsqmKLlBaZruA +LcmmS7ZBQQx93NAB6BBLC5W5yCq2K5FnF/C6oJvimQKBgQDKZAIsi+SLfshqLxwB +tCx6xE4/94hhbBmKKDMgsREQLUHwxL31V9WO92dl9n7DSe/TFIrEnjhNZcOH7SSM +rsszYbRkmngxeU25xZQIqY8/cSzzobmdWAQ9HMZkcyqJlPxjFervW0HKvAJf1Yxk +B0I1ZZWGXRGVe+IZi0JKyu1iiwKBgQDURDaJUuRyGLK/T5lIkq2Mq2fSFxnXs+5t +uLoYhMmt8LQAi0fae+wEggHQcInOPmo/RR8sBMkQLpXoEdGLDuilEzuaFxCDq6xj +h2UenM5pVUFdmWKikXVDkwOZcVBz1vpLDqhNEZKTdhAMjQ1Y/yt2SFRXshiRQ6d/ +JHSn9Jx++QKBgCUhGcEAXQo2VSAdyl6JpktTbiOQDsYIpdrRqyeJNF8qXlmETnEP +Dw+uVZrAENtU2hl4QEj32c5hJ/Ds0XK5sm+XshduuzQgU+EL3S+Fo9D4u01q0vmJ +pyVq/P56nkglO/QVlkcZD8XYnfrk21+ScVQIcdj3g/1Pf9g90wiTl98nAoGAfCcZ +ruBo9cM0aWlpQmVoVOq8YslYOC3puwtc7ZJdU/uBjP/qGwR4W3qfDQeF0rf29OK9 +BMYXw/s7eu1RHoCt2j+RkOMEqb7zqZM7tdgJctqGzPQ2GNfzOn72j/0TDW4kH1qH +Xex1SwK3CGBH+lHlXd2YV2K3s99aTKdKBCKyliECgYAIzxFHKtE5rR4s6jjCvkVR +chCdB4+ZOhcq6kxFQZfL3wEHGPNACgV8SxaYKxvMC901tfnwF8mYFjNOKHvCv51v +zGU1MZeRNqvylA1qEBk2YC8EYW++8vxtPZi3sqt5XWPuPAsiM+wq0CTEsnRbnMBz +1CN0ZpoW8SJlP1I/0+D1UQ== +-----END PRIVATE KEY----- diff --git a/Admin.NET-v2/docker/nginx/key/abc.admin.com.pem b/Admin.NET-v2/docker/nginx/key/abc.admin.com.pem new file mode 100644 index 0000000..1fbbd08 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/abc.admin.com.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWzCCAkMCFGGtfh5AXKkyuDGfnTa1i/qhdKxrMA0GCSqGSIb3DQEBCwUAMGcx +CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ0wCwYDVQQHDARMZWhpMRkwFwYD +VQQKDBBBQkMgQ29tcGFueSxJbmMuMQswCQYDVQQLDAJJVDESMBAGA1UEAwwJYWRt +aW4uY29tMCAXDTIzMDMyODA4NDQwNFoYDzIxMjMwMzA0MDg0NDA0WjBrMQswCQYD +VQQGEwJVUzENMAsGA1UECAwEVXRhaDENMAsGA1UEBwwETGVoaTEZMBcGA1UECgwQ +QUJDIENvbXBhbnksSW5jLjELMAkGA1UECwwCSVQxFjAUBgNVBAMMDWFiYy5hZG1p +bi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCn0L97ITGfi2KF +MBm8lzz9VGfZ0iKMi0ijHH5pVOPqsgA19phQ0DZ6G4Wy1EbdF3wnWZdDEYkenqxH +VykzulFN9W3Tc5Ao67zBZLcBc8Oe9zo88nh/J03ds8nyPhY3zTptAGKsEeoSKo0L +Y7m1Ozf2npLa2E+NH815/+WzMoM3jvi3iCXWJ4bvvhjNPDp+oqY+6wzcU9SMT+zM +vRPoVmVvevVhzSpfH2dhZG44mBcTdkaxnvxuEJm8uAfAA1vFCyM/2n3gDUGOIxQS +v6nNKgVnIqsa9/gWOjR3EoOejw6a+Hlx9hFL9Tv4hsJ0o2AKsjGl5WchNvItONQg +eeHAGUMzAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADvGF9b/ih7+ru3U9+YBN1SI +h5MMK9WI5IZBQfeyY5aD1UmMb68e0SjnfFhDhpl0U1xUNkqINq/8+JAAwUkOm2B1 +nb/2uySOvONO8WnGvWlYPHPycnlAOwQNAlArE+F8TC6aKdlcB63NByacxnUt9Gcz +92N2PJJr3QU0uKF5xDnWgDKCPStH8cCRn/PufSngljuevxou45roKXNRAV6sAGJ7 +wybuZVZ9RxjaLB5CKuLoO42CxJFzZyhUG7ixpWLsoC62P7bOK6IVo8G77wn185qX +gskY2bwNisuMYD+k2SoTiLmaqUh4n+j+BJaHyQc4vFRZ475+y6hC0NRU0cNxMlc= +-----END CERTIFICATE----- diff --git a/Admin.NET-v2/docker/nginx/key/admin.com.crt b/Admin.NET-v2/docker/nginx/key/admin.com.crt new file mode 100644 index 0000000..dce294f --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/admin.com.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIUNv3hC7V8ueWe5ztnEpC5drzUE70wDQYJKoZIhvcNAQEL +BQAwZzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDTALBgNVBAcMBExlaGkx +GTAXBgNVBAoMEEFCQyBDb21wYW55LEluYy4xCzAJBgNVBAsMAklUMRIwEAYDVQQD +DAlhZG1pbi5jb20wHhcNMjMwMzI4MDg0NDA0WhcNMzYxMjA0MDg0NDA0WjBnMQsw +CQYDVQQGEwJVUzENMAsGA1UECAwEVXRhaDENMAsGA1UEBwwETGVoaTEZMBcGA1UE +CgwQQUJDIENvbXBhbnksSW5jLjELMAkGA1UECwwCSVQxEjAQBgNVBAMMCWFkbWlu +LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALcCIGMfuoXia4zK +Zp6JFiqjz9MFWsepJjVF3IkrvJuMB8dfGoVEaB82frVoEzFFMyfnD+vM1yib0oj+ +gj3z5cxLeo5zuoy6KGaPfOMLyMgCiNGgLCbEvQ/yNtDZx26qAGw10S51EeOQFzg3 +wW4ysouh8RyT+ljkmMeB4ym7GuMDh00mI/ecHOK/Prl7hkBPk+dpc7/A+EUvmqNf +IcgbZiw49NqbWGsFCAAYCWRVUhOU36GJTSX1pojTsKUwyifHgSGE7BQRBddQTaAz +j2VnoJhMcj4/Lk7xxoRi2m/xQEHMnfYprhyajUCe38/zK81Wuh10K8QT2deaP8Qb +uwVbWR8CAwEAAaNTMFEwHQYDVR0OBBYEFJFdp03B0hlqnbc+Ko98wa7Wj7EpMB8G +A1UdIwQYMBaAFJFdp03B0hlqnbc+Ko98wa7Wj7EpMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAGzMI5EIQ5VICdxFTBz+9DhDcRt44pBmwGbVEo3L +a2ndjRA2Mz0eUcsTWtzy8ZEjBQdFyCo63xj96yHAhUR6yUJo4lUVMvfi3SswW/AO +aataw55ya7yK7yR8PHw3Bxh69OmEXzQJWV0r8vMBrm7Bwn1AEcyLj/m5mnZA6Rjt +2CdLtWMPF26mVrRBRw8uLLm4f1/WO38XRPPiR7pc1ZhcrXUZfzZ/nLx/a3w4D66G +FCWVdyfvBAK7ajoO9htZufVdPgO1xr1zmQlbEZ5/Wri3HufohnnbjLQr8Gu/eL+d +qvUpj3mgAX+2jg+voviSlZWb1r1QSTTWvUPiKgcLbAH6cZY= +-----END CERTIFICATE----- diff --git a/Admin.NET-v2/docker/nginx/key/admin.com.key b/Admin.NET-v2/docker/nginx/key/admin.com.key new file mode 100644 index 0000000..ea67849 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/admin.com.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3AiBjH7qF4muM +ymaeiRYqo8/TBVrHqSY1RdyJK7ybjAfHXxqFRGgfNn61aBMxRTMn5w/rzNcom9KI +/oI98+XMS3qOc7qMuihmj3zjC8jIAojRoCwmxL0P8jbQ2cduqgBsNdEudRHjkBc4 +N8FuMrKLofEck/pY5JjHgeMpuxrjA4dNJiP3nBzivz65e4ZAT5PnaXO/wPhFL5qj +XyHIG2YsOPTam1hrBQgAGAlkVVITlN+hiU0l9aaI07ClMMonx4EhhOwUEQXXUE2g +M49lZ6CYTHI+Py5O8caEYtpv8UBBzJ32Ka4cmo1Ant/P8yvNVroddCvEE9nXmj/E +G7sFW1kfAgMBAAECggEAFcXN/p0Gvuj6LKzj2paqqXYFwrBPZZOtNQdTvooSjVry +jfi1mgdSb+w21PRF3EXEWUn3LfGX43/uY1gMPLyoqU6NjScdmaKILfOCQyzivVD3 +4CCzQAWGDMCfXueZ/4OAO1+HvIQ3FaDN8mVHwQmNDmm82s6MKUlBF/NPCGb3Quyz +Uad7o/SiZowUjHaBZA3homssP508+zWHfZSD59eMoIpAWUFT4BLnhZGLCzryUK1J +R+lw+hWJjE2cqdVsC8gULW0vrgASGE46c/ZBwprjA8pWUR05OWQHV+ukLcn68yby +q+hCN27wAvOfxS86TjAcpmXAWR515dBcbpKKDkQKcQKBgQC3HvzrKvaZSe4H0ESA +cIMudgv6zjZveSrCW1brr1Xxp43cs9yFJEDmobhd4zOQlqcbNZjBOeOGQEsk3OYQ +5Z4qzDQDA90Do9aRl0lttQuZZlfgRwrHyGfwBJXwuWUXdTk8td0Tz7aD1IhDJgez +JTdrpOq8reQQKTrtW4og2qXkSQKBgQD/16b2j4CQqJgdUO7cOgEzwaYDA4zxyFuR +KURpTgMAe6SpprTwMHJjnEMVwokpjHBpZtOoNQJYbuj6YfK8G7WzRG2JlBG6SL2k +MRQ7mUoUsz2e0khRsGza3iFsFFBQKWX6ObwiAoTF9Ckr/Jf+ToovCGYD7G3f7tkf +8jTLjeUCJwKBgQCjT3iCBlPcS0mEIGInJbBoLBDtASEc8zOGF82B7WG5XROwQ5uk +Bbv3szxoRurCxQiMxJTRpl3aadZaLsLjSNRxGKI+GiDuURxXxVNQCskoalRuiQz9 +NSY0sPJDuCOG8x0znoFmXLVKBq3rLKxrQQKW9oH9+RrOquaJrjyWpkiSOQKBgDHD +9QpI56073jr1n0DfV5SFupEjg6sUWhtmd5Q0RIk3g9QsRU3jXpzZrILzEFMwqj0W +b11s0kP5bwAlRV4p1bJFQTldwAUIWTszAMiHDM3x/66BIOgi9Umto7quSOEO7HM7 +/8htzP3kfI292KLzDBYSACYLO2QvxbRdHL/rnfxJAoGBAKFJ+XojL5d0uVZ/rrlf +s0QwWpG8wIlyLqsiB4VOfwhAI+bTQd8D+wY72iDqu86e2QMWmf4lSeYufOBjVyCN +uIj5o4ILRKhEj33/Q1fCtcB+zE4RQVihtsnCJYYGymDgY2mW4mGCXzBderMMehuL +/Qina3knHwaK/RPH/xn5UkCG +-----END PRIVATE KEY----- diff --git a/Admin.NET-v2/docker/nginx/key/admin.com.pem b/Admin.NET-v2/docker/nginx/key/admin.com.pem new file mode 100644 index 0000000..4435394 --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/admin.com.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApmgAwIBAgIUWwu7jhpXsbRfKhA6SZ+hhCZAEKIwDQYJKoZIhvcNAQEL +BQAwZzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDTALBgNVBAcMBExlaGkx +GTAXBgNVBAoMEEFCQyBDb21wYW55LEluYy4xCzAJBgNVBAsMAklUMRIwEAYDVQQD +DAlhZG1pbi5jb20wIBcNMjMwMzI4MDg0NDA0WhgPMjEyMzAzMDQwODQ0MDRaMGcx +CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ0wCwYDVQQHDARMZWhpMRkwFwYD +VQQKDBBBQkMgQ29tcGFueSxJbmMuMQswCQYDVQQLDAJJVDESMBAGA1UEAwwJYWRt +aW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtwIgYx+6heJr +jMpmnokWKqPP0wVax6kmNUXciSu8m4wHx18ahURoHzZ+tWgTMUUzJ+cP68zXKJvS +iP6CPfPlzEt6jnO6jLooZo984wvIyAKI0aAsJsS9D/I20NnHbqoAbDXRLnUR45AX +ODfBbjKyi6HxHJP6WOSYx4HjKbsa4wOHTSYj95wc4r8+uXuGQE+T52lzv8D4RS+a +o18hyBtmLDj02ptYawUIABgJZFVSE5TfoYlNJfWmiNOwpTDKJ8eBIYTsFBEF11BN +oDOPZWegmExyPj8uTvHGhGLab/FAQcyd9imuHJqNQJ7fz/MrzVa6HXQrxBPZ15o/ +xBu7BVtZHwIDAQABo1MwUTAdBgNVHQ4EFgQUkV2nTcHSGWqdtz4qj3zBrtaPsSkw +HwYDVR0jBBgwFoAUkV2nTcHSGWqdtz4qj3zBrtaPsSkwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEARdXGosx7WvGYQl4hD5ROuiu/iixPera8+tkj +aFSOlSouhl9MrFocqzlz5vnpgtCgc8d5TKFkhKGXMa/CDC4GOSC0KNzdoAuZagJJ +0XtthkQ4t3mhEgwdEFyHBYUEVvJFwxQFXlNvPqykeVeqZNRrzpHZCxG5atjtvWWV +APHACk7IvHWsu84lEvwomUr4oHnjv7DldCZ6e41j6bCjGg+/GiIuCcOKwK2WzhUZ +aoK4FyWoy1oNfBUI4/tJS4ea95n4UehQ9OklwL8yRh+w6LwyJFEhCQsSR+k+95/9 +8OPgmo0bFBDHknGXTQp/inFGTgghrOoQtSayLonWbWe+rg906w== +-----END CERTIFICATE----- diff --git a/Admin.NET-v2/docker/nginx/key/ssl.sh b/Admin.NET-v2/docker/nginx/key/ssl.sh new file mode 100644 index 0000000..cbf2a1c --- /dev/null +++ b/Admin.NET-v2/docker/nginx/key/ssl.sh @@ -0,0 +1,13 @@ +# 生成根证书 +openssl genrsa -out admin.com.key 2048 +openssl req -x509 -new -nodes -key admin.com.key -subj "/C=US/ST=Utah/L=Lehi/O=ABC Company,Inc./OU=IT/CN=admin.com" -days 5000 -out admin.com.crt +# 生成服务证书请求 +openssl genrsa -out abc.admin.com.key 2048 +openssl req -new -key abc.admin.com.key -subj "/C=US/ST=Utah/L=Lehi/O=ABC Company,Inc./OU=IT/CN=abc.admin.com" -out abc.admin.com.csr +# 用根证书签发服务器证书 +openssl x509 -req -in abc.admin.com.csr -CA admin.com.crt -CAkey admin.com.key -CAcreateserial -out abc.admin.com.crt -days 5000 -extfile <(printf "subjectAltName=IP:127.0.0.1,IP:192.168.1.1,IP:192.168.0.252,DNS:abc.admin.com,DNS:mx.admin.com,DNS:localhost") +# extfile可选,可以为服务器证书关联多个IP或者域名 + +openssl req -x509 -new -nodes -key admin.com.key -sha256 -days 36500 -out admin.com.pem -subj "/C=US/ST=Utah/L=Lehi/O=ABC Company,Inc./OU=IT/CN=admin.com" +openssl x509 -req -in abc.admin.com.csr -CA admin.com.pem -CAkey admin.com.key -CAcreateserial -out abc.admin.com.pem -days 36500 -sha256 + diff --git a/Admin.NET-v2/docker/redis/redis.conf b/Admin.NET-v2/docker/redis/redis.conf new file mode 100644 index 0000000..43e4f90 --- /dev/null +++ b/Admin.NET-v2/docker/redis/redis.conf @@ -0,0 +1,15 @@ +bind 0.0.0.0 +protected-mode no +port 6379 +timeout 0 +save 900 1 # 900s内至少一次写操作则执行bgsave进行RDB持久化 +save 300 10 +save 60 10000 +rdbcompression yes +dbfilename dump.rdb +# dir data +# 开启数据持久化[aof] +appendonly yes +appendfsync everysec +# 开启密码验证 +requirepass 123456 \ No newline at end of file diff --git a/Admin.NET-v2/hw_portal.sql b/Admin.NET-v2/hw_portal.sql new file mode 100644 index 0000000..7e21c3c --- /dev/null +++ b/Admin.NET-v2/hw_portal.sql @@ -0,0 +1,651 @@ +/* + Navicat Premium Dump SQL + + Source Server : 1.13.177.47,3306 + Source Server Type : MySQL + Source Server Version : 80033 (8.0.33) + Source Host : 1.13.177.47:3306 + Source Schema : hwsaas-cloud + + Target Server Type : MySQL + Target Server Version : 80033 (8.0.33) + File Encoding : 65001 + + Date: 10/03/2026 14:48:39 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for hw_portal_config +-- ---------------------------- +DROP TABLE IF EXISTS `hw_portal_config`; +CREATE TABLE `hw_portal_config` ( + `portal_config_id` int NOT NULL AUTO_INCREMENT COMMENT '主键标识', + `portal_config_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '类型(1首页大图 2产品中心大图)', + `portal_config_title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题', + `portal_config_order` int NOT NULL COMMENT '顺序', + `portal_config_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '内容', + `button_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '按钮名称', + `router_address` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '按钮跳转地址', + `portal_config_pic` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主图地址', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人', + `portal_config_type_id` int NULL DEFAULT NULL COMMENT '如果类型是2的,则需要关联hw_portal_config_type', + PRIMARY KEY (`portal_config_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '门户网站配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_portal_config +-- ---------------------------- +INSERT INTO `hw_portal_config` VALUES (9, '1', '工业物联网定制化解决方案提供商', 1, 'INDUSTRIAL IOT CUSTOMIZATION SOLUTION PROVIDER', NULL, NULL, 'http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724094638A009.jpg', '2024-12-11 18:46:35', NULL, '2025-07-24 09:46:42', NULL, NULL); +INSERT INTO `hw_portal_config` VALUES (11, '2', '物联网解决方案', 1, '青岛海威物联科技有限公司,致力于工业物联网软硬件系统研发、生产和销售,提供感知互联的工业化联网整体解决方案', NULL, NULL, 'http://1.13.177.47:9665/statics/2024/12/19/2产品中心-banner 不带文字_20241219172422A023.png', '2024-12-11 20:29:40', NULL, '2025-07-28 14:54:25', NULL, 33); +INSERT INTO `hw_portal_config` VALUES (12, '2', '智能制造', 2, '智能制造智能制造解决方案', NULL, NULL, 'http://1.13.177.47:9665/statics/2024/12/19/04新闻中心-banner 不带文字_20241219173129A024.png', '2024-12-11 20:30:03', NULL, '2025-07-28 15:02:02', NULL, 34); +INSERT INTO `hw_portal_config` VALUES (13, '2', '快递物流', 1, '快递物流快递物流快递物流快递物流快递物流', NULL, NULL, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-banner 不带文字_20241225105754A120.png', '2024-12-13 09:51:20', NULL, '2024-12-25 10:57:56', NULL, 6); +INSERT INTO `hw_portal_config` VALUES (14, '2', '产品中心智能轮胎', 1, '产品中心智能轮胎解决方案', NULL, NULL, 'http://1.13.177.47:9665/statics/2024/12/19/2产品中心-banner 不带文字_20241219173249A026.png', '2024-12-13 09:51:52', NULL, '2024-12-19 17:32:51', NULL, 7); +INSERT INTO `hw_portal_config` VALUES (15, '2', '1', 1, NULL, NULL, NULL, 'http://1.13.177.47:9665/statics/2025/07/28/图片1_20250728090135A002.png', '2025-06-18 18:10:00', NULL, '2025-07-28 09:01:37', NULL, 27); + +-- ---------------------------- +-- Table structure for hw_portal_config_type +-- ---------------------------- +DROP TABLE IF EXISTS `hw_portal_config_type`; +CREATE TABLE `hw_portal_config_type` ( + `config_type_id` int NOT NULL AUTO_INCREMENT COMMENT '主键标识', + `config_type_classfication` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '大类(1产品中心,2案例)', + `config_type_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称', + `home_config_type_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '首页名称', + `config_type_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `config_type_icon` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标地址', + `home_config_type_pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '首页图片地址', + `parent_id` int NOT NULL COMMENT '父级ID', + `ancestors` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '祖级列表', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`config_type_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 73 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '门户网站配置类型;首页的产品中心信息从这读取,首页的典型案例标题从这读取,然后根据此从product_case_info获取典型案例信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_portal_config_type +-- ---------------------------- +INSERT INTO `hw_portal_config_type` VALUES (4, '1', '工业物联网', '物联网解决方案', '青岛海威物联科技有限公司,致力于工业物联网软硬件系统研发、生产和销售,提供感知互联的工业化联网整体解决方案', 'http://1.13.177.47:9665/statics/2024/12/19/2产品中心-子菜单-工业物联网_20241219171906A019.png', 'http://1.13.177.47:9665/statics/2024/12/19/1首页-产品中心01_20241219165916A006.png', 0, '0', '2024-12-11 20:23:21', NULL, '2025-07-28 09:05:27', NULL); +INSERT INTO `hw_portal_config_type` VALUES (5, '1', '智能制造', '智能制造解决方案', '智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。\n', 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-子菜单-智能制造_20241223163321A031.png', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-产品中心02_20241223163248A030.png', 0, '0', '2024-12-11 20:24:01', NULL, '2025-07-28 09:05:46', NULL); +INSERT INTO `hw_portal_config_type` VALUES (6, '1', '快递物流', '快递物流解决方案', '通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。\n“RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。\n', 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-子菜单-快递物流_20241223163340A032.png', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-产品中心03_20241223163344A033.png', 0, '0', '2024-12-11 20:24:28', NULL, '2025-07-28 09:06:14', NULL); +INSERT INTO `hw_portal_config_type` VALUES (7, '1', '智能轮胎', '智能轮胎解决方案', 'RIFD技术作为轮胎的新一代轮胎标识,已经被轮胎产业链接受并应用。轮胎用RFID电子标签植入到轮胎内部,实现轮胎生产、销售、使用、翻新过程的自动化数据识别,为轮胎管理提供有效的识别手段,实现轮胎的全生命周期管理和追溯。\n', 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-子菜单-智能轮胎_20241223163415A034.png', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-产品中心04_20241223163419A035.png', 0, '0', '2024-12-11 20:25:00', NULL, '2025-07-28 09:06:40', NULL); +INSERT INTO `hw_portal_config_type` VALUES (8, '2', '工业物联网案例', '物联网', '工业物联网案例工业物联网案例工业物联网案例工业物联网案例备注', 'http://1.13.177.47:9665/statics/2024/12/27/2产品中心-子菜单-工业物联网_20241227171052A121.png', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-banner 不带文字_20241227171615A124.png', 0, '0', '2024-12-11 20:25:56', NULL, '2024-12-27 17:16:17', NULL); +INSERT INTO `hw_portal_config_type` VALUES (9, '2', '智能制造案例', '制造中心', '智能制造案例备注智能制造案例备注智能制造案例备注智能制造案例备注', 'http://1.13.177.47:9665/statics/2024/12/27/2产品中心-子菜单-智能制造_20241227171344A122.png', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-banner 不带文字_20241227171628A125.png', 0, '0', '2024-12-11 20:26:28', NULL, '2024-12-27 17:16:31', NULL); +INSERT INTO `hw_portal_config_type` VALUES (10, '2', '快递物流案例', '快递物流', '通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。\n“RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。\n', 'http://1.13.177.47:9665/statics/2024/12/27/2产品中心-子菜单-快递物流_20241227171352A123.png', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-banner 不带文字_20241227171640A126.png', 0, '0', '2024-12-11 20:27:07', NULL, '2025-07-28 09:10:10', NULL); +INSERT INTO `hw_portal_config_type` VALUES (11, '2', '智慧变电站', '智慧变电站', '智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注', NULL, 'http://localhost:9665/statics/2024/12/11/1733200900674_20241211202237A014.png', 16, '0,8,16', '2024-12-11 20:27:43', NULL, '2024-12-12 15:05:19', NULL); +INSERT INTO `hw_portal_config_type` VALUES (12, '2', '智慧制造', '智慧制造', '', '', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-典型案例_20241223164804A037.png', 17, '0,9,17', '2024-12-11 20:28:15', NULL, '2025-07-28 09:09:36', NULL); +INSERT INTO `hw_portal_config_type` VALUES (13, '2', '顺丰快递', '顺丰快递', '顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递顺丰快递', 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-子菜单-快递物流_20241223155827A027.png', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-产品中心03_20241223155832A028.png', 18, '0,9,18', '2024-12-11 20:28:39', NULL, '2024-12-23 15:58:34', NULL); +INSERT INTO `hw_portal_config_type` VALUES (14, '1', '物联网平台', NULL, NULL, NULL, NULL, 4, NULL, '2024-12-11 20:30:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (15, '1', '物联网硬件产品系列', NULL, NULL, NULL, NULL, 4, '0,4', '2024-12-11 20:30:45', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (16, '2', '智慧电网', NULL, NULL, NULL, NULL, 8, '0,8', '2024-12-12 15:05:01', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (17, '2', '智慧工业', NULL, NULL, NULL, NULL, 9, '0,9', '2024-12-12 15:05:39', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (18, '2', '智慧快递', NULL, NULL, NULL, NULL, 10, '0,10', '2024-12-12 15:06:23', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (19, '1', '智能制造平台', NULL, NULL, NULL, NULL, 5, '0,5', '2024-12-24 13:28:19', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (20, '1', '设备管理系统', NULL, NULL, NULL, NULL, 5, '0,5', '2024-12-24 13:28:28', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (21, '1', 'RFID生产管理系统', NULL, NULL, NULL, NULL, 5, '0,5', '2024-12-24 13:28:38', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (24, '1', '快递物流整体解决方案', NULL, NULL, NULL, NULL, 6, '0,5', '2024-12-24 14:23:46', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (27, '3', '公司概况', '公司概况', '公司概况', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172405A006.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172403A005.png', 0, '0', '2025-06-17 17:12:31', NULL, '2025-06-17 17:24:11', NULL); +INSERT INTO `hw_portal_config_type` VALUES (28, '3', '荣誉资质', '荣誉资质', '荣誉资质', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172415A007.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172420A008.png', 0, '0', '2025-06-17 17:12:31', NULL, '2025-06-17 17:24:21', NULL); +INSERT INTO `hw_portal_config_type` VALUES (29, '3', '合作伙伴', '合作伙伴', '合作伙伴', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172432A010.png', 0, '0', '2025-06-17 17:12:31', NULL, '2025-06-17 17:24:33', NULL); +INSERT INTO `hw_portal_config_type` VALUES (30, '3', '联系我们', '联系我们', '联系我们', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (32, '4', '轮胎RFID', '轮胎RFID', '轮胎RFID', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (33, '4', '超高频RFID', '超高频RFID', '超高频RFID', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (34, '4', '高频RFID', '高频RFID', '高频RFID', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (35, '4', '传感器', '传感器', '传感器', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (36, '4', '物联终端', '物联终端', '物联终端', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (37, '4', '工业软件', '工业软件', '工业软件', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172937A012.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (39, '5', '智能轮胎', '智能轮胎', '智能轮胎', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (40, '5', '轮胎工厂', '轮胎工厂', '轮胎工厂', 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (41, '5', '快递物流', '快递物流', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (42, '5', '新能源', '新能源', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (43, '5', '畜牧食品', '畜牧食品', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (44, '5', '智能制造', '智能制造', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (45, '5', '工业物联', '工业物联', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (47, '6', '售前服务', '售前服务', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (48, '6', '售后服务', '售后服务', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (49, '6', '资料下载', '资料下载', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (50, '3', '媒体中心', '媒体中心', NULL, 'http://1.13.177.47:9665/statics/2025/06/17/图标_20250617172426A009.png', 'http://1.13.177.47:9665/statics/2025/06/17/1首页-产品中心01_20241219165916A006_20250617172952A014.png', 0, '0', '2025-06-17 17:12:31', NULL, NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (53, NULL, '定制化耐高温抗金属RFID标签', NULL, '定制化耐高温抗金属RFID标签', NULL, NULL, 32, '0,32', '2025-06-18 09:39:33', 'admin', '2025-07-28 09:53:20', NULL); +INSERT INTO `hw_portal_config_type` VALUES (55, '3', '546', NULL, '546', NULL, NULL, 54, '0,27,54', '2025-06-18 15:54:36', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (56, NULL, 'zchzch', NULL, 'zchzch', NULL, NULL, 53, '0,32,53', '2025-06-19 14:14:06', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (57, NULL, 'zch2', NULL, 'zch2', NULL, NULL, 53, '0,32,53', '2025-06-19 14:43:34', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (58, NULL, 'zch3333', NULL, 'zch3', NULL, NULL, 53, '0,32,53', '2025-06-19 14:43:42', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (60, NULL, '荣誉资质', NULL, NULL, NULL, NULL, 28, '0,28', '2025-06-19 15:55:10', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (61, NULL, '1', NULL, '1', '', 'http://1.13.177.47:9665/statics/2025/07/28/图片1_20250728091103A004.png', 27, '0,27', '2025-07-28 09:11:04', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (62, NULL, 'MES', NULL, NULL, NULL, NULL, 37, '0,37', '2025-07-28 10:04:27', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (63, NULL, 'RFID通道机', NULL, 'RFID通道机', NULL, NULL, 33, '0,33', '2025-07-28 10:19:08', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (64, NULL, '高频', NULL, NULL, NULL, NULL, 34, '0,34', '2025-07-28 10:31:03', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (65, NULL, '传感器', NULL, NULL, NULL, NULL, 35, '0,35', '2025-07-28 10:31:12', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (66, NULL, '工业物联云智能终端', NULL, '工业物联云智能终端', NULL, NULL, 36, '0,36', '2025-07-28 10:32:52', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (67, NULL, '智能制造系统', NULL, NULL, NULL, NULL, 44, '0,44', '2025-07-28 10:34:51', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (68, NULL, 'RFID轮胎应用场景', NULL, 'RFID轮胎应用场景', NULL, NULL, 39, '0,39', '2025-07-28 10:39:01', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (69, NULL, 'RFID轮胎', NULL, NULL, NULL, NULL, 40, '0,40', '2025-07-28 10:56:45', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (70, NULL, 'rfid智慧物流', NULL, NULL, NULL, NULL, 41, '0,41', '2025-07-28 13:23:41', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (71, NULL, '能源监控', NULL, NULL, NULL, NULL, 42, '0,42', '2025-07-28 13:29:50', 'admin', NULL, NULL); +INSERT INTO `hw_portal_config_type` VALUES (72, NULL, '屠宰行业RFID应用', NULL, NULL, NULL, NULL, 43, '0,43', '2025-07-28 13:32:10', 'admin', NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_product_case_info +-- ---------------------------- +DROP TABLE IF EXISTS `hw_product_case_info`; +CREATE TABLE `hw_product_case_info` ( + `case_info_id` int NOT NULL AUTO_INCREMENT COMMENT '主键标识', + `case_info_title` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '案例标题', + `config_type_id` int NOT NULL COMMENT '配置类型ID', + `typical_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '典型案例标识(1是0否)', + `case_info_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '案例内容', + `case_info_pic` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '案例内容图片', + `case_info_html` longblob NOT NULL COMMENT '案例详情', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`case_info_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '案例内容' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_product_case_info +-- ---------------------------- +INSERT INTO `hw_product_case_info` VALUES (1, '智慧变电站', 16, '1', '智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注智慧变电站备注', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-典型案例_20241223164058A036.png', 0x25334370253230636C6173733D253232716C2D616C69676E2D63656E7465722532322533452545362542352538422545382541462539352545362542352538422545382541462539352533432F7025334525334370253230636C6173733D253232716C2D616C69676E2D63656E746572253232253345253343696D672532307372633D253232687474703A2F2F3137352E32372E3231352E39323A393636352F737461746963732F323032342F31322F31392F312545392541362539362545392541312542352D2545352538352542382545352539452538422545362541312538382545342542452538425F3230323431323139313731353532413031342E706E67253232253230646174612D66697273742D656E7465722D696D6167653D253232747275652532322533452533432F7025334525334370253230636C6173733D253232716C2D616C69676E2D63656E7465722532322533452545362542352538422545382541462539352545362542352538422545382541462539352533432F70253345, '2024-12-12 15:28:26', NULL, '2024-12-30 16:16:29', NULL); +INSERT INTO `hw_product_case_info` VALUES (2, '智慧变电站2', 16, '0', '智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2智慧变电站2', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-图_20241227171712A127.png', 0x253343702533452532353343702532353345253235453625323539392532354241253235453625323538352532354137253235453525323538462532353938253235453725323539342532354235253235453725323541422532353939322532354536253235393925323542412532354536253235383525323541372532354535253235384625323539382532354537253235393425323542352532354537253235414225323539393225323533432F70253235334525323533437025323533452532353343696D6725323532307372633D2532353232687474703A2F2F6C6F63616C686F73743A393636352F737461746963732F323032342F31322F31322F313733323236313435353631355F3230323431323132313633313333413030332E706E672532353232253235334525323533432F7025323533452533432F70253345, '2024-12-12 16:31:37', NULL, '2024-12-27 17:17:14', NULL); +INSERT INTO `hw_product_case_info` VALUES (3, '智慧变电站3', 16, '0', '智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3智慧变电站3', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-图_20241227171740A128.png', 0x2533437025334525323533437025323533452532354536253235393925323542412532354536253235383525323541372532354535253235384625323539382532354537253235393425323542352532354537253235414225323539393325323533432F70253235334525323533437025323533452532353343696D6725323532307372633D2532353232687474703A2F2F6C6F63616C686F73743A393636352F737461746963732F323032342F31322F31322F313733333230303930303637345F3230323431323132313633323134413030352E706E672532353232253235334525323533432F70253235334525323533437025323533452532353343696D6725323532307372633D2532353232687474703A2F2F6C6F63616C686F73743A393636352F737461746963732F323032342F31322F31322F313733323834383034303235365F3230323431323132313633323233413030362E706E672532353232253235334525323533432F7025323533452533432F70253345, '2024-12-12 16:32:26', NULL, '2024-12-27 17:17:42', NULL); +INSERT INTO `hw_product_case_info` VALUES (4, '智慧工业', 17, '1', '智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。\n', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-典型案例_20241223164921A039.png', 0x25334370253345253343696D672532307372633D253232687474703A2F2F3137352E32372E3231352E39323A393636352F737461746963732F323032342F31322F31392F312545392541362539362545392541312542352D2545352538352542382545352539452538422545362541312538382545342542452538425F3230323431323139313731363233413031362E706E672532322533452533432F70253345253343702533452545362541312538382545342542452538422545352538362538352545352541452542392533432F70253345, '2024-12-12 16:33:40', NULL, '2025-07-28 09:17:23', NULL); +INSERT INTO `hw_product_case_info` VALUES (5, '智慧工业案例2', 17, '0', '智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2智慧工业案例2', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-图_20241227171803A129.png', 0x2533437025334525323533437025323533452532354536253235393925323542412532354536253235383525323541372532354535253235423725323541352532354534253235423825323539412532354536253235413125323538382532354534253235424525323538423225323545362532353939253235424125323545362532353835253235413725323545352532354237253235413525323545342532354238253235394125323545362532354131253235383825323545342532354245253235384232253235453625323539392532354241253235453625323538352532354137253235453525323542372532354135253235453425323542382532353941253235453625323541312532353838253235453425323542452532353842322532354536253235393925323542412532354536253235383525323541372532354535253235423725323541352532354534253235423825323539412532354536253235413125323538382532354534253235424525323538423225323533432F70253235334525323533437025323533452532353343696D6725323532307372633D2532353232687474703A2F2F6C6F63616C686F73743A393636352F737461746963732F323032342F31322F31322F313733333230303930303637345F3230323431323132313633343131413031302E706E672532353232253235334525323533432F7025323533452533432F70253345, '2024-12-12 16:34:14', NULL, '2024-12-27 17:18:05', NULL); +INSERT INTO `hw_product_case_info` VALUES (6, '智慧快递案例1', 18, '1', '智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1智慧快递案例1', 'http://1.13.177.47:9665/statics/2024/12/23/1首页-典型案例_20241223164938A040.png', 0x2533437025334525453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312545362539392542412545362538352541372545352542462541422545392538302539322545362541312538382545342542452538423125453625393925424125453625383525413725453525424625414225453925383025393225453625413125383825453425424525384231254536253939254241254536253835254137254535254246254142254539253830253932254536254131253838254534254245253842312533432F7025334525334370253345253343696D672532307372633D253232687474703A2F2F3137352E32372E3231352E39323A393636352F737461746963732F323032342F31322F31392F312545392541362539362545392541312542352D2545352538352542382545352539452538422545362541312538382545342542452538425F3230323431323139313731363535413031382E706E672532322533452533432F70253345, '2024-12-12 16:34:53', NULL, '2024-12-23 16:49:41', NULL); +INSERT INTO `hw_product_case_info` VALUES (7, '智慧快递案例2', 18, '0', '智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2智慧快递案例2', 'http://1.13.177.47:9665/statics/2024/12/27/3案例与客户-图_20241227171814A130.png', 0x25334370253345746573742533432F7025334525334370253230636C6173733D253232716C2D616C69676E2D63656E746572253232253345253343696D672532307372633D253232687474703A2F2F3137352E32372E3231352E39323A393636352F737461746963732F323032342F31322F32372F312545392541362539362545392541312542352D62616E6E65722532302545342542382538442545352542382541362545362539362538372545352541442539375F3230323431323237313731383239413133312E706E672532322533452533432F7025334525334370253345746573742533432F702533452533437025334574736573742533432F70253345, '2024-12-12 16:35:26', NULL, '2024-12-30 16:01:52', NULL); +INSERT INTO `hw_product_case_info` VALUES (8, '123111', 5, '1', '1321', NULL, 0x253343702533457472737431312533432F70253345, '2025-01-03 19:28:02', NULL, '2025-01-03 19:57:59', NULL); +INSERT INTO `hw_product_case_info` VALUES (9, '666的标题', 54, '1', '内容66666666', 'http://1.13.177.47:9665/statics/2025/06/18/a31439c3ee4847856bf0feb582ac5ad_20250618162338A024.png', 0x25323533437025323533452532354536253235413125323538382532354534253235424525323538422532354538253235414625323541362532354536253235383325323538353636363636363636363625323533432F702532353345, '2025-06-18 16:23:52', NULL, NULL, NULL); +INSERT INTO `hw_product_case_info` VALUES (10, '546案例标题', 55, '1', '546内容', 'http://1.13.177.47:9665/statics/2025/06/18/Snipaste_2025-02-10_09-19-40_20250618162423A025.png', 0x253235334370253235334525323545382532354146253235413625323545362532353833253235383535343625323533432F702532353345, '2025-06-18 16:24:24', NULL, NULL, NULL); +INSERT INTO `hw_product_case_info` VALUES (11, '公司', 27, '1', '公司', 'http://1.13.177.47:9665/statics/2025/06/18/Snipaste_2025-02-10_09-25-22_20250618162448A026.png', 0x25334370253345253235334370253235334525323545352532353835253235414325323545352532353846253235423825323533432F7025323533452533432F70253345, '2025-06-18 16:24:50', NULL, '2025-07-28 09:12:02', NULL); +INSERT INTO `hw_product_case_info` VALUES (12, 'c', 32, '1', 'c', NULL, 0x25323533437025323533456325323533432F702532353345, '2025-07-27 19:16:55', NULL, NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_product_info +-- ---------------------------- +DROP TABLE IF EXISTS `hw_product_info`; +CREATE TABLE `hw_product_info` ( + `product_info_id` int NOT NULL AUTO_INCREMENT COMMENT '主键标识', + `config_type_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '配置类型(例如物联网解决方案下的物联网平台和物联网硬件产品系列)', + `tab_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '是否按tab显示(1是0否)', + `config_modal` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8左图右图9上图下内容,一行4个)', + `product_info_etitle` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '英文标题', + `product_info_ctitle` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '中文标题', + `product_info_order` int NOT NULL COMMENT '顺序', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`product_info_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '产品信息配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_product_info +-- ---------------------------- +INSERT INTO `hw_product_info` VALUES (1, '14', '0', '2', 'PLATFORM INTRODUCTION', '平台简介', 1, '2024-12-11 20:37:13', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (2, '14', '0', '5', 'PLATFORM ARCHITECTURE', '平台架构', 2, '2024-12-12 11:22:09', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (3, '14', '0', '7', 'PLATFORM FEATURES', '平台功能', 3, '2024-12-12 11:23:08', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (4, '15', '1', '8', 'HIGH FREQUENCY RFID', '高频RFID产品系列', 1, '2024-12-12 13:56:07', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (5, '15', '1', '9', 'ULTRA HIGH FREQUENCY RFID', '超高频RFID产品系列', 2, '2024-12-12 14:11:03', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (6, '15', '0', '9', 'DATA ACQUISTION SENSING', '数采传感硬件系列', 3, '2024-12-12 14:41:42', NULL, '2024-12-23 15:58:53', NULL); +INSERT INTO `hw_product_info` VALUES (7, '19', '0', '2', 'INTELLIGENT MANUFACTURING PLATFORM', '智能制造平台', 1, '2024-12-24 13:33:56', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (8, '19', '0', '7', 'SYSTEM GOALS', '系统目标', 2, '2024-12-24 13:40:35', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (9, '19', '0', '1', 'SYSTEM INTEGRATION', '系统一体', 3, '2024-12-24 13:42:16', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (10, '20', '0', '6', 'DEVICE MANAGEMENT SYSTEM', '设备管理系统', 1, '2024-12-24 14:04:58', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (11, '21', '0', '1', 'CHARACTERISTICS OF PRODUCTION MANAGEMENT BASED ON RFID APPLICATIONS', '基于RFID应用的生产管理特点', 1, '2024-12-24 14:17:17', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (12, '21', '0', '11', 'INSTALLATION PROGRAM FOR FIXTURE RFID TAG', '工装RFID标签安装方案', 2, '2024-12-24 14:19:48', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (13, '21', '0', '12', 'RFID READER INSTALLATION PROGRAM', 'RFID读写器安装方案', 3, '2024-12-24 14:21:00', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (14, '24', '0', '12', 'HARDWARE PRODUCT SERIES', '硬件产品系列', 1, '2024-12-24 14:26:14', NULL, '2024-12-24 14:27:50', NULL); +INSERT INTO `hw_product_info` VALUES (15, '24', '0', '3', 'INTRODUCTION TO SYSTEM EQUIPMENT', '系统设备介绍', 2, '2024-12-24 14:35:06', NULL, '2024-12-25 08:51:51', NULL); +INSERT INTO `hw_product_info` VALUES (16, '24', '0', '6', 'SCHEME INTRODUCTION', '方案介绍', 3, '2024-12-25 08:55:28', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (17, '24', '0', '11', 'SMALL PIECE RFID READER', '小件隔口RFID读写器', 4, '2024-12-25 09:03:55', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (18, '24', '0', '11', 'RFID CHANNEL DOOR', 'RFID通道门', 5, '2024-12-25 09:04:34', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (20, '54', '1', '2', 'ABOUT US', '关于我们', 0, '2025-06-18 16:06:12', NULL, '2025-06-19 15:50:20', NULL); +INSERT INTO `hw_product_info` VALUES (24, '27', '0', '13', 'abc', '公司概况下的设备详情', 3, '2025-06-19 09:40:06', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (26, '53', '0', '1', '超高频RFID\r读写器\r', '超高频RFID\r读写器\r', 2, '2025-06-19 14:00:52', NULL, '2025-07-28 13:42:10', NULL); +INSERT INTO `hw_product_info` VALUES (27, '56', '0', '13', 'zchzch', 'zchzch', 0, '2025-06-19 14:14:33', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (31, '56', '0', '13', 'asdasd', 'adad', 1, '2025-06-19 14:45:27', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (32, '60', '1', '2', 'honor', '荣誉', 0, '2025-06-19 15:56:44', NULL, '2025-07-28 09:40:02', NULL); +INSERT INTO `hw_product_info` VALUES (33, '28', '1', '1', 'honor', 'honor', 0, '2025-07-28 09:32:02', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (34, '29', '1', '5', 'mate', 'mate', 0, '2025-07-28 09:42:41', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (35, '62', '1', '6', 'MES', '智能制造', 0, '2025-07-28 10:06:03', NULL, '2025-07-28 10:12:25', NULL); +INSERT INTO `hw_product_info` VALUES (37, '63', '1', '2', 'RFID通道机', 'RFID通道机', 0, '2025-07-28 10:20:16', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (38, '64', '1', '3', 'HIGHT', '高频', 0, '2025-07-28 10:25:45', NULL, '2025-07-28 10:32:05', NULL); +INSERT INTO `hw_product_info` VALUES (39, '65', '1', '7', '传感器', '传感器', 0, '2025-07-28 10:27:29', NULL, '2025-07-28 10:32:14', NULL); +INSERT INTO `hw_product_info` VALUES (40, '66', '1', '8', '工业物联云智能终端', '工业物联云智能终端', 0, '2025-07-28 10:33:23', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (41, '67', '1', '9', '智能制造系统', '智能制造系统', 0, '2025-07-28 10:35:22', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (42, '68', '1', '10', 'RFID轮胎应用场景', 'RFID轮胎应用场景', 0, '2025-07-28 10:39:29', NULL, NULL, NULL); +INSERT INTO `hw_product_info` VALUES (43, '69', '1', '11', 'RFID轮胎', 'RFID轮胎', 0, '2025-07-28 10:57:15', NULL, '2025-07-28 11:04:29', NULL); +INSERT INTO `hw_product_info` VALUES (44, '70', '1', '12', 'RFID智慧物流', 'RFID智慧物流', 0, '2025-07-28 13:25:21', NULL, '2025-07-28 13:25:39', NULL); +INSERT INTO `hw_product_info` VALUES (45, '71', '1', '8', '产业园能源监控', '产业园能源监控', 0, '2025-07-28 13:30:21', NULL, '2025-07-28 13:30:42', NULL); +INSERT INTO `hw_product_info` VALUES (46, '72', '1', '4', '屠宰行业RFID应用', '屠宰行业RFID应用', 0, '2025-07-28 13:32:37', NULL, '2025-07-28 13:32:46', NULL); + +-- ---------------------------- +-- Table structure for hw_product_info_detail +-- ---------------------------- +DROP TABLE IF EXISTS `hw_product_info_detail`; +CREATE TABLE `hw_product_info_detail` ( + `product_info_detail_id` int NOT NULL AUTO_INCREMENT COMMENT '主键标识', + `parent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父级ID,有tab的就是此父级ID', + `product_info_id` int NULL DEFAULT NULL COMMENT '产品信息配置ID', + `config_modal` varchar(3) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置模式(1图标 +文字+内容横铺4个2左标题+内容,右图片;3左图标,右标题+内容,一行2个;4左大图右标题+内容,一行2个;5上标题+下图片,6上标题+内容,下图片;7图标标题内容,一行3个,8一张图9上图下内容,一行4个);针对右children时配置的', + `product_info_detail_title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题', + `product_info_detail_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '内容', + `product_info_detail_order` int NOT NULL COMMENT '顺序', + `product_info_detail_pic` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片地址', + `ancestors` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '祖级列表', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`product_info_detail_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '产品信息明细配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_product_info_detail +-- ---------------------------- +INSERT INTO `hw_product_info_detail` VALUES (2, '0', 1, '2', '物联网平台是什么', '海威物联网SaaS平台旨在打造通用的工业物联网平台,实现快速接入多租户和海量设备上云,能够在不同场景下监控和控制其设备,从而降低成本和提高效率。', 1, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台简介_20241223165735A041.png', '0', '2024-12-12 11:20:28', NULL, '2024-12-23 17:01:13', NULL); +INSERT INTO `hw_product_info_detail` VALUES (3, '0', 2, NULL, '平台架构', '平台架构22', 2, 'http://1.13.177.47:9665/statics/2024/12/23/物联网平台架构_20241223170238A042.png', '0', '2024-12-12 11:22:37', NULL, '2024-12-23 17:02:40', NULL); +INSERT INTO `hw_product_info_detail` VALUES (4, '0', 3, NULL, '场景管理', '针对不同行业和业务需求,我们支持多种场景的接入,并能够通过拖拽方式轻松自定义配置,实现对各种场景的再现与监控。', 1, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170437A043.png', '0', '2024-12-12 13:47:57', NULL, '2024-12-23 17:25:19', NULL); +INSERT INTO `hw_product_info_detail` VALUES (5, '0', 3, NULL, '设备管理', '通过“物”模型的方式,我们能够高效地支持多种设备协议、各种类型和不同属性的设备快速接入。', 2, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170517A044.png', '0', '2024-12-12 13:48:13', NULL, '2024-12-23 17:30:04', NULL); +INSERT INTO `hw_product_info_detail` VALUES (6, '0', 3, NULL, '报警管理', '支持多样化的报警规则配置,可设置联动拍摄和联动数据采集。此外,还提供多种报警通知方式,确保及时有效地传递警报信息。', 3, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170523A045.png', '0', '2024-12-12 13:48:27', NULL, '2024-12-23 17:34:31', NULL); +INSERT INTO `hw_product_info_detail` VALUES (7, '0', 3, NULL, '报表配置', '支持多样化的报表配置,能够满足不同用户查看报表的需求。', 4, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170529A046.png', '0', '2024-12-12 13:50:34', NULL, '2024-12-23 17:48:22', NULL); +INSERT INTO `hw_product_info_detail` VALUES (8, '0', 3, NULL, '国际化支持', '支持多种国家的语言,确保不同地区的用户都能方便地使用我们的服务。', 5, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170540A047.png', '0', '2024-12-12 13:51:00', NULL, '2024-12-23 17:50:28', NULL); +INSERT INTO `hw_product_info_detail` VALUES (9, '0', 3, NULL, '手机端监控', '手机端支持设备的快速接入,并且您可以根据场景、监控单元或具体设备来进行监控,这样可以方便地查看和管理。', 6, 'http://1.13.177.47:9665/statics/2024/12/23/2产品中心-平台优势01-蓝_20241223170546A048.png', '0', '2024-12-12 13:51:29', NULL, '2024-12-23 17:44:48', NULL); +INSERT INTO `hw_product_info_detail` VALUES (10, '0', 4, NULL, '高频RFID读头', '产品', 1, 'http://localhost:9665/statics/2024/12/12/1732673820696_20241212141332A032.png', '0', '2024-12-12 14:13:34', NULL, '2024-12-12 14:13:58', NULL); +INSERT INTO `hw_product_info_detail` VALUES (11, '0', 4, NULL, '高频RFID一体机', '高频RFID一体机', 2, 'http://1.13.177.47:9665/statics/2025/07/28/高频_20250728114640A035.png', '0', '2024-12-12 14:14:55', NULL, '2025-07-28 11:46:42', NULL); +INSERT INTO `hw_product_info_detail` VALUES (14, '12', 5, NULL, '一体式读写器', '一体式读写器,内置高性能圆极化天线,用最小体积实现最优识别性能。', 1, 'http://1.13.177.47:9665/statics/2024/12/24/一体式读写器_20241224093404A051.png', '0,12', '2024-12-12 14:18:54', NULL, '2024-12-24 09:34:06', NULL); +INSERT INTO `hw_product_info_detail` VALUES (15, '12', 5, NULL, '小型一体式读写器', '小型一体式读写器,体型小巧,内置低增益天线,识别距离近场可控,有效避免识别串读问题。', 2, 'http://1.13.177.47:9665/statics/2024/12/24/小型一体式读写器_20241224093417A052.png', '0,12', '2024-12-12 14:20:28', NULL, '2024-12-24 09:34:19', NULL); +INSERT INTO `hw_product_info_detail` VALUES (16, '12', 5, NULL, '四通道读写器', '四通道读写器,拥有四通道射频输出口,丰富的外设接口能灵活满足不同的应用需求。', 3, 'http://1.13.177.47:9665/statics/2024/12/24/四通道读写器_20241224093430A053.png', '0,12', '2024-12-12 14:21:26', NULL, '2024-12-24 09:34:33', NULL); +INSERT INTO `hw_product_info_detail` VALUES (17, '12', 5, NULL, '两通道车载式读写器', '两通道式车载读写器,采用工业化防振动设计理念,支持蓝牙连接,提供更加稳定可靠的物理及电器连接方式。', 4, 'http://1.13.177.47:9665/statics/2024/12/24/两通道车载式读写器_20241224093445A054.png', '0,12', '2024-12-12 14:22:21', NULL, '2024-12-24 09:34:47', NULL); +INSERT INTO `hw_product_info_detail` VALUES (18, '12', 5, NULL, 'RFID手持机', 'RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间', 5, 'http://1.13.177.47:9665/statics/2024/12/24/RFID手持机_20241224093506A055.png', '0,12', '2024-12-12 14:23:09', NULL, '2024-12-24 09:35:08', NULL); +INSERT INTO `hw_product_info_detail` VALUES (19, '12', 5, NULL, '蓝牙手持式读写器', '蓝牙手持式读写器,蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署', 6, 'http://1.13.177.47:9665/statics/2024/12/24/蓝牙手持式读写器_20241224093523A056.png', '0,12', '2024-12-12 14:24:04', NULL, '2024-12-24 09:35:25', NULL); +INSERT INTO `hw_product_info_detail` VALUES (20, '12', 5, NULL, 'RFID通道门', 'RFID通道门具有窄波束、高增益特点,适用于超高频门禁通道类、物流仓储、人员、图书、档案馆、医疗系统、设备资产等应用', 7, 'http://1.13.177.47:9665/statics/2024/12/24/RFID通道门_20241224093549A057.png', '0,12', '2024-12-12 14:25:05', NULL, '2024-12-24 09:35:51', NULL); +INSERT INTO `hw_product_info_detail` VALUES (21, '12', 5, NULL, 'RFID通道机', 'RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如服装、皮具箱包、酒类、电力等行业。', 8, 'http://1.13.177.47:9665/statics/2024/12/24/RFID通道机_20241224093600A058.png', '0,12', '2024-12-12 14:26:05', NULL, '2024-12-24 09:36:02', NULL); +INSERT INTO `hw_product_info_detail` VALUES (26, '0', 6, NULL, '无线传感器接收显示仪', '无线传感器接收显示仪。支持WiFi、以太网、4G、LORA等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。', 1, 'http://1.13.177.47:9665/statics/2024/12/24/无线传感器接收显示仪_20241224095210A059.png', '0', '2024-12-12 14:44:23', NULL, '2024-12-24 09:52:12', NULL); +INSERT INTO `hw_product_info_detail` VALUES (27, '0', 6, NULL, '无线温度传感器', '无线温度传感器,电池可用3年;温度测量范围:-40~125℃;精度:±0.5℃(25℃);通讯距离:视距范围1500米。', 2, 'http://1.13.177.47:9665/statics/2024/12/24/无线温度传感器_20241224095221A060.png', '0', '2024-12-12 14:45:32', NULL, '2024-12-24 09:52:23', NULL); +INSERT INTO `hw_product_info_detail` VALUES (28, '0', 6, NULL, '工业物联云智能终端', '工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络通信方式上传到上位机系统,进行数据的集中处理。', 3, 'http://1.13.177.47:9665/statics/2024/12/24/工业物联云智能终端_20241224095238A061.png', '0', '2024-12-12 14:47:03', NULL, '2024-12-24 09:52:39', NULL); +INSERT INTO `hw_product_info_detail` VALUES (29, '0', 6, NULL, '工业物联云智能终端', '工业物联云智能终端具有8路开关量输入检测、16开关量输出、RS845总线、网络等硬件接口。', 4, 'http://1.13.177.47:9665/statics/2024/12/24/工业物联云智能终端2_20241224095252A062.png', '0', '2024-12-12 14:47:55', NULL, '2024-12-24 09:52:54', NULL); +INSERT INTO `hw_product_info_detail` VALUES (30, '0', 6, NULL, '温湿度传感器', '温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式', 5, 'http://1.13.177.47:9665/statics/2024/12/24/温湿度传感器_20241224095304A063.png', '0', '2024-12-12 14:49:40', NULL, '2024-12-24 09:53:06', NULL); +INSERT INTO `hw_product_info_detail` VALUES (31, '0', 6, NULL, '车载物联定位终端', '车载物联定位终端,支持GPS/北斗定位,通过内置TPMS或RFID读写器模块读取到的设备相关数据,用4G无线通讯的方式上传到企业数据平台或阿里云物联网平台。', 6, 'http://1.13.177.47:9665/statics/2024/12/24/车载物联定位终端_20241224095327A064.png', '0', '2024-12-12 14:51:19', NULL, '2024-12-24 09:53:29', NULL); +INSERT INTO `hw_product_info_detail` VALUES (32, '0', 6, NULL, 'EPD无线显示器', 'EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。', 7, 'http://1.13.177.47:9665/statics/2024/12/24/EPD无线显示器_20241224095340A065.png', '0', '2024-12-12 14:52:25', NULL, '2024-12-24 09:53:42', NULL); +INSERT INTO `hw_product_info_detail` VALUES (33, '0', 6, NULL, '红外温度传感器', '红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。', 8, 'http://1.13.177.47:9665/statics/2024/12/24/红外温度传感器_20241224095358A066.png', '0', '2024-12-12 14:53:49', NULL, '2024-12-24 09:54:00', NULL); +INSERT INTO `hw_product_info_detail` VALUES (34, '10', 4, NULL, '高频RFID读头', 'HW-RFR-050-B-003-B1204S', 1, 'http://1.13.177.47:9665/statics/2024/12/24/高频RFID读头_20241224091158A049.png', '0,10', '2024-12-19 09:19:06', NULL, '2024-12-24 09:25:14', NULL); +INSERT INTO `hw_product_info_detail` VALUES (35, '11', 4, NULL, '高频RFID一体机', 'HW-RSLIM-HF', 1, 'http://1.13.177.47:9665/statics/2024/12/24/高频RFID一体机_20241224092728A050.png', '0,11', '2024-12-19 09:19:36', NULL, '2024-12-24 09:27:30', NULL); +INSERT INTO `hw_product_info_detail` VALUES (36, '10', 4, NULL, '射频协议', '符合ISO/IEC 13.56MHz', 1, NULL, '0,10', '2024-12-24 09:12:29', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (37, '10', 4, NULL, '盘点速度', '20ms/次', 2, NULL, '0,10', '2024-12-24 09:12:44', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (38, '10', 4, NULL, '通信协议', 'RS485', 3, NULL, '0,10', '2024-12-24 09:12:56', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (39, '10', 4, NULL, '输入电压', '12~24VDC', 4, NULL, '0,10', '2024-12-24 09:13:15', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (40, '10', 4, NULL, '尺寸(mm)', '40W*72L*13H', 5, NULL, '0,10', '2024-12-24 09:13:40', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (41, '10', 4, NULL, '工作温度', '-30℃~+70℃', 6, NULL, '0,10', '2024-12-24 09:14:05', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (42, '10', 4, NULL, '防护等级', 'IP67', 7, NULL, '0,10', '2024-12-24 09:14:30', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (43, '11', 4, NULL, '射频协议', '符合ISO/IEC 13.56 MHz', 1, NULL, '0,11', '2024-12-24 09:27:54', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (44, '11', 4, NULL, '盘点速度', '10张/秒', 2, NULL, '0,11', '2024-12-24 09:28:19', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (45, '11', 4, NULL, '通信协议', 'TCP/IP、RS232、RS485 (三选一)', 3, NULL, '0,11', '2024-12-24 09:28:41', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (46, '11', 4, NULL, '输入电压', '9~32VDC', 4, NULL, '0,11', '2024-12-24 09:28:51', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (47, '11', 4, NULL, '尺寸', '65L*65W*30H', 5, NULL, '0,11', '2024-12-24 09:29:04', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (48, '11', 4, NULL, '工作温度', '-30℃~+70℃', 6, NULL, '0,11', '2024-12-24 09:29:19', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (49, '11', 4, NULL, '工作湿度', '5%~95%RH 无冷凝', 7, NULL, '0,11', '2024-12-24 09:29:34', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (50, '0', 7, NULL, '设备互联系统', '智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。', 1, 'http://1.13.177.47:9665/statics/2024/12/25/设备互联系统架构图_20241225105356A119.png', '0', '2024-12-24 13:44:48', NULL, '2024-12-25 10:53:58', NULL); +INSERT INTO `hw_product_info_detail` VALUES (52, '0', 8, NULL, '智能物流配送', '优化及管理由库房到生产线及回仓的物流发放、RFID、条码、PDA移动终端', 1, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224134831A068.png', '0', '2024-12-24 13:48:33', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (53, '0', 8, NULL, '全程质量追溯与分析', '基于产品的全生命周期的追溯分析,监控并及时发现缺陷、提高质量', 2, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224134916A069.png', '0', '2024-12-24 13:49:18', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (54, '0', 8, NULL, '数字化质量检验', '基于标准工艺约束的检验指标完成单件产品的数字化检验和质量数据采集', 3, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224134948A070.png', '0', '2024-12-24 13:49:49', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (55, '0', 8, NULL, '数字化看板与分析', '报表、报警、实时看板、SPC以及运作看板', 4, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135016A071.png', '0', '2024-12-24 13:50:18', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (56, '0', 8, NULL, '无纸化生产过程控制', '无纸化制程执行,流程跟踪,工艺路径控制,数据收集分析', 5, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135052A072.png', '0', '2024-12-24 13:50:54', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (57, '0', 8, NULL, '作业计划导入和排产', '通过ERP系统接口将车间及作业计划导入到MES系统,并进行APS排产分析', 6, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135125A073.png', '0', '2024-12-24 13:51:27', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (58, '0', 9, NULL, '软硬一体', '采用软件+硬件的数采方式,实现设备运行、车间环境、生产过程的监控。', 1, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135232A074.png', '0', '2024-12-24 13:52:34', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (59, '0', 9, NULL, '中央治理', '一体化平台,统一数据源,消除信息孤岛、实现多工厂的统一管理和分布式应用。', 2, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135303A075.png', '0', '2024-12-24 13:53:05', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (60, '0', 9, NULL, '柔性可配', '柔性可配置,可以迅速建模以满足个性需求并能通过调整模型应对需求的快速变化。', 3, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135340A076.png', '0', '2024-12-24 13:53:42', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (61, '0', 9, NULL, '数据驱动', '获取准确、实施的数据,让数据驱动业务,进行监控、预测、控制和决策优化。', 4, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135410A077.png', '0', '2024-12-24 13:54:12', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (62, '0', 9, NULL, '智能管控', '实现制造过程的防呆防错、管理的自动化、优化资源配置、人机协同与决策。', 5, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135451A078.png', '0', '2024-12-24 13:54:53', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (63, '0', 9, NULL, '高效协同', '实现制造相关的计划、生产、委外、质量、仓储、设备管理等职能部门的高效协作。', 6, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135535A079.png', '0', '2024-12-24 13:55:36', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (64, '0', 9, NULL, '精益求本', '通过数字化、智能化转型,可以充分发挥人员效能、有效控制生产节拍,提高效率。', 7, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135609A080.png', '0', '2024-12-24 13:56:11', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (65, '0', 9, NULL, '生产闭环', '实现生产断点的有效控制,防止生产呆滞,提高企业效益。', 8, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224135635A081.png', '0', '2024-12-24 13:56:37', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (66, '0', 10, NULL, '系统功能', '海威物联设备管理系统致力于设备管理科学化,以全生命周期为主线,预防性维护为中心,提供巡检、点检、维修、保养等作业流程管理,支持二维码扫描、RFID、NFC等多种巡检终端,兼顾设备档案、备品备件的管理,同时引入物联技术实现设备状态的实时监控与故障预警,应用人工智能算法实现设备监控状态评估,提供设备维护保养策略,为实现设备零停机提供智慧管理解决方案,帮助企业实现设备的规范化、科学化、智能化管理,降低设备故障率,保持设备稳定性,实现企业资产效益的全面提升。', 1, 'http://1.13.177.47:9665/statics/2024/12/24/设备管理系统_20241224140838A082.png', '0', '2024-12-24 14:08:41', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (67, '0', 11, NULL, '自动', '自动扫描、自动采集代替人工和条码,提高生产和物流效率。', 1, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224142128A083.png', '0', '2024-12-24 14:21:31', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (68, '0', 11, NULL, '防误', '物料验证,过程防错,智能提醒,提升质量管控水平。', 2, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224142158A084.png', '0', '2024-12-24 14:22:00', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (69, '0', 11, NULL, '追溯', '实时监控,透明生产,全生命周期信息记录和追溯。', 3, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224142226A085.png', '0', '2024-12-24 14:22:28', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (70, '0', 11, NULL, '集成', '成熟接口,为其他信息化系统提供业务和数据制程。', 4, 'http://1.13.177.47:9665/statics/2024/12/24/2产品中心-平台优势01-蓝_20241224142247A086.png', '0', '2024-12-24 14:22:48', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (71, '0', 14, NULL, '硬件产品1', '硬件产品1', 1, 'http://1.13.177.47:9665/statics/2024/12/24/硬件产品系列1_20241224143001A087.png', '0', '2024-12-24 14:30:03', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (72, '0', 14, NULL, '硬件产品2', '硬件产品2', 2, 'http://1.13.177.47:9665/statics/2024/12/24/硬件产品系列2_20241224143016A088.png', '0', '2024-12-24 14:30:18', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (73, '0', 14, NULL, '硬件产品3', '硬件产品3', 3, 'http://1.13.177.47:9665/statics/2024/12/24/硬件产品系列3_20241224143027A089.png', '0', '2024-12-24 14:30:29', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (74, '0', 14, NULL, '硬件产品4', '硬件产品4', 4, 'http://1.13.177.47:9665/statics/2024/12/24/硬件产品系列4_20241224143040A090.png', '0', '2024-12-24 14:30:43', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (75, '0', 15, NULL, '手持RFID阅读器', '手持RFID阅读器主要用于RFID标签、总包信息的手动绑定/写入,以及批量空带的手动识读。', 1, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-平台优势01-蓝_20241225084841A091.png', '0', '2024-12-25 08:48:44', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (76, '0', 15, NULL, '矩阵RFID识读通道机', '矩阵RFID识读通道机主要用于识别邮袋RFID标签。', 2, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-平台优势01-蓝_20241225084922A092.png', '0', '2024-12-25 08:49:24', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (77, '0', 15, NULL, '小件分拣机RFID读写系统', '小件分拣机RFID读写系统主要用于RFID标签、总包信息的自动绑定/写入。', 3, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-平台优势01-蓝_20241225085003A093.png', '0', '2024-12-25 08:50:05', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (78, '0', 15, NULL, 'RFID通道门', 'RFID通道门主要用于批量空袋的自动识读/擦除。', 4, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-平台优势01-蓝_20241225085036A094.png', '0', '2024-12-25 08:50:38', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (79, '0', 16, NULL, '方案介绍', '快递物流行业,从2006年开始逐步引入RFID技术,应用方式方法多种多样,成效不一。通过与合作伙伴的共同摸索推动,SF在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020大规模推广使用,效果非凡。JD、YZ、ZT等也在批量推广。', 1, 'http://1.13.177.47:9665/statics/2024/12/25/快递物流行业方案介绍_20241225090018A095.png', '0', '2024-12-25 09:00:21', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (80, '0', 17, NULL, '小件隔口RFID读写器1', '小件隔口RFID读写器1', 1, 'http://1.13.177.47:9665/statics/2024/12/25/小件隔口RFID读写器1_20241225091650A099.png', '0', '2024-12-25 09:15:33', NULL, '2024-12-25 09:16:52', NULL); +INSERT INTO `hw_product_info_detail` VALUES (81, '0', 17, NULL, '小件隔口RFID读写器2', '小件隔口RFID读写器2', 2, 'http://1.13.177.47:9665/statics/2024/12/25/小件隔口RFID读写器2_20241225091711A100.png', '0', '2024-12-25 09:15:53', NULL, '2024-12-25 09:17:14', NULL); +INSERT INTO `hw_product_info_detail` VALUES (82, '0', 17, NULL, '小件隔口RFID读写器3', '小件隔口RFID读写器3', 3, 'http://1.13.177.47:9665/statics/2024/12/25/小件隔口RFID读写器3_20241225091727A101.png', '0', '2024-12-25 09:16:12', NULL, '2024-12-25 09:17:29', NULL); +INSERT INTO `hw_product_info_detail` VALUES (83, '0', 18, NULL, 'RFID通道门1', 'RFID通道门1', 1, 'http://1.13.177.47:9665/statics/2024/12/25/RFID通道门1_20241225091808A102.png', '0', '2024-12-25 09:18:10', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (84, '0', 18, NULL, 'RFID通道门2', 'RFID通道门2', 2, 'http://1.13.177.47:9665/statics/2024/12/25/RFID通道门2_20241225091820A103.png', '0', '2024-12-25 09:18:22', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (85, '0', 18, NULL, 'RFID通道门3', 'RFID通道门3', 3, 'http://1.13.177.47:9665/statics/2024/12/25/RFID通道门3_20241225091831A104.png', '0', '2024-12-25 09:18:34', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (88, '0', 21, NULL, '222', '2222', 3, 'http://1.13.177.47:9665/statics/2024/12/24/设备管理系统_20241224140838A082.png', '', '2025-06-18 17:34:50', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (89, '0', 22, NULL, '333', '3333', 3, 'http://1.13.177.47:9665/statics/2024/12/25/2产品中心-平台优势01-蓝_20241225085003A093.png', NULL, '2025-06-18 17:34:53', NULL, NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (90, '0', 24, NULL, '公司概况', '产品信息明细配置', 0, 'http://1.13.177.47:9665/statics/2025/07/28/图片1_20250728091408A006.png', '0', '2025-06-19 11:31:36', 'admin', '2025-07-28 09:14:10', NULL); +INSERT INTO `hw_product_info_detail` VALUES (91, '0', 23, NULL, '666的', '666的', 1, 'http://1.13.177.47:9665/statics/2025/07/28/mesnac_20250728093014A011.jpg', '0', '2025-06-19 11:32:22', 'admin', '2025-07-28 09:30:16', NULL); +INSERT INTO `hw_product_info_detail` VALUES (92, '91', 23, NULL, '66子', '66子', 0, 'http://1.13.177.47:9665/statics/2025/06/19/弃用_20250619113341A003.png', '0,91', '2025-06-19 11:33:42', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (93, '0', 26, NULL, '定制化耐高温抗金属RFID标签', '读写器产品包含一体式读写器、车载读写器、四通道读写器等一系列产品,具备丰富的通讯接口和公用的软件接口平台,具有稳定性强、性价比高、易开发、易部署等特点。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/、定制化耐高温抗金属RFID标签_20250728095441A016.png', '0', '2025-06-19 14:01:57', 'admin', '2025-07-28 13:42:32', NULL); +INSERT INTO `hw_product_info_detail` VALUES (94, '0', 27, NULL, 'zchzchzchzch', 'zchzch', 0, NULL, '0', '2025-06-19 14:14:57', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (95, '0', 31, NULL, 'adasda', 'dasdasdasd', 0, 'http://1.13.177.47:9665/statics/2025/06/19/mesnac_20250619144619A002.jpg', '0', '2025-06-19 14:46:21', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (97, '90', 20, NULL, '1', '青岛海威物联科技有限公司,致力于工业物联网软硬件系统研发、生产和销售,提供感知互联的工业物联网整体解决方案。作为轮胎用RFID电子标签相关4项ISO国际标准与国家标准的制定者,公司具备全球最先进的RFID智能轮胎整体解决方案,包括系列化轮胎用RFID电子标签产品、RFID轮胎生产配套自动化设备、RFID轮胎应用信息化管理系统及数据平台。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/图片1_20250728090040A010.png', '0', '2025-06-19 15:01:55', 'admin', '2025-07-28 09:01:11', NULL); +INSERT INTO `hw_product_info_detail` VALUES (99, '0', 33, NULL, 'honor', 'honor', 0, 'http://1.13.177.47:9665/statics/2025/07/28/mesnac_20250728093234A010.jpg', '0', '2025-07-28 09:32:37', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (100, '0', 32, NULL, 'honor', '高新技术企业\n科技型中小企业\n专精特新“小巨人”\nISO:9001质量管理体系认证\n青岛市技术创新中心', 0, 'http://1.13.177.47:9665/statics/2025/07/28/图片2_20250728093940A013.png', '0', '2025-07-28 09:33:44', 'admin', '2025-07-28 09:39:42', NULL); +INSERT INTO `hw_product_info_detail` VALUES (101, '0', 34, '13', 'mate', 'mate', 0, 'http://1.13.177.47:9665/statics/2025/07/28/PixPin_2025-07-28_09-42-06_20250728094818A015.png', '0', '2025-07-28 09:48:28', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (102, '0', 35, '13', 'mes', '智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/mes_20250728100746A017.png', '0', '2025-07-28 10:07:15', 'admin', '2025-07-28 10:07:47', NULL); +INSERT INTO `hw_product_info_detail` VALUES (103, '0', 35, '13', 'mes1', 'mes1', 1, 'http://1.13.177.47:9665/statics/2025/07/28/mes1_20250728101444A019.png', '0', '2025-07-28 10:14:52', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (104, '0', 37, '13', 'RFID通道机', 'RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如快递、服装、皮具箱包、酒类等行业。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/超高频_20250728102334A022.png', '0', '2025-07-28 10:23:42', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (105, '0', 38, '13', '高频可旋转式读写器', '高频可旋转式读写器,外观设计小巧、天线一体集成。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/高频_20250728102632A023.png', '0', '2025-07-28 10:26:06', 'admin', '2025-07-28 10:26:34', NULL); +INSERT INTO `hw_product_info_detail` VALUES (106, '0', 39, '13', '无线传感器接收显示仪', '无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/cgq_20250728102948A027.png', '0', '2025-07-28 10:29:41', 'admin', '2025-07-28 10:29:49', NULL); +INSERT INTO `hw_product_info_detail` VALUES (107, '0', 40, '13', '工业物联云智能终端', '工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/zhongduan_20250728103400A028.png', '0', '2025-07-28 10:34:01', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (108, '0', 41, '13', '智能制造系统', '智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/znzz_20250728103716A029.png', '0', '2025-07-28 10:36:49', 'admin', '2025-07-28 10:37:17', NULL); +INSERT INTO `hw_product_info_detail` VALUES (109, '0', 42, '13', 'RFID轮胎应用场景', 'RIFD技术作为轮胎的新一代轮胎标识,已经被轮胎产业链接受并应用。轮胎用RFID电子标签植入到轮胎内部,实现轮胎生产、销售、使用、翻新过程的自动化数据识别,为轮胎管理提供有效的识别手段,实现轮胎的全生命周期管理和追溯。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/luntai_20250728104120A031.png', '0', '2025-07-28 10:40:09', 'admin', '2025-07-28 10:41:21', NULL); +INSERT INTO `hw_product_info_detail` VALUES (110, '0', 43, '13', 'RFID轮胎', 'RIFD技术作为轮胎的新一代轮胎标识,已经被轮胎产业链接受并应用。轮胎用RFID电子标签植入到轮胎内部,实现轮胎生产、销售、使用、翻新过程的自动化数据识别,为轮胎管理提供有效的识别手段,实现轮胎的全生命周期管理和追溯。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/RFIDlt_20250728105902A034.png', '0', '2025-07-28 10:59:05', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (111, '0', 44, '13', 'RFID智慧物流', '通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。\n“RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/transoform_20250728132750A037.png', '0', '2025-07-28 13:28:01', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (112, '0', 45, '13', '产业园能源监控', '在新能源电池生产行业,实现前工序的物料追溯及信息化、数字化管理。对当前采用纸质条码产生的错扫、漏扫、信息丢失、资源使用不充分等进行完全的改善,同时实现卷套的有效管理、生产信息的自动采集和修改、使用信息的及时更新、资源的节约和充分利用、生产过程的物料追溯等。', 0, 'http://1.13.177.47:9665/statics/2025/07/28/能源_20250728133113A038.png', '0', '2025-07-28 13:31:16', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (113, '0', 46, '13', '屠宰行业RFID应用', '在扁担钩安装RFID标签,在输送线安装RFID,使得商品猪肉的追溯更加透明化,实现精准化管控,提高生产效率,降低人力成本,避免质量安全事故。\n', 0, 'http://1.13.177.47:9665/statics/2025/07/28/xmy_20250728133404A040.png', '0', '2025-07-28 13:34:14', 'admin', NULL, NULL); +INSERT INTO `hw_product_info_detail` VALUES (114, '0', 39, NULL, '2', '无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。', 2, 'http://1.13.177.47:9665/statics/2025/08/11/cgq_20250728102948A027_20250811141926A041.png', '0', '2025-08-11 14:19:31', 'admin', NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_template +-- ---------------------------- +DROP TABLE IF EXISTS `hw_template`; +CREATE TABLE `hw_template` ( + `template_id` bigint NOT NULL AUTO_INCREMENT COMMENT '模板id', + `template_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '模板名称', + `template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模板内容', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`template_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模板表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_template +-- ---------------------------- +INSERT INTO `hw_template` VALUES (24, 'cs', '{\"center\":\"UpDown\",\"centerChildren\":{\"separate\":50,\"top\":\"UpDown\",\"topChildren\":{\"separate\":15.6},\"down\":\"LeftRight\",\"downChildren\":{\"separate\":19.042316258351892}}}', '', '2024-09-20 09:42:51', NULL, '2024-09-23 10:42:01', NULL); +INSERT INTO `hw_template` VALUES (26, '1', '{\"center\":\"UpDown\",\"centerChildren\":{\"separate\":50,\"top\":\"LeftRight\",\"topChildren\":{\"separate\":66.92650334075724},\"down\":\"LeftRight\",\"downChildren\":{\"separate\":34.18708240534521}}}', '', '2024-09-20 10:50:26', NULL, '2024-09-20 13:58:46', NULL); +INSERT INTO `hw_template` VALUES (30, '', '{\"center\":null,\"centerChildren\":{}}', '', '2024-09-20 10:50:35', NULL, NULL, NULL); +INSERT INTO `hw_template` VALUES (33, '', '{\"center\":null,\"centerChildren\":{}}', '', '2024-09-20 10:50:39', NULL, NULL, NULL); +INSERT INTO `hw_template` VALUES (35, '测试1', '{\"center\":\"UpDown\",\"centerChildren\":{\"separate\":50,\"top\":\"LeftRight\",\"topChildren\":{\"separate\":50}}}', '', '2024-09-26 16:47:48', NULL, NULL, NULL); +INSERT INTO `hw_template` VALUES (36, '测试2', '{\"center\":\"UpDown\",\"centerChildren\":{\"separate\":50,\"top\":\"LeftRight\",\"topChildren\":{\"separate\":62.69487750556792},\"down\":\"UpDown\",\"downChildren\":{\"separate\":50,\"top\":\"LeftRight\",\"topChildren\":{\"separate\":26.614699331848552}}}}', '', '2024-09-26 16:48:52', NULL, NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_tenant +-- ---------------------------- +DROP TABLE IF EXISTS `hw_tenant`; +CREATE TABLE `hw_tenant` ( + `tenant_id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID', + `tenant_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '租户类型,(1、企业,2、个人)', + `tenant_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '租户名称', + `tenant_industry` int NULL DEFAULT NULL COMMENT '行业类型,关联sys_dict_data的dict_type是hw_tenant_industry的dict_value', + `contact_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '联系人姓名', + `contact_phone` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '联系人电话', + `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱地址', + `area_id` bigint NULL DEFAULT NULL COMMENT '区域ID,管理区域hw_area', + `contact_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系人地址', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `tenant_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '状态(1、正常 9、删除)', + `is_register` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '是否外部注册(1、是 0、否)', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建人', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新人', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `tenant_grade` bigint NULL DEFAULT NULL COMMENT '租户等级,预留字段', + `tenant_field` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '预留字段 首页标识 1为展示图片,2为展示地图', + `tenant_board_topic` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '租户看板标题', + `tenant_board_pic` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '租户看板图片地址', + `tenant_map_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '行政区域代码 如甘肃省620000', + PRIMARY KEY (`tenant_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 123 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '租户信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_tenant +-- ---------------------------- +INSERT INTO `hw_tenant` VALUES (1, '1', '金瑞铭', 8, '金瑞铭', '18888888881', 'szc@genrace.com', NULL, '1', '', '1', '0', NULL, NULL, 'admin', '2024-08-06 14:59:49', NULL, '2', '智慧物联监控平台', 'http://127.0.0.1:9665/statics/2024/07/05/Snipaste_2024-07-05_14-12-59_20240705141308A001.png', '620000'); +INSERT INTO `hw_tenant` VALUES (114, '1', '龙华街道办', 1, '华木林', '13433773977', NULL, NULL, NULL, NULL, '9', '0', 'admin', '2024-08-01 10:10:38', 'admin', '2024-08-06 14:43:21', NULL, '2', NULL, NULL, '1'); +INSERT INTO `hw_tenant` VALUES (115, '1', '云南******科技有限公司', 1, '张先生', '15045678910', '123@eeee.com', NULL, '云南洱海', '客户自用平台', '9', '0', 'admin', '2024-08-01 10:45:13', 'admin', '2024-08-01 10:51:54', NULL, '1', '物联测温监控平台', NULL, NULL); +INSERT INTO `hw_tenant` VALUES (116, '1', '白银供电所', 1, '王海宝', '13433783988', 'szc@genrace.com', NULL, '白银市市厅局103号', NULL, '1', '0', 'admin', '2024-08-19 11:40:25', 'BYadmin', '2024-10-30 16:19:52', NULL, '2', ' ', NULL, '620400'); +INSERT INTO `hw_tenant` VALUES (117, '1', '云南*****科技有限公司', 1, '刘先生', '15034555555', '', NULL, NULL, NULL, '9', '0', 'admin', '2024-08-19 15:39:07', NULL, NULL, NULL, '1', NULL, NULL, NULL); +INSERT INTO `hw_tenant` VALUES (118, '1', '白银XXXX公司', 5, '王先生', '15234253041', NULL, NULL, NULL, NULL, '9', '0', 'admin', '2024-09-26 11:17:40', 'admin', '2024-09-26 11:18:00', NULL, '2', '白银市', NULL, '620000'); +INSERT INTO `hw_tenant` VALUES (119, '1', '深圳市XXX公司', 5, '黄先生', '15207486494', NULL, NULL, NULL, NULL, '9', '0', 'admin', '2024-09-26 11:20:04', NULL, NULL, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `hw_tenant` VALUES (120, '1', '南京供电', 6, '曾XX', '18368938116', NULL, NULL, NULL, NULL, '1', '0', 'admin', '2025-06-03 16:08:03', NULL, NULL, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `hw_tenant` VALUES (121, '1', '信息科技有限公司', 8, '孙XX', '15763917508', NULL, NULL, NULL, NULL, '9', '0', 'admin', '2025-06-05 17:09:55', 'admin', '2025-06-05 17:10:39', NULL, NULL, NULL, NULL, NULL); +INSERT INTO `hw_tenant` VALUES (122, '1', '华为科技有限公司', 8, '孙XX', '15763917508', NULL, NULL, NULL, NULL, '1', '0', 'admin', '2025-06-05 17:12:04', NULL, NULL, NULL, NULL, NULL, NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_web +-- ---------------------------- +DROP TABLE IF EXISTS `hw_web`; +CREATE TABLE `hw_web` ( + `web_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `web_json` json NULL COMMENT 'json', + `web_json_string` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'json字符串', + `web_code` bigint NULL DEFAULT NULL COMMENT '页面', + `is_delete` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '逻辑删除()', + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `web_json_english` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + PRIMARY KEY (`web_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'haiwei官网json' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_web +-- ---------------------------- +INSERT INTO `hw_web` VALUES (1, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"222\",\"value\":\"333\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"333\",\"value\":\"444\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"444\",\"value\":\"555\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"555\",\"value\":\"666\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"666\",\"value\":\"777\"},{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"666\",\"value\":\"777\"}]}},{\"type\":2,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"contentTitle\":\"关于我们\",\"contentSubTitle\":\"ABOUT US\",\"contentInfo\":\"青岛海威物联科技有限公司,致力于工业物联网软硬件系统研发、生产和销售,提供感知互联的工业化联网整体解决方案。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/shijie_20250822160139A177.jpg\"}},{\"type\":2,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"contentTitle\":\"关于我们\",\"contentSubTitle\":\"ABOUT US\",\"contentInfo\":\"同时,公司大力推进基于RFID、传感、边缘采集计算、数据传输等技术的工业互联网解决方案在轮胎产业链中的深入应用,为轮胎生产管理、仓储物流、销售跟踪、车队管理、轮胎翻新等环节提供一站式解决方案。所有核心技术及产品均具有自主知识产权\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/2_20250822160452A178.jpg\"}},{\"type\":2,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"contentTitle\":\"关于我们\",\"contentSubTitle\":\"ABOUT US\",\"contentInfo\":\"公司承担国家橡胶与轮胎工程技术研究中心RFID研究所;被认定为国家级专精特新“小巨人”企业、国家高新技术企业、青岛市技术创新中心、科技型中小企业、青岛市雏鹰企业;通过ISO 9001:2015质量管理体系认证、ISO 14001:2015环境体系认证、IS045001:2018职业健康安全管理体系认证;通过信息系统建设和服务能力评估CS2级;主持制定轮胎用RFID电子标签四项ISO国际标准、四项国家标准。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/3_20250822160536A179.jpg\"}}]', 3, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (2, NULL, '[{\"type\":9,\"value\":{\"title\":\"RFID应用案例\",\"subTitle\":\"\",\"list\":[{\"value\":\"请输全厂自动物流线装配RFID读写器,碎胶工序、小料工序、密炼半制品、成型、胎胚管理、全流程输送RFID方案。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片1_20250822132316A070.png\"},{\"value\":\"半制品立库,各供胶机台RFID防误验证。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片2_20250822132317A071.png\"}]}},{\"type\":9,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"list\":[{\"value\":\"硫化模具、口型板RFID管理。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片3_20250822132322A072.png\"},{\"value\":\"\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片4_20250822132325A073.png\"}]}},{\"type\":9,\"value\":{\"title\":\"RFID智能化方案\",\"subTitle\":\"RFID-智能工厂基石\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片8_20250822132701A077.png\",\"value\":\"    RFID应用,对于自动化程度高场景,成效更显著。\"},{\"value\":\"   通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。 通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片9_20250822132705A078.png\"}]}},{\"type\":9,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"list\":[]}}]', 6, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (3, NULL, '[]', 5, '1', '2025-12-08 09:32:11', NULL, NULL); +INSERT INTO `hw_web` VALUES (4, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\"}]}},{\"type\":9,\"value\":{\"title\":\"关于我们\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/公司简介_20250822141644A128.jpg\",\"value\":\"公司简介:\\n公司致力于工业物联网技术的研发和创新,推动工业全流程物联发展\"},{\"value\":\"团队风貌:\\n不忘初心,励志前行\\n凝心聚力,携手奋进\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/团队风貌_20250822141723A129.jpg\"},{\"value\":\"荣誉资质:\\n坚持走自主创新研发道路,团队多项技术成果进入国际领先行列\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/荣誉资质_20250822141910A132.png\"},{\"value\":\"人力资源:\\n坚持以人为本、以才用人,员工是海威最宝贵的财富\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/坚持以人为本、以才用人,员工是海威最宝贵的财富_20250822141900A131.jpg\"}]}}]', 1, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (5, NULL, '{\"banner\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\",\"bannerTitle\":\"\",\"productList\":[{\"id\":11,\"name\":\"轮胎RFID\",\"list\":[{\"name\":\"分类1 \",\"list\":[{\"name\":\"RFID轮胎\",\"value\":\"软控为轮胎产业链提供RFID轮胎整体解决方案,包括:系列化轮胎用RFID标签,系列化RFID标签封胶设备、定制RFID标签贴合设备,RFID 轮胎生产过程数据采集系统,及基于不同应用场景的信息化管理系统等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/轮胎_20250917144033A235.png\",\"id\":2},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":3},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":4}],\"id\":1}]},{\"id\":12,\"name\":\"超高频RFID\",\"list\":[{\"name\":\"超高频一体式读写器 \",\"list\":[{\"name\":\"HW-145L-6系列\",\"value\":\"      远距离、高增益、多接口,POE宽电压直流供电,多路GPIO。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/a7ed9fc42af9b6386126eee351f4937_20250929143910A279.png\",\"id\":2},{\"name\":\"HW-R300系列\",\"value\":\"                  中短距离,1路输入,体积小巧,TCP/RS485\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250926144121_20250929144331A280.png\",\"id\":3},{\"name\":\"HW-D100系列\",\"value\":\"                                 短距离,桌面式,USB接口\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片bbb_20250930172331A326.png\",\"id\":4},{\"name\":\"蓝牙手持式读写器\",\"value\":\"蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/蓝牙手持式读写器_20250917144525A239.png\",\"id\":5}],\"id\":1},{\"name\":\"超高频分体式读写器  \",\"list\":[{\"name\":\"HW-R200系列\",\"value\":\"             两通道/四通道天线,体积小巧,TCP通讯,远距读取\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片1_20250929162825A294.png\",\"id\":2},{\"name\":\"HW-R400系列\",\"value\":\"    四通道外接天线,超高性能,高阅读灵敏度,稳定可靠的性能\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片2_20250929163017A295.png\",\"id\":3},{\"name\":\"HW-RX系列\",\"value\":\"                 四通道天线,体积小巧,TCP通讯,快速响应。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片3_20250929163215A296.png\",\"id\":4}],\"id\":2},{\"name\":\"超高频扩展式读写器  \",\"list\":[{\"name\":\"HW-R110系列\",\"value\":\"     1路内置天线,1路外接天线接口,体积紧凑,可单机可扩展\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片vvv_20250930171716A325.png\",\"id\":2},{\"name\":\"HW-R170系列\",\"value\":\"             1路内置窄波束天线,1路外接扩展天线接口,圆极化\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片6_20250929164228A298.png\",\"id\":3}],\"id\":3},{\"name\":\"超高频天线 \",\"list\":[{\"name\":\"ACB5040系列\",\"value\":\"                               圆极化、低驻波比,超小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片8_20250929164750A299.png\",\"id\":2},{\"name\":\"A130D06系列\",\"value\":\"                         圆极化、低驻波比、高性能、小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片9_20250929165030A300.png\",\"id\":3},{\"name\":\"AB7020系列\",\"value\":\"                       圆极化、窄波束、低驻波比、中小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片10_20250929165329A301.png\",\"id\":4},{\"name\":\"AB10010系列\",\"value\":\"                           天线具有定向性、窄波束、高增益\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250929165850_20250929165911A302.png\",\"id\":5}],\"id\":5},{\"name\":\"超高频标签 \",\"list\":[{\"name\":\"HW-A6320\",\"value\":\"          适用于仓储物流、生产制造、铁路以及循环运输管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片_20250929170754A304.png\",\"id\":2},{\"name\":\"HW-A6020\",\"value\":\"                              适用于资产管理、托盘管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片22_20250929171525A306.png\",\"id\":3},{\"name\":\"HW-PV8654\",\"value\":\"适用于多标签识别的应用场合,可用打印机打印图案等个性化信息\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片15_20250929172804A311.png\",\"id\":4},{\"name\":\"HW-QT5050\",\"value\":\"                                 无纺布或卡片型,全向标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片333_20250930090410A313.png\",\"id\":5},{\"name\":\"HW-HT4634\",\"value\":\"                                           矩形、耐酸碱\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片55_20250930090733A314.png\",\"id\":6},{\"name\":\"HW-P6020LM\",\"value\":\"                                          长条形,亮灯标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片666_20250930091535A317.png\",\"id\":7},{\"name\":\"HW-P9010\",\"value\":\"                                        长条型,PCB标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片7777_20250930170214A318.png\",\"id\":8},{\"name\":\"T0505\",\"value\":\"                          正方形、耐高温、应用于模具管理\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片gg_20250930170722A322.png\",\"id\":9},{\"name\":\"HW-TL1621\",\"value\":\"                                   螺栓型、耐高温、抗金属\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片sss_20250930171327A323.png\",\"id\":10}],\"id\":6}]},{\"id\":13,\"name\":\"高频RFID\",\"list\":[{\"name\":\"高频一体式读写器  \",\"list\":[{\"name\":\"HW-RFR-050系列\",\"value\":\"                         体积小,三防性能优,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片bb_20251015105305A346.png\",\"id\":2},{\"name\":\"HW-RFR-050系列\",\"value\":\"             螺栓形状读写器,易固定;尺寸小,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片h_20251015105320A347.png\",\"id\":3},{\"name\":\"HW-RFR-020系列\",\"value\":\"            螺栓形状读写器,易固定;尺寸小,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片jj_20251015101618A337.png\",\"id\":4},{\"name\":\"HW-RFR-010系列\",\"value\":\"                   尺寸更加小巧,短距稳定识别,三防性能优\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片d_20251015105254A345.png\",\"id\":5},{\"name\":\"HW-RFR-RFLY\",\"value\":\"                               TCP网络通讯,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片M_20251015105452A348.png\",\"id\":6},{\"name\":\"HW-RFR-RFLY-I90\",\"value\":\"                             TCP网络通讯,超远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片jjjjj_20251015110254A349.png\",\"id\":7},{\"name\":\"HW-D80系列\",\"value\":\"                            UID通讯,桌面发卡,近距识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/gdfdf_20251015111010A350.png\",\"id\":8},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":9}],\"id\":2},{\"name\":\"高频RFID标签 \",\"list\":[{\"name\":\"HW-HF-PVS-8654\",\"value\":\"独特的天线设计,优异的天线性能 ;可用于自动化识别、托盘管理、资产管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片12_20251015111534A351.png\",\"id\":2},{\"name\":\"HW-HF-PPS-D50\",\"value\":\"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片D50_20251015113514A352.png\",\"id\":3},{\"name\":\"HW-HF-PPS-D30\",\"value\":\"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片D30_20251015113547A353.png\",\"id\":4},{\"name\":\"HW-HF-PVS-D30\",\"value\":\"        PVC材质,可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片1PVS-D30_20251015130809A354.png\",\"id\":5},{\"name\":\"HW-TR6244-HF-PET\",\"value\":\"            圆环形状标签,PET材质,用于圆柱电芯托杯管理\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片YH6244_20251015131136A355.png\",\"id\":6}],\"id\":3}]},{\"id\":14,\"name\":\"传感器\",\"list\":[{\"name\":\"物联网硬件产品系列\",\"list\":[{\"name\":\"无线传感器接收显示仪\",\"value\":\"支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/无线传感器接收显示仪_20250917145129A247.png\",\"id\":2},{\"name\":\"车载物联定位终端\",\"value\":\"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/车载物联定位终端_20250917145144A248.png\",\"id\":3},{\"name\":\"工业物联云智能终端\",\"value\":\"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/工业物联云智能终端_20250917145224A249.png\",\"id\":4}],\"id\":1}]},{\"id\":15,\"name\":\"物联终端\",\"list\":[{\"name\":\"\\n物联网硬件产品系列\",\"list\":[{\"name\":\"温湿度传感器\",\"value\":\"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/温湿度传感器_20250917145325A250.png\",\"id\":2},{\"name\":\"无线温度传感器\",\"value\":\"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/无线温度传感器_20250917145344A251.png\",\"id\":3},{\"name\":\"EPD无线显示器\",\"value\":\"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/EPD无线显示器_20250917145418A252.png\",\"id\":4},{\"name\":\"红外温度传感器\",\"value\":\"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/红外温度传感器_20250917145433A253.png\",\"id\":5}],\"id\":1}]},{\"id\":16,\"name\":\"工业软件\",\"list\":[{\"name\":\"MOM\",\"list\":[{\"name\":\"MOM\",\"value\":\"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/MOM_20250917145527A254.png\",\"id\":2}],\"id\":1}]}]}', 7, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (6, NULL, '{\"banner\":\"https://www.genrace.com/template/default/images/pages/prodDetail-banner.jpg\",\"banner1\":\"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中端距离,1路输入,体积小巧,TCP/RS485。\",\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/08/25/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91_20250825192156A181.png\"},{\"url\":\"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"}],\"params\":[{\"title\":\"参数\",\"list\":[{\"name\":\"射频协议\",\"value\":\"ISO/IEC 18000-63 /EPC Gen2v2\"},{\"name\":\"工作频段\",\"value\":\"中国频段:920-925MHz\\n其他国家频段(可定制)\"},{\"name\":\"发射功率\",\"value\":\"10-26dBm 可调,步进 1 dB,精度±1dB\"},{\"name\":\"天线增益\",\"value\":\"3dBi\"},{\"name\":\"天线驻波比\",\"value\":\"≦2:1\"},{\"name\":\"盘点速率\",\"value\":\"120 tag/s\"},{\"name\":\"读卡距离\",\"value\":\"读取TPALN9662标签,最大读距:1.4m(读距随标签型号不同而有所差异)\"},{\"name\":\"通讯接口\",\"value\":\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"},{\"name\":\"通讯协议\",\"value\":\"Modbus-RTU通信协议,网口TCP提供基于C#的SDK接口;适配工业总线协议(外接网关)(选配)\"},{\"name\":\"GPIO端口\",\"value\":\"M12-5-B 航空接口,兼容 5~24V 电平,1路输入\"},{\"name\":\"声音指示\",\"value\":\"1个蜂鸣器\"},{\"name\":\"开发接口\",\"value\":\"支持 C#、JAVA\"},{\"name\":\"供电电源\",\"value\":\"12~24VDC(推荐 12V 标配适配器)\"},{\"name\":\"整机峰值功率\",\"value\":\"2.5W\"},{\"name\":\"外形尺寸\",\"value\":\"90mm*90mm*32 mm\"},{\"name\":\"产品重量\",\"value\":\"约288g\"},{\"name\":\"外壳材料\",\"value\":\"铝合金+PC\"},{\"name\":\"外壳颜色\",\"value\":\"黑色、银色\"},{\"name\":\"工作温度\",\"value\":\"-25℃~60℃\"},{\"name\":\"存储温度\",\"value\":\"-40℃~85℃\"},{\"name\":\"工作湿度\",\"value\":\"5%~95%RH 无冷凝\"},{\"name\":\"IP等级\",\"value\":\"IP54\"}]}],\"fileList\":[{\"name\":\"操作指南\",\"value\":\"硬件操作指南\",\"url\":\"\"},{\"name\":\"操作指南\",\"value\":\"硬件操作指南\",\"url\":\"\"}]}', 11, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (7, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/产品中心_20250822091555A013.jpg\",\"title\":\"\\n\",\"value\":\"\\n\"},{\"title\":\"\\n\",\"value\":\"\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/1首页-产品中心01_20241219165916A006_20250822091622A014.png\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\"}]}},{\"type\":8,\"value\":{\"title\":\"高频RFID读头\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/高频RFID读头_20250822100334A019.png\",\"title\":\"高频RFID读头 \",\"leftTitle\":\"HW-RFR-050-B-003-B1204S\",\"leftInfo\":\"\\n\",\"infos\":[{\"title\":\"射频协议\",\"info\":\"符合ISO/IEC 13.56 MHZ\",\"icon\":\"\"},{\"title\":\"盘点速度\",\"info\":\"20ms/次\",\"icon\":\"\"},{\"title\":\"通信协议\",\"info\":\"RS485\",\"icon\":\"\"},{\"title\":\"输入电压\",\"info\":\"12~24VDC\",\"icon\":\"\"},{\"title\":\"尺寸(mm)\",\"info\":\"40W*72L*13H\",\"icon\":\"\"},{\"title\":\"工作温度\",\"info\":\"-30℃~+70℃\",\"icon\":\"\"},{\"title\":\"防护等级\",\"info\":\"IP67\",\"icon\":\"\"}]}]}},{\"type\":8,\"value\":{\"title\":\"高频RFID一体机  \",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/高频RFID一体机_20250822100352A020.png\",\"title\":\"高频RFID一体机\",\"leftTitle\":\"HW-RSLIM-HF\",\"leftInfo\":\"\\n\",\"infos\":[{\"title\":\"射频协议\",\"info\":\"符合ISO/IEC 13.56 MHz\",\"icon\":\"\"},{\"title\":\"盘点速度\",\"info\":\"10张/秒\",\"icon\":\"\"},{\"title\":\"通信协议\",\"info\":\"TCP/IP、RS232、RS485 \",\"icon\":\"\"},{\"title\":\"输入电压\",\"info\":\"9~32VDC\",\"icon\":\"\"},{\"title\":\"尺寸(mm)\",\"info\":\"65L*65W*30H\",\"icon\":\"\"},{\"title\":\"工作温度\",\"info\":\"-30℃~+70℃\",\"icon\":\"\"},{\"title\":\"工作湿度\",\"info\":\"5%~95%RH 无冷凝\",\"icon\":\"\"}]}]}},{\"type\":8,\"value\":{\"title\":\"高频RFID标签  \",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/高频RFID圆环标签_20250822093525A018.png\",\"title\":\"高频RFID标签  \",\"leftTitle\":\"HW-PD50-HF\",\"leftInfo\":\"\",\"infos\":[{\"title\":\"国际标准\",\"info\":\"ISO15693\",\"icon\":\"\"},{\"title\":\"封装材料\",\"info\":\"PVC/PET\",\"icon\":\"\"},{\"title\":\"工艺\",\"info\":\"印刷、打码、写数据等\",\"icon\":\"\"},{\"title\":\"尺寸\",\"info\":\"内径Φ44mm 外径Φ64mm\",\"icon\":\"\"},{\"title\":\"读取距离\",\"info\":\"0-10cm\",\"icon\":\"\"},{\"title\":\"数据存储时间\",\"info\":\"50年\",\"icon\":\"\"},{\"title\":\"可擦写次数\",\"info\":\"10万次\",\"icon\":\"\"}]}]}}]', 13, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (8, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\"},{\"title\":\"\\n\",\"value\":\"\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/25/应用开发_20250825192156A181.png\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png\"}]}},{\"type\":9,\"value\":{\"title\":\"超高频RFID读写器\",\"subTitle\":\"读写器产品包含一体式读写器、车载读写器、四通道读写器、各尺寸读写器天线等一系列产品,具 备丰富的通讯接口和公用的软件接口平台,具有稳定性强、性价比高、易开发、易部署等特点\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/一体式读写器(高性能),内置高性能圆极化天线,实现远距离识别。_20250822101411A021.png\",\"value\":\"一体式读写器(高性能),内置高性能圆极化天线,实现远距离识别。\\n\\n\"},{\"value\":\"一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。 一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。_20250822101455A022.png\"},{\"value\":\"四通道读写器,拥有4通道射频输出口,丰富的外设接口能灵活满足不同的应用需求。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/四通道读写器,拥有4通道射频输出口,丰富的外设接口能灵活满足不同的应用需求。_20250822101513A023.png\"},{\"value\":\"两通道车载式读写器,采用工业化防振动设计理念,支持蓝牙连接,提供更加稳定可靠的物理及电器连接方式。\\n请输入\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/两通道车载式读写器,采用工业化防振动设计理念,支持蓝牙连接,提供更加稳定可靠的物理及电器连接方式。_20250822101528A024.png\"},{\"value\":\"RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。 输入 请RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。 输入 \",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。_20250822101611A025.png\"},{\"value\":\"蓝牙手持式读写器,蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/蓝牙手持式读写器_20250822101633A026.png\"},{\"value\":\"RFID通道门具有窄波束、高增益特点,适用于超高频门禁通道类、物流仓储、人员、图书、档案馆、医疗系统、设备资产等应用。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/RFID通道门具有窄波束、高增益特点,适用于超高频门禁通道类、物流仓储、人员、图书、档案馆、医疗系统、设备资产等应用。_20250822101703A027.png\"},{\"value\":\"RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如快递、服装、皮具箱包、酒类等行业。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如快递、服装、皮具箱包、酒类等行业。_20250822101716A028.png\"}]}},{\"type\":9,\"value\":{\"title\":\"超高频RFID标签\",\"subTitle\":\"根据客户需求,提供适用于物流、仓储、资产管理等应用场景的各类标签。并且有多种功能等定制化标签供客户选择。\\n\\n\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/PixPin_2025-08-22_10-18-50_20250822101910A029.png\",\"value\":\"\"},{\"value\":\"\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/PixPin_2025-08-22_10-19-20_20250822101931A030.png\"},{\"value\":\"\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/11_20250822101953A031.png\"},{\"value\":\"\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/111_20250822102005A032.png\"},{\"value\":\"耐高温标签\\n:耐温范围为100℃~230℃。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/22_20250822102031A033.png\"},{\"value\":\"测温标签:测量温度范围为-25℃~100℃,温度传感器无电池。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/222_20250822102049A034.png\"},{\"value\":\"电缆标签:粘贴缠绕于物体表面,允许多次翻折缠绕。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/2222_20250822102107A035.png\"},{\"value\":\"铝模板标签:耐酸碱,耐腐蚀。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/22222_20250822102125A036.png\"}]}}]', 12, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (9, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"}]}},{\"type\":9,\"value\":{\"title\":\"物联网硬件产品系列\\n\\n\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。_20250822103145A037.png\",\"value\":\"无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。\\n\\n\"},{\"value\":\"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/车载_20250822103928A038.png\"},{\"value\":\"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。_20250822103947A039.png\"},{\"value\":\"工业物联云智能终端具有 8 路开关量输入检测、16 开关量输出、RS485 总线、网络等硬件接口。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/工业物联云智能终端具有 8 路开关量输入检测、16 开关量输出、RS485 总线、网络等硬件接口。_20250822104006A040.png\"}]}}]', 14, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (10, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"}]}},{\"type\":9,\"value\":{\"title\":\"物联网硬件产品系列\\n\\n\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式。_20250822104331A042.png\",\"value\":\"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式\"},{\"value\":\"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。 \\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/无线温度传感器_20250822104419A043.png\"},{\"value\":\"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。_20250822104449A044.png\"},{\"value\":\"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。_20250822104525A045.png\"}]}}]', 15, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (11, NULL, '[{\"type\":2,\"value\":{\"title\":\"物联网平台\\n\",\"subTitle\":\"\",\"contentTitle\":\"系统架构\",\"contentSubTitle\":\"\\n\",\"contentInfo\":\"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\"}},{\"type\":2,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"contentTitle\":\"功能介绍\",\"contentSubTitle\":\"\\n\",\"contentInfo\":\"可以实现海量的设备遥测、远程控制、告警事件处理、设备台账管理。其它应用程序或者功能模块通过与实时数据库、接口交互而实现其功能及扩展。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/0_20250822132605A076.png\"}}]', 16, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (12, NULL, '[{\"type\":2,\"value\":{\"title\":\"快递物流整体解决方案\",\"subTitle\":\"系统设备主要包括手持 RFID 阅读器、矩阵 RFID 识读通道机、小件分拣机 RFID 读写系统、RFID 通道门等。\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"方案介绍\",\"contentInfo\":\" 1.手持 RFID 阅读器主要用于 RFID 标签、总包信息的手动绑定/写入,以及批量空袋的手动识读。   2.矩阵 RFID 识读通道机主要用于识别邮袋 RFID 标签。                                                   3.小件分拣机 RFID 读写系统主要用于 RFID 标签、总包信息的自动绑定/写入。                 4.RFID 通道门主要用于批量空袋的自动识读/擦除。                                                 5.RFID标签缝制到循环邮袋上,作为资产管理和邮袋识别的数据载体。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片17_20250822141810A130.jpg\"}},{\"type\":9,\"value\":{\"title\":\"系列化通道机产品解决方案\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/26/92_20250826090151A189.png\",\"value\":\"兼容不同皮带内宽 ;改造款无需改造皮带机 ;模块化,安装简单。\"},{\"value\":\"结构简单,价格便宜, 兼容不同DWS、六面扫协议 指令。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/26/93_20250826090207A190.png\"},{\"value\":\"优化算法,可启停读取, 有效防止串读等。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/26/94_20250826090227A191.png\"},{\"value\":\"整体发货,无需安装, 配套标准程序,即开即用。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/26/95_20250826090240A192.png\"}]}},{\"type\":2,\"value\":{\"title\":\"标准化小件机RFID产品解决方案\",\"subTitle\":\"\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"方案介绍\",\"contentInfo\":\"1、“自动感应”、“刷卡”多种扫描形式 2、分体式一体式、配套定制支架等,适配各种分拣机 3、三级组网结构,环形分拣机、直线分拣机灵活组网 4、线缆T型标准化、设备模块化设计,检修维护简单快速\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/Snipaste_2025-08-22_14-31-33_20250822143139A142.png\"}},{\"type\":2,\"value\":{\"title\":\"皮带机磁检测设备\",\"subTitle\":\"\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"方案介绍\",\"contentInfo\":\"皮带机磁检测设备是我司独立自主设计且拥有专利知识产权的一款磁检测产品。用于物流包裹分拣机供件 台,检测包裹内的磁性物质的作用,防止由于包裹内有磁性物质导致包裹吸附在滑槽上,影响快递包裹时效。 特点:磁性物质,1.5m/s皮带机速度快速检测 ,低成本皮带机宽度范围内检测全覆盖; 多种适配皮带机安装方式;UJM高检测灵敏度,检测灵敏度大于等于3mT。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/26/96_20250826090302A193.png\"}}]', 19, '1', '2025-11-21 17:26:35', NULL, NULL); +INSERT INTO `hw_web` VALUES (13, NULL, '[{\"type\":10,\"value\":{\"title\":\"\",\"subTitle\":\"在畜牧屠宰行业,RFID助力车间自动化,智能化生产,合作客户包括双汇、正大、牧原等\\n\",\"list\":[{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/01_20250822135102A119.png\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/13_20250822135108A120.png\"},{\"title\":\"请输入\",\"value\":\"请输入\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"}]}},{\"type\":11,\"value\":{\"title\":\"载具管理\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/牧原屠宰线扁担钩管理_20250822151858A161.png\",\"title\":\"牧原屠宰线扁担钩管理\",\"value\":\"在扁担钩安装RFID标签,在输送线安装RFID读写器,将定级称重等信息与RFID绑定,实现白条全流程追溯。\\n\\n\"},{\"title\":\"正大料框桶车管理\",\"value\":\"在料筐预铸RFID标签,场内循环扫描。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/正大料框桶车管理_20250822151912A162.png\"},{\"title\":\"正大料框桶车管理\",\"value\":\"在桶车安装RFID标签,实现投料验证,追溯管理。\\n\\n\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/在料筐预铸RFID标签,场内循环扫描_20250822151922A163.png\"}]}}]', 20, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (14, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片24_20250822135610A123.png\",\"title\":\"\\n\",\"value\":\"\\n\"}]}},{\"type\":6,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片36_20250822142617A134.jpg\",\"title\":\"设备管理系统\",\"subTitle\":\"RFID生产管理系统\",\"info\":\"基于RFID应用的生产管理特点\\n\\n基于RFID应用的生产管理具有自动,防误、追溯、集成的特点\"}},{\"type\":6,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片27_20250822140450A125.png\",\"title\":\"\",\"subTitle\":\"仓储物流管理系统\",\"info\":\"通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。\"}},{\"type\":6,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片31_20250822140953A127.png\",\"title\":\"\",\"subTitle\":\"RFID模具管理系统\",\"info\":\"专为工装管理温区定制:存放温度-10~250摄氏度,读取工作温度0~120摄氏度。 专为钢制工装管理外形定制:标签尺寸缩减到极限尺寸,小尺寸钢制工装开槽植入无压力。 专为钢制工装管理性能定制:标签在植入钢制工装开槽并封装后,配合软控提供的手持机读取距离>=25cm(配合固定式操作性能更佳)。\"}},{\"type\":2,\"value\":{\"title\":\"智能制造平台\",\"subTitle\":\"\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\" 设备互联系统\",\"contentInfo\":\"智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片25_20250822135851A124.png\"}},{\"type\":6,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/图片30_20250822140807A126.png\",\"title\":\"\",\"subTitle\":\"能源管理系统\",\"info\":\"在企业扩大产能的同时,通过遍布全场的三级计量监控,合理计划和利用能源,以达到建立合理KPI指标体系,增强全员节能意识,提高能源利用率为目的信息化管控系统。\"}}]', 22, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (15, NULL, '[{\"type\":11,\"value\":{\"title\":\"企业资质\",\"subTitle\":\"\",\"list\":[{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/国家级专精特新“小巨人”企业_20250822152510A168.png\",\"title\":\"\\n\",\"value\":\"国家级专精特新“小巨人”企业\"},{\"title\":\"\\n\",\"value\":\"国家高新技术企业\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/国家高新技术企业_20250822152526A169.jpg\"},{\"title\":\"\\n\",\"value\":\"青岛市专精特新中小企业\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/青岛市专精特新中小企业_20250822152636A170.png\"},{\"title\":\"\\n\",\"value\":\"通过信息系统建设和服务能力评估CS2级\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/通过信息系统建设和服务能力评估CS2级_20250822152811A171.png\"},{\"title\":\"\\n\",\"value\":\"第十一届LT中国物流技术奖-创新应用奖\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/第十一届LT中国物流技术奖-创新应用奖_20250822152846A172.jpg\"},{\"title\":\"\\n\",\"value\":\"国产鲲鹏系统软件兼容性测试技术认证\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/国产鲲鹏系统软件兼容性测试技术认证_20250822153004A173.png\"}]}},{\"type\":11,\"value\":{\"title\":\"产品认证\\n\",\"subTitle\":\"RFID产品系列取得产品认证(CE、ROHS、SGS)\\n\",\"list\":[{\"title\":\"\\n\",\"value\":\"CE\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/CE_20250822153037A174.png\"},{\"title\":\"\\n\",\"value\":\"ROHS\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/ROHS_20250822153056A175.png\"},{\"title\":\"\\n\",\"value\":\"SGS\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/SGS_20250822153118A176.png\"}]}}]', 10, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (16, NULL, '[{\"type\":2,\"value\":{\"title\":\"海威物联参加2024智能物流装备供应链集采大会暨第十二次青浦圆桌会议\",\"subTitle\":\"\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"\\n\",\"contentInfo\":\"11月26-27日,2024智能物流装备供应链集采大会暨第十二次青浦圆桌会议在南陵举办。本次大会以“向新而行、以质致远、绿色发展、共创未来”为主题,200余家智能物流装备供应链企业参加,共同探讨智能物流装备领域的新趋势、新机遇,旨在达成更多富有成效的新合作、新成果。青岛海威物联科技有限公司作为智能物流装备优质供应商应邀出席,并发表《物畅其流 “芯”质提效》的主题演讲。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/mt_20250822151343A158.jpg\"}},{\"type\":2,\"value\":{\"title\":\"\",\"subTitle\":\"\",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"\\n\",\"contentInfo\":\"海威物联总经理助理张东辉在主题演讲中详细介绍了快递行业如何通过RFID技术的应用显著提升生产效率,并描绘了生产仓储行业RFID技术的发展蓝图;同时分享了边缘物联技术在物流和制造行业的赋能解决方案及成功案例,为行业同仁提供了实践参考。 此次会议不仅是一次思想的盛宴,也是海威物联在快递物流行业的重要展示,为海威物联的发展注入了新动力,也为推动工业物联网技术的发展和物流企业的数字化升级奠定了坚实的基础。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/mt2_20250822151416A159.jpg\"}}]', 9, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (17, NULL, '[{\"type\":5,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/合作伙伴_20250822152100A165.png\",\"title\":\"合作伙伴\",\"subTitle\":\"\"}},{\"type\":5,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/08/22/合作伙伴2_20250822152147A167.png\",\"title\":\"\",\"subTitle\":\"\"}}]', 8, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (18, NULL, '{\"banner\":\"https://www.genrace.com/template/default/images/pages/prodDetail-banner.jpg\",\"banner1\":\"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中端距离,1路输入,体积小巧,TCP/RS485。\",\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/08/25/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91_20250825192156A181.png\"},{\"url\":\"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"}],\"params\":[{\"title\":\"参数\",\"list\":[{\"name\":\"射频协议\",\"value\":\"ISO/IEC 18000-63 /EPC Gen2v2\"},{\"name\":\"工作频段\",\"value\":\"中国频段:920-925MHz\\n其他国家频段(可定制)\"},{\"name\":\"发射功率\",\"value\":\"10-26dBm 可调,步进 1 dB,精度±1dB\"},{\"name\":\"天线增益\",\"value\":\"3dBi\"},{\"name\":\"天线驻波比\",\"value\":\"≦2:1\"},{\"name\":\"盘点速率\",\"value\":\"120 tag/s\"},{\"name\":\"读卡距离\",\"value\":\"读取TPALN9662标签,最大读距:1.4m(读距随标签型号不同而有所差异)\"},{\"name\":\"通讯接口\",\"value\":\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"},{\"name\":\"通讯协议\",\"value\":\"Modbus-RTU通信协议,网口TCP提供基于C#的SDK接口;适配工业总线协议(外接网关)(选配)\"},{\"name\":\"GPIO端口\",\"value\":\"M12-5-B 航空接口,兼容 5~24V 电平,1路输入\"},{\"name\":\"声音指示\",\"value\":\"1个蜂鸣器\"},{\"name\":\"开发接口\",\"value\":\"支持 C#、JAVA\"},{\"name\":\"供电电源\",\"value\":\"12~24VDC(推荐 12V 标配适配器)\"},{\"name\":\"整机峰值功率\",\"value\":\"2.5W\"},{\"name\":\"外形尺寸\",\"value\":\"90mm*90mm*32 mm\"},{\"name\":\"产品重量\",\"value\":\"约288g\"},{\"name\":\"外壳材料\",\"value\":\"铝合金+PC\"},{\"name\":\"外壳颜色\",\"value\":\"黑色、银色\"},{\"name\":\"工作温度\",\"value\":\"-25℃~60℃\"},{\"name\":\"存储温度\",\"value\":\"-40℃~85℃\"},{\"name\":\"工作湿度\",\"value\":\"5%~95%RH 无冷凝\"},{\"name\":\"IP等级\",\"value\":\"IP54\"}]}],\"fileList\":[{\"name\":\"操作指南\",\"value\":\"硬件操作指南\",\"url\":\"\"},{\"name\":\"操作指南\",\"value\":\"硬件操作指南\",\"url\":\"\"}]}', 11, '0', NULL, NULL, NULL); +INSERT INTO `hw_web` VALUES (19, NULL, '[]', 2, '0', NULL, '2025-11-17 10:46:54', NULL); +INSERT INTO `hw_web` VALUES (20, NULL, '[{\"type\":5,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png\",\"title\":\"方案介绍\",\"subTitle\":\"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。\\n“RFID小件包的自动识别分拣及追踪管理解决方案\\\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。\\n\"}},{\"type\":2,\"value\":{\"title\":\" \",\"subTitle\":\" \",\"contentTitle\":\"\\n\",\"contentSubTitle\":\"\\n\",\"contentInfo\":\"\\n\\n1.手持 RFID 阅读器主要用于 RFID 标签、总包信息的手动绑定/写入,以及批量空袋的手动识读。 \\n2.矩阵 RFID 识读通道机主要用于识别邮袋 RFID 标签。 \\n3.小件分拣机 RFID 读写系统主要用于 RFID 标签、总包信息的自动绑定/写入。\\n4.RFID 通道门主要用于批量空袋的自动识读/擦除。\\n5.RFID标签缝制到循环邮袋上,作为资产管理和邮袋识别的数据载体。\\n\\n\",\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\"}}]', 19, '1', '2025-11-28 09:56:44', '2025-11-21 17:26:35', NULL); +INSERT INTO `hw_web` VALUES (21, NULL, '[{\"type\":5,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png\",\"title\":\"方案介绍\",\"subTitle\":\"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。 “RFID小件包的自动识别分拣及追踪管理解决方案\\\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效整体系统设备主要包括手持 RFID 阅读器、小件分拣机 RFID 读写系统、擦除RFID设备、矩阵 RFID 识读通道机、包裹分拣机、RFID 通道门\\n。\"}}]', 19, '1', '2025-11-28 09:58:22', '2025-11-28 09:56:44', NULL); +INSERT INTO `hw_web` VALUES (22, NULL, '[{\"type\":5,\"value\":{\"icon\":\"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png\",\"title\":\"方案介绍\",\"subTitle\":\"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。 “RFID小件包的自动识别分拣及追踪管理解决方案\\\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。整体系统设备主要包括手持 RFID 阅读器、小件分拣机 RFID 读写系统、擦除RFID设备、矩阵 RFID 识读通道机、包裹分拣机、RFID 通道门\\n。\"}}]', 19, '0', NULL, '2025-11-28 09:58:22', NULL); +INSERT INTO `hw_web` VALUES (23, NULL, '[{\"type\":1,\"value\":{\"title\":\"123\",\"subTitle\":\"456\",\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"}]}}]', 5, '1', '2025-12-08 09:44:08', '2025-12-08 09:32:11', NULL); +INSERT INTO `hw_web` VALUES (24, NULL, '[{\"type\":1,\"value\":{\"title\":\"123\",\"subTitle\":\"456\",\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"}]}}]', 5, '0', NULL, '2025-12-08 09:44:08', NULL); +INSERT INTO `hw_web` VALUES (25, NULL, '[]', -1, '1', '2025-12-08 10:13:47', '2025-12-08 10:09:55', NULL); +INSERT INTO `hw_web` VALUES (26, NULL, '[]', -1, '1', '2025-12-08 10:14:34', '2025-12-08 10:13:47', NULL); +INSERT INTO `hw_web` VALUES (27, NULL, '[]', -1, '1', '2025-12-08 10:15:39', '2025-12-08 10:14:34', NULL); +INSERT INTO `hw_web` VALUES (28, NULL, '[]', -1, '1', '2025-12-08 10:16:48', '2025-12-08 10:15:39', NULL); +INSERT INTO `hw_web` VALUES (29, NULL, '[]', -1, '1', '2025-12-08 10:17:06', '2025-12-08 10:16:48', NULL); +INSERT INTO `hw_web` VALUES (30, NULL, '[]', -1, '1', '2025-12-08 10:17:50', '2025-12-08 10:17:06', NULL); +INSERT INTO `hw_web` VALUES (31, NULL, '{}', -1, '1', '2025-12-08 10:17:52', '2025-12-08 10:17:50', NULL); +INSERT INTO `hw_web` VALUES (32, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"id\":11,\"name\":\"轮胎RFID\",\"list\":[{\"name\":\"分类1 \",\"list\":[{\"name\":\"RFID轮胎\",\"value\":\"软控为轮胎产业链提供RFID轮胎整体解决方案,包括:系列化轮胎用RFID标签,系列化RFID标签封胶设备、定制RFID标签贴合设备,RFID 轮胎生产过程数据采集系统,及基于不同应用场景的信息化管理系统等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/轮胎_20250917144033A235.png\",\"id\":2},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":3},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":4}],\"id\":1}]},{\"id\":12,\"name\":\"超高频RFID\",\"list\":[{\"name\":\"超高频一体式读写器 \",\"list\":[{\"name\":\"HW-145L-6系列\",\"value\":\"      远距离、高增益、多接口,POE宽电压直流供电,多路GPIO。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/a7ed9fc42af9b6386126eee351f4937_20250929143910A279.png\",\"id\":2},{\"name\":\"HW-R300系列\",\"value\":\"                  中短距离,1路输入,体积小巧,TCP/RS485\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250926144121_20250929144331A280.png\",\"id\":3},{\"name\":\"HW-D100系列\",\"value\":\"                                 短距离,桌面式,USB接口\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片bbb_20250930172331A326.png\",\"id\":4},{\"name\":\"蓝牙手持式读写器\",\"value\":\"蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/蓝牙手持式读写器_20250917144525A239.png\",\"id\":5}],\"id\":1},{\"name\":\"超高频分体式读写器  \",\"list\":[{\"name\":\"HW-R200系列\",\"value\":\"             两通道/四通道天线,体积小巧,TCP通讯,远距读取\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片1_20250929162825A294.png\",\"id\":2},{\"name\":\"HW-R400系列\",\"value\":\"    四通道外接天线,超高性能,高阅读灵敏度,稳定可靠的性能\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片2_20250929163017A295.png\",\"id\":3},{\"name\":\"HW-RX系列\",\"value\":\"                 四通道天线,体积小巧,TCP通讯,快速响应。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片3_20250929163215A296.png\",\"id\":4}],\"id\":2},{\"name\":\"超高频扩展式读写器  \",\"list\":[{\"name\":\"HW-R110系列\",\"value\":\"     1路内置天线,1路外接天线接口,体积紧凑,可单机可扩展\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片vvv_20250930171716A325.png\",\"id\":2},{\"name\":\"HW-R170系列\",\"value\":\"             1路内置窄波束天线,1路外接扩展天线接口,圆极化\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片6_20250929164228A298.png\",\"id\":3}],\"id\":3},{\"name\":\"超高频天线 \",\"list\":[{\"name\":\"ACB5040系列\",\"value\":\"                               圆极化、低驻波比,超小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片8_20250929164750A299.png\",\"id\":2},{\"name\":\"A130D06系列\",\"value\":\"                         圆极化、低驻波比、高性能、小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片9_20250929165030A300.png\",\"id\":3},{\"name\":\"AB7020系列\",\"value\":\"                       圆极化、窄波束、低驻波比、中小尺寸。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片10_20250929165329A301.png\",\"id\":4},{\"name\":\"AB10010系列\",\"value\":\"                           天线具有定向性、窄波束、高增益\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250929165850_20250929165911A302.png\",\"id\":5}],\"id\":5},{\"name\":\"超高频标签 \",\"list\":[{\"name\":\"HW-A6320\",\"value\":\"          适用于仓储物流、生产制造、铁路以及循环运输管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片_20250929170754A304.png\",\"id\":2},{\"name\":\"HW-A6020\",\"value\":\"                              适用于资产管理、托盘管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片22_20250929171525A306.png\",\"id\":3},{\"name\":\"HW-PV8654\",\"value\":\"适用于多标签识别的应用场合,可用打印机打印图案等个性化信息\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/29/图片15_20250929172804A311.png\",\"id\":4},{\"name\":\"HW-QT5050\",\"value\":\"                                 无纺布或卡片型,全向标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片333_20250930090410A313.png\",\"id\":5},{\"name\":\"HW-HT4634\",\"value\":\"                                           矩形、耐酸碱\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片55_20250930090733A314.png\",\"id\":6},{\"name\":\"HW-P6020LM\",\"value\":\"                                          长条形,亮灯标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片666_20250930091535A317.png\",\"id\":7},{\"name\":\"HW-P9010\",\"value\":\"                                        长条型,PCB标签\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片7777_20250930170214A318.png\",\"id\":8},{\"name\":\"T0505\",\"value\":\"                          正方形、耐高温、应用于模具管理\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片gg_20250930170722A322.png\",\"id\":9},{\"name\":\"HW-TL1621\",\"value\":\"                                   螺栓型、耐高温、抗金属\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/30/图片sss_20250930171327A323.png\",\"id\":10}],\"id\":6}]},{\"id\":13,\"name\":\"高频RFID\",\"list\":[{\"name\":\"高频一体式读写器  \",\"list\":[{\"name\":\"HW-RFR-050系列\",\"value\":\"                         体积小,三防性能优,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片bb_20251015105305A346.png\",\"id\":2},{\"name\":\"HW-RFR-050系列\",\"value\":\"             螺栓形状读写器,易固定;尺寸小,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片h_20251015105320A347.png\",\"id\":3},{\"name\":\"HW-RFR-020系列\",\"value\":\"            螺栓形状读写器,易固定;尺寸小,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片jj_20251015101618A337.png\",\"id\":4},{\"name\":\"HW-RFR-010系列\",\"value\":\"                   尺寸更加小巧,短距稳定识别,三防性能优\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片d_20251015105254A345.png\",\"id\":5},{\"name\":\"HW-RFR-RFLY\",\"value\":\"                               TCP网络通讯,远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片M_20251015105452A348.png\",\"id\":6},{\"name\":\"HW-RFR-RFLY-I90\",\"value\":\"                             TCP网络通讯,超远距稳定识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片jjjjj_20251015110254A349.png\",\"id\":7},{\"name\":\"HW-D80系列\",\"value\":\"                            UID通讯,桌面发卡,近距识别\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/gdfdf_20251015111010A350.png\",\"id\":8},{\"name\":\"请输入\",\"value\":\"请输入\",\"icon\":\"\",\"id\":9}],\"id\":2},{\"name\":\"高频RFID标签 \",\"list\":[{\"name\":\"HW-HF-PVS-8654\",\"value\":\"独特的天线设计,优异的天线性能 ;可用于自动化识别、托盘管理、资产管理等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片12_20251015111534A351.png\",\"id\":2},{\"name\":\"HW-HF-PPS-D50\",\"value\":\"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片D50_20251015113514A352.png\",\"id\":3},{\"name\":\"HW-HF-PPS-D30\",\"value\":\"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片D30_20251015113547A353.png\",\"id\":4},{\"name\":\"HW-HF-PVS-D30\",\"value\":\"        PVC材质,可用于自动化识别、资产管理、设备巡检等\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片1PVS-D30_20251015130809A354.png\",\"id\":5},{\"name\":\"HW-TR6244-HF-PET\",\"value\":\"            圆环形状标签,PET材质,用于圆柱电芯托杯管理\",\"icon\":\"http://1.13.177.47:9665/statics/2025/10/15/图片YH6244_20251015131136A355.png\",\"id\":6}],\"id\":3}]},{\"id\":14,\"name\":\"传感器\",\"list\":[{\"name\":\"物联网硬件产品系列\",\"list\":[{\"name\":\"无线传感器接收显示仪\",\"value\":\"支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/无线传感器接收显示仪_20250917145129A247.png\",\"id\":2},{\"name\":\"车载物联定位终端\",\"value\":\"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/车载物联定位终端_20250917145144A248.png\",\"id\":3},{\"name\":\"工业物联云智能终端\",\"value\":\"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/工业物联云智能终端_20250917145224A249.png\",\"id\":4}],\"id\":1}]},{\"id\":15,\"name\":\"物联终端\",\"list\":[{\"name\":\"\\n物联网硬件产品系列\",\"list\":[{\"name\":\"温湿度传感器\",\"value\":\"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/温湿度传感器_20250917145325A250.png\",\"id\":2},{\"name\":\"无线温度传感器\",\"value\":\"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/无线温度传感器_20250917145344A251.png\",\"id\":3},{\"name\":\"EPD无线显示器\",\"value\":\"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/EPD无线显示器_20250917145418A252.png\",\"id\":4},{\"name\":\"红外温度传感器\",\"value\":\"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/红外温度传感器_20250917145433A253.png\",\"id\":5}],\"id\":1}]},{\"id\":16,\"name\":\"工业软件\",\"list\":[{\"name\":\"MOM\",\"list\":[{\"name\":\"MOM\",\"value\":\"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。\",\"icon\":\"http://1.13.177.47:9665/statics/2025/09/17/MOM_20250917145527A254.png\",\"id\":2}],\"id\":1}]}]}', -1, '1', '2025-12-08 10:18:59', '2025-12-08 10:17:52', NULL); +INSERT INTO `hw_web` VALUES (33, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFID\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFID\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFID\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"}]}', -1, '1', '2025-12-08 10:23:09', '2025-12-08 10:18:59', NULL); +INSERT INTO `hw_web` VALUES (34, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传123感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"}]}', -1, '1', '2025-12-08 10:30:08', '2025-12-08 10:23:09', NULL); +INSERT INTO `hw_web` VALUES (35, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传123感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"\",\"configTypeDesc\":\"介绍\",\"homeConfigTypeName\":\"名称\"}]}', -1, '1', '2025-12-08 10:30:13', '2025-12-08 10:30:08', NULL); +INSERT INTO `hw_web` VALUES (36, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传123感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"}]}', -1, '1', '2025-12-08 10:34:37', '2025-12-08 10:30:13', NULL); +INSERT INTO `hw_web` VALUES (37, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传123感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"\",\"configTypeDesc\":\"介绍\",\"homeConfigTypeName\":\"名称\"}]}', -1, '1', '2025-12-08 10:36:28', '2025-12-08 10:34:37', NULL); +INSERT INTO `hw_web` VALUES (38, NULL, '{\"classicCaseData\":[{\"configTypeId\":5,\"homeConfigTypeName\":\"智能轮胎\",\"caseInfoTitle\":\"智能轮胎\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":18,\"homeConfigTypeName\":\"轮胎工厂\",\"caseInfoTitle\":\"轮胎工厂\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":19,\"homeConfigTypeName\":\"快递物流\",\"caseInfoTitle\":\"快递物流\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg\"},{\"configTypeId\":20,\"homeConfigTypeName\":\"畜牧食品\",\"caseInfoTitle\":\"畜牧食品\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png\"},{\"configTypeId\":21,\"homeConfigTypeName\":\"新能源\",\"caseInfoTitle\":\"新能源\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"},{\"configTypeId\":22,\"homeConfigTypeName\":\"智能制造\",\"caseInfoTitle\":\"智能制造\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg\"},{\"configTypeId\":23,\"homeConfigTypeName\":\"工业物联\",\"caseInfoTitle\":\"工业物联\",\"caseInfoDesc\":\"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。\",\"caseInfoPic\":\"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg\"}],\"productCenterData\":[{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png\",\"configTypeId\":\"11\",\"homeConfigTypeName\":\"轮胎RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\",\"linkData\":[11,1,2]},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png\",\"configTypeId\":\"12\",\"homeConfigTypeName\":\"超高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\",\"linkData\":[12,1,2]},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png\",\"configTypeId\":\"13\",\"homeConfigTypeName\":\"高频RFI123D\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png\",\"configTypeId\":\"14\",\"homeConfigTypeName\":\"传123感器\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png\",\"configTypeId\":\"15\",\"homeConfigTypeName\":\"物联终端\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png\",\"configTypeId\":\"16\",\"homeConfigTypeName\":\"工业软件\",\"configTypeDesc\":\"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。\"},{\"homeConfigTypePic\":\"\",\"configTypeDesc\":\"介绍\",\"homeConfigTypeName\":\"名称\"}]}', -1, '0', NULL, '2025-12-08 10:36:28', NULL); + +-- ---------------------------- +-- Table structure for hw_web_document +-- ---------------------------- +DROP TABLE IF EXISTS `hw_web_document`; +CREATE TABLE `hw_web_document` ( + `document_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键', + `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户id', + `document_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件存储地址', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `web_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '页面编码,用来连表查询', + `secretKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密钥', + `json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'json', + `type` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '类型', + `is_delete` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0', + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`document_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_web_document +-- ---------------------------- +INSERT INTO `hw_web_document` VALUES ('25524e27-e63c-4e80-961c-d9d0c12f8e0c', NULL, 'http://1.13.177.47:9665/statics/2025/09/23/logo_20250923142115A275.png', '2025-09-23 14:21:15', NULL, NULL, NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('2afcd285-dc83-48bc-aa1d-d76ad6ad563d', NULL, 'http://1.13.177.47:9665/statics/2025/09/23/logo_20250923142152A276.png', '2025-09-23 14:21:53', NULL, '123', NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('51439411-9749-4107-9056-9157bb9eeb9a', NULL, 'http://1.13.177.47:9665/statics/2025/12/08/饼图背景_20251208085755A002.png', '2025-12-08 08:57:55', NULL, NULL, NULL, NULL, '1', '2025-12-08 08:58:02'); +INSERT INTO `hw_web_document` VALUES ('66850b4c-a469-4c8e-9473-265e9d5be465', NULL, 'http://1.13.177.47:9665/statics/2025/09/22/logo_20250922144002A271.png', '2025-09-22 14:40:03', NULL, NULL, NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('7a6650ec-c7d6-4d43-9579-df327d258675', NULL, 'http://1.13.177.47:9665/statics/2025/10/24/HW-145L-6_20251024165346A450.pdf', '2025-10-24 16:53:46', NULL, NULL, NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('8a875aba-b5e2-4577-bebc-6c822ac176ae', NULL, 'http://1.13.177.47:9665/statics/2025/12/08/电_20251208085700A001.png', '2025-12-08 08:57:00', NULL, NULL, NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('ba58e044-3e51-4d3e-8bd6-d2f45fa955f3', NULL, 'http://1.13.177.47:9665/statics/2025/10/21/参数_20251021112555A449.png', '2025-10-21 11:25:55', NULL, NULL, NULL, NULL, '0', '2025-11-03 14:51:29'); +INSERT INTO `hw_web_document` VALUES ('ce6b9f86-38cf-43f9-b535-170b7d2fdb15', NULL, 'http://1.13.177.47:9665/statics/2025/10/24/HW-R300_20251024172333A451.pdf', '2025-10-24 17:23:34', NULL, NULL, NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('d2439826-9ee6-43a4-827d-614a420193cb', NULL, 'http://1.13.177.47:9665/statics/2025/10/29/38fd1a7962e9db88dce43b6dc0c569dc_20251029163740A452.png', '2025-10-29 16:37:41', NULL, NULL, NULL, NULL, '1', '2025-10-29 16:40:40'); +INSERT INTO `hw_web_document` VALUES ('e5799e21-b284-44fa-b54c-7aec21b6fd81', NULL, 'http://1.13.177.47:9665/statics/2025/09/22/logo_20250922141014A270.png', '2025-09-22 14:10:14', NULL, '123', NULL, NULL, '0', NULL); +INSERT INTO `hw_web_document` VALUES ('f0869cb1-9832-4cbb-84f8-fabb25354af5', NULL, 'http://1.13.177.47:9665/statics/2025/09/22/logo_20250922145423A274.png', '2025-09-22 14:54:24', NULL, '123', NULL, NULL, '0', NULL); + +-- ---------------------------- +-- Table structure for hw_web_menu +-- ---------------------------- +DROP TABLE IF EXISTS `hw_web_menu`; +CREATE TABLE `hw_web_menu` ( + `web_menu_id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单主键id', + `parent` bigint NULL DEFAULT NULL COMMENT '父节点', + `ancestors` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '祖先', + `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '状态', + `web_menu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称', + `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户', + `web_menu__pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片地址', + `web_menu_type` int NULL DEFAULT NULL COMMENT '官网菜单类型', + `is_delete` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0', + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `web_menu_name_english` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `order` int NULL DEFAULT 0 COMMENT '菜单排序', + PRIMARY KEY (`web_menu_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_web_menu +-- ---------------------------- +INSERT INTO `hw_web_menu` VALUES (1, 0, '0', NULL, '首页', NULL, NULL, 1, '0', '2026-02-28 17:01:26', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (2, 0, '0', NULL, '关于海威', NULL, NULL, 1, '0', '2026-02-28 17:01:26', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (3, 2, '0,2', NULL, '公司概况', NULL, NULL, NULL, '0', '2026-02-28 17:01:26', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (4, 0, '0', NULL, '行业方案', NULL, NULL, 2, '0', '2026-02-28 17:01:26', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (5, 4, '0,4', NULL, '智能轮胎', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (6, 5, '0,4,5', NULL, 'RFID轮胎应用场景', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (7, 0, '0', NULL, '产品中心', NULL, NULL, 1, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (8, 2, '0,2', NULL, '合作伙伴', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (9, 2, '0,2', NULL, '媒体中心', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (10, 2, '0,2', NULL, '荣誉资质', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (11, 7, '0,7', NULL, '轮胎RFID', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (12, 7, '0,7', NULL, '超高频RFID', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (13, 7, '0,7', NULL, '高频RFID', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (14, 7, '0,7', NULL, '采集终端', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (16, 7, '0,7', NULL, '工业软件', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (18, 4, '0,4', NULL, '轮胎工厂', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (19, 4, '0,4', NULL, '快递物流', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (20, 4, '0,4', NULL, '畜牧食品', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (21, 4, '0,4', NULL, '新能源', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (22, 4, '0,4', NULL, '智能制造', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (23, 4, '0,4', NULL, '工业物联', NULL, NULL, 2, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (24, 0, '0', NULL, '联系我们', NULL, NULL, 0, '0', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (25, 24, '0,24', NULL, '售前服务', NULL, NULL, 2, '1', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (26, 24, '0,24', NULL, '售后服务', NULL, NULL, NULL, '1', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (27, 24, '0,24', NULL, '资料下载', NULL, NULL, NULL, '1', '2026-02-28 17:01:27', NULL, NULL, 0); +INSERT INTO `hw_web_menu` VALUES (28, 7, '0,7', NULL, '传感器', NULL, NULL, 2, '0', '2026-02-28 17:01:27', '2026-02-28 15:41:30', NULL, 0); +INSERT INTO `hw_web_menu` VALUES (29, 7, '0,7', NULL, '自动化装备', NULL, NULL, 2, '0', '2026-02-28 17:01:27', '2026-02-28 15:41:47', NULL, 0); +INSERT INTO `hw_web_menu` VALUES (30, 24, '0,24', NULL, '加入我们', NULL, NULL, NULL, '0', '2026-02-28 17:01:27', '2026-02-28 15:42:04', NULL, 0); + +-- ---------------------------- +-- Table structure for hw_web_menu1 +-- ---------------------------- +DROP TABLE IF EXISTS `hw_web_menu1`; +CREATE TABLE `hw_web_menu1` ( + `web_menu_id` bigint NOT NULL AUTO_INCREMENT COMMENT '设备d', + `parent` bigint NULL DEFAULT NULL COMMENT '父节点', + `ancestors` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '祖先', + `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '状态', + `web_menu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备名称', + `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户', + `web_menu__pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片地址', + `web_menu_type` int NULL DEFAULT NULL COMMENT '官网菜单类型', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '介绍', + `is_delete` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0', + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `web_menu_name_english` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`web_menu_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_web_menu1 +-- ---------------------------- +INSERT INTO `hw_web_menu1` VALUES (1, 0, '0', NULL, '超高', NULL, NULL, NULL, '123', '0', NULL, NULL, NULL); +INSERT INTO `hw_web_menu1` VALUES (2, 1, '0,1', NULL, '产品11', NULL, NULL, NULL, '321', '0', NULL, NULL, NULL); + +-- ---------------------------- +-- Table structure for hw_web1 +-- ---------------------------- +DROP TABLE IF EXISTS `hw_web1`; +CREATE TABLE `hw_web1` ( + `web_id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `web_json` json NULL COMMENT 'json', + `web_json_string` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'json字符串', + `web_code` bigint NULL DEFAULT NULL COMMENT '页面', + `device_id` bigint NULL DEFAULT NULL COMMENT '设备id', + `file_address` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文件地址', + `secret_ket` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '密钥', + `typeId` int NULL DEFAULT NULL COMMENT '类型id', + `is_delete` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0', + `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `web_json_english` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + PRIMARY KEY (`web_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 52 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'haiwei官网json' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of hw_web1 +-- ---------------------------- +INSERT INTO `hw_web1` VALUES (21, NULL, '[{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"logo_20250922145423A274.png\",\"uuid\":\"f0869cb1-9832-4cbb-84f8-fabb25354af5\"}]}}]', 11, 2, NULL, NULL, 1, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (22, NULL, '[]', 12, 3, NULL, NULL, 1, '1', '2025-10-20 17:08:44', NULL, NULL); +INSERT INTO `hw_web1` VALUES (23, NULL, '[{\"type\":16,\"value\":{\"params\":[{\"title\":\"表名\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63/EPC Gen2v2\",\"ISO/IEC 18000-63/EPC Gen2v2\"],\"merge\":false},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"其他国家频段(可定制)\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1dB,精度 ±1dB\",\"5-33dBm 可调,步进 1dB,精度\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\",\"6dBi\"],\"merge\":false},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3: 1\",\"≦1.3: 1\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:1(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\",\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":false},{\"name\":\"GPIO 端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平限 Max.500mA\",\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平限 Max.500mA\"],\"merge\":false},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\",\"1 个蜂鸣器\"],\"merge\":false},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"或 802.3af POE 供电\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm129mm53mm\",\"129mm129mm53mm\"],\"merge\":false},{\"name\":\"产品重量\",\"values\":[\"约 520g\",\"约 520g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金 + PC\",\"铝合金 + PC\"],\"merge\":false},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\",\"白色、银色\"],\"merge\":false},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\",\"-25℃~60℃\"],\"merge\":false},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\",\"-40℃~85℃\"],\"merge\":false},{\"name\":\"工作湿度\",\"values\":[\"5%~95% RH 无冷凝\",\"5%~95% RH 无冷凝\"],\"merge\":false},{\"name\":\"IP 等级\",\"values\":[\"-\",\"-\"],\"merge\":false},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\",\"支持 C#、JAVA\"],\"merge\":false}]}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"表名\",\"columns\":[],\"rows\":[]}]}}]', 16, 2, NULL, NULL, 1, '1', '2025-10-21 11:25:57', NULL, NULL); +INSERT INTO `hw_web1` VALUES (24, NULL, '[]', 13, 3, NULL, NULL, 1, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (25, NULL, '[{\"type\":13,\"value\":{\"params\":[{\"title\":\"\",\"list\":[]}]}}]', 12, 2, NULL, NULL, 5, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (26, NULL, '[]', 12, 3, NULL, NULL, 5, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (27, NULL, '[{\"type\":3,\"value\":{\"title\":\"123\",\"subTitle\":\"456\",\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"itemTitle\":\"111\",\"itemInfo\":\"222\"}]}}]', 12, 5, NULL, NULL, 1, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (28, NULL, '[{\"type\":3,\"value\":{\"title\":\"123\",\"subTitle\":\"456\",\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"itemTitle\":\"111\",\"itemInfo\":\"222\"}]}}]', 12, 4, NULL, NULL, 1, '1', '2025-10-21 09:37:06', NULL, NULL); +INSERT INTO `hw_web1` VALUES (29, NULL, '[{\"type\":3,\"value\":{\"title\":\"123\",\"subTitle\":\"456\",\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"itemTitle\":\"111\",\"itemInfo\":\"222\"}]}}]', 12, 4, NULL, NULL, 5, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (30, NULL, '[{\"type\":\"carousel\",\"value\":{\"list\":[{\"icon\":\"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg\",\"title\":\"111\",\"value\":\"222\"}]}}]', 12, 5, NULL, NULL, 5, '0', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (31, NULL, '[{\"type\":16,\"value\":{\"params\":[{\"title\":\"参数\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925 MHz(其他国家频段可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26 dBm 可调,步进 1 dB,精度 ±1 dB\",\"5-33 dBm 可调,步进 1 dB,精度 ±1 dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6 dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,最大读距:8 m(距离随标签型号不同而有所差异)\"],\"merge\":true},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5-24 V 电平)光隔离,输出口低电平限 Max. 500 mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12-24 VDC(推荐 12 V 标配适配器)或 802.3af PoE 供电\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6 W\",\"12 W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129 mm × 129 mm × 53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 520 g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金 + PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25 °C ~ 60 °C\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40 °C ~ 85 °C\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5% ~ 95% RH 无冷凝\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、Java\"],\"merge\":true}]}]}}]', 16, 2, NULL, NULL, 1, '1', NULL, NULL, NULL); +INSERT INTO `hw_web1` VALUES (32, NULL, '[{\"type\":16,\"value\":{\"params\":[{\"title\":\"参数\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925 MHz(其他国家频段可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26 dBm 可调,步进 1 dB,精度 ±1 dB\",\"5-33 dBm 可调,步进 1 dB,精度 ±1 dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6 dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,最大读距:8 m(距离随标签型号不同而有所差异)\"],\"merge\":true},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5-24 V 电平)光隔离,输出口低电平限 Max. 500 mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12-24 VDC(推荐 12 V 标配适配器)或 802.3af PoE 供电\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6 W\",\"12 W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129 mm × 129 mm × 53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 520 g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金 + PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25 °C ~ 60 °C\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40 °C ~ 85 °C\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5% ~ 95% RH 无冷凝\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-ip\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、Java\"],\"merge\":true}]}]}}]', 16, 2, NULL, NULL, 1, '1', '2025-10-21 11:25:57', NULL, NULL); +INSERT INTO `hw_web1` VALUES (33, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"\"}],\"features\":[]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:14:54', NULL, NULL); +INSERT INTO `hw_web1` VALUES (34, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:17:42', NULL, NULL); +INSERT INTO `hw_web1` VALUES (35, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:19:11', NULL, NULL); +INSERT INTO `hw_web1` VALUES (36, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"HW-R145L-6B\",\"columns\":[\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 /EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"其他国家频段(可定制)\",\"values\":[\"\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1 dB,精度±1dB\",\"5-33dBm 可调,步进 1 dB,精度±1dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取TPALN9662标签,组大读距:19m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP( RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"或 802.3af POE供电\",\"values\":[\"\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm*129mm*53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约520g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:30:29', NULL, NULL); +INSERT INTO `hw_web1` VALUES (37, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"HW-R145L-6B\",\"columns\":[\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 /EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"其他国家频段(可定制)\",\"values\":[\"\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1 dB,精度±1dB\",\"5-33dBm 可调,步进 1 dB,精度±1dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取TPALN9662标签,组大读距:19m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP( RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"或 802.3af POE供电\",\"values\":[\"\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm*129mm*53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约520g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-145L-6系列\",\"value\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:35:34', NULL, NULL); +INSERT INTO `hw_web1` VALUES (38, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\",\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 /EPC Gen2v2\",\"\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz\",\"\"],\"merge\":true},{\"name\":\"其他国家频段(可定制)\",\"values\":[\"\",\"\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1 dB,精度±1dB\",\"5-33dBm 可调,步进 1 dB,精度±1dB\",\"\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\",\"\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\",\"\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3:1\",\"\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取TPALN9662标签,组大读距:19m(读距随标签型号不同而有所差异)\",\"\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP( RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\",\"\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\",\"\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1个蜂鸣器\",\"\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)或 802.3af POE供电\",\"\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\",\"\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm*129mm*53 mm\",\"\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约520g\",\"\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\",\"\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\",\"\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\",\"\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\",\"\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\",\"\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-\",\"\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\",\"\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-145L-6系列\",\"value\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:45:44', NULL, NULL); +INSERT INTO `hw_web1` VALUES (39, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\",\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 /EPC Gen2v2\",\"\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz;其他国家频段(可定制)\",\"\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1 dB,精度±1dB\",\"5-33dBm 可调,步进 1 dB,精度±1dB\",\"\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\",\"\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\",\"\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3:1\",\"\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取TPALN9662标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取TPALN9662标签,组大读距:19m(读距随标签型号不同而有所差异)\",\"\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP( RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\",\"\"],\"merge\":true},{\"name\":\"GPIO端口\",\"values\":[\"M12-12 航空接口,4输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\",\"\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1个蜂鸣器\",\"\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)或 802.3af POE供电 \",\"\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\",\"\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm*129mm*53 mm\",\"\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约520g\",\"\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\",\"\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\",\"\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\",\"\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\",\"\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\",\"\"],\"merge\":true},{\"name\":\"IP等级\",\"values\":[\"-\",\"\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\",\"\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-145L-6系列\",\"value\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-10-20 16:52:11', NULL, NULL); +INSERT INTO `hw_web1` VALUES (40, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:19m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)或 802.3af POE 供电\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm129mm53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 520g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-145L-6系列\",\"value\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 1, '1', '2025-12-08 09:01:27', NULL, NULL); +INSERT INTO `hw_web1` VALUES (41, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"表名\",\"columns\":[],\"rows\":[]}]}},{\"type\":14,\"value\":{\"fileList\":[]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-20 17:17:55', NULL, NULL); +INSERT INTO `hw_web1` VALUES (42, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"\"},{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-20 17:30:41', NULL, NULL); +INSERT INTO `hw_web1` VALUES (43, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"\"},{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-24 17:23:35', NULL, NULL); +INSERT INTO `hw_web1` VALUES (44, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/21/DeWatermark.ai_1760515051851_20251021092843A437.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/21/nfngh_20251021092851A438.png\",\"bannerTitle\":\"HW-D100系列\",\"bannerValue\":\"短距离,桌面式\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/21/nfngh_20251021092908A439.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/21/jfjgf_20251021092913A440.png\"}],\"features\":[{\"name\":\"紧凑设计,桌面放置\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"USB供电和连接\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-D100\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\"],\"merge\":true},{\"name\":\"天线增益\",\"values\":[\"3dBi\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"50 tag/s\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:0.3m(读距随标签型号不同而有所差异)\"],\"merge\":true},{\"name\":\"通讯接口\",\"values\":[\"USB-Type B\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"USB 虚拟串口\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"5VDC\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\"],\"merge\":true},{\"name\":\"外形尺寸\",\"values\":[\"129mm80mm22 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 60g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-20℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"10%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-D100系列产品手册\",\"value\":\"短距离,桌面式,USB接口\",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 4, NULL, NULL, 1, '0', NULL, '2025-10-21 09:37:06', NULL); +INSERT INTO `hw_web1` VALUES (45, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/21/DeWatermark.ai_1760515051851_20251021094731A441.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/21/nfhyjnf_20251021094738A442.png\",\"bannerTitle\":\"HW-R200系列\",\"bannerValue\":\"两通道/四通道天线,体积小巧,TCP通讯,远距读取\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/21/nfhyjnf_20251021094814A443.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/21/5tdgd_20251021094839A446.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/21/hbgfg_20251021094851A448.png\"}],\"features\":[{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCPsoket\"},{\"name\":\"两路天线ANT\"},{\"name\":\"远距读取\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R200\",\"HW-R200-S4\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-30dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"SMA 天线接口\",\"values\":[\"2 接口,SMA-K 接头\",\"4 接口,SMA-K 接头\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"400 tag/s\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"至 10m(8dBi 天线),读距随标签型号不同而有所差异\",\"至 12m(8dBi 天线),读距随标签型号不同而有所差异\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP( RJ-45)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"2 输入、2 输出(兼容 5-24V 电平)光电隔离,输出口低电平电流容限 Max 500mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"10W\"],\"merge\":true},{\"name\":\"外形尺寸\",\"values\":[\"124mm109mm30 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 350g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-20℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95% RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true},{\"name\":\"一致性认证\",\"values\":[\"SRRC(中国无委)、CE、ROHS、TELEC(日本无委)\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R200系列\",\"value\":\"两通道/四通道天线,体积小巧,TCP通讯,远距读取\",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 2, '0', NULL, '2025-10-21 09:53:39', NULL); +INSERT INTO `hw_web1` VALUES (46, NULL, '[{\"type\":16,\"value\":{\"params\":[{\"title\":\"表名\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63/EPC Gen2v2\",\"ISO/IEC 18000-63/EPC Gen2v2\"],\"merge\":false},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"其他国家频段(可定制)\",\"values\":[\"中国频段:920-925MHz\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1dB,精度 ±1dB\",\"5-33dBm 可调,步进 1dB,精度\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\",\"6dBi\"],\"merge\":false},{\"name\":\"天线驻波比\",\"values\":[\"≦1.3: 1\",\"≦1.3: 1\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:1(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\",\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":false},{\"name\":\"GPIO 端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平限 Max.500mA\",\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平限 Max.500mA\"],\"merge\":false},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\",\"1 个蜂鸣器\"],\"merge\":false},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"或 802.3af POE 供电\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm129mm53mm\",\"129mm129mm53mm\"],\"merge\":false},{\"name\":\"产品重量\",\"values\":[\"约 520g\",\"约 520g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金 + PC\",\"铝合金 + PC\"],\"merge\":false},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\",\"白色、银色\"],\"merge\":false},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\",\"-25℃~60℃\"],\"merge\":false},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\",\"-40℃~85℃\"],\"merge\":false},{\"name\":\"工作湿度\",\"values\":[\"5%~95% RH 无冷凝\",\"5%~95% RH 无冷凝\"],\"merge\":false},{\"name\":\"IP 等级\",\"values\":[\"-\",\"-\"],\"merge\":false},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\",\"支持 C#、JAVA\"],\"merge\":false}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"参数_20251021112555A449.png\",\"uuid\":\"ba58e044-3e51-4d3e-8bd6-d2f45fa955f3\"}]}}]', 16, 2, NULL, NULL, 1, '0', NULL, '2025-10-21 11:25:57', NULL); +INSERT INTO `hw_web1` VALUES (47, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"HW-R300_20251024172333A451.pdf\",\"uuid\":\"ce6b9f86-38cf-43f9-b535-170b7d2fdb15\"},{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-29 16:37:47', '2025-10-24 17:23:35', NULL); +INSERT INTO `hw_web1` VALUES (48, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"HW-R300_20251024172333A451.pdf\",\"uuid\":\"ce6b9f86-38cf-43f9-b535-170b7d2fdb15\"},{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"38fd1a7962e9db88dce43b6dc0c569dc_20251029163740A452.png\",\"uuid\":\"d2439826-9ee6-43a4-827d-614a420193cb\"}]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-29 16:38:12', '2025-10-29 16:37:47', NULL); +INSERT INTO `hw_web1` VALUES (49, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"HW-R300_20251024172333A451.pdf\",\"uuid\":\"ce6b9f86-38cf-43f9-b535-170b7d2fdb15\"},{\"name\":\"名称\",\"value\":\"介绍\",\"url\":\"\",\"fileName\":\"38fd1a7962e9db88dce43b6dc0c569dc_20251029163740A452.png\",\"uuid\":\"d2439826-9ee6-43a4-827d-614a420193cb\"}]}}]', 12, 3, NULL, NULL, 1, '1', '2025-10-29 16:40:43', '2025-10-29 16:38:12', NULL); +INSERT INTO `hw_web1` VALUES (50, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020170508A432.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170522A433.png\",\"bannerTitle\":\"HW-R300系列\",\"bannerValue\":\"中短距离,1路输入,体积小巧,TCP/RS485.\"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片ddddddd_20251020170550A434.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片fffffff_20251020170602A435.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片gggg_20251020170613A436.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"RS485 Modbus RTU通讯\"},{\"name\":\"一路输入I/O\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R300\",\"HW-R300D\"],\"rows\":[{\"name\":\"射频协议\",\"values\":[\"ISO/IEC 18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"10-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"8dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤2:1\"],\"merge\":true},{\"name\":\"盘点速率\",\"values\":[\"120 tag/s\",\"200 tag/s\"],\"merge\":false},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:1.4m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:10m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)\"],\"merge\":true},{\"name\":\"通讯协议\",\"values\":[\"Modbus-RTU 通信协议网口 TCP 提供基于 C#的 SDK 接口;适配工业总线协议(接网关)(选配)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-5-B 航空接口,兼容 5-24V 电平,1 路输入\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"2.5W\",\"10W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"90mm90mm32 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 288g\",\"约 300g\"],\"merge\":false},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"黑色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"IP54\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-R300系列产品手册\",\"value\":\"中短距离,1路输入,体积小巧,TCPO/RS485\",\"url\":\"\",\"fileName\":\"HW-R300_20251024172333A451.pdf\",\"uuid\":\"ce6b9f86-38cf-43f9-b535-170b7d2fdb15\"}]}}]', 12, 3, NULL, NULL, 1, '0', NULL, '2025-10-29 16:40:43', NULL); +INSERT INTO `hw_web1` VALUES (51, NULL, '[{\"type\":15,\"value\":{\"banner\":\"http://1.13.177.47:9665/statics/2025/10/20/DeWatermark.ai_1760515051851_20251020105858A419.jpeg\",\"banner1\":\"http://1.13.177.47:9665/statics/2025/10/20/i7y9_20251020105912A420.png\",\"bannerTitle\":\"HW-145L-6系列\",\"bannerValue\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \"}},{\"type\":12,\"value\":{\"imgList\":[{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/53453_20251020161415A428.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/42354_20251020161433A429.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/图片3242_20251020161442A430.png\"},{\"url\":\"http://1.13.177.47:9665/statics/2025/10/20/6546456_20251020161450A431.png\"}],\"features\":[{\"name\":\"紧凑型圆极化天线\"},{\"name\":\"POE,宽电压直流供电\"},{\"name\":\"卓越的防碰撞算法\"},{\"name\":\"TCP soket通讯\"},{\"name\":\"优异的阅读距离\"}]}},{\"type\":16,\"value\":{\"params\":[{\"title\":\"\",\"columns\":[\"HW-R145L-6B\",\"HW-R145L-6D\"],\"rows\":[{\"name\":\"射频特性\",\"values\":[\"\"],\"merge\":true},{\"name\":\"射频协议\",\"values\":[\"ISO/IEC18000-63 / EPC Gen2v2\"],\"merge\":true},{\"name\":\"工作频段\",\"values\":[\"中国频段:920-925MHz其他国家频段(可定制)\"],\"merge\":true},{\"name\":\"发射功率\",\"values\":[\"5-26dBm 可调,步进 1dB,精度±1dB\",\"5-33dBm 可调,步进 1dB,精度±1dB\"],\"merge\":false},{\"name\":\"盘点速率\",\"values\":[\"100 tag/s\",\"200tag/s\"],\"merge\":false},{\"name\":\"天线增益\",\"values\":[\"6dBi\"],\"merge\":true},{\"name\":\"天线驻波比\",\"values\":[\"≤1.3:1\"],\"merge\":true},{\"name\":\"读卡距离\",\"values\":[\"读取 TPALN9662 标签,组大读距:8m(读距随标签型号不同而有所差异)\",\"读取 TPALN9662 标签,组大读距:19m(读距随标签型号不同而有所差异)\"],\"merge\":false},{\"name\":\"通讯接口\",\"values\":[\"TCP/IP(RJ-45)、RS232 和隔离 RS485(M12-6 航空头,可定制)\"],\"merge\":true},{\"name\":\"GPIO 端口\",\"values\":[\"M12-12 航空接口,4 输入、4 输出(兼容 5~24V 电平)光耦隔离,输出口低电平电流容限 Max.500mA\"],\"merge\":true},{\"name\":\"声音指示\",\"values\":[\"1 个蜂鸣器\"],\"merge\":true},{\"name\":\"供电电源\",\"values\":[\"12~24VDC(推荐 12V 标配适配器)或 802.3af POE 供电\"],\"merge\":true},{\"name\":\"整机峰值功率\",\"values\":[\"6W\",\"12W\"],\"merge\":false},{\"name\":\"外形尺寸\",\"values\":[\"129mm129mm53 mm\"],\"merge\":true},{\"name\":\"产品重量\",\"values\":[\"约 520g\"],\"merge\":true},{\"name\":\"外壳材料\",\"values\":[\"铝合金+PC\"],\"merge\":true},{\"name\":\"外壳颜色\",\"values\":[\"白色、银色\"],\"merge\":true},{\"name\":\"工作温度\",\"values\":[\"-25℃~60℃\"],\"merge\":true},{\"name\":\"存储温度\",\"values\":[\"-40℃~85℃\"],\"merge\":true},{\"name\":\"工作湿度\",\"values\":[\"5%~95%RH 无冷凝\"],\"merge\":true},{\"name\":\"IP 等级\",\"values\":[\"-\"],\"merge\":true},{\"name\":\"开发接口\",\"values\":[\"支持 C#、JAVA\"],\"merge\":true}]}]}},{\"type\":14,\"value\":{\"fileList\":[{\"name\":\"HW-145L-6系列123\",\"value\":\"远距离,高增益,多借口,POE宽电压直流供电,多路GPIO \",\"url\":\"\",\"fileName\":\"\"}]}}]', 12, 2, NULL, NULL, 1, '0', NULL, '2025-12-08 09:01:27', NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/Admin.NET-v2/hw_web_PostgreSQL.sql b/Admin.NET-v2/hw_web_PostgreSQL.sql new file mode 100644 index 0000000..cf6a125 --- /dev/null +++ b/Admin.NET-v2/hw_web_PostgreSQL.sql @@ -0,0 +1,86 @@ +/* + Navicat Premium Dump SQL + + Source Server : 10.11.186.154PG + Source Server Type : PostgreSQL + Source Server Version : 180000 (180000) + Source Host : 10.11.186.154:5432 + Source Catalog : hw-portal + Source Schema : public + + Target Server Type : PostgreSQL + Target Server Version : 180000 (180000) + File Encoding : 65001 + + Date: 10/03/2026 14:56:26 +*/ + + +-- ---------------------------- +-- Table structure for hw_web +-- ---------------------------- +DROP TABLE IF EXISTS "public"."hw_web"; +CREATE TABLE "public"."hw_web" ( + "web_id" int8 NOT NULL, + "web_json" text COLLATE "pg_catalog"."default", + "web_json_string" text COLLATE "pg_catalog"."default", + "web_code" int8, + "is_delete" char(1) COLLATE "pg_catalog"."default" NOT NULL, + "update_time" timestamp(6), + "create_time" timestamp(6), + "web_json_english" text COLLATE "pg_catalog"."default" +) +; +COMMENT ON COLUMN "public"."hw_web"."web_id" IS '主键'; +COMMENT ON COLUMN "public"."hw_web"."web_json" IS 'json'; +COMMENT ON COLUMN "public"."hw_web"."web_json_string" IS 'json字符串'; +COMMENT ON COLUMN "public"."hw_web"."web_code" IS '页面'; +COMMENT ON COLUMN "public"."hw_web"."is_delete" IS '逻辑删除()'; +COMMENT ON TABLE "public"."hw_web" IS 'haiwei官网json'; + +-- ---------------------------- +-- Records of hw_web +-- ---------------------------- +INSERT INTO "public"."hw_web" VALUES (1, NULL, '[{"type":"carousel","value":{"list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"222","value":"333"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"333","value":"444"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"444","value":"555"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"555","value":"666"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"666","value":"777"},{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"666","value":"777"}]}},{"type":2,"value":{"title":"","subTitle":"","contentTitle":"关于我们","contentSubTitle":"ABOUT US","contentInfo":"青岛海威物联科技有限公司,致力于工业物联网软硬件系统研发、生产和销售,提供感知互联的工业化联网整体解决方案。","icon":"http://1.13.177.47:9665/statics/2025/08/22/shijie_20250822160139A177.jpg"}},{"type":2,"value":{"title":"","subTitle":"","contentTitle":"关于我们","contentSubTitle":"ABOUT US","contentInfo":"同时,公司大力推进基于RFID、传感、边缘采集计算、数据传输等技术的工业互联网解决方案在轮胎产业链中的深入应用,为轮胎生产管理、仓储物流、销售跟踪、车队管理、轮胎翻新等环节提供一站式解决方案。所有核心技术及产品均具有自主知识产权","icon":"http://1.13.177.47:9665/statics/2025/08/22/2_20250822160452A178.jpg"}},{"type":2,"value":{"title":"","subTitle":"","contentTitle":"关于我们","contentSubTitle":"ABOUT US","contentInfo":"公司承担国家橡胶与轮胎工程技术研究中心RFID研究所;被认定为国家级专精特新“小巨人”企业、国家高新技术企业、青岛市技术创新中心、科技型中小企业、青岛市雏鹰企业;通过ISO 9001:2015质量管理体系认证、ISO 14001:2015环境体系认证、IS045001:2018职业健康安全管理体系认证;通过信息系统建设和服务能力评估CS2级;主持制定轮胎用RFID电子标签四项ISO国际标准、四项国家标准。","icon":"http://1.13.177.47:9665/statics/2025/08/22/3_20250822160536A179.jpg"}}]', 3, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (2, NULL, '[{"type":9,"value":{"title":"RFID应用案例","subTitle":"","list":[{"value":"请输全厂自动物流线装配RFID读写器,碎胶工序、小料工序、密炼半制品、成型、胎胚管理、全流程输送RFID方案。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片1_20250822132316A070.png"},{"value":"半制品立库,各供胶机台RFID防误验证。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片2_20250822132317A071.png"}]}},{"type":9,"value":{"title":"","subTitle":"","list":[{"value":"硫化模具、口型板RFID管理。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片3_20250822132322A072.png"},{"value":"","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片4_20250822132325A073.png"}]}},{"type":9,"value":{"title":"RFID智能化方案","subTitle":"RFID-智能工厂基石","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片8_20250822132701A077.png","value":"    RFID应用,对于自动化程度高场景,成效更显著。"},{"value":"   通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。 通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片9_20250822132705A078.png"}]}},{"type":9,"value":{"title":"","subTitle":"","list":[]}}]', 6, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (3, NULL, '[]', 5, '1', '2025-12-08 09:32:11', NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (4, NULL, '[{"type":"carousel","value":{"list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"},{"title":"请输入","value":"请输入","icon":""}]}},{"type":9,"value":{"title":"关于我们","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/公司简介_20250822141644A128.jpg","value":"公司简介:\n公司致力于工业物联网技术的研发和创新,推动工业全流程物联发展"},{"value":"团队风貌:\n不忘初心,励志前行\n凝心聚力,携手奋进","icon":"http://1.13.177.47:9665/statics/2025/08/22/团队风貌_20250822141723A129.jpg"},{"value":"荣誉资质:\n坚持走自主创新研发道路,团队多项技术成果进入国际领先行列","icon":"http://1.13.177.47:9665/statics/2025/08/22/荣誉资质_20250822141910A132.png"},{"value":"人力资源:\n坚持以人为本、以才用人,员工是海威最宝贵的财富","icon":"http://1.13.177.47:9665/statics/2025/08/22/坚持以人为本、以才用人,员工是海威最宝贵的财富_20250822141900A131.jpg"}]}}]', 1, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (5, NULL, '{"banner":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg","bannerTitle":"","productList":[{"id":11,"name":"轮胎RFID","list":[{"name":"分类1 ","list":[{"name":"RFID轮胎","value":"软控为轮胎产业链提供RFID轮胎整体解决方案,包括:系列化轮胎用RFID标签,系列化RFID标签封胶设备、定制RFID标签贴合设备,RFID 轮胎生产过程数据采集系统,及基于不同应用场景的信息化管理系统等","icon":"http://1.13.177.47:9665/statics/2025/09/17/轮胎_20250917144033A235.png","id":2},{"name":"请输入","value":"请输入","icon":"","id":3},{"name":"请输入","value":"请输入","icon":"","id":4}],"id":1}]},{"id":12,"name":"超高频RFID","list":[{"name":"超高频一体式读写器 ","list":[{"name":"HW-145L-6系列","value":"      远距离、高增益、多接口,POE宽电压直流供电,多路GPIO。","icon":"http://1.13.177.47:9665/statics/2025/09/29/a7ed9fc42af9b6386126eee351f4937_20250929143910A279.png","id":2},{"name":"HW-R300系列","value":"                  中短距离,1路输入,体积小巧,TCP/RS485","icon":"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250926144121_20250929144331A280.png","id":3},{"name":"HW-D100系列","value":"                                 短距离,桌面式,USB接口","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片bbb_20250930172331A326.png","id":4},{"name":"蓝牙手持式读写器","value":"蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。","icon":"http://1.13.177.47:9665/statics/2025/09/17/蓝牙手持式读写器_20250917144525A239.png","id":5}],"id":1},{"name":"超高频分体式读写器  ","list":[{"name":"HW-R200系列","value":"             两通道/四通道天线,体积小巧,TCP通讯,远距读取","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片1_20250929162825A294.png","id":2},{"name":"HW-R400系列","value":"    四通道外接天线,超高性能,高阅读灵敏度,稳定可靠的性能","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片2_20250929163017A295.png","id":3},{"name":"HW-RX系列","value":"                 四通道天线,体积小巧,TCP通讯,快速响应。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片3_20250929163215A296.png","id":4}],"id":2},{"name":"超高频扩展式读写器  ","list":[{"name":"HW-R110系列","value":"     1路内置天线,1路外接天线接口,体积紧凑,可单机可扩展","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片vvv_20250930171716A325.png","id":2},{"name":"HW-R170系列","value":"             1路内置窄波束天线,1路外接扩展天线接口,圆极化","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片6_20250929164228A298.png","id":3}],"id":3},{"name":"超高频天线 ","list":[{"name":"ACB5040系列","value":"                               圆极化、低驻波比,超小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片8_20250929164750A299.png","id":2},{"name":"A130D06系列","value":"                         圆极化、低驻波比、高性能、小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片9_20250929165030A300.png","id":3},{"name":"AB7020系列","value":"                       圆极化、窄波束、低驻波比、中小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片10_20250929165329A301.png","id":4},{"name":"AB10010系列","value":"                           天线具有定向性、窄波束、高增益","icon":"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250929165850_20250929165911A302.png","id":5}],"id":5},{"name":"超高频标签 ","list":[{"name":"HW-A6320","value":"          适用于仓储物流、生产制造、铁路以及循环运输管理等","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片_20250929170754A304.png","id":2},{"name":"HW-A6020","value":"                              适用于资产管理、托盘管理等","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片22_20250929171525A306.png","id":3},{"name":"HW-PV8654","value":"适用于多标签识别的应用场合,可用打印机打印图案等个性化信息","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片15_20250929172804A311.png","id":4},{"name":"HW-QT5050","value":"                                 无纺布或卡片型,全向标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片333_20250930090410A313.png","id":5},{"name":"HW-HT4634","value":"                                           矩形、耐酸碱","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片55_20250930090733A314.png","id":6},{"name":"HW-P6020LM","value":"                                          长条形,亮灯标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片666_20250930091535A317.png","id":7},{"name":"HW-P9010","value":"                                        长条型,PCB标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片7777_20250930170214A318.png","id":8},{"name":"T0505","value":"                          正方形、耐高温、应用于模具管理","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片gg_20250930170722A322.png","id":9},{"name":"HW-TL1621","value":"                                   螺栓型、耐高温、抗金属","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片sss_20250930171327A323.png","id":10}],"id":6}]},{"id":13,"name":"高频RFID","list":[{"name":"高频一体式读写器  ","list":[{"name":"HW-RFR-050系列","value":"                         体积小,三防性能优,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片bb_20251015105305A346.png","id":2},{"name":"HW-RFR-050系列","value":"             螺栓形状读写器,易固定;尺寸小,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片h_20251015105320A347.png","id":3},{"name":"HW-RFR-020系列","value":"            螺栓形状读写器,易固定;尺寸小,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片jj_20251015101618A337.png","id":4},{"name":"HW-RFR-010系列","value":"                   尺寸更加小巧,短距稳定识别,三防性能优","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片d_20251015105254A345.png","id":5},{"name":"HW-RFR-RFLY","value":"                               TCP网络通讯,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片M_20251015105452A348.png","id":6},{"name":"HW-RFR-RFLY-I90","value":"                             TCP网络通讯,超远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片jjjjj_20251015110254A349.png","id":7},{"name":"HW-D80系列","value":"                            UID通讯,桌面发卡,近距识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/gdfdf_20251015111010A350.png","id":8},{"name":"请输入","value":"请输入","icon":"","id":9}],"id":2},{"name":"高频RFID标签 ","list":[{"name":"HW-HF-PVS-8654","value":"独特的天线设计,优异的天线性能 ;可用于自动化识别、托盘管理、资产管理等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片12_20251015111534A351.png","id":2},{"name":"HW-HF-PPS-D50","value":"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片D50_20251015113514A352.png","id":3},{"name":"HW-HF-PPS-D30","value":"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片D30_20251015113547A353.png","id":4},{"name":"HW-HF-PVS-D30","value":"        PVC材质,可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片1PVS-D30_20251015130809A354.png","id":5},{"name":"HW-TR6244-HF-PET","value":"            圆环形状标签,PET材质,用于圆柱电芯托杯管理","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片YH6244_20251015131136A355.png","id":6}],"id":3}]},{"id":14,"name":"传感器","list":[{"name":"物联网硬件产品系列","list":[{"name":"无线传感器接收显示仪","value":"支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。","icon":"http://1.13.177.47:9665/statics/2025/09/17/无线传感器接收显示仪_20250917145129A247.png","id":2},{"name":"车载物联定位终端","value":"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。","icon":"http://1.13.177.47:9665/statics/2025/09/17/车载物联定位终端_20250917145144A248.png","id":3},{"name":"工业物联云智能终端","value":"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。","icon":"http://1.13.177.47:9665/statics/2025/09/17/工业物联云智能终端_20250917145224A249.png","id":4}],"id":1}]},{"id":15,"name":"物联终端","list":[{"name":"\n物联网硬件产品系列","list":[{"name":"温湿度传感器","value":"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式","icon":"http://1.13.177.47:9665/statics/2025/09/17/温湿度传感器_20250917145325A250.png","id":2},{"name":"无线温度传感器","value":"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。","icon":"http://1.13.177.47:9665/statics/2025/09/17/无线温度传感器_20250917145344A251.png","id":3},{"name":"EPD无线显示器","value":"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。","icon":"http://1.13.177.47:9665/statics/2025/09/17/EPD无线显示器_20250917145418A252.png","id":4},{"name":"红外温度传感器","value":"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。","icon":"http://1.13.177.47:9665/statics/2025/09/17/红外温度传感器_20250917145433A253.png","id":5}],"id":1}]},{"id":16,"name":"工业软件","list":[{"name":"MOM","list":[{"name":"MOM","value":"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。","icon":"http://1.13.177.47:9665/statics/2025/09/17/MOM_20250917145527A254.png","id":2}],"id":1}]}]}', 7, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (6, NULL, '{"banner":"https://www.genrace.com/template/default/images/pages/prodDetail-banner.jpg","banner1":"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png","bannerTitle":"HW-R300系列","bannerValue":"中端距离,1路输入,体积小巧,TCP/RS485。","imgList":[{"url":"http://1.13.177.47:9665/statics/2025/08/25/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91_20250825192156A181.png"},{"url":"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png"},{"url":"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png"}],"features":[{"name":"紧凑型圆极化天线"}],"params":[{"title":"参数","list":[{"name":"射频协议","value":"ISO/IEC 18000-63 /EPC Gen2v2"},{"name":"工作频段","value":"中国频段:920-925MHz\n其他国家频段(可定制)"},{"name":"发射功率","value":"10-26dBm 可调,步进 1 dB,精度±1dB"},{"name":"天线增益","value":"3dBi"},{"name":"天线驻波比","value":"≦2:1"},{"name":"盘点速率","value":"120 tag/s"},{"name":"读卡距离","value":"读取TPALN9662标签,最大读距:1.4m(读距随标签型号不同而有所差异)"},{"name":"通讯接口","value":"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)"},{"name":"通讯协议","value":"Modbus-RTU通信协议,网口TCP提供基于C#的SDK接口;适配工业总线协议(外接网关)(选配)"},{"name":"GPIO端口","value":"M12-5-B 航空接口,兼容 5~24V 电平,1路输入"},{"name":"声音指示","value":"1个蜂鸣器"},{"name":"开发接口","value":"支持 C#、JAVA"},{"name":"供电电源","value":"12~24VDC(推荐 12V 标配适配器)"},{"name":"整机峰值功率","value":"2.5W"},{"name":"外形尺寸","value":"90mm*90mm*32 mm"},{"name":"产品重量","value":"约288g"},{"name":"外壳材料","value":"铝合金+PC"},{"name":"外壳颜色","value":"黑色、银色"},{"name":"工作温度","value":"-25℃~60℃"},{"name":"存储温度","value":"-40℃~85℃"},{"name":"工作湿度","value":"5%~95%RH 无冷凝"},{"name":"IP等级","value":"IP54"}]}],"fileList":[{"name":"操作指南","value":"硬件操作指南","url":""},{"name":"操作指南","value":"硬件操作指南","url":""}]}', 11, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (7, NULL, '[{"type":"carousel","value":{"list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/产品中心_20250822091555A013.jpg","title":"\n","value":"\n"},{"title":"\n","value":"\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/1首页-产品中心01_20241219165916A006_20250822091622A014.png"},{"title":"请输入","value":"请输入","icon":""}]}},{"type":8,"value":{"title":"高频RFID读头","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/高频RFID读头_20250822100334A019.png","title":"高频RFID读头 ","leftTitle":"HW-RFR-050-B-003-B1204S","leftInfo":"\n","infos":[{"title":"射频协议","info":"符合ISO/IEC 13.56 MHZ","icon":""},{"title":"盘点速度","info":"20ms/次","icon":""},{"title":"通信协议","info":"RS485","icon":""},{"title":"输入电压","info":"12~24VDC","icon":""},{"title":"尺寸(mm)","info":"40W*72L*13H","icon":""},{"title":"工作温度","info":"-30℃~+70℃","icon":""},{"title":"防护等级","info":"IP67","icon":""}]}]}},{"type":8,"value":{"title":"高频RFID一体机  ","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/高频RFID一体机_20250822100352A020.png","title":"高频RFID一体机","leftTitle":"HW-RSLIM-HF","leftInfo":"\n","infos":[{"title":"射频协议","info":"符合ISO/IEC 13.56 MHz","icon":""},{"title":"盘点速度","info":"10张/秒","icon":""},{"title":"通信协议","info":"TCP/IP、RS232、RS485 ","icon":""},{"title":"输入电压","info":"9~32VDC","icon":""},{"title":"尺寸(mm)","info":"65L*65W*30H","icon":""},{"title":"工作温度","info":"-30℃~+70℃","icon":""},{"title":"工作湿度","info":"5%~95%RH 无冷凝","icon":""}]}]}},{"type":8,"value":{"title":"高频RFID标签  ","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/高频RFID圆环标签_20250822093525A018.png","title":"高频RFID标签  ","leftTitle":"HW-PD50-HF","leftInfo":"","infos":[{"title":"国际标准","info":"ISO15693","icon":""},{"title":"封装材料","info":"PVC/PET","icon":""},{"title":"工艺","info":"印刷、打码、写数据等","icon":""},{"title":"尺寸","info":"内径Φ44mm 外径Φ64mm","icon":""},{"title":"读取距离","info":"0-10cm","icon":""},{"title":"数据存储时间","info":"50年","icon":""},{"title":"可擦写次数","info":"10万次","icon":""}]}]}}]', 13, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (13, NULL, '[{"type":10,"value":{"title":"","subTitle":"在畜牧屠宰行业,RFID助力车间自动化,智能化生产,合作客户包括双汇、正大、牧原等\n","list":[{"title":"请输入","value":"请输入","icon":"http://1.13.177.47:9665/statics/2025/08/22/01_20250822135102A119.png"},{"title":"请输入","value":"请输入","icon":"http://1.13.177.47:9665/statics/2025/08/22/13_20250822135108A120.png"},{"title":"请输入","value":"请输入","icon":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"}]}},{"type":11,"value":{"title":"载具管理","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/牧原屠宰线扁担钩管理_20250822151858A161.png","title":"牧原屠宰线扁担钩管理","value":"在扁担钩安装RFID标签,在输送线安装RFID读写器,将定级称重等信息与RFID绑定,实现白条全流程追溯。\n\n"},{"title":"正大料框桶车管理","value":"在料筐预铸RFID标签,场内循环扫描。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/正大料框桶车管理_20250822151912A162.png"},{"title":"正大料框桶车管理","value":"在桶车安装RFID标签,实现投料验证,追溯管理。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/在料筐预铸RFID标签,场内循环扫描_20250822151922A163.png"}]}}]', 20, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (8, NULL, '[{"type":"carousel","value":{"list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"},{"title":"请输入","value":"请输入","icon":""},{"title":"\n","value":"\n","icon":"http://1.13.177.47:9665/statics/2025/08/25/应用开发_20250825192156A181.png"},{"title":"请输入","value":"请输入","icon":"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png"}]}},{"type":9,"value":{"title":"超高频RFID读写器","subTitle":"读写器产品包含一体式读写器、车载读写器、四通道读写器、各尺寸读写器天线等一系列产品,具 备丰富的通讯接口和公用的软件接口平台,具有稳定性强、性价比高、易开发、易部署等特点","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/一体式读写器(高性能),内置高性能圆极化天线,实现远距离识别。_20250822101411A021.png","value":"一体式读写器(高性能),内置高性能圆极化天线,实现远距离识别。\n\n"},{"value":"一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。 一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。","icon":"http://1.13.177.47:9665/statics/2025/08/22/一体式读写器(标准型),内置高性能圆极化天线,用最小体积实现最优识别性能。_20250822101455A022.png"},{"value":"四通道读写器,拥有4通道射频输出口,丰富的外设接口能灵活满足不同的应用需求。","icon":"http://1.13.177.47:9665/statics/2025/08/22/四通道读写器,拥有4通道射频输出口,丰富的外设接口能灵活满足不同的应用需求。_20250822101513A023.png"},{"value":"两通道车载式读写器,采用工业化防振动设计理念,支持蓝牙连接,提供更加稳定可靠的物理及电器连接方式。\n请输入","icon":"http://1.13.177.47:9665/statics/2025/08/22/两通道车载式读写器,采用工业化防振动设计理念,支持蓝牙连接,提供更加稳定可靠的物理及电器连接方式。_20250822101528A024.png"},{"value":"RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。 输入 请RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。 输入 ","icon":"http://1.13.177.47:9665/statics/2025/08/22/RFID手持机,高性能RFID读写单元与高性能PDA的完美结合,大容量电池超长工作与待机时间。_20250822101611A025.png"},{"value":"蓝牙手持式读写器,蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/蓝牙手持式读写器_20250822101633A026.png"},{"value":"RFID通道门具有窄波束、高增益特点,适用于超高频门禁通道类、物流仓储、人员、图书、档案馆、医疗系统、设备资产等应用。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/RFID通道门具有窄波束、高增益特点,适用于超高频门禁通道类、物流仓储、人员、图书、档案馆、医疗系统、设备资产等应用。_20250822101703A027.png"},{"value":"RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如快递、服装、皮具箱包、酒类等行业。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/RFID通道机,解决在供应链流通中,标签漏读串读等问题,应用于单品级物品识别,如快递、服装、皮具箱包、酒类等行业。_20250822101716A028.png"}]}},{"type":9,"value":{"title":"超高频RFID标签","subTitle":"根据客户需求,提供适用于物流、仓储、资产管理等应用场景的各类标签。并且有多种功能等定制化标签供客户选择。\n\n","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/PixPin_2025-08-22_10-18-50_20250822101910A029.png","value":""},{"value":"","icon":"http://1.13.177.47:9665/statics/2025/08/22/PixPin_2025-08-22_10-19-20_20250822101931A030.png"},{"value":"","icon":"http://1.13.177.47:9665/statics/2025/08/22/11_20250822101953A031.png"},{"value":"","icon":"http://1.13.177.47:9665/statics/2025/08/22/111_20250822102005A032.png"},{"value":"耐高温标签\n:耐温范围为100℃~230℃。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/22_20250822102031A033.png"},{"value":"测温标签:测量温度范围为-25℃~100℃,温度传感器无电池。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/222_20250822102049A034.png"},{"value":"电缆标签:粘贴缠绕于物体表面,允许多次翻折缠绕。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/2222_20250822102107A035.png"},{"value":"铝模板标签:耐酸碱,耐腐蚀。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/22222_20250822102125A036.png"}]}}]', 12, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (9, NULL, '[{"type":"carousel","value":{"list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"}]}},{"type":9,"value":{"title":"物联网硬件产品系列\n\n","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。_20250822103145A037.png","value":"无线传感器接收显示仪。支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。\n\n"},{"value":"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/车载_20250822103928A038.png"},{"value":"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。_20250822103947A039.png"},{"value":"工业物联云智能终端具有 8 路开关量输入检测、16 开关量输出、RS485 总线、网络等硬件接口。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/工业物联云智能终端具有 8 路开关量输入检测、16 开关量输出、RS485 总线、网络等硬件接口。_20250822104006A040.png"}]}}]', 14, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (10, NULL, '[{"type":"carousel","value":{"list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"}]}},{"type":9,"value":{"title":"物联网硬件产品系列\n\n","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式。_20250822104331A042.png","value":"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式"},{"value":"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。 \n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/无线温度传感器_20250822104419A043.png"},{"value":"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。_20250822104449A044.png"},{"value":"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。_20250822104525A045.png"}]}}]', 15, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (11, NULL, '[{"type":2,"value":{"title":"物联网平台\n","subTitle":"","contentTitle":"系统架构","contentSubTitle":"\n","contentInfo":"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。\n\n","icon":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png"}},{"type":2,"value":{"title":"","subTitle":"","contentTitle":"功能介绍","contentSubTitle":"\n","contentInfo":"可以实现海量的设备遥测、远程控制、告警事件处理、设备台账管理。其它应用程序或者功能模块通过与实时数据库、接口交互而实现其功能及扩展。","icon":"http://1.13.177.47:9665/statics/2025/08/22/0_20250822132605A076.png"}}]', 16, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (12, NULL, '[{"type":2,"value":{"title":"快递物流整体解决方案","subTitle":"系统设备主要包括手持 RFID 阅读器、矩阵 RFID 识读通道机、小件分拣机 RFID 读写系统、RFID 通道门等。","contentTitle":"\n","contentSubTitle":"方案介绍","contentInfo":" 1.手持 RFID 阅读器主要用于 RFID 标签、总包信息的手动绑定/写入,以及批量空袋的手动识读。   2.矩阵 RFID 识读通道机主要用于识别邮袋 RFID 标签。                                                   3.小件分拣机 RFID 读写系统主要用于 RFID 标签、总包信息的自动绑定/写入。                 4.RFID 通道门主要用于批量空袋的自动识读/擦除。                                                 5.RFID标签缝制到循环邮袋上,作为资产管理和邮袋识别的数据载体。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片17_20250822141810A130.jpg"}},{"type":9,"value":{"title":"系列化通道机产品解决方案","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/26/92_20250826090151A189.png","value":"兼容不同皮带内宽 ;改造款无需改造皮带机 ;模块化,安装简单。"},{"value":"结构简单,价格便宜, 兼容不同DWS、六面扫协议 指令。","icon":"http://1.13.177.47:9665/statics/2025/08/26/93_20250826090207A190.png"},{"value":"优化算法,可启停读取, 有效防止串读等。","icon":"http://1.13.177.47:9665/statics/2025/08/26/94_20250826090227A191.png"},{"value":"整体发货,无需安装, 配套标准程序,即开即用。","icon":"http://1.13.177.47:9665/statics/2025/08/26/95_20250826090240A192.png"}]}},{"type":2,"value":{"title":"标准化小件机RFID产品解决方案","subTitle":"","contentTitle":"\n","contentSubTitle":"方案介绍","contentInfo":"1、“自动感应”、“刷卡”多种扫描形式 2、分体式一体式、配套定制支架等,适配各种分拣机 3、三级组网结构,环形分拣机、直线分拣机灵活组网 4、线缆T型标准化、设备模块化设计,检修维护简单快速","icon":"http://1.13.177.47:9665/statics/2025/08/22/Snipaste_2025-08-22_14-31-33_20250822143139A142.png"}},{"type":2,"value":{"title":"皮带机磁检测设备","subTitle":"","contentTitle":"\n","contentSubTitle":"方案介绍","contentInfo":"皮带机磁检测设备是我司独立自主设计且拥有专利知识产权的一款磁检测产品。用于物流包裹分拣机供件 台,检测包裹内的磁性物质的作用,防止由于包裹内有磁性物质导致包裹吸附在滑槽上,影响快递包裹时效。 特点:磁性物质,1.5m/s皮带机速度快速检测 ,低成本皮带机宽度范围内检测全覆盖; 多种适配皮带机安装方式;UJM高检测灵敏度,检测灵敏度大于等于3mT。","icon":"http://1.13.177.47:9665/statics/2025/08/26/96_20250826090302A193.png"}}]', 19, '1', '2025-11-21 17:26:35', NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (14, NULL, '[{"type":"carousel","value":{"list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片24_20250822135610A123.png","title":"\n","value":"\n"}]}},{"type":6,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片36_20250822142617A134.jpg","title":"设备管理系统","subTitle":"RFID生产管理系统","info":"基于RFID应用的生产管理特点\n\n基于RFID应用的生产管理具有自动,防误、追溯、集成的特点"}},{"type":6,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片27_20250822140450A125.png","title":"","subTitle":"仓储物流管理系统","info":"通过仓储物流系统、超高频RFID技术,解决了仓储物流管理中数据量大,出入库流程复杂等难点,实现数据的自动识别与采集、物料的自动输送、分拣和防误验证等功能,提高仓储物流运营效益。"}},{"type":6,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片31_20250822140953A127.png","title":"","subTitle":"RFID模具管理系统","info":"专为工装管理温区定制:存放温度-10~250摄氏度,读取工作温度0~120摄氏度。 专为钢制工装管理外形定制:标签尺寸缩减到极限尺寸,小尺寸钢制工装开槽植入无压力。 专为钢制工装管理性能定制:标签在植入钢制工装开槽并封装后,配合软控提供的手持机读取距离>=25cm(配合固定式操作性能更佳)。"}},{"type":2,"value":{"title":"智能制造平台","subTitle":"","contentTitle":"\n","contentSubTitle":" 设备互联系统","contentInfo":"智能制造系统是以制造执行系统(MES)为核心,基于业务化思想,对企业整个业务流程的全生命周期进行管理,实现合理安排生产排程、优化工艺流程、改善产品质量、加强车间物流管理、降低能源损耗、减少库存、降低成本的目的。同时,无缝连接ERP、PLM、WMS、EMS、工业物联等上下游系统,消除信息孤岛,切实提高生产制造运营管理效率。","icon":"http://1.13.177.47:9665/statics/2025/08/22/图片25_20250822135851A124.png"}},{"type":6,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/图片30_20250822140807A126.png","title":"","subTitle":"能源管理系统","info":"在企业扩大产能的同时,通过遍布全场的三级计量监控,合理计划和利用能源,以达到建立合理KPI指标体系,增强全员节能意识,提高能源利用率为目的信息化管控系统。"}}]', 22, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (15, NULL, '[{"type":11,"value":{"title":"企业资质","subTitle":"","list":[{"icon":"http://1.13.177.47:9665/statics/2025/08/22/国家级专精特新“小巨人”企业_20250822152510A168.png","title":"\n","value":"国家级专精特新“小巨人”企业"},{"title":"\n","value":"国家高新技术企业","icon":"http://1.13.177.47:9665/statics/2025/08/22/国家高新技术企业_20250822152526A169.jpg"},{"title":"\n","value":"青岛市专精特新中小企业","icon":"http://1.13.177.47:9665/statics/2025/08/22/青岛市专精特新中小企业_20250822152636A170.png"},{"title":"\n","value":"通过信息系统建设和服务能力评估CS2级","icon":"http://1.13.177.47:9665/statics/2025/08/22/通过信息系统建设和服务能力评估CS2级_20250822152811A171.png"},{"title":"\n","value":"第十一届LT中国物流技术奖-创新应用奖","icon":"http://1.13.177.47:9665/statics/2025/08/22/第十一届LT中国物流技术奖-创新应用奖_20250822152846A172.jpg"},{"title":"\n","value":"国产鲲鹏系统软件兼容性测试技术认证","icon":"http://1.13.177.47:9665/statics/2025/08/22/国产鲲鹏系统软件兼容性测试技术认证_20250822153004A173.png"}]}},{"type":11,"value":{"title":"产品认证\n","subTitle":"RFID产品系列取得产品认证(CE、ROHS、SGS)\n","list":[{"title":"\n","value":"CE","icon":"http://1.13.177.47:9665/statics/2025/08/22/CE_20250822153037A174.png"},{"title":"\n","value":"ROHS","icon":"http://1.13.177.47:9665/statics/2025/08/22/ROHS_20250822153056A175.png"},{"title":"\n","value":"SGS","icon":"http://1.13.177.47:9665/statics/2025/08/22/SGS_20250822153118A176.png"}]}}]', 10, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (16, NULL, '[{"type":2,"value":{"title":"海威物联参加2024智能物流装备供应链集采大会暨第十二次青浦圆桌会议","subTitle":"","contentTitle":"\n","contentSubTitle":"\n","contentInfo":"11月26-27日,2024智能物流装备供应链集采大会暨第十二次青浦圆桌会议在南陵举办。本次大会以“向新而行、以质致远、绿色发展、共创未来”为主题,200余家智能物流装备供应链企业参加,共同探讨智能物流装备领域的新趋势、新机遇,旨在达成更多富有成效的新合作、新成果。青岛海威物联科技有限公司作为智能物流装备优质供应商应邀出席,并发表《物畅其流 “芯”质提效》的主题演讲。","icon":"http://1.13.177.47:9665/statics/2025/08/22/mt_20250822151343A158.jpg"}},{"type":2,"value":{"title":"","subTitle":"","contentTitle":"\n","contentSubTitle":"\n","contentInfo":"海威物联总经理助理张东辉在主题演讲中详细介绍了快递行业如何通过RFID技术的应用显著提升生产效率,并描绘了生产仓储行业RFID技术的发展蓝图;同时分享了边缘物联技术在物流和制造行业的赋能解决方案及成功案例,为行业同仁提供了实践参考。 此次会议不仅是一次思想的盛宴,也是海威物联在快递物流行业的重要展示,为海威物联的发展注入了新动力,也为推动工业物联网技术的发展和物流企业的数字化升级奠定了坚实的基础。","icon":"http://1.13.177.47:9665/statics/2025/08/22/mt2_20250822151416A159.jpg"}}]', 9, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (17, NULL, '[{"type":5,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/合作伙伴_20250822152100A165.png","title":"合作伙伴","subTitle":""}},{"type":5,"value":{"icon":"http://1.13.177.47:9665/statics/2025/08/22/合作伙伴2_20250822152147A167.png","title":"","subTitle":""}}]', 8, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (18, NULL, '{"banner":"https://www.genrace.com/template/default/images/pages/prodDetail-banner.jpg","banner1":"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png","bannerTitle":"HW-R300系列","bannerValue":"中端距离,1路输入,体积小巧,TCP/RS485。","imgList":[{"url":"http://1.13.177.47:9665/statics/2025/08/25/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91_20250825192156A181.png"},{"url":"https://www.genrace.com/static/upload/image/20250414/1744615648137412.png"},{"url":"http://1.13.177.47:9665/statics/2025/08/25/23_20250825192255A182.png"}],"features":[{"name":"紧凑型圆极化天线"}],"params":[{"title":"参数","list":[{"name":"射频协议","value":"ISO/IEC 18000-63 /EPC Gen2v2"},{"name":"工作频段","value":"中国频段:920-925MHz\n其他国家频段(可定制)"},{"name":"发射功率","value":"10-26dBm 可调,步进 1 dB,精度±1dB"},{"name":"天线增益","value":"3dBi"},{"name":"天线驻波比","value":"≦2:1"},{"name":"盘点速率","value":"120 tag/s"},{"name":"读卡距离","value":"读取TPALN9662标签,最大读距:1.4m(读距随标签型号不同而有所差异)"},{"name":"通讯接口","value":"TCP/IP(M12-4-A航空头)、 RS485(M12-5-B 航空头)"},{"name":"通讯协议","value":"Modbus-RTU通信协议,网口TCP提供基于C#的SDK接口;适配工业总线协议(外接网关)(选配)"},{"name":"GPIO端口","value":"M12-5-B 航空接口,兼容 5~24V 电平,1路输入"},{"name":"声音指示","value":"1个蜂鸣器"},{"name":"开发接口","value":"支持 C#、JAVA"},{"name":"供电电源","value":"12~24VDC(推荐 12V 标配适配器)"},{"name":"整机峰值功率","value":"2.5W"},{"name":"外形尺寸","value":"90mm*90mm*32 mm"},{"name":"产品重量","value":"约288g"},{"name":"外壳材料","value":"铝合金+PC"},{"name":"外壳颜色","value":"黑色、银色"},{"name":"工作温度","value":"-25℃~60℃"},{"name":"存储温度","value":"-40℃~85℃"},{"name":"工作湿度","value":"5%~95%RH 无冷凝"},{"name":"IP等级","value":"IP54"}]}],"fileList":[{"name":"操作指南","value":"硬件操作指南","url":""},{"name":"操作指南","value":"硬件操作指南","url":""}]}', 11, '0', NULL, NULL, NULL); +INSERT INTO "public"."hw_web" VALUES (19, NULL, '[]', 2, '0', NULL, '2025-11-17 10:46:54', NULL); +INSERT INTO "public"."hw_web" VALUES (20, NULL, '[{"type":5,"value":{"icon":"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png","title":"方案介绍","subTitle":"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。\n“RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。\n"}},{"type":2,"value":{"title":" ","subTitle":" ","contentTitle":"\n","contentSubTitle":"\n","contentInfo":"\n\n1.手持 RFID 阅读器主要用于 RFID 标签、总包信息的手动绑定/写入,以及批量空袋的手动识读。 \n2.矩阵 RFID 识读通道机主要用于识别邮袋 RFID 标签。 \n3.小件分拣机 RFID 读写系统主要用于 RFID 标签、总包信息的自动绑定/写入。\n4.RFID 通道门主要用于批量空袋的自动识读/擦除。\n5.RFID标签缝制到循环邮袋上,作为资产管理和邮袋识别的数据载体。\n\n","icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"}}]', 19, '1', '2025-11-28 09:56:44', '2025-11-21 17:26:35', NULL); +INSERT INTO "public"."hw_web" VALUES (21, NULL, '[{"type":5,"value":{"icon":"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png","title":"方案介绍","subTitle":"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。 “RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效整体系统设备主要包括手持 RFID 阅读器、小件分拣机 RFID 读写系统、擦除RFID设备、矩阵 RFID 识读通道机、包裹分拣机、RFID 通道门\n。"}}]', 19, '1', '2025-11-28 09:58:22', '2025-11-28 09:56:44', NULL); +INSERT INTO "public"."hw_web" VALUES (22, NULL, '[{"type":5,"value":{"icon":"http://1.13.177.47:9665/statics/2025/11/21/ef0adff907c5fc4b3f9b4a28af06b1d_20251121153001A454.png","title":"方案介绍","subTitle":"海威物联通过与合作伙伴的共同摸索推动,在2018年启动了新一轮RFID的模式探索,2019年确立了主要应用模式并试点使用,2020年大规模推广使用,效果非凡。 “RFID小件包的自动识别分拣及追踪管理解决方案\"获得 LT创新应用奖,相关产品为快递行业补足了全自动化分拣的最后一环,在顺丰、京东、邮政等主要行业客户进行大规模配套,产生了显著的降费增效。整体系统设备主要包括手持 RFID 阅读器、小件分拣机 RFID 读写系统、擦除RFID设备、矩阵 RFID 识读通道机、包裹分拣机、RFID 通道门\n。"}}]', 19, '0', NULL, '2025-11-28 09:58:22', NULL); +INSERT INTO "public"."hw_web" VALUES (23, NULL, '[{"type":1,"value":{"title":"123","subTitle":"456","list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"}]}}]', 5, '1', '2025-12-08 09:44:08', '2025-12-08 09:32:11', NULL); +INSERT INTO "public"."hw_web" VALUES (24, NULL, '[{"type":1,"value":{"title":"123","subTitle":"456","list":[{"icon":"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg","title":"111","value":"222"}]}}]', 5, '0', NULL, '2025-12-08 09:44:08', NULL); +INSERT INTO "public"."hw_web" VALUES (25, NULL, '[]', -1, '1', '2025-12-08 10:13:47', '2025-12-08 10:09:55', NULL); +INSERT INTO "public"."hw_web" VALUES (26, NULL, '[]', -1, '1', '2025-12-08 10:14:34', '2025-12-08 10:13:47', NULL); +INSERT INTO "public"."hw_web" VALUES (27, NULL, '[]', -1, '1', '2025-12-08 10:15:39', '2025-12-08 10:14:34', NULL); +INSERT INTO "public"."hw_web" VALUES (28, NULL, '[]', -1, '1', '2025-12-08 10:16:48', '2025-12-08 10:15:39', NULL); +INSERT INTO "public"."hw_web" VALUES (29, NULL, '[]', -1, '1', '2025-12-08 10:17:06', '2025-12-08 10:16:48', NULL); +INSERT INTO "public"."hw_web" VALUES (30, NULL, '[]', -1, '1', '2025-12-08 10:17:50', '2025-12-08 10:17:06', NULL); +INSERT INTO "public"."hw_web" VALUES (31, NULL, '{}', -1, '1', '2025-12-08 10:17:52', '2025-12-08 10:17:50', NULL); +INSERT INTO "public"."hw_web" VALUES (32, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"id":11,"name":"轮胎RFID","list":[{"name":"分类1 ","list":[{"name":"RFID轮胎","value":"软控为轮胎产业链提供RFID轮胎整体解决方案,包括:系列化轮胎用RFID标签,系列化RFID标签封胶设备、定制RFID标签贴合设备,RFID 轮胎生产过程数据采集系统,及基于不同应用场景的信息化管理系统等","icon":"http://1.13.177.47:9665/statics/2025/09/17/轮胎_20250917144033A235.png","id":2},{"name":"请输入","value":"请输入","icon":"","id":3},{"name":"请输入","value":"请输入","icon":"","id":4}],"id":1}]},{"id":12,"name":"超高频RFID","list":[{"name":"超高频一体式读写器 ","list":[{"name":"HW-145L-6系列","value":"      远距离、高增益、多接口,POE宽电压直流供电,多路GPIO。","icon":"http://1.13.177.47:9665/statics/2025/09/29/a7ed9fc42af9b6386126eee351f4937_20250929143910A279.png","id":2},{"name":"HW-R300系列","value":"                  中短距离,1路输入,体积小巧,TCP/RS485","icon":"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250926144121_20250929144331A280.png","id":3},{"name":"HW-D100系列","value":"                                 短距离,桌面式,USB接口","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片bbb_20250930172331A326.png","id":4},{"name":"蓝牙手持式读写器","value":"蓝牙数据上传,采集数据可轻松接入用户Android移动终端,易于安装与部署。","icon":"http://1.13.177.47:9665/statics/2025/09/17/蓝牙手持式读写器_20250917144525A239.png","id":5}],"id":1},{"name":"超高频分体式读写器  ","list":[{"name":"HW-R200系列","value":"             两通道/四通道天线,体积小巧,TCP通讯,远距读取","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片1_20250929162825A294.png","id":2},{"name":"HW-R400系列","value":"    四通道外接天线,超高性能,高阅读灵敏度,稳定可靠的性能","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片2_20250929163017A295.png","id":3},{"name":"HW-RX系列","value":"                 四通道天线,体积小巧,TCP通讯,快速响应。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片3_20250929163215A296.png","id":4}],"id":2},{"name":"超高频扩展式读写器  ","list":[{"name":"HW-R110系列","value":"     1路内置天线,1路外接天线接口,体积紧凑,可单机可扩展","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片vvv_20250930171716A325.png","id":2},{"name":"HW-R170系列","value":"             1路内置窄波束天线,1路外接扩展天线接口,圆极化","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片6_20250929164228A298.png","id":3}],"id":3},{"name":"超高频天线 ","list":[{"name":"ACB5040系列","value":"                               圆极化、低驻波比,超小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片8_20250929164750A299.png","id":2},{"name":"A130D06系列","value":"                         圆极化、低驻波比、高性能、小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片9_20250929165030A300.png","id":3},{"name":"AB7020系列","value":"                       圆极化、窄波束、低驻波比、中小尺寸。","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片10_20250929165329A301.png","id":4},{"name":"AB10010系列","value":"                           天线具有定向性、窄波束、高增益","icon":"http://1.13.177.47:9665/statics/2025/09/29/微信图片_20250929165850_20250929165911A302.png","id":5}],"id":5},{"name":"超高频标签 ","list":[{"name":"HW-A6320","value":"          适用于仓储物流、生产制造、铁路以及循环运输管理等","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片_20250929170754A304.png","id":2},{"name":"HW-A6020","value":"                              适用于资产管理、托盘管理等","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片22_20250929171525A306.png","id":3},{"name":"HW-PV8654","value":"适用于多标签识别的应用场合,可用打印机打印图案等个性化信息","icon":"http://1.13.177.47:9665/statics/2025/09/29/图片15_20250929172804A311.png","id":4},{"name":"HW-QT5050","value":"                                 无纺布或卡片型,全向标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片333_20250930090410A313.png","id":5},{"name":"HW-HT4634","value":"                                           矩形、耐酸碱","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片55_20250930090733A314.png","id":6},{"name":"HW-P6020LM","value":"                                          长条形,亮灯标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片666_20250930091535A317.png","id":7},{"name":"HW-P9010","value":"                                        长条型,PCB标签","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片7777_20250930170214A318.png","id":8},{"name":"T0505","value":"                          正方形、耐高温、应用于模具管理","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片gg_20250930170722A322.png","id":9},{"name":"HW-TL1621","value":"                                   螺栓型、耐高温、抗金属","icon":"http://1.13.177.47:9665/statics/2025/09/30/图片sss_20250930171327A323.png","id":10}],"id":6}]},{"id":13,"name":"高频RFID","list":[{"name":"高频一体式读写器  ","list":[{"name":"HW-RFR-050系列","value":"                         体积小,三防性能优,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片bb_20251015105305A346.png","id":2},{"name":"HW-RFR-050系列","value":"             螺栓形状读写器,易固定;尺寸小,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片h_20251015105320A347.png","id":3},{"name":"HW-RFR-020系列","value":"            螺栓形状读写器,易固定;尺寸小,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片jj_20251015101618A337.png","id":4},{"name":"HW-RFR-010系列","value":"                   尺寸更加小巧,短距稳定识别,三防性能优","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片d_20251015105254A345.png","id":5},{"name":"HW-RFR-RFLY","value":"                               TCP网络通讯,远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片M_20251015105452A348.png","id":6},{"name":"HW-RFR-RFLY-I90","value":"                             TCP网络通讯,超远距稳定识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片jjjjj_20251015110254A349.png","id":7},{"name":"HW-D80系列","value":"                            UID通讯,桌面发卡,近距识别","icon":"http://1.13.177.47:9665/statics/2025/10/15/gdfdf_20251015111010A350.png","id":8},{"name":"请输入","value":"请输入","icon":"","id":9}],"id":2},{"name":"高频RFID标签 ","list":[{"name":"HW-HF-PVS-8654","value":"独特的天线设计,优异的天线性能 ;可用于自动化识别、托盘管理、资产管理等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片12_20251015111534A351.png","id":2},{"name":"HW-HF-PPS-D50","value":"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片D50_20251015113514A352.png","id":3},{"name":"HW-HF-PPS-D30","value":"耐高温、耐磨、耐腐蚀 ;可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片D30_20251015113547A353.png","id":4},{"name":"HW-HF-PVS-D30","value":"        PVC材质,可用于自动化识别、资产管理、设备巡检等","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片1PVS-D30_20251015130809A354.png","id":5},{"name":"HW-TR6244-HF-PET","value":"            圆环形状标签,PET材质,用于圆柱电芯托杯管理","icon":"http://1.13.177.47:9665/statics/2025/10/15/图片YH6244_20251015131136A355.png","id":6}],"id":3}]},{"id":14,"name":"传感器","list":[{"name":"物联网硬件产品系列","list":[{"name":"无线传感器接收显示仪","value":"支持 WiFi、以太网、4G、LORA 等通讯方式;Modbus_RTU,Modbus_TCP,支持客户定制通讯协议开发。","icon":"http://1.13.177.47:9665/statics/2025/09/17/无线传感器接收显示仪_20250917145129A247.png","id":2},{"name":"车载物联定位终端","value":"车载物联定位终端, 支持 GPS/ 北斗定位 , 通过 内置 TPMS 或 RFID 读写器 模块 读取到的设备相关数据 ,用 4G无线 通讯 的方式上传 到企业数据平台或阿里云物 联网平台。","icon":"http://1.13.177.47:9665/statics/2025/09/17/车载物联定位终端_20250917145144A248.png","id":3},{"name":"工业物联云智能终端","value":"工业物联云智能终端,可以对电力、蒸汽等智能仪表及传感器进行数据采集,通过网络 通信方式上传到上位机系统,进行数据的集中处理。","icon":"http://1.13.177.47:9665/statics/2025/09/17/工业物联云智能终端_20250917145224A249.png","id":4}],"id":1}]},{"id":15,"name":"物联终端","list":[{"name":"\n物联网硬件产品系列","list":[{"name":"温湿度传感器","value":"温湿度传感器,支持2路输入,采用传感器探头+采集主机的形式","icon":"http://1.13.177.47:9665/statics/2025/09/17/温湿度传感器_20250917145325A250.png","id":2},{"name":"无线温度传感器","value":"无线温度传感器,电池可用 3 年; 温度测量范围:-40~125℃;精度:±0.5℃ (25℃);通讯距离:视距范围 1500 米。","icon":"http://1.13.177.47:9665/statics/2025/09/17/无线温度传感器_20250917145344A251.png","id":3},{"name":"EPD无线显示器","value":"EPD无线显示器,300米视距范围内的通信,电池2年使用寿命,提供显示模板定制化的服务。","icon":"http://1.13.177.47:9665/statics/2025/09/17/EPD无线显示器_20250917145418A252.png","id":4},{"name":"红外温度传感器","value":"红外温度传感器,无接触式测温,DC9~36V供电,2.4寸TFT显示屏显示,温度测量范围:0~150℃,精度±1℃,支持RS485、无线LORA通讯,通讯距离1500米。","icon":"http://1.13.177.47:9665/statics/2025/09/17/红外温度传感器_20250917145433A253.png","id":5}],"id":1}]},{"id":16,"name":"工业软件","list":[{"name":"MOM","list":[{"name":"MOM","value":"平台向下连接海量设备,支撑设备数据采集和反向控制;向上为业务系统提供统一调用接口,为应用层的系统提供物联能力。并且自身具有完整的数据可视化组件、设备模型、数据中心能力。","icon":"http://1.13.177.47:9665/statics/2025/09/17/MOM_20250917145527A254.png","id":2}],"id":1}]}]}', -1, '1', '2025-12-08 10:18:59', '2025-12-08 10:17:52', NULL); +INSERT INTO "public"."hw_web" VALUES (33, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFID","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFID","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFID","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"}]}', -1, '1', '2025-12-08 10:23:09', '2025-12-08 10:18:59', NULL); +INSERT INTO "public"."hw_web" VALUES (34, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传123感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"}]}', -1, '1', '2025-12-08 10:30:08', '2025-12-08 10:23:09', NULL); +INSERT INTO "public"."hw_web" VALUES (35, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传123感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"","configTypeDesc":"介绍","homeConfigTypeName":"名称"}]}', -1, '1', '2025-12-08 10:30:13', '2025-12-08 10:30:08', NULL); +INSERT INTO "public"."hw_web" VALUES (36, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传123感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"}]}', -1, '1', '2025-12-08 10:34:37', '2025-12-08 10:30:13', NULL); +INSERT INTO "public"."hw_web" VALUES (37, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传123感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"","configTypeDesc":"介绍","homeConfigTypeName":"名称"}]}', -1, '1', '2025-12-08 10:36:28', '2025-12-08 10:34:37', NULL); +INSERT INTO "public"."hw_web" VALUES (38, NULL, '{"classicCaseData":[{"configTypeId":5,"homeConfigTypeName":"智能轮胎","caseInfoTitle":"智能轮胎","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":18,"homeConfigTypeName":"轮胎工厂","caseInfoTitle":"轮胎工厂","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":19,"homeConfigTypeName":"快递物流","caseInfoTitle":"快递物流","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8717_20250822141810A130.jpg"},{"configTypeId":20,"homeConfigTypeName":"畜牧食品","caseInfoTitle":"畜牧食品","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/12_20250822135124A121.png"},{"configTypeId":21,"homeConfigTypeName":"新能源","caseInfoTitle":"新能源","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"},{"configTypeId":22,"homeConfigTypeName":"智能制造","caseInfoTitle":"智能制造","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/08/22/%E5%9B%BE%E7%89%8736_20250822142617A134.jpg"},{"configTypeId":23,"homeConfigTypeName":"工业物联","caseInfoTitle":"工业物联","caseInfoDesc":"智能轮胎是一种基于人工智能技术的轮胎,它可以根据车辆的运行状态和环境条件,自动调整轮胎的性能和参数,以提高车辆的行驶安全性和燃油效率。","caseInfoPic":"http://1.13.177.47:9665/statics/2025/07/24/mesnac_20250724093713A002.jpg"}],"productCenterData":[{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%B5%84%E6%BA%90%208@4x_20250822150647A156.png","configTypeId":"11","homeConfigTypeName":"轮胎RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。","linkData":[11,1,2]},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E4%B8%80%E4%BD%93%E5%BC%8F%E8%AF%BB%E5%86%99%E5%99%A8%EF%BC%88%E6%A0%87%E5%87%86%E5%9E%8B%EF%BC%89%EF%BC%8C%E5%86%85%E7%BD%AE%E9%AB%98%E6%80%A7%E8%83%BD%E5%9C%86%E6%9E%81%E5%8C%96%E5%A4%A9%E7%BA%BF%EF%BC%8C%E7%94%A8%E6%9C%80%E5%B0%8F%E4%BD%93%E7%A7%AF%E5%AE%9E%E7%8E%B0%E6%9C%80%E4%BC%98%E8%AF%86%E5%88%AB%E6%80%A7%E8%83%BD%E3%80%82_20250822101455A022.png","configTypeId":"12","homeConfigTypeName":"超高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。","linkData":[12,1,2]},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E9%AB%98%E9%A2%91RFID%E8%AF%BB%E5%A4%B4_20250822100334A019.png","configTypeId":"13","homeConfigTypeName":"高频RFI123D","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E8%BD%A6%E8%BD%BD_20250822103928A038.png","configTypeId":"14","homeConfigTypeName":"传123感器","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/%E6%B8%A9%E6%B9%BF%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%EF%BC%8C%E6%94%AF%E6%8C%812%E8%B7%AF%E8%BE%93%E5%85%A5%EF%BC%8C%E9%87%87%E7%94%A8%E4%BC%A0%E6%84%9F%E5%99%A8%E6%8E%A2%E5%A4%B4+%E9%87%87%E9%9B%86%E4%B8%BB%E6%9C%BA%E7%9A%84%E5%BD%A2%E5%BC%8F%E3%80%82_20250822104331A042.png","configTypeId":"15","homeConfigTypeName":"物联终端","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"http://1.13.177.47:9665/statics/2025/08/22/mes_20250822132455A075.png","configTypeId":"16","homeConfigTypeName":"工业软件","configTypeDesc":"轮胎RFID是一种基于RFID技术的轮胎识别系统,它可以通过RFID标签读取轮胎的信息,实现轮胎的识别、定位、监控等功能。"},{"homeConfigTypePic":"","configTypeDesc":"介绍","homeConfigTypeName":"名称"}]}', -1, '0', NULL, '2025-12-08 10:36:28', NULL); + +-- ---------------------------- +-- Primary Key structure for table hw_web +-- ---------------------------- +ALTER TABLE "public"."hw_web" ADD CONSTRAINT "hw_web_pkey" PRIMARY KEY ("web_id"); diff --git a/Admin.NET-v2/一键净化项目.bat b/Admin.NET-v2/一键净化项目.bat new file mode 100644 index 0000000..8b405c8 --- /dev/null +++ b/Admin.NET-v2/一键净化项目.bat @@ -0,0 +1,22 @@ +@echo OFF +setlocal enabledelayedexpansion + +echo Ŀļ... + +REM ɾǰnode_modulesЧķʽ +if exist ".\Web\node_modules" ( + echo ɾ Web node_modules... + rd /s /q ".\Web\node_modules" 2>nul +) + +REM Admin.NETĿbinobjpublicļ +for /d /r ".\Admin.NET\" %%b in (bin obj public) do ( + if exist "%%b" ( + echo ɾ %%~b... + rd /s /q "%%b" 2>nul + ) +) + +echo ϣ˳ +pause >nul +exit /b 0 \ No newline at end of file diff --git a/doc/hw-portal-抽离迁移指导.md b/doc/hw-portal-抽离迁移指导.md index 4ce4347..fe6d903 100644 --- a/doc/hw-portal-抽离迁移指导.md +++ b/doc/hw-portal-抽离迁移指导.md @@ -490,11 +490,13 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/aboutUsInfo") public class HwAboutUsInfoController extends BaseController { - @Autowired - private IHwAboutUsInfoService hwAboutUsInfoService; + + private final IHwAboutUsInfoService hwAboutUsInfoService; /** * 查询关于我们信息列表 @@ -592,11 +594,13 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/aboutUsInfoDetail") public class HwAboutUsInfoDetailController extends BaseController { - @Autowired - private IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; + + private final IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; /** * 查询关于我们信息明细列表 @@ -694,11 +698,13 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/contactUsInfo") public class HwContactUsInfoController extends BaseController { - @Autowired - private IHwContactUsInfoService hwContactUsInfoService; + + private final IHwContactUsInfoService hwContactUsInfoService; /** * 查询联系我们信息列表 @@ -798,14 +804,16 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/portalConfig") public class HwPortalConfigController extends BaseController { - @Autowired - private IHwPortalConfigService hwPortalConfigService; + + private final IHwPortalConfigService hwPortalConfigService; - @Autowired - private IHwPortalConfigTypeService hwPortalConfigTypeService; + + private final IHwPortalConfigTypeService hwPortalConfigTypeService; /** * 查询门户网站配置列表 @@ -913,11 +921,13 @@ import java.util.List; * @date 2024-12-11 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/portalConfigType") public class HwPortalConfigTypeController extends BaseController { - @Autowired - private IHwPortalConfigTypeService hwPortalConfigTypeService; + + private final IHwPortalConfigTypeService hwPortalConfigTypeService; /** * 查询门户网站配置类型列表 @@ -1012,32 +1022,34 @@ import java.util.List; * @date 2024-12-12 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/portal") public class HwPortalController extends BaseController { - @Autowired - private IHwPortalConfigService hwPortalConfigService; + + private final IHwPortalConfigService hwPortalConfigService; - @Autowired - private IHwPortalConfigTypeService hwPortalConfigTypeService; + + private final IHwPortalConfigTypeService hwPortalConfigTypeService; - @Autowired - private IHwProductCaseInfoService hwProductCaseInfoService; + + private final IHwProductCaseInfoService hwProductCaseInfoService; - @Autowired - private IHwContactUsInfoService hwContactUsInfoService; + + private final IHwContactUsInfoService hwContactUsInfoService; - @Autowired - private IHwProductInfoService productInfoService; + + private final IHwProductInfoService productInfoService; - @Autowired - private IHwProductInfoDetailService hwProductInfoDetailService; + + private final IHwProductInfoDetailService hwProductInfoDetailService; - @Autowired - private IHwAboutUsInfoService hwAboutUsInfoService; + + private final IHwAboutUsInfoService hwAboutUsInfoService; - @Autowired - private IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; + + private final IHwAboutUsInfoDetailService hwAboutUsInfoDetailService; /** @@ -1210,14 +1222,16 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/productCaseInfo") public class HwProductCaseInfoController extends BaseController { - @Autowired - private IHwProductCaseInfoService hwProductCaseInfoService; + + private final IHwProductCaseInfoService hwProductCaseInfoService; - @Autowired - private IHwPortalConfigTypeService hwPortalConfigTypeService; + + private final IHwPortalConfigTypeService hwPortalConfigTypeService; /** * 查询案例内容列表 @@ -1331,14 +1345,16 @@ import java.util.List; * @date 2024-12-01 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/productInfo") public class HwProductInfoController extends BaseController { - @Autowired - private IHwProductInfoService hwProductInfoService; + + private final IHwProductInfoService hwProductInfoService; - @Autowired - private IHwPortalConfigTypeService hwPortalConfigTypeService; + + private final IHwPortalConfigTypeService hwPortalConfigTypeService; /** @@ -1450,11 +1466,13 @@ import java.util.List; * @date 2024-12-11 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/productInfoDetail") public class HwProductInfoDetailController extends BaseController { - @Autowired - private IHwProductInfoDetailService hwProductInfoDetailService; + + private final IHwProductInfoDetailService hwProductInfoDetailService; /** * 查询产品信息明细配置列表 @@ -1560,11 +1578,13 @@ import com.ruoyi.common.core.web.page.TableDataInfo; * @date 2025-08-18 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/hwWeb") public class HwWebController extends BaseController { - @Autowired - private IHwWebService hwWebService; + + private final IHwWebService hwWebService; /** * 查询haiwei官网json列表 @@ -1669,12 +1689,14 @@ import java.util.List; * @date 2025-08-18 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/hwWeb1") public class HwWebController1 extends BaseController { - @Autowired - private IHwWebService1 hwWebService1; + + private final IHwWebService1 hwWebService1; /** * 查询haiwei官网json列表 @@ -1790,11 +1812,13 @@ import com.ruoyi.common.core.web.page.TableDataInfo; * @date 2025-09-22 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/hwWebDocument") public class HwWebDocumentController extends BaseController { - @Autowired - private IHwWebDocumentService hwWebDocumentService; + + private final IHwWebDocumentService hwWebDocumentService; /** * 查询Hw资料文件列表 @@ -1931,11 +1955,13 @@ import com.ruoyi.common.core.utils.poi.ExcelUtil; * @date 2025-08-18 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/hwWebMenu") public class HwWebMenuController extends BaseController { - @Autowired - private IHwWebMenuService hwWebMenuService; + + private final IHwWebMenuService hwWebMenuService; /** * 查询haiwei官网菜单列表 @@ -2038,11 +2064,13 @@ import java.util.List; * @date 2025-08-18 */ @RestController +@RequiredArgsConstructor + @RequestMapping("/hwWebMenu1") public class HwWebMenuController1 extends BaseController { - @Autowired - private IHwWebMenuService1 hwWebMenuService1; + + private final IHwWebMenuService1 hwWebMenuService1; /** * 查询haiwei官网菜单列表 @@ -6065,11 +6093,12 @@ import java.util.List; * @author ruoyi * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwAboutUsInfoDetailServiceImpl implements IHwAboutUsInfoDetailService { - @Autowired - private HwAboutUsInfoDetailMapper hwAboutUsInfoDetailMapper; + + private final HwAboutUsInfoDetailMapper hwAboutUsInfoDetailMapper; /** * 查询关于我们信息明细 @@ -6166,11 +6195,12 @@ import java.util.List; * @author xins * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwAboutUsInfoServiceImpl implements IHwAboutUsInfoService { - @Autowired - private HwAboutUsInfoMapper hwAboutUsInfoMapper; + + private final HwAboutUsInfoMapper hwAboutUsInfoMapper; /** * 查询关于我们信息 @@ -6267,11 +6297,12 @@ import java.util.List; * @author xins * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwContactUsInfoServiceImpl implements IHwContactUsInfoService { - @Autowired - private HwContactUsInfoMapper hwContactUsInfoMapper; + + private final HwContactUsInfoMapper hwContactUsInfoMapper; /** * 查询联系我们信息 @@ -6369,11 +6400,12 @@ import java.util.List; * @author xins * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwPortalConfigServiceImpl implements IHwPortalConfigService { - @Autowired - private HwPortalConfigMapper hwPortalConfigMapper; + + private final HwPortalConfigMapper hwPortalConfigMapper; /** * 查询门户网站配置 @@ -6498,11 +6530,12 @@ import java.util.stream.Collectors; * @author xins * @date 2024-12-11 */ -@Service +@RestController +@RequiredArgsConstructor public class HwPortalConfigTypeServiceImpl implements IHwPortalConfigTypeService { - @Autowired - private HwPortalConfigTypeMapper hwPortalConfigTypeMapper; + + private final HwPortalConfigTypeMapper hwPortalConfigTypeMapper; /** * 查询门户网站配置类型 @@ -6744,11 +6777,12 @@ import java.util.stream.Collectors; * @author xins * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwProductCaseInfoServiceImpl implements IHwProductCaseInfoService { - @Autowired - private HwProductCaseInfoMapper hwProductCaseInfoMapper; + + private final HwProductCaseInfoMapper hwProductCaseInfoMapper; /** * 查询案例内容 @@ -6878,11 +6912,12 @@ import java.util.List; * @author xins * @date 2024-12-11 */ -@Service +@RestController +@RequiredArgsConstructor public class HwProductInfoDetailServiceImpl implements IHwProductInfoDetailService { - @Autowired - private HwProductInfoDetailMapper hwProductInfoDetailMapper; + + private final HwProductInfoDetailMapper hwProductInfoDetailMapper; /** * 查询产品信息明细配置 @@ -6999,11 +7034,12 @@ import java.util.stream.Collectors; * @author xins * @date 2024-12-01 */ -@Service +@RestController +@RequiredArgsConstructor public class HwProductInfoServiceImpl implements IHwProductInfoService { - @Autowired - private HwProductInfoMapper hwProductInfoMapper; + + private final HwProductInfoMapper hwProductInfoMapper; /** * 查询产品信息配置 @@ -7218,11 +7254,12 @@ import com.ruoyi.portal.service.IHwWebDocumentService; * @author zch * @date 2025-09-22 */ -@Service +@RestController +@RequiredArgsConstructor public class HwWebDocumentServiceImpl implements IHwWebDocumentService { - @Autowired - private HwWebDocumentMapper hwWebDocumentMapper; + + private final HwWebDocumentMapper hwWebDocumentMapper; /** * 查询Hw资料文件 @@ -7357,11 +7394,12 @@ import com.ruoyi.portal.service.IHwWebMenuService; * @author zch * @date 2025-08-18 */ -@Service +@RestController +@RequiredArgsConstructor public class HwWebMenuServiceImpl implements IHwWebMenuService { - @Autowired - private HwWebMenuMapper hwWebMenuMapper; + + private final HwWebMenuMapper hwWebMenuMapper; /** * 查询haiwei官网菜单 @@ -7528,11 +7566,12 @@ import java.util.stream.Collectors; * @author zch * @date 2025-08-18 */ -@Service +@RestController +@RequiredArgsConstructor public class HwWebMenuServiceImpl1 implements IHwWebMenuService1 { - @Autowired - private HwWebMenuMapper1 HwWebMenuMapper1; + + private final HwWebMenuMapper1 HwWebMenuMapper1; /** * 查询haiwei官网菜单 @@ -7695,11 +7734,12 @@ import org.springframework.transaction.annotation.Transactional; * @author ruoyi * @date 2025-08-18 */ -@Service +@RestController +@RequiredArgsConstructor public class HwWebServiceImpl implements IHwWebService { - @Autowired - private HwWebMapper hwWebMapper; + + private final HwWebMapper hwWebMapper; /** * 查询haiwei官网json @@ -7810,11 +7850,12 @@ import org.springframework.transaction.annotation.Transactional; * @author ruoyi * @date 2025-08-18 */ -@Service +@RestController +@RequiredArgsConstructor public class HwWebServiceImpl1 implements IHwWebService1 { - @Autowired - private HwWebMapper1 hwWebMapper1; + + private final HwWebMapper1 hwWebMapper1; /** * 查询haiwei官网json