ソースを参照

chore: checkpoint backup 2026-04-06 — version 2.4.35 / 1.0.2, rules, smart_ops docs, sync tracked tree

aidopdev 4 ヶ月 前
コミット
c62c03c96d
83 ファイル変更3049 行追加2752 行削除
  1. 1 0
      .cursor/rules/README.md
  2. 35 0
      .cursor/rules/version-bump-on-commit.mdc
  3. 1 0
      AGENTS.md
  4. 45 45
      Web/api_build/build.sh
  5. 1 1
      Web/package.json
  6. BIN
      Web/public/app-icons/aidop-logo.png
  7. BIN
      Web/src/assets/401.png
  8. BIN
      Web/src/assets/404.png
  9. BIN
      Web/src/assets/icons/font-awesome/fontawesome-webfont.eot
  10. BIN
      Web/src/assets/icons/font-awesome/fontawesome-webfont.ttf
  11. BIN
      Web/src/assets/icons/font-awesome/fontawesome-webfont.woff
  12. BIN
      Web/src/assets/icons/font-awesome/fontawesome-webfont.woff2
  13. BIN
      Web/src/assets/icons/iconfont/font_2298093_rnp72ifj3ba.ttf
  14. BIN
      Web/src/assets/logo.png
  15. 60 60
      Web/src/utils/auto-update.ts
  16. BIN
      ai-dop-platform/docs/db/aidopcore_远端落库表字段说明.xlsx
  17. 19 0
      ai-dop-platform/docs/db/smart_ops_data_model/00_表清单与分层角色.csv
  18. 113 0
      ai-dop-platform/docs/db/smart_ops_data_model/01_字段明细_逻辑结构.csv
  19. 47 0
      ai-dop-platform/docs/db/smart_ops_data_model/02_表间关系说明.txt
  20. 40 0
      ai-dop-platform/docs/db/smart_ops_data_model/03_指标呈现与期量差约定.txt
  21. 12 0
      ai-dop-platform/docs/db/smart_ops_data_model/README.txt
  22. BIN
      doc/img/1.png
  23. BIN
      doc/img/10.png
  24. BIN
      doc/img/11.png
  25. BIN
      doc/img/12.png
  26. BIN
      doc/img/13.png
  27. BIN
      doc/img/14.png
  28. BIN
      doc/img/15.png
  29. BIN
      doc/img/16.png
  30. BIN
      doc/img/2.png
  31. BIN
      doc/img/3.png
  32. BIN
      doc/img/4.png
  33. BIN
      doc/img/5.png
  34. BIN
      doc/img/6.png
  35. BIN
      doc/img/7.png
  36. BIN
      doc/img/8.png
  37. BIN
      doc/img/9.png
  38. BIN
      doc/img/pay.png
  39. BIN
      doc/shop/iMES/1.png
  40. BIN
      doc/shop/iMES/2.png
  41. BIN
      doc/shop/iMES/3.png
  42. BIN
      doc/shop/iMES/4.png
  43. 28 28
      docker/README.md
  44. 34 34
      docker/app/Configuration/Cache.json
  45. 28 0
      docker/app/Configuration/ElasticSearch.json
  46. 1 1
      docker/docker-compose-builder.yml
  47. 15 14
      docker/redis/redis.conf
  48. 15 15
      server/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs
  49. 15 15
      server/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs
  50. 15 15
      server/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs
  51. 38 38
      server/Admin.NET.Core/Attribute/MaxValueAttribute.cs
  52. 38 38
      server/Admin.NET.Core/Attribute/MinValueAttribute.cs
  53. 15 15
      server/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs
  54. 15 15
      server/Admin.NET.Core/Attribute/OwnerUserAttribute.cs
  55. 42 42
      server/Admin.NET.Core/Cache/CacheSetup.cs
  56. 206 206
      server/Admin.NET.Core/Entity/SysCodeGenConfig.cs
  57. 190 190
      server/Admin.NET.Core/Entity/SysWechatPay.cs
  58. 96 96
      server/Admin.NET.Core/Entity/SysWechatRefund.cs
  59. 31 31
      server/Admin.NET.Core/Enum/FilterLogicEnum.cs
  60. 67 67
      server/Admin.NET.Core/Enum/FilterOperatorEnum.cs
  61. 25 25
      server/Admin.NET.Core/Enum/PrintTypeEnum.cs
  62. 27 27
      server/Admin.NET.Core/EventBus/EventHandlerMonitor.cs
  63. 164 164
      server/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs
  64. 201 201
      server/Admin.NET.Core/EventBus/RedisQueue.cs
  65. 43 43
      server/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs
  66. 366 366
      server/Admin.NET.Core/Extension/SqlSugarExtension.cs
  67. 57 57
      server/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs
  68. 50 50
      server/Admin.NET.Core/Job/OnlineUserJob.cs
  69. 100 100
      server/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs
  70. 1 1
      server/Admin.NET.Core/SeedData/SysConfigSeedData.cs
  71. 2 2
      server/Admin.NET.Core/SeedData/SysMenuSeedData.cs
  72. 198 198
      server/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs
  73. 67 67
      server/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs
  74. 2 2
      server/Admin.NET.Core/Service/Config/SysConfigService.cs
  75. 53 53
      server/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs
  76. 261 261
      server/Admin.NET.Core/Service/Dict/SysDictTypeService.cs
  77. 97 97
      server/Admin.NET.Core/Service/Job/JobClusterServer.cs
  78. 69 69
      server/Admin.NET.Core/Utils/MiniExcelUtil.cs
  79. 3 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  80. BIN
      server/Admin.NET.Web.Entry/GeoLite2-City.mmdb
  81. BIN
      server/Admin.NET.Web.Entry/ip2region.db
  82. BIN
      server/Admin.NET.Web.Entry/wwwroot/images/logo.png
  83. BIN
      server/Admin.NET.Web.Entry/wwwroot/upload/logo.png

+ 1 - 0
.cursor/rules/README.md

@@ -12,6 +12,7 @@
 | 文件 | 说明 |
 |------|------|
 | `collaboration-scope.mdc` | 改前清单待确认、跨模块影响须先预警、小问题最小改动不大重构等 |
+| `version-bump-on-commit.mdc` | 发版必升号;凡产生 Git 提交的变动须在同一次提交中递增版本号(package.json / Web.Entry.csproj) |
 
 ## 非 Cursor 成员
 

+ 35 - 0
.cursor/rules/version-bump-on-commit.mdc

@@ -0,0 +1,35 @@
+---
+description: 发版必升号;凡产生 Git 提交的变动须在同一次提交中递增版本号
+alwaysApply: true
+---
+
+# 版本号:发版与每次提交
+
+## 发版必升号
+
+凡视为**发版**(打 tag、上预发/生产、对外交付构建物),发布所依据的提交中**必须**已更新版本号,且与变更说明一致;禁止「已部署仍沿用旧号」。
+
+## 每次 Git 提交:有变动则同提交内升号
+
+凡执行 `git commit` 且本次提交**包含对工作区已跟踪文件的任何实质修改**(新增、删除、内容变更),**必须在同一提交中**完成版本递增,**不得**只改业务代码而不改版本号。
+
+### 递增范围(本仓库约定:有提交则双端同升 patch)
+
+凡满足上一节的提交,**须在同一次提交中同时**:
+
+1. **前端**:递增 `Web/package.json` 的 `version`(SemVer:**只升 patch** 末位,如 `2.4.33` → `2.4.34`;minor/major 仅在明确里程碑发版时由负责人决定)。构建时 `__NEXT_VERSION__` 来自该字段,无需另改 `vite.config.ts`。
+2. **后端**:递增 `server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 中的 `<Version>`、`<AssemblyVersion>`、`<FileVersion>`(**三处保持同号**,patch +1,如 `1.0.0` → `1.0.1`)。
+
+即:**不论本次 diff 只动前端、只动后端或只动文档/规则**,只要产生实质提交,**前后端版本号均递增**,与「每次提交有变动就升号」一致。
+
+### 锁文件
+
+若项目使用 `package-lock.json` / `pnpm-lock.yaml` 且会因版本或依赖变更而更新,版本提交中应包含与 `package.json` 一致的锁文件变更。
+
+### 提交说明
+
+建议在 commit message 中带一句版本,例如:`chore: bump version 2.4.34` 或与功能说明合并。
+
+## AI / 协作者执行时注意
+
+在准备帮用户**提交或收尾 PR** 时:若本轮改动了仓库文件,应**主动提醒或代为**完成上述版本号递增,避免遗漏。

+ 1 - 0
AGENTS.md

@@ -7,5 +7,6 @@
 - 动手改代码前先列改动清单,**经确认后再改**  
 - 改动若**波及其他功能模块**,须单独写出跨模块影响并**等待决策**  
 - 小问题**最小改动**,非必要不大重构;更大方案以**建议**形式交负责人决策  
+- **发版必升号**;**每次 Git 提交只要包含实质文件变动,须在同一提交中同时递增前端与后端版本号**(`Web/package.json` 的 `version` patch +1;`server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` 的 `<Version>` / `<AssemblyVersion>` / `<FileVersion>` 同步 patch +1)。详见 **[`.cursor/rules/version-bump-on-commit.mdc`](.cursor/rules/version-bump-on-commit.mdc)**。
 
 说明与索引:[`.cursor/rules/README.md`](.cursor/rules/README.md)

+ 45 - 45
Web/api_build/build.sh

@@ -1,46 +1,46 @@
-#!/bin/bash
-
-dir="$(dirname "$0")"
-
-moduleName="apiServices"
-apiServicesPath="$dir/../src/api-services/"
-apiUrl="http://localhost:5005/swagger/Default/swagger.json"
-
-case "$1" in
-    approvalFlow)
-        moduleName="approvalFlow"
-        apiServicesPath="$dir/../src/api-plugins/approvalFlow/"
-        apiUrl="http://localhost:5005/swagger/ApprovalFlow/swagger.json"
-        ;;
-    dingTalk)
-        moduleName="dingTalk"
-        apiServicesPath="$dir/../src/api-plugins/dingTalk/"
-        apiUrl="http://localhost:5005/swagger/DingTalk/swagger.json"
-        ;;
-    goView)
-        moduleName="goView"
-        apiServicesPath="$dir/../src/api-plugins/goView/"
-        apiUrl="http://localhost:5005/swagger/GoView/swagger.json"
-        ;;
-esac
-
-if [ -d "$apiServicesPath" ]; then
-    echo "================================ 删除目录 $apiServicesPath ================================"
-    rm -rf "$apiServicesPath"
-fi
-
-echo "================================ 开始生成 $moduleName ================================"
-
-java -jar "$dir/swagger-codegen-cli.jar" generate -i "$apiUrl" -l typescript-axios -o "$apiServicesPath"
-
-# 删除不必要的文件和文件夹
-rm -rf "$apiServicesPath/.swagger-codegen"
-rm -f "$apiServicesPath/.gitignore"
-rm -f "$apiServicesPath/.npmignore"
-rm -f "$apiServicesPath/.swagger-codegen-ignore"
-rm -f "$apiServicesPath/git_push.sh"
-rm -f "$apiServicesPath/package.json"
-rm -f "$apiServicesPath/README.md"
-rm -f "$apiServicesPath/tsconfig.json"
-
+#!/bin/bash
+
+dir="$(dirname "$0")"
+
+moduleName="apiServices"
+apiServicesPath="$dir/../src/api-services/"
+apiUrl="http://localhost:5005/swagger/Default/swagger.json"
+
+case "$1" in
+    approvalFlow)
+        moduleName="approvalFlow"
+        apiServicesPath="$dir/../src/api-plugins/approvalFlow/"
+        apiUrl="http://localhost:5005/swagger/ApprovalFlow/swagger.json"
+        ;;
+    dingTalk)
+        moduleName="dingTalk"
+        apiServicesPath="$dir/../src/api-plugins/dingTalk/"
+        apiUrl="http://localhost:5005/swagger/DingTalk/swagger.json"
+        ;;
+    goView)
+        moduleName="goView"
+        apiServicesPath="$dir/../src/api-plugins/goView/"
+        apiUrl="http://localhost:5005/swagger/GoView/swagger.json"
+        ;;
+esac
+
+if [ -d "$apiServicesPath" ]; then
+    echo "================================ 删除目录 $apiServicesPath ================================"
+    rm -rf "$apiServicesPath"
+fi
+
+echo "================================ 开始生成 $moduleName ================================"
+
+java -jar "$dir/swagger-codegen-cli.jar" generate -i "$apiUrl" -l typescript-axios -o "$apiServicesPath"
+
+# 删除不必要的文件和文件夹
+rm -rf "$apiServicesPath/.swagger-codegen"
+rm -f "$apiServicesPath/.gitignore"
+rm -f "$apiServicesPath/.npmignore"
+rm -f "$apiServicesPath/.swagger-codegen-ignore"
+rm -f "$apiServicesPath/git_push.sh"
+rm -f "$apiServicesPath/package.json"
+rm -f "$apiServicesPath/README.md"
+rm -f "$apiServicesPath/tsconfig.json"
+
 echo "================================ 生成结束 ================================"

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.33",
+	"version": "2.4.35",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

BIN
Web/public/app-icons/aidop-logo.png


BIN
Web/src/assets/401.png


BIN
Web/src/assets/404.png


BIN
Web/src/assets/icons/font-awesome/fontawesome-webfont.eot


BIN
Web/src/assets/icons/font-awesome/fontawesome-webfont.ttf


BIN
Web/src/assets/icons/font-awesome/fontawesome-webfont.woff


BIN
Web/src/assets/icons/font-awesome/fontawesome-webfont.woff2


BIN
Web/src/assets/icons/iconfont/font_2298093_rnp72ifj3ba.ttf


BIN
Web/src/assets/logo.png


+ 60 - 60
Web/src/utils/auto-update.ts

@@ -1,60 +1,60 @@
-/**
- *通过监听当前页面的JS的SRC来判断当前网页是否有更新
- *使用方法   main.js   import checkUpdate from "/@/utils/auto-update";
- */
-
-let lastSrcs: any[] | null; //上次js地址集合
-// const scriptReg = /(?<=<script.*src=["']).*?(?=["'])/gm; //IOS 不支持断言匹配
-const scriptReg = /<script.*?src=['"](.*?)['"]/gm;
-
-/**
- * 获取最新的js集合
- * @returns
- */
-async function extractNewScripts() {
-	const html = await fetch('/?_t=' + Date.now()).then((res) => res.text());
-	scriptReg.lastIndex = 0;
-	const result = html.match(scriptReg);
-	return result;
-}
-/**
- * 判断是否有更新
- * @returns
- */
-async function checkUpdate() {
-	const newScripts = await extractNewScripts();
-	if (!lastSrcs) {
-		lastSrcs = newScripts;
-		return false;
-	}
-	if (newScripts == null) return false;
-	let result = false;
-	if (lastSrcs.length !== newScripts.length) {
-		result = true;
-	}
-	for (let i = 0; i < lastSrcs.length; i++) {
-		if (lastSrcs[i] !== newScripts[i]) {
-			result = true;
-			break;
-		}
-	}
-	lastSrcs = newScripts;
-	return result;
-}
-
-/**
- * 定时器定时检测是否更新,有更新则执行回调函数
- * @param callbackFn
- * @param interval
- */
-function checkUpdateInterval(callbackFn: any, interval: number = 60000) {
-	setTimeout(async () => {
-		const willUpdate = await checkUpdate();
-		if (willUpdate) {
-			callbackFn();
-		}
-		checkUpdateInterval(callbackFn, interval);
-	}, interval);
-}
-
-export default checkUpdateInterval;
+/**
+ *通过监听当前页面的JS的SRC来判断当前网页是否有更新
+ *使用方法   main.js   import checkUpdate from "/@/utils/auto-update";
+ */
+
+let lastSrcs: any[] | null; //上次js地址集合
+// const scriptReg = /(?<=<script.*src=["']).*?(?=["'])/gm; //IOS 不支持断言匹配
+const scriptReg = /<script.*?src=['"](.*?)['"]/gm;
+
+/**
+ * 获取最新的js集合
+ * @returns
+ */
+async function extractNewScripts() {
+	const html = await fetch('/?_t=' + Date.now()).then((res) => res.text());
+	scriptReg.lastIndex = 0;
+	const result = html.match(scriptReg);
+	return result;
+}
+/**
+ * 判断是否有更新
+ * @returns
+ */
+async function checkUpdate() {
+	const newScripts = await extractNewScripts();
+	if (!lastSrcs) {
+		lastSrcs = newScripts;
+		return false;
+	}
+	if (newScripts == null) return false;
+	let result = false;
+	if (lastSrcs.length !== newScripts.length) {
+		result = true;
+	}
+	for (let i = 0; i < lastSrcs.length; i++) {
+		if (lastSrcs[i] !== newScripts[i]) {
+			result = true;
+			break;
+		}
+	}
+	lastSrcs = newScripts;
+	return result;
+}
+
+/**
+ * 定时器定时检测是否更新,有更新则执行回调函数
+ * @param callbackFn
+ * @param interval
+ */
+function checkUpdateInterval(callbackFn: any, interval: number = 60000) {
+	setTimeout(async () => {
+		const willUpdate = await checkUpdate();
+		if (willUpdate) {
+			callbackFn();
+		}
+		checkUpdateInterval(callbackFn, interval);
+	}, interval);
+}
+
+export default checkUpdateInterval;

BIN
ai-dop-platform/docs/db/aidopcore_远端落库表字段说明.xlsx


+ 19 - 0
ai-dop-platform/docs/db/smart_ops_data_model/00_表清单与分层角色.csv

@@ -0,0 +1,19 @@
+表名(物理),分层角色,业务含义,与指标建模关系,现状/目标,主要关联键/备注
+ado_s9_kpi_value_l1_day,中间结果层(L1日),首页/九宫格一级指标日汇总结果,前端 home-l1 读取;建模中 L1 展示名与 metric_code 与此表对齐后合并输出,现状在用,module_code + metric_code + tenant_id + factory_id + biz_date
+ado_s9_kpi_value_l2_day,中间结果层(L2日),模块详情二级指标日汇总结果,module-detail L2 来源;建模 L2 与此对齐,现状在用,同上
+ado_s9_kpi_value_l3_day,中间结果层(L3日),三级/更细下钻指标日结果,详情页 L3 区或下钻,现状在用,同上
+ado_s9_kpi_value_l1_month,中间结果层(L1月),一级指标月汇总(可选),长周期九宫格查询加速,目标可选,metric_code + 月粒度分区/日期
+ado_s9_kpi_value_l2_month,中间结果层(L2月),二级指标月汇总(可选),长周期详情查询加速,目标可选,同上
+ado_s9_kpi_def,主数据/字典(可演进),指标定义主数据,可扩展为全站 Catalog:metric_code 默认名/层级/单位;或独立新表替代,现状壳表+迁移可加列,tenant_id + metric_code(建议唯一)
+ado_s9_kpi_relation,KPI关系,父子/分解关系,可与 L2→L1 建模关系互补或迁移到布局表,现状壳表,父metric_code/子metric_code(需落地列)
+ado_s9_kpi_formula,公式与版本,KPI公式版本化,存储可执行规则或公式说明版本;供定时任务解析,现状壳表,rule_type + payload + version
+ado_s9_kpi_page_binding,页面绑定,KPI与页面/组件绑定,可选:绑定九宫格区域编码,现状壳表,page_code + metric_code
+ado_s9_calc_task,任务调度,KPI计算任务定义/队列,定时任务注册哪些 metric 参与计算,现状壳表,task_code + cron 等(需落地)
+ado_s9_calc_log,任务日志,计算执行日志,排查公式/任务失败,现状壳表,task_id + run_time
+ado_s9_dashboard_snapshot,读模型快照,看板快照历史,大屏或审计可选,现状壳表,
+ado_s9_drilldown_cache,读缓存,下钻结果缓存,性能优化可选,现状壳表,
+ado_s8_alert_record,中间结果/事件,S8 异常告警列表,s8-alerts 接口,现状在用,tenant_id + factory_id + alert_time
+ado_smart_ops_metric_catalog,主数据(建议新增),指标字典 Catalog,metric_code 全局定义:模块/层级/默认中文名/单位/是否允许上首页,目标建议,metric_code 全局或 (tenant_id+metric_code) 唯一
+ado_smart_ops_layout_item,配置层(建议新增),租户运营指标建模,用户配置的 L1/L2 行:排序、展示名、启用、父级、绑定 metric_code、公式说明文案,目标建议,tenant_id + factory_id + layout_id + sort_no
+ado_smart_ops_home_module,配置层(建议新增),九宫格按模块呈现模板,每模块一行:决定该格用卡片式(card_grid)还是列表式(table_list);与 layout_item 中该模块 L1 行合并输出给前端,目标建议,tenant_id + factory_id + module_code 唯一;layout_pattern 见 03 文档第五节
+ado_smart_ops_metric_rule,计算配置(建议新增),指标计算规则,规则类型+依赖表/表达式/SQL模板;定时任务消费写入 L1/L2 日表,目标可选进阶,metric_code + version + enabled

+ 113 - 0
ai-dop-platform/docs/db/smart_ops_data_model/01_字段明细_逻辑结构.csv

@@ -0,0 +1,113 @@
+表名,字段逻辑名,类型建议,必填,说明,主键/索引建议
+ado_s9_kpi_value_l1_day,id,bigint,是,主键,PK
+ado_s9_kpi_value_l1_day,tenant_id,bigint,是,租户,idx(tenant_id factory_id biz_date)
+ado_s9_kpi_value_l1_day,factory_id,bigint,是,工厂,同上
+ado_s9_kpi_value_l1_day,org_id,bigint,否,集团,扩展
+ado_s9_kpi_value_l1_day,company_id,bigint,否,公司,扩展
+ado_s9_kpi_value_l1_day,status,varchar(32),否,行状态,
+ado_s9_kpi_value_l1_day,biz_date,date,否,业务日期(日粒度),筛选核心
+ado_s9_kpi_value_l1_day,create_time,datetime(3),是,创建时间,
+ado_s9_kpi_value_l1_day,update_time,datetime(3),否,更新时间,
+ado_s9_kpi_value_l1_day,is_deleted,tinyint,是,软删,
+ado_s9_kpi_value_l1_day,is_active,tinyint,是,有效,
+ado_s9_kpi_value_l1_day,module_code,varchar(8),否,S1~S9 模块,与 metric_code 联合查
+ado_s9_kpi_value_l1_day,metric_code,varchar(64),否,指标编码(对齐建模),与 layout 绑定键
+ado_s9_kpi_value_l1_day,metric_value,decimal(18,4),否,指标值,
+ado_s9_kpi_value_l1_day,target_value,decimal(18,4),否,目标值,
+ado_s9_kpi_value_l1_day,status_color,varchar(16),否,绿黄红等,
+ado_s9_kpi_value_l1_day,trend_flag,varchar(8),否,趋势 up/down/flat,
+ado_s9_kpi_value_l1_day,calc_time,datetime(3),否,计算完成时间,
+ado_s9_kpi_value_l2_day,(同 L1 日表字段集),,,结构与 L1 日表同类;存 L2 粒度结果,
+ado_s9_kpi_value_l3_day,(同 L1 日表字段集),,,L3 结果,
+ado_s9_kpi_value_l1_month,id,bigint,是,主键,PK
+ado_s9_kpi_value_l1_month,tenant_id,bigint,是,租户,idx
+ado_s9_kpi_value_l1_month,factory_id,bigint,是,工厂,
+ado_s9_kpi_value_l1_month,biz_month,varchar(7),否,会计月YYYY-MM,月粒度核心
+ado_s9_kpi_value_l1_month,module_code,varchar(8),否,模块,
+ado_s9_kpi_value_l1_month,metric_code,varchar(64),否,指标编码,
+ado_s9_kpi_value_l1_month,metric_value,decimal(18,4),否,汇总值,
+ado_s9_kpi_value_l1_month,target_value,decimal(18,4),否,目标,
+ado_s9_kpi_value_l1_month,status_color,varchar(16),否,状态色,
+ado_s9_kpi_value_l1_month,trend_flag,varchar(8),否,趋势,
+ado_s9_kpi_value_l1_month,calc_time,datetime(3),否,计算时间,
+ado_s9_kpi_value_l1_month,is_deleted,tinyint,是,软删,
+ado_s9_kpi_value_l1_month,is_active,tinyint,是,有效,
+ado_s9_kpi_value_l2_month,(同 L1 月表字段集),,,L2 月汇总,
+ado_s9_kpi_def,id,bigint,是,主键,PK
+ado_s9_kpi_def,tenant_id,bigint,是,租户,uk(tenant_id metric_code) 建议
+ado_s9_kpi_def,factory_id,bigint,是,工厂,若字典按工厂可纳入 UK
+ado_s9_kpi_def,metric_code,varchar(64),否,指标编码,对齐全链路
+ado_s9_kpi_def,metric_name,varchar(128),否,默认中文名,
+ado_s9_kpi_def,module_code,varchar(8),否,S1~S9,
+ado_s9_kpi_def,metric_level,tinyint,否,1=L1 2=L2 3=L3,
+ado_s9_kpi_def,unit,varchar(16),否,单位,
+ado_s9_kpi_def,target_value,decimal(18,4),否,默认目标(可选),
+ado_s9_kpi_def,warn_threshold,decimal(18,4),否,预警,
+ado_s9_kpi_def,danger_threshold,decimal(18,4),否,告警,
+ado_s9_kpi_def,(基础审计字段),,,create_time update_time is_deleted is_active 等同其它 ado 表,
+ado_s9_kpi_formula,id,bigint,是,主键,PK
+ado_s9_kpi_formula,tenant_id,bigint,是,租户,
+ado_s9_kpi_formula,metric_code,varchar(64),否,适用指标,
+ado_s9_kpi_formula,version,int,否,公式版本,
+ado_s9_kpi_formula,rule_type,varchar(32),否,EXPR/SQL_TEMPLATE/AGG 等,
+ado_s9_kpi_formula,rule_payload,json/text,否,规则参数(白名单表名/表达式),
+ado_s9_kpi_formula,formula_text,varchar(2000),否,给人看的公式说明,
+ado_s9_kpi_formula,effective_from,date,否,生效起,
+ado_s9_kpi_formula,effective_to,date,否,生效止,
+ado_s9_kpi_formula,is_active,tinyint,是,是否启用,
+ado_s9_calc_task,id,bigint,是,主键,PK
+ado_s9_calc_task,task_code,varchar(64),否,任务编码,
+ado_s9_calc_task,task_name,varchar(128),否,名称,
+ado_s9_calc_task,cron_expr,varchar(64),否,cron,
+ado_s9_calc_task,target_layer,varchar(16),否,L1_DAY/L2_DAY,
+ado_s9_calc_task,is_enabled,tinyint,是,启用,
+ado_s8_alert_record,id,bigint,是,主键,PK
+ado_s8_alert_record,tenant_id,bigint,是,租户,
+ado_s8_alert_record,factory_id,bigint,是,工厂,
+ado_s8_alert_record,alert_time,datetime(3),否,告警时间,排序
+ado_s8_alert_record,level_code,varchar(16),否,等级,
+ado_s8_alert_record,message,varchar(500),否,文案,
+ado_s8_alert_record,module_code,varchar(8),否,模块,
+ado_s8_alert_record,metric_code,varchar(64),否,关联指标,
+ado_smart_ops_metric_catalog,id,bigint,是,主键,PK
+ado_smart_ops_metric_catalog,metric_code,varchar(64),是,全局指标编码,UK(或 tenant+code)
+ado_smart_ops_metric_catalog,tenant_id,bigint,否,空=全局默认;非空=租户覆盖,
+ado_smart_ops_metric_catalog,module_code,varchar(8),是,所属模块 S1~S7,
+ado_smart_ops_metric_catalog,metric_level,tinyint,是,1=L1 2=L2,
+ado_smart_ops_metric_catalog,default_name,varchar(128),是,默认中文名,
+ado_smart_ops_metric_catalog,unit,varchar(16),否,单位,
+ado_smart_ops_metric_catalog,allow_home_grid,tinyint,是,L1 是否可上九宫格,
+ado_smart_ops_metric_catalog,remark,varchar(500),否,备注,
+ado_smart_ops_metric_catalog,is_active,tinyint,是,启用,
+ado_smart_ops_layout_item,id,bigint,是,主键,PK
+ado_smart_ops_layout_item,tenant_id,bigint,是,租户,idx(tenant_id factory_id)
+ado_smart_ops_layout_item,factory_id,bigint,是,工厂,
+ado_smart_ops_layout_item,layout_id,varchar(32),是,布局实例(默认 default),多方案时可扩展
+ado_smart_ops_layout_item,row_id,varchar(64),是,建模行稳定 ID 如 S3-L1-D,UK 成分
+ado_smart_ops_layout_item,metric_level,tinyint,是,1=L1 2=L2,
+ado_smart_ops_layout_item,module_code,varchar(8),是,S1~S7,
+ado_smart_ops_layout_item,metric_code,varchar(64),是,绑定字典指标,FK 逻辑关联 catalog
+ado_smart_ops_layout_item,display_name,varchar(128),否,覆盖展示名(空则用字典默认),
+ado_smart_ops_layout_item,sort_no,int,是,同模块内排序,
+ado_smart_ops_layout_item,parent_row_id,varchar(64),否,L2 填父 L1 的 row_id,
+ado_smart_ops_layout_item,formula_text,varchar(2000),否,公式说明(展示用),
+ado_smart_ops_layout_item,display_widget,varchar(32),否,详情页等细粒度组件可选;九宫格模块级模板见 home_module.layout_pattern,
+ado_smart_ops_layout_item,is_enabled,tinyint,是,是否显示,
+ado_smart_ops_layout_item,update_time,datetime(3),否,最后修改,
+ado_smart_ops_home_module,id,bigint,是,主键,PK
+ado_smart_ops_home_module,tenant_id,bigint,是,租户,uk(tenant_id factory_id module_code)
+ado_smart_ops_home_module,factory_id,bigint,是,工厂,
+ado_smart_ops_home_module,module_code,varchar(8),是,九宫格业务模块 S1~S7,
+ado_smart_ops_home_module,layout_pattern,varchar(32),是,card_grid=卡片式(如S4四宫格) table_list=列表式(如S5表),
+ado_smart_ops_home_module,remark,varchar(256),否,备注,
+ado_smart_ops_home_module,update_time,datetime(3),否,最后修改,
+ado_smart_ops_metric_rule,id,bigint,是,主键,PK
+ado_smart_ops_metric_rule,tenant_id,bigint,是,租户,
+ado_smart_ops_metric_rule,factory_id,bigint,否,工厂级规则可空表示租户通用,
+ado_smart_ops_metric_rule,metric_code,varchar(64),是,目标指标(写入对应中间表行),
+ado_smart_ops_metric_rule,rule_type,varchar(32),是,规则类型 EXPR/SQL_TEMPLATE 等,
+ado_smart_ops_metric_rule,rule_payload,json,否,依赖表/表达式/SQL模板参数,
+ado_smart_ops_metric_rule,target_table,varchar(64),否,写入中间表逻辑名 L1_DAY/L2_DAY,
+ado_smart_ops_metric_rule,priority,int,否,执行顺序,
+ado_smart_ops_metric_rule,is_enabled,tinyint,是,启用,
+ado_smart_ops_metric_rule,remark,varchar(500),否,备注,

+ 47 - 0
ai-dop-platform/docs/db/smart_ops_data_model/02_表间关系说明.txt

@@ -0,0 +1,47 @@
+智慧运营看板 — 表间逻辑关系(数据流)
+
+一、主键对齐键(全链路)
+  · tenant_id + factory_id:多租户/多工厂隔离。
+  · module_code:S1~S7(九宫格与详情模块);S8/S9 若单独展示仍可用同维度。
+  · metric_code:指标技术主键;建模、中间结果、接口合并均依赖此键一致。
+  · biz_date(及未来的 biz_month):统计粒度;长周期可读月汇总表。
+
+二、配置层(目标)
+  ado_smart_ops_metric_catalog
+    → 定义「系统里有哪些指标」:编码、默认名、层级、单位、是否可上首页。
+  ado_smart_ops_layout_item
+    → 定义「某租户某工厂首页/详情要展示哪些行、顺序、展示名覆盖、L2 父级、公式说明文案」。
+    → 每行必须引用 catalog 中已存在的 metric_code(或先登记再引用)。
+  ado_smart_ops_home_module(建议新增)
+    → 九宫格内「单个模块区域」用卡片式还是列表式:layout_pattern = card_grid | table_list。
+    → 与 layout_item 中同 module_code 的 L1 行配套:数据行一致,仅前端模板不同;呈现字段(当前值/目标/期量差/绿黄红)见 03_指标呈现与期量差约定.txt 第五节。
+
+三、计算层(定时任务)
+  ado_smart_ops_metric_rule(可选进阶)
+    → 描述如何从更底层汇总表计算并写入 L1/L2 日表。
+  ado_s9_calc_task / ado_s9_calc_log
+    → 任务注册与执行日志(可与现有壳表演进)。
+  ado_s9_kpi_formula(可与 metric_rule 合并演进)
+    → 公式版本与规则载体。
+
+四、中间结果层(现状 + 可选扩展)
+  ado_s9_kpi_value_l1_day
+    → 供九宫格 / home-l1:按 module_code + metric_code + biz_date 取数。
+  ado_s9_kpi_value_l2_day / ado_s9_kpi_value_l3_day
+    → 供 module-detail 等:按 module_code + metric_code + biz_date。
+  ado_s9_kpi_value_l1_month / ado_s9_kpi_value_l2_month(目标可选)
+    → 月粒度 rollup,减轻长区间扫描日表压力。
+
+五、接口合并逻辑(逻辑非表)
+  1)读 layout_item 得当前应展示的 metric_code 列表与顺序、展示名。
+  2)读 home_module(或等价配置)得各 module_code 的 layout_pattern,供九宫格选卡片模板或列表模板。
+  3)批量读 L1 或 L2 日表(及可选月表)取 metric_value、target_value,并按 03 文档计算期量差与 achievementLevel。
+  4)左连接合并:无数据行仍输出,数值为空,供前端占位。
+
+六、与现有 ado_s9_kpi_def / relation / page_binding
+  · 若继续沿用:可将 kpi_def 强化为与 catalog 等价,避免重复表。
+  · relation:可用于指标树;与 layout_item.parent_row_id 二选一或并存迁移。
+  · page_binding:可选,将 metric 绑定到 page_code(九宫格 cell 等)。
+
+七、S8 告警
+  ado_s8_alert_record 独立链路;可通过 metric_code/module_code 与 KPI 体系弱关联。

+ 40 - 0
ai-dop-platform/docs/db/smart_ops_data_model/03_指标呈现与期量差约定.txt

@@ -0,0 +1,40 @@
+智慧运营看板 — 指标呈现规范(写入整体方案,与动态建模 / 中间表方案一致)
+
+一、呈现内容(每个指标单元统一三部分)
+  1)当前值:业务口径下的实测/汇总结果(与中间表 metric_value 对齐)。
+  2)目标值:目标或阈值(与中间表 target_value 对齐,或来自指标字典默认目标)。
+  3)期量差:当前值相对目标值的偏差量(名称统一为「期量差」;具体算法见下节)。
+     展示形式建议:带方向的符号(如 ▲/▼)+ 数值或百分比差,整体用颜色编码,与设计稿一致。
+
+二、期量差颜色语义(三档,全站统一)
+  · 绿色:达成目标(合格)。
+  · 黄色:部分达成(临界 / 需关注)。
+  · 红色:未达成(不合格)。
+
+三、期量差计算与三档判定(逻辑层约定,由后台在写中间表或接口合并时完成)
+  · 不同指标「好」的方向不同,必须在指标字典或规则中配置 direction:
+      - higher_is_better:当前值越高越好(如满足率、人效)。
+      - lower_is_better:当前值越低越好(如周期、不良率、在途天数)。
+  · 期量差数值(delta)可为「当前值 − 目标值」或业务规定的「缺口」;方向箭头与文案与 delta 符号一致。
+  · 黄档建议用「带宽」或容忍区间界定,避免非绿即红过于生硬,例如:
+      - 相对目标偏差在 [0, ε1] 内为绿;在 (ε1, ε2] 为黄;超出 ε2 为红;
+      - 或按达标比例(如满足率 ≥ 目标为绿,[目标−δ, 目标) 为黄,< 目标−δ 为红。
+  · 具体 ε、δ 可按 metric_code 在字典或阈值字段(warn_threshold / danger_threshold)中配置,与现有 ado_s9_kpi_def 扩展字段可合并设计。
+
+四、与中间表、接口的对应关系
+  · 中间表除 metric_value、target_value 外,已有 status_color、trend_flag 的,应演进为与「绿/黄/红」枚举一致(或映射),避免前端自行推断导致口径分裂。
+  · 接口返回建议显式携带,便于九宫格与 S1–S7 等复用同一套卡片组件:
+      currentValue, targetValue, gapValue(期量差), gapDirection(up/down/flat), achievementLevel(green | yellow | red)。
+  · 动态指标建模(layout_item)侧:配置展示名、顺序、绑定 metric_code;**不**在配置里写死颜色,颜色由后台据阈值与 direction 计算。
+
+五、九宫格内「卡片式」与「列表式」是否都能动态配置
+  · 可以。二者共用同一份「该模块下的 L1 指标行」数据模型(metric_code、展示名、当前值、目标值、期量差、绿/黄/红)。
+  · 差异仅在模块(或九宫格子区域)的呈现模板,建议在布局配置中增加例如:
+      layout_pattern = card_grid | table_list
+    或为该模块绑定前端已实现的少数几种模板枚举(避免无限自定义导致无法维护)。
+  · 卡片式:多行数据映射为多张 KPI 卡(如 2×2),列上展示当前值 / 目标 / 期量差(带颜色箭头)。
+  · 列表式:同一批行映射为表格列「指标 | 目标 | 实际 | 状态」,其中「实际」= 当前值,「状态」= 期量差三档的圆点或等价样式。
+  · 动态建模负责:该模块展示哪些指标、顺序、名称覆盖、绑定 metric_code;不负责写死是卡还是表,由 layout_pattern 选择模板。
+
+六、与后续 Trend 图的关系
+  · 本规范优先约束「标量 KPI 卡片 / 列表」类呈现;时序图仍用独立序列接口,但图例/最新点 Tooltip 可与同一 achievementLevel 对齐。

+ 12 - 0
ai-dop-platform/docs/db/smart_ops_data_model/README.txt

@@ -0,0 +1,12 @@
+智慧运营看板 · 指标建模与中间表 — 数据表汇总(逻辑结构)
+
+本目录文件(可用 Excel 直接打开 CSV):
+  1) 00_表清单与分层角色.csv     — 每张表的用途、现状/规划、与其它表关系
+  2) 01_字段明细_逻辑结构.csv   — 按表列出字段逻辑名、类型建议、说明、主键/索引建议
+  3) 02_表间关系说明.txt        — 文字描述数据流与关联键
+  4) 03_指标呈现与期量差约定.txt — 方案约定:当前值/目标值/期量差 + 绿黄红三档;九宫格单模块「卡片式 / 列表式」均由动态建模 + layout_pattern 驱动(详见该文件第五节)
+
+说明:
+  · 「现状」以仓库内 AidopKanban 接口与 ai-dop-platform/docs/db 迁移脚本为准;
+    ado_s9_kpi_def / relation / formula 等在 V1 DDL 中多为壳表,业务字段以实际库中 ALTER 为准。
+  · 「目标」为动态指标建模方案建议新增或加固的表/字段,实施时可与现有 ado_s9_* 合并演进。

BIN
doc/img/1.png


BIN
doc/img/10.png


BIN
doc/img/11.png


BIN
doc/img/12.png


BIN
doc/img/13.png


BIN
doc/img/14.png


BIN
doc/img/15.png


BIN
doc/img/16.png


BIN
doc/img/2.png


BIN
doc/img/3.png


BIN
doc/img/4.png


BIN
doc/img/5.png


BIN
doc/img/6.png


BIN
doc/img/7.png


BIN
doc/img/8.png


BIN
doc/img/9.png


BIN
doc/img/pay.png


BIN
doc/shop/iMES/1.png


BIN
doc/shop/iMES/2.png


BIN
doc/shop/iMES/3.png


BIN
doc/shop/iMES/4.png


+ 28 - 28
docker/README.md

@@ -1,28 +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`
-
-# 访问入口
-
-***<http://ip:9100>***
-***<https://ip:9103>***
+# 启动前准备
+
+*   安装 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`
+
+# 访问入口
+
+***<http://ip:9100>***
+***<https://ip:9103>***

+ 34 - 34
docker/app/Configuration/Cache.json

@@ -1,35 +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:"
-    }
-  }
+{
+  "$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:"
+    }
+  }
 }

+ 28 - 0
docker/app/Configuration/ElasticSearch.json

@@ -0,0 +1,28 @@
+{
+  "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
+
+  "ElasticSearch:Logging": {
+    "Enabled": false,
+    "AuthType": "None",
+    "User": "elastic",
+    "Password": "123456",
+    "ApiId": "",
+    "ApiKey": "",
+    "Base64ApiKey": "",
+    "Fingerprint": "",
+    "ServerUris": [ "http://127.0.0.1:9200" ],
+    "DefaultIndex": "adminnet"
+  },
+  "ElasticSearch:Business": {
+    "Enabled": false,
+    "AuthType": "None",
+    "User": "admin",
+    "Password": "123456",
+    "ApiId": "",
+    "ApiKey": "",
+    "Base64ApiKey": "",
+    "Fingerprint": "",
+    "ServerUris": [ "http://127.0.0.1:9200" ],
+    "DefaultIndex": "adminnet"
+  }
+}

+ 1 - 1
docker/docker-compose-builder.yml

@@ -11,7 +11,7 @@ services:
        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
+    image: mcr.microsoft.com/dotnet/sdk:10.0
     volumes:
       - ../server/:/app
     working_dir: /app

+ 15 - 14
docker/redis/redis.conf

@@ -1,15 +1,16 @@
-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
-# 开启密码验证
+bind 0.0.0.0
+protected-mode no
+port 6379
+timeout 0
+# 900s 内至少一次写操作则执行 bgsave(RDB);注释须单独一行,否则 Redis 8+ 会报 Invalid save parameters
+save 900 1
+save 300 10
+save 60 10000
+rdbcompression yes
+dbfilename dump.rdb
+# dir data
+# 开启数据持久化[aof]
+appendonly yes
+appendfsync everysec
+# 开启密码验证
 requirepass 123456

+ 15 - 15
server/Admin.NET.Core/Attribute/IgnoreTableAttribute.cs

@@ -1,16 +1,16 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 忽略表结构初始化特性(标记在实体)
-/// </summary>
-[SuppressSniffer]
-[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
-public class IgnoreTableAttribute : Attribute
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 忽略表结构初始化特性(标记在实体)
+/// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
+public class IgnoreTableAttribute : Attribute
+{
 }

+ 15 - 15
server/Admin.NET.Core/Attribute/IgnoreUpdateSeedAttribute.cs

@@ -1,16 +1,16 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 忽略更新种子特性(标记在种子类)
-/// </summary>
-[SuppressSniffer]
-[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
-public class IgnoreUpdateSeedAttribute : Attribute
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 忽略更新种子特性(标记在种子类)
+/// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
+public class IgnoreUpdateSeedAttribute : Attribute
+{
 }

+ 15 - 15
server/Admin.NET.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs

@@ -1,16 +1,16 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 忽略更新种子列特性(标记在实体属性)
-/// </summary>
-[SuppressSniffer]
-[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
-public class IgnoreUpdateSeedColumnAttribute : Attribute
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 忽略更新种子列特性(标记在实体属性)
+/// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
+public class IgnoreUpdateSeedColumnAttribute : Attribute
+{
 }

+ 38 - 38
server/Admin.NET.Core/Attribute/MaxValueAttribute.cs

@@ -1,39 +1,39 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 最大值校验
-/// </summary>
-[SuppressSniffer]
-public class MaxValueAttribute : ValidationAttribute
-{
-    private double MaxValue { get; }
-
-    /// <summary>
-    /// 最大值
-    /// </summary>
-    /// <param name="value"></param>
-    public MaxValueAttribute(double value) => this.MaxValue = value;
-
-    /// <summary>
-    /// 最大值校验
-    /// </summary>
-    /// <param name="value"></param>
-    /// <returns></returns>
-    public override bool IsValid(object value)
-    {
-        return value == null || Convert.ToDouble(value) <= this.MaxValue;
-    }
-
-    /// <summary>
-    /// 错误信息
-    /// </summary>
-    /// <param name="name"></param>
-    /// <returns></returns>
-    public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name);
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 最大值校验
+/// </summary>
+[SuppressSniffer]
+public class MaxValueAttribute : ValidationAttribute
+{
+    private double MaxValue { get; }
+
+    /// <summary>
+    /// 最大值
+    /// </summary>
+    /// <param name="value"></param>
+    public MaxValueAttribute(double value) => this.MaxValue = value;
+
+    /// <summary>
+    /// 最大值校验
+    /// </summary>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    public override bool IsValid(object value)
+    {
+        return value == null || Convert.ToDouble(value) <= this.MaxValue;
+    }
+
+    /// <summary>
+    /// 错误信息
+    /// </summary>
+    /// <param name="name"></param>
+    /// <returns></returns>
+    public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name);
 }

+ 38 - 38
server/Admin.NET.Core/Attribute/MinValueAttribute.cs

@@ -1,39 +1,39 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 最小值校验
-/// </summary>
-[SuppressSniffer]
-public class MinValueAttribute : ValidationAttribute
-{
-    private double MinValue { get; set; }
-
-    /// <summary>
-    /// 最小值
-    /// </summary>
-    /// <param name="value"></param>
-    public MinValueAttribute(double value) => this.MinValue = value;
-
-    /// <summary>
-    /// 最小值校验
-    /// </summary>
-    /// <param name="value"></param>
-    /// <returns></returns>
-    public override bool IsValid(object value)
-    {
-        return value == null || Convert.ToDouble(value) > this.MinValue;
-    }
-
-    /// <summary>
-    /// 错误信息
-    /// </summary>
-    /// <param name="name"></param>
-    /// <returns></returns>
-    public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name);
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 最小值校验
+/// </summary>
+[SuppressSniffer]
+public class MinValueAttribute : ValidationAttribute
+{
+    private double MinValue { get; set; }
+
+    /// <summary>
+    /// 最小值
+    /// </summary>
+    /// <param name="value"></param>
+    public MinValueAttribute(double value) => this.MinValue = value;
+
+    /// <summary>
+    /// 最小值校验
+    /// </summary>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    public override bool IsValid(object value)
+    {
+        return value == null || Convert.ToDouble(value) > this.MinValue;
+    }
+
+    /// <summary>
+    /// 错误信息
+    /// </summary>
+    /// <param name="name"></param>
+    /// <returns></returns>
+    public override string FormatErrorMessage(string name) => base.FormatErrorMessage(name);
 }

+ 15 - 15
server/Admin.NET.Core/Attribute/OwnerOrgAttribute.cs

@@ -1,16 +1,16 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 所属机构数据权限
-/// </summary>
-[SuppressSniffer]
-[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
-public class OwnerOrgAttribute : Attribute
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 所属机构数据权限
+/// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
+public class OwnerOrgAttribute : Attribute
+{
 }

+ 15 - 15
server/Admin.NET.Core/Attribute/OwnerUserAttribute.cs

@@ -1,16 +1,16 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 所属用户数据权限
-/// </summary>
-[SuppressSniffer]
-[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
-public class OwnerUserAttribute : Attribute
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 所属用户数据权限
+/// </summary>
+[SuppressSniffer]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
+public class OwnerUserAttribute : Attribute
+{
 }

+ 42 - 42
server/Admin.NET.Core/Cache/CacheSetup.cs

@@ -1,43 +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
-{
-    /// <summary>
-    /// 缓存注册(新生命Redis组件)
-    /// </summary>
-    /// <param name="services"></param>
-    public static void AddCache(this IServiceCollection services)
-    {
-        var cacheOptions = App.GetConfig<CacheOptions>("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<bool>("Cache:Redis:AutoDetect", true)
-            };
-            // 最大消息大小
-            if (cacheOptions.Redis.MaxMessageSize > 0)
-                redis.MaxMessageSize = cacheOptions.Redis.MaxMessageSize;
-
-            // 注入 Redis 缓存提供者
-            services.AddSingleton<ICacheProvider>(u => new RedisCacheProvider(u) { Cache = redis });
-        }
-
-        // 内存缓存兜底。在没有配置Redis时,使用内存缓存,逻辑代码无需修改
-        services.TryAddSingleton<ICacheProvider, CacheProvider>();
-    }
+// 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
+{
+    /// <summary>
+    /// 缓存注册(新生命Redis组件)
+    /// </summary>
+    /// <param name="services"></param>
+    public static void AddCache(this IServiceCollection services)
+    {
+        var cacheOptions = App.GetConfig<CacheOptions>("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<bool>("Cache:Redis:AutoDetect", true)
+            };
+            // 最大消息大小
+            if (cacheOptions.Redis.MaxMessageSize > 0)
+                redis.MaxMessageSize = cacheOptions.Redis.MaxMessageSize;
+
+            // 注入 Redis 缓存提供者
+            services.AddSingleton<ICacheProvider>(u => new RedisCacheProvider(u) { Cache = redis });
+        }
+
+        // 内存缓存兜底。在没有配置Redis时,使用内存缓存,逻辑代码无需修改
+        services.TryAddSingleton<ICacheProvider, CacheProvider>();
+    }
 }

+ 206 - 206
server/Admin.NET.Core/Entity/SysCodeGenConfig.cs

@@ -1,207 +1,207 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 代码生成字段配置表
-/// </summary>
-[SugarTable(null, "代码生成字段配置表")]
-[SysTable]
-public partial class SysCodeGenConfig : EntityBase
-{
-    /// <summary>
-    /// 代码生成主表Id
-    /// </summary>
-    [SugarColumn(ColumnDescription = "主表Id")]
-    public long CodeGenId { get; set; }
-
-    /// <summary>
-    /// 数据库字段名
-    /// </summary>
-    [SugarColumn(ColumnDescription = "字段名称", Length = 128)]
-    [Required, MaxLength(128)]
-    public virtual string ColumnName { get; set; }
-
-    /// <summary>
-    /// 主键
-    /// </summary>
-    [SugarColumn(ColumnDescription = "主键", Length = 8)]
-    [MaxLength(8)]
-    public string? ColumnKey { get; set; }
-
-    /// <summary>
-    /// 实体属性名
-    /// </summary>
-    [SugarColumn(ColumnDescription = "属性名称", Length = 128)]
-    [Required, MaxLength(128)]
-    public virtual string PropertyName { get; set; }
-
-    /// <summary>
-    /// 字段数据长度
-    /// </summary>
-    [SugarColumn(ColumnDescription = "字段数据长度", DefaultValue = "0")]
-    public int ColumnLength { get; set; }
-
-    /// <summary>
-    /// 字段描述
-    /// </summary>
-    [SugarColumn(ColumnDescription = "字段描述", Length = 128)]
-    [MaxLength(128)]
-    public string? ColumnComment { get; set; }
-
-    /// <summary>
-    /// 数据库中类型(物理类型)
-    /// </summary>
-    [SugarColumn(ColumnDescription = "数据库中类型", Length = 64)]
-    [MaxLength(64)]
-    public string? DataType { get; set; }
-
-    /// <summary>
-    /// .NET数据类型
-    /// </summary>
-    [SugarColumn(ColumnDescription = "NET数据类型", Length = 64)]
-    [MaxLength(64)]
-    public string? NetType { get; set; }
-
-    /// <summary>
-    /// 字段数据默认值
-    /// </summary>
-    [SugarColumn(ColumnDescription = "默认值")]
-    public string? DefaultValue { get; set; }
-
-    /// <summary>
-    /// 作用类型(字典)
-    /// </summary>
-    [SugarColumn(ColumnDescription = "作用类型", Length = 64)]
-    [MaxLength(64)]
-    public string? EffectType { get; set; }
-
-    /// <summary>
-    /// 外键库标识
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键库标识", Length = 20)]
-    [MaxLength(20)]
-    public string? FkConfigId { get; set; }
-
-    /// <summary>
-    /// 外键实体名称
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键实体名称", Length = 64)]
-    [MaxLength(64)]
-    public string? FkEntityName { get; set; }
-
-    /// <summary>
-    /// 外键表名称
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键表名称", Length = 128)]
-    [MaxLength(128)]
-    public string? FkTableName { get; set; }
-
-    /// <summary>
-    /// 外键显示字段
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键显示字段", Length = 64)]
-    [MaxLength(64)]
-    public string? FkDisplayColumns { get; set; }
-
-    /// <summary>
-    /// 外键链接字段
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键链接字段", Length = 64)]
-    [MaxLength(64)]
-    public string? FkLinkColumnName { get; set; }
-
-    /// <summary>
-    /// 外键显示字段.NET类型
-    /// </summary>
-    [SugarColumn(ColumnDescription = "外键显示字段.NET类型", Length = 64)]
-    [MaxLength(64)]
-    public string? FkColumnNetType { get; set; }
-
-    /// <summary>
-    /// 父级字段
-    /// </summary>
-    [SugarColumn(ColumnDescription = "父级字段", Length = 128)]
-    [MaxLength(128)]
-    public string? PidColumn { get; set; }
-
-    /// <summary>
-    /// 字典编码
-    /// </summary>
-    [SugarColumn(ColumnDescription = "字典编码", Length = 64)]
-    [MaxLength(64)]
-    public string? DictTypeCode { get; set; }
-
-    /// <summary>
-    /// 查询方式
-    /// </summary>
-    [SugarColumn(ColumnDescription = "查询方式", Length = 16)]
-    [MaxLength(16)]
-    public string? QueryType { get; set; }
-
-    /// <summary>
-    /// 是否是查询条件
-    /// </summary>
-    [SugarColumn(ColumnDescription = "是否是查询条件", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherQuery { get; set; }
-
-    /// <summary>
-    /// 列表是否缩进(字典)
-    /// </summary>
-    [SugarColumn(ColumnDescription = "列表是否缩进", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherRetract { get; set; }
-
-    /// <summary>
-    /// 是否必填(字典)
-    /// </summary>
-    [SugarColumn(ColumnDescription = "是否必填", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherRequired { get; set; }
-
-    /// <summary>
-    /// 是否可排序(字典)
-    /// </summary>
-    [SugarColumn(ColumnDescription = "是否可排序", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherSortable { get; set; }
-
-    /// <summary>
-    /// 列表显示
-    /// </summary>
-    [SugarColumn(ColumnDescription = "列表显示", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherTable { get; set; }
-
-    /// <summary>
-    /// 增改
-    /// </summary>
-    [SugarColumn(ColumnDescription = "增改", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherAddUpdate { get; set; }
-
-    /// <summary>
-    /// 导入
-    /// </summary>
-    [SugarColumn(ColumnDescription = "导入", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherImport { get; set; }
-
-    /// <summary>
-    /// 是否通用字段
-    /// </summary>
-    [SugarColumn(ColumnDescription = "是否通用字段", Length = 8)]
-    [MaxLength(8)]
-    public string? WhetherCommon { get; set; }
-
-    /// <summary>
-    /// 排序
-    /// </summary>
-    [SugarColumn(ColumnDescription = "排序")]
-    public int OrderNo { get; set; } = 100;
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 代码生成字段配置表
+/// </summary>
+[SugarTable(null, "代码生成字段配置表")]
+[SysTable]
+public partial class SysCodeGenConfig : EntityBase
+{
+    /// <summary>
+    /// 代码生成主表Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "主表Id")]
+    public long CodeGenId { get; set; }
+
+    /// <summary>
+    /// 数据库字段名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字段名称", Length = 128)]
+    [Required, MaxLength(128)]
+    public virtual string ColumnName { get; set; }
+
+    /// <summary>
+    /// 主键
+    /// </summary>
+    [SugarColumn(ColumnDescription = "主键", Length = 8)]
+    [MaxLength(8)]
+    public string? ColumnKey { get; set; }
+
+    /// <summary>
+    /// 实体属性名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "属性名称", Length = 128)]
+    [Required, MaxLength(128)]
+    public virtual string PropertyName { get; set; }
+
+    /// <summary>
+    /// 字段数据长度
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字段数据长度", DefaultValue = "0")]
+    public int ColumnLength { get; set; }
+
+    /// <summary>
+    /// 字段描述
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字段描述", Length = 128)]
+    [MaxLength(128)]
+    public string? ColumnComment { get; set; }
+
+    /// <summary>
+    /// 数据库中类型(物理类型)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "数据库中类型", Length = 64)]
+    [MaxLength(64)]
+    public string? DataType { get; set; }
+
+    /// <summary>
+    /// .NET数据类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "NET数据类型", Length = 64)]
+    [MaxLength(64)]
+    public string? NetType { get; set; }
+
+    /// <summary>
+    /// 字段数据默认值
+    /// </summary>
+    [SugarColumn(ColumnDescription = "默认值")]
+    public string? DefaultValue { get; set; }
+
+    /// <summary>
+    /// 作用类型(字典)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "作用类型", Length = 64)]
+    [MaxLength(64)]
+    public string? EffectType { get; set; }
+
+    /// <summary>
+    /// 外键库标识
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键库标识", Length = 20)]
+    [MaxLength(20)]
+    public string? FkConfigId { get; set; }
+
+    /// <summary>
+    /// 外键实体名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键实体名称", Length = 64)]
+    [MaxLength(64)]
+    public string? FkEntityName { get; set; }
+
+    /// <summary>
+    /// 外键表名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键表名称", Length = 128)]
+    [MaxLength(128)]
+    public string? FkTableName { get; set; }
+
+    /// <summary>
+    /// 外键显示字段
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键显示字段", Length = 64)]
+    [MaxLength(64)]
+    public string? FkDisplayColumns { get; set; }
+
+    /// <summary>
+    /// 外键链接字段
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键链接字段", Length = 64)]
+    [MaxLength(64)]
+    public string? FkLinkColumnName { get; set; }
+
+    /// <summary>
+    /// 外键显示字段.NET类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "外键显示字段.NET类型", Length = 64)]
+    [MaxLength(64)]
+    public string? FkColumnNetType { get; set; }
+
+    /// <summary>
+    /// 父级字段
+    /// </summary>
+    [SugarColumn(ColumnDescription = "父级字段", Length = 128)]
+    [MaxLength(128)]
+    public string? PidColumn { get; set; }
+
+    /// <summary>
+    /// 字典编码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字典编码", Length = 64)]
+    [MaxLength(64)]
+    public string? DictTypeCode { get; set; }
+
+    /// <summary>
+    /// 查询方式
+    /// </summary>
+    [SugarColumn(ColumnDescription = "查询方式", Length = 16)]
+    [MaxLength(16)]
+    public string? QueryType { get; set; }
+
+    /// <summary>
+    /// 是否是查询条件
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否是查询条件", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherQuery { get; set; }
+
+    /// <summary>
+    /// 列表是否缩进(字典)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "列表是否缩进", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherRetract { get; set; }
+
+    /// <summary>
+    /// 是否必填(字典)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否必填", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherRequired { get; set; }
+
+    /// <summary>
+    /// 是否可排序(字典)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否可排序", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherSortable { get; set; }
+
+    /// <summary>
+    /// 列表显示
+    /// </summary>
+    [SugarColumn(ColumnDescription = "列表显示", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherTable { get; set; }
+
+    /// <summary>
+    /// 增改
+    /// </summary>
+    [SugarColumn(ColumnDescription = "增改", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherAddUpdate { get; set; }
+
+    /// <summary>
+    /// 导入
+    /// </summary>
+    [SugarColumn(ColumnDescription = "导入", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherImport { get; set; }
+
+    /// <summary>
+    /// 是否通用字段
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否通用字段", Length = 8)]
+    [MaxLength(8)]
+    public string? WhetherCommon { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序")]
+    public int OrderNo { get; set; } = 100;
 }

+ 190 - 190
server/Admin.NET.Core/Entity/SysWechatPay.cs

@@ -1,191 +1,191 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 系统微信支付表
-/// </summary>
-[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
-{
-    /// <summary>
-    /// 微信商户号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "微信商户号")]
-    [Required]
-    public virtual string MerchantId { get; set; }
-
-    /// <summary>
-    /// 服务商AppId
-    /// </summary>
-    [SugarColumn(ColumnDescription = "服务商AppId")]
-    [Required]
-    public virtual string AppId { get; set; }
-
-    /// <summary>
-    /// 商户订单号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "商户订单号")]
-    [Required]
-    public virtual string OutTradeNumber { get; set; }
-
-    /// <summary>
-    /// 支付订单号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "支付订单号")]
-    [Required]
-    public virtual string TransactionId { get; set; }
-
-    /// <summary>
-    /// 交易类型
-    /// </summary>
-    [SugarColumn(ColumnDescription = "交易类型")]
-    public string? TradeType { get; set; }
-
-    /// <summary>
-    /// 交易状态
-    /// </summary>
-    [SugarColumn(ColumnDescription = "交易状态")]
-    public string? TradeState { get; set; }
-
-    /// <summary>
-    /// 交易状态描述
-    /// </summary>
-    [SugarColumn(ColumnDescription = "交易状态描述")]
-    public string? TradeStateDescription { get; set; }
-
-    /// <summary>
-    /// 付款银行类型
-    /// </summary>
-    [SugarColumn(ColumnDescription = "付款银行类型")]
-    public string? BankType { get; set; }
-
-    /// <summary>
-    /// 订单总金额
-    /// </summary>
-    [SugarColumn(ColumnDescription = "订单总金额")]
-    public int Total { get; set; }
-
-    /// <summary>
-    /// 用户支付金额
-    /// </summary>
-    [SugarColumn(ColumnDescription = "用户支付金额")]
-    public int? PayerTotal { get; set; }
-
-    /// <summary>
-    /// 支付完成时间
-    /// </summary>
-    [SugarColumn(ColumnDescription = "支付完成时间")]
-    public DateTime? SuccessTime { get; set; }
-
-    /// <summary>
-    /// 交易结束时间
-    /// </summary>
-    [SugarColumn(ColumnDescription = "交易结束时间")]
-    public DateTime? ExpireTime { get; set; }
-
-    /// <summary>
-    /// 商品描述
-    /// </summary>
-    [SugarColumn(ColumnDescription = "商品描述")]
-    public string? Description { get; set; }
-
-    /// <summary>
-    /// 场景信息
-    /// </summary>
-    [SugarColumn(ColumnDescription = "场景信息")]
-    public string? Scene { get; set; }
-
-    /// <summary>
-    /// 附加数据
-    /// </summary>
-    [SugarColumn(ColumnDescription = "附加数据")]
-    public string? Attachment { get; set; }
-
-    /// <summary>
-    /// 优惠标记
-    /// </summary>
-    [SugarColumn(ColumnDescription = "优惠标记")]
-    public string? GoodsTag { get; set; }
-
-    /// <summary>
-    /// 结算信息
-    /// </summary>
-    [SugarColumn(ColumnDescription = "结算信息")]
-    public string? Settlement { get; set; }
-
-    /// <summary>
-    /// 回调通知地址
-    /// </summary>
-    [SugarColumn(ColumnDescription = "回调通知地址")]
-    public string? NotifyUrl { get; set; }
-
-    /// <summary>
-    /// 备注
-    /// </summary>
-    [SugarColumn(ColumnDescription = "备注")]
-    public string? Remark { get; set; }
-
-    /// <summary>
-    /// 微信OpenId标识
-    /// </summary>
-    [SugarColumn(ColumnDescription = "微信OpenId标识")]
-    public string? OpenId { get; set; }
-
-    /// <summary>
-    /// 业务标签,用来区分做什么业务
-    /// </summary>
-    /// <remarks>
-    /// Tags标识用来区分这个支付记录对应什么业务从而确定相关联的表名,
-    /// 再结合BusinessId保存了对应的业务数据的ID,就可以确定这个支付
-    /// 记录与哪一条业务数据相关联
-    /// </remarks>
-    [SugarColumn(ColumnDescription = "业务标签,用来区分做什么业务", Length = 64)]
-    public string? Tags { get; set; }
-
-    /// <summary>
-    /// 对应业务的主键
-    /// </summary>
-    [SugarColumn(ColumnDescription = "对应业务的主键")]
-    public long BusinessId { get; set; }
-
-    /// <summary>
-    /// 付款二维码内容
-    /// </summary>
-    [SugarColumn(ColumnDescription = "付款二维码内容")]
-    public string? QrcodeContent { get; set; }
-
-    /// <summary>
-    /// 关联微信用户
-    /// </summary>
-    [Newtonsoft.Json.JsonIgnore]
-    [System.Text.Json.Serialization.JsonIgnore]
-    [Navigate(NavigateType.OneToOne, nameof(OpenId))]
-    public SysWechatUser SysWechatUser { get; set; }
-
-    /// <summary>
-    /// 子商户号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "子商户号")]
-    public string? SubMerchantId { get; set; }
-
-    /// <summary>
-    /// 子商户AppId
-    /// </summary>
-    [SugarColumn(ColumnDescription = "回调通知地址")]
-    public string? SubAppId { get; set; }
-
-    /// <summary>
-    /// 子商户唯一标识
-    /// </summary>
-    [SugarColumn(ColumnDescription = "子商户唯一标识")]
-    public string? SubOpenId { get; set; }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统微信支付表
+/// </summary>
+[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
+{
+    /// <summary>
+    /// 微信商户号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "微信商户号")]
+    [Required]
+    public virtual string MerchantId { get; set; }
+
+    /// <summary>
+    /// 服务商AppId
+    /// </summary>
+    [SugarColumn(ColumnDescription = "服务商AppId")]
+    [Required]
+    public virtual string AppId { get; set; }
+
+    /// <summary>
+    /// 商户订单号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "商户订单号")]
+    [Required]
+    public virtual string OutTradeNumber { get; set; }
+
+    /// <summary>
+    /// 支付订单号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "支付订单号")]
+    [Required]
+    public virtual string TransactionId { get; set; }
+
+    /// <summary>
+    /// 交易类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "交易类型")]
+    public string? TradeType { get; set; }
+
+    /// <summary>
+    /// 交易状态
+    /// </summary>
+    [SugarColumn(ColumnDescription = "交易状态")]
+    public string? TradeState { get; set; }
+
+    /// <summary>
+    /// 交易状态描述
+    /// </summary>
+    [SugarColumn(ColumnDescription = "交易状态描述")]
+    public string? TradeStateDescription { get; set; }
+
+    /// <summary>
+    /// 付款银行类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "付款银行类型")]
+    public string? BankType { get; set; }
+
+    /// <summary>
+    /// 订单总金额
+    /// </summary>
+    [SugarColumn(ColumnDescription = "订单总金额")]
+    public int Total { get; set; }
+
+    /// <summary>
+    /// 用户支付金额
+    /// </summary>
+    [SugarColumn(ColumnDescription = "用户支付金额")]
+    public int? PayerTotal { get; set; }
+
+    /// <summary>
+    /// 支付完成时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "支付完成时间")]
+    public DateTime? SuccessTime { get; set; }
+
+    /// <summary>
+    /// 交易结束时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "交易结束时间")]
+    public DateTime? ExpireTime { get; set; }
+
+    /// <summary>
+    /// 商品描述
+    /// </summary>
+    [SugarColumn(ColumnDescription = "商品描述")]
+    public string? Description { get; set; }
+
+    /// <summary>
+    /// 场景信息
+    /// </summary>
+    [SugarColumn(ColumnDescription = "场景信息")]
+    public string? Scene { get; set; }
+
+    /// <summary>
+    /// 附加数据
+    /// </summary>
+    [SugarColumn(ColumnDescription = "附加数据")]
+    public string? Attachment { get; set; }
+
+    /// <summary>
+    /// 优惠标记
+    /// </summary>
+    [SugarColumn(ColumnDescription = "优惠标记")]
+    public string? GoodsTag { get; set; }
+
+    /// <summary>
+    /// 结算信息
+    /// </summary>
+    [SugarColumn(ColumnDescription = "结算信息")]
+    public string? Settlement { get; set; }
+
+    /// <summary>
+    /// 回调通知地址
+    /// </summary>
+    [SugarColumn(ColumnDescription = "回调通知地址")]
+    public string? NotifyUrl { get; set; }
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注")]
+    public string? Remark { get; set; }
+
+    /// <summary>
+    /// 微信OpenId标识
+    /// </summary>
+    [SugarColumn(ColumnDescription = "微信OpenId标识")]
+    public string? OpenId { get; set; }
+
+    /// <summary>
+    /// 业务标签,用来区分做什么业务
+    /// </summary>
+    /// <remarks>
+    /// Tags标识用来区分这个支付记录对应什么业务从而确定相关联的表名,
+    /// 再结合BusinessId保存了对应的业务数据的ID,就可以确定这个支付
+    /// 记录与哪一条业务数据相关联
+    /// </remarks>
+    [SugarColumn(ColumnDescription = "业务标签,用来区分做什么业务", Length = 64)]
+    public string? Tags { get; set; }
+
+    /// <summary>
+    /// 对应业务的主键
+    /// </summary>
+    [SugarColumn(ColumnDescription = "对应业务的主键")]
+    public long BusinessId { get; set; }
+
+    /// <summary>
+    /// 付款二维码内容
+    /// </summary>
+    [SugarColumn(ColumnDescription = "付款二维码内容")]
+    public string? QrcodeContent { get; set; }
+
+    /// <summary>
+    /// 关联微信用户
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    [Navigate(NavigateType.OneToOne, nameof(OpenId))]
+    public SysWechatUser SysWechatUser { get; set; }
+
+    /// <summary>
+    /// 子商户号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "子商户号")]
+    public string? SubMerchantId { get; set; }
+
+    /// <summary>
+    /// 子商户AppId
+    /// </summary>
+    [SugarColumn(ColumnDescription = "回调通知地址")]
+    public string? SubAppId { get; set; }
+
+    /// <summary>
+    /// 子商户唯一标识
+    /// </summary>
+    [SugarColumn(ColumnDescription = "子商户唯一标识")]
+    public string? SubOpenId { get; set; }
 }

+ 96 - 96
server/Admin.NET.Core/Entity/SysWechatRefund.cs

@@ -1,97 +1,97 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 系统微信支付退款表
-/// </summary>
-[SugarTable(null, "系统微信支付退款表")]
-[SysTable]
-[SugarIndex("index_{table}_W", nameof(WechatPayId), OrderByType.Asc)]
-public partial class SysWechatRefund : EntityBase
-{
-    /// <summary>
-    /// 定单主键
-    /// </summary>
-    [SugarColumn(ColumnDescription = "定单主键")]
-    public long WechatPayId { get; set; }
-
-    /// <summary>
-    /// 商户退款号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "商户退款号")]
-    [Required]
-    public virtual string OutRefundNumber { get; set; }
-
-    /// <summary>
-    /// 退款订单号
-    /// </summary>
-    [SugarColumn(ColumnDescription = "退款订单号")]
-    [Required]
-    public virtual string TransactionId { get; set; }
-
-    /// <summary>
-    /// 退款原因
-    /// </summary>
-    [SugarColumn(ColumnDescription = "退款原因")]
-    public string? Reason { get; set; }
-
-    /// <summary>
-    /// 退款渠道
-    /// </summary>
-    [SugarColumn(ColumnDescription = "退款渠道")]
-    public string? Channel { get; set; }
-
-    /// <summary>
-    /// 退款入账账户
-    /// </summary>
-    /// <remarks>
-    /// 取当前退款单的退款入账方,有以下几种情况:
-    /// 1)退回银行卡:{银行名称}{卡类型}{ 卡尾号}
-    /// 2)退回支付用户零钱: 支付用户零钱
-    /// 3)退还商户: 商户基本账户商户结算银行账户
-    /// 4)退回支付用户零钱通: 支付用户零钱通
-    /// </remarks>
-    [SugarColumn(ColumnDescription = "退款入账账户")]
-    public string? UserReceivedAccount { get; set; }
-
-    /// <summary>
-    /// 退款状态
-    /// </summary>
-    [SugarColumn(ColumnDescription = "退款状态")]
-    public string? TradeState { get; set; }
-
-    /// <summary>
-    /// 交易状态描述
-    /// </summary>
-    [SugarColumn(ColumnDescription = "交易状态描述")]
-    public string? TradeStateDescription { get; set; }
-
-    /// <summary>
-    /// 订单总金额
-    /// </summary>
-    [SugarColumn(ColumnDescription = "退款金额")]
-    public int Refund { get; set; }
-
-    /// <summary>
-    /// 支完成时间
-    /// </summary>
-    [SugarColumn(ColumnDescription = "完成时间")]
-    public DateTime? SuccessTime { get; set; }
-
-    /// <summary>
-    /// 回调通知地址
-    /// </summary>
-    [SugarColumn(ColumnDescription = "回调通知地址")]
-    public string? NotifyUrl { get; set; }
-
-    /// <summary>
-    /// 备注
-    /// </summary>
-    [SugarColumn(ColumnDescription = "备注")]
-    public string? Remark { get; set; }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统微信支付退款表
+/// </summary>
+[SugarTable(null, "系统微信支付退款表")]
+[SysTable]
+[SugarIndex("index_{table}_W", nameof(WechatPayId), OrderByType.Asc)]
+public partial class SysWechatRefund : EntityBase
+{
+    /// <summary>
+    /// 定单主键
+    /// </summary>
+    [SugarColumn(ColumnDescription = "定单主键")]
+    public long WechatPayId { get; set; }
+
+    /// <summary>
+    /// 商户退款号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "商户退款号")]
+    [Required]
+    public virtual string OutRefundNumber { get; set; }
+
+    /// <summary>
+    /// 退款订单号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "退款订单号")]
+    [Required]
+    public virtual string TransactionId { get; set; }
+
+    /// <summary>
+    /// 退款原因
+    /// </summary>
+    [SugarColumn(ColumnDescription = "退款原因")]
+    public string? Reason { get; set; }
+
+    /// <summary>
+    /// 退款渠道
+    /// </summary>
+    [SugarColumn(ColumnDescription = "退款渠道")]
+    public string? Channel { get; set; }
+
+    /// <summary>
+    /// 退款入账账户
+    /// </summary>
+    /// <remarks>
+    /// 取当前退款单的退款入账方,有以下几种情况:
+    /// 1)退回银行卡:{银行名称}{卡类型}{ 卡尾号}
+    /// 2)退回支付用户零钱: 支付用户零钱
+    /// 3)退还商户: 商户基本账户商户结算银行账户
+    /// 4)退回支付用户零钱通: 支付用户零钱通
+    /// </remarks>
+    [SugarColumn(ColumnDescription = "退款入账账户")]
+    public string? UserReceivedAccount { get; set; }
+
+    /// <summary>
+    /// 退款状态
+    /// </summary>
+    [SugarColumn(ColumnDescription = "退款状态")]
+    public string? TradeState { get; set; }
+
+    /// <summary>
+    /// 交易状态描述
+    /// </summary>
+    [SugarColumn(ColumnDescription = "交易状态描述")]
+    public string? TradeStateDescription { get; set; }
+
+    /// <summary>
+    /// 订单总金额
+    /// </summary>
+    [SugarColumn(ColumnDescription = "退款金额")]
+    public int Refund { get; set; }
+
+    /// <summary>
+    /// 支完成时间
+    /// </summary>
+    [SugarColumn(ColumnDescription = "完成时间")]
+    public DateTime? SuccessTime { get; set; }
+
+    /// <summary>
+    /// 回调通知地址
+    /// </summary>
+    [SugarColumn(ColumnDescription = "回调通知地址")]
+    public string? NotifyUrl { get; set; }
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注")]
+    public string? Remark { get; set; }
 }

+ 31 - 31
server/Admin.NET.Core/Enum/FilterLogicEnum.cs

@@ -1,32 +1,32 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 过滤条件
-/// </summary>
-[Description("过滤条件")]
-public enum FilterLogicEnum
-{
-    /// <summary>
-    /// 并且
-    /// </summary>
-    [Description("并且")]
-    And,
-
-    /// <summary>
-    /// 或者
-    /// </summary>
-    [Description("或者")]
-    Or,
-
-    /// <summary>
-    /// 异或
-    /// </summary>
-    [Description("异或")]
-    Xor
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 过滤条件
+/// </summary>
+[Description("过滤条件")]
+public enum FilterLogicEnum
+{
+    /// <summary>
+    /// 并且
+    /// </summary>
+    [Description("并且")]
+    And,
+
+    /// <summary>
+    /// 或者
+    /// </summary>
+    [Description("或者")]
+    Or,
+
+    /// <summary>
+    /// 异或
+    /// </summary>
+    [Description("异或")]
+    Xor
 }

+ 67 - 67
server/Admin.NET.Core/Enum/FilterOperatorEnum.cs

@@ -1,68 +1,68 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 过滤逻辑运算符
-/// </summary>
-[Description("过滤逻辑运算符")]
-public enum FilterOperatorEnum
-{
-    /// <summary>
-    /// 等于(=)
-    /// </summary>
-    [Description("等于")]
-    EQ,
-
-    /// <summary>
-    /// 不等于(!=)
-    /// </summary>
-    [Description("不等于")]
-    NEQ,
-
-    /// <summary>
-    /// 小于<![CDATA[ < ]]>
-    /// </summary>
-    [Description("小于")]
-    LT,
-
-    /// <summary>
-    /// 小于等于<![CDATA[ <= ]]>
-    /// </summary>
-    [Description("小于等于")]
-    LTE,
-
-    /// <summary>
-    /// 大于(>)
-    /// </summary>
-    [Description("大于")]
-    GT,
-
-    /// <summary>
-    /// 大于等于(>=)
-    /// </summary>
-    [Description("大于等于")]
-    GTE,
-
-    /// <summary>
-    /// 开始包含
-    /// </summary>
-    [Description("开始包含")]
-    StartsWith,
-
-    /// <summary>
-    /// 末尾包含
-    /// </summary>
-    [Description("末尾包含")]
-    EndsWith,
-
-    /// <summary>
-    /// 包含
-    /// </summary>
-    [Description("包含")]
-    Contains
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 过滤逻辑运算符
+/// </summary>
+[Description("过滤逻辑运算符")]
+public enum FilterOperatorEnum
+{
+    /// <summary>
+    /// 等于(=)
+    /// </summary>
+    [Description("等于")]
+    EQ,
+
+    /// <summary>
+    /// 不等于(!=)
+    /// </summary>
+    [Description("不等于")]
+    NEQ,
+
+    /// <summary>
+    /// 小于<![CDATA[ < ]]>
+    /// </summary>
+    [Description("小于")]
+    LT,
+
+    /// <summary>
+    /// 小于等于<![CDATA[ <= ]]>
+    /// </summary>
+    [Description("小于等于")]
+    LTE,
+
+    /// <summary>
+    /// 大于(>)
+    /// </summary>
+    [Description("大于")]
+    GT,
+
+    /// <summary>
+    /// 大于等于(>=)
+    /// </summary>
+    [Description("大于等于")]
+    GTE,
+
+    /// <summary>
+    /// 开始包含
+    /// </summary>
+    [Description("开始包含")]
+    StartsWith,
+
+    /// <summary>
+    /// 末尾包含
+    /// </summary>
+    [Description("末尾包含")]
+    EndsWith,
+
+    /// <summary>
+    /// 包含
+    /// </summary>
+    [Description("包含")]
+    Contains
 }

+ 25 - 25
server/Admin.NET.Core/Enum/PrintTypeEnum.cs

@@ -1,26 +1,26 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 打印类型枚举
-/// </summary>
-[Description("打印类型枚举")]
-public enum PrintTypeEnum
-{
-    /// <summary>
-    /// 浏览器打印
-    /// </summary>
-    [Description("浏览器打印")]
-    Browser = 1,
-
-    /// <summary>
-    /// 浏览器打印
-    /// </summary>
-    [Description("客户端打印")]
-    Client = 2,
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 打印类型枚举
+/// </summary>
+[Description("打印类型枚举")]
+public enum PrintTypeEnum
+{
+    /// <summary>
+    /// 浏览器打印
+    /// </summary>
+    [Description("浏览器打印")]
+    Browser = 1,
+
+    /// <summary>
+    /// 浏览器打印
+    /// </summary>
+    [Description("客户端打印")]
+    Client = 2,
 }

+ 27 - 27
server/Admin.NET.Core/EventBus/EventHandlerMonitor.cs

@@ -1,28 +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;
-    }
+// 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;
+    }
 }

+ 164 - 164
server/Admin.NET.Core/EventBus/RedisEventSourceStorer.cs

@@ -1,165 +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;
-
-/// <summary>
-/// Redis自定义事件源存储器
-/// </summary>
-/// <remarks>
-/// 在集群部署时,一般每一个消息只由一个服务节点消费一次。
-/// 有些特殊情情要通知到服务器群中的每一个节点(比如需要强制加载某些配置、重点服务等),
-/// 在这种情况下就要以“broadcast:”开头来定义EventId,
-/// 本系统会把“broadcast:”开头的事件视为“广播消息”保证集群中的每一个服务节点都能消费得到这个消息
-/// </remarks>
-public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable
-{
-    /// <summary>
-    /// 消费者
-    /// </summary>
-    private readonly EventConsumer<ChannelEventSource> _eventConsumer;
-
-    /// <summary>
-    /// 内存通道事件源存储器
-    /// </summary>
-    private readonly Channel<IEventSource> _channel;
-
-    private IProducerConsumer<ChannelEventSource> _queueSingle;
-
-    private RedisStream<string> _queueBroadcast;
-
-    private ILogger<RedisEventSourceStorer> _logger;
-
-    /// <summary>
-    /// 构造函数
-    /// </summary>
-    /// <param name="cacheProvider">Redis 连接对象</param>
-    /// <param name="routeKey">路由键</param>
-    /// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param>
-    public RedisEventSourceStorer(ICacheProvider cacheProvider, string routeKey, int capacity)
-    {
-        _logger = App.GetRequiredService<ILogger<RedisEventSourceStorer>>();
-
-        // 配置通道,设置超出默认容量后进入等待
-        var boundedChannelOptions = new BoundedChannelOptions(capacity)
-        {
-            FullMode = BoundedChannelFullMode.Wait
-        };
-
-        // 创建有限容量通道
-        _channel = Channel.CreateBounded<IEventSource>(boundedChannelOptions);
-
-        //_redis = redis as FullRedis;
-
-        // 创建广播消息订阅者,即所有服务器节点都能收到消息(用来发布重启、Reload配置等消息)
-        FullRedis redis = (FullRedis)cacheProvider.Cache;
-        var clusterOpt = App.GetConfig<ClusterOptions>("Cluster", true);
-        _queueBroadcast = redis.GetStream<string>(routeKey + ":broadcast");
-        _queueBroadcast.Group = clusterOpt.ServerId;//根据服务器标识分配到不同的分组里
-        _queueBroadcast.Expire = TimeSpan.FromSeconds(10);//消息10秒过期()
-        _queueBroadcast.ConsumeAsync(OnConsumeBroadcast);
-
-        // 创建队列消息订阅者,只要有一个服务节点消费了消息即可
-        _queueSingle = redis.GetQueue<ChannelEventSource>(routeKey + ":single");
-        _eventConsumer = new EventConsumer<ChannelEventSource>(_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<ChannelEventSource>(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);
-    }
-
-    /// <summary>
-    /// 将事件源写入存储器
-    /// </summary>
-    /// <param name="eventSource">事件源对象</param>
-    /// <param name="cancellationToken">取消任务 Token</param>
-    /// <returns><see cref="ValueTask"/></returns>
-    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);
-        }
-    }
-
-    /// <summary>
-    /// 从存储器中读取一条事件源
-    /// </summary>
-    /// <param name="cancellationToken">取消任务 Token</param>
-    /// <returns>事件源对象</returns>
-    public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken)
-    {
-        // 读取一条事件源
-        var eventSource = await _channel.Reader.ReadAsync(cancellationToken);
-        return eventSource;
-    }
-
-    /// <summary>
-    /// 释放非托管资源
-    /// </summary>
-    public async void Dispose()
-    {
-        await _eventConsumer.Stop();
-        GC.SuppressFinalize(this);
-    }
+// 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;
+
+/// <summary>
+/// Redis自定义事件源存储器
+/// </summary>
+/// <remarks>
+/// 在集群部署时,一般每一个消息只由一个服务节点消费一次。
+/// 有些特殊情情要通知到服务器群中的每一个节点(比如需要强制加载某些配置、重点服务等),
+/// 在这种情况下就要以“broadcast:”开头来定义EventId,
+/// 本系统会把“broadcast:”开头的事件视为“广播消息”保证集群中的每一个服务节点都能消费得到这个消息
+/// </remarks>
+public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable
+{
+    /// <summary>
+    /// 消费者
+    /// </summary>
+    private readonly EventConsumer<ChannelEventSource> _eventConsumer;
+
+    /// <summary>
+    /// 内存通道事件源存储器
+    /// </summary>
+    private readonly Channel<IEventSource> _channel;
+
+    private IProducerConsumer<ChannelEventSource> _queueSingle;
+
+    private RedisStream<string> _queueBroadcast;
+
+    private ILogger<RedisEventSourceStorer> _logger;
+
+    /// <summary>
+    /// 构造函数
+    /// </summary>
+    /// <param name="cacheProvider">Redis 连接对象</param>
+    /// <param name="routeKey">路由键</param>
+    /// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param>
+    public RedisEventSourceStorer(ICacheProvider cacheProvider, string routeKey, int capacity)
+    {
+        _logger = App.GetRequiredService<ILogger<RedisEventSourceStorer>>();
+
+        // 配置通道,设置超出默认容量后进入等待
+        var boundedChannelOptions = new BoundedChannelOptions(capacity)
+        {
+            FullMode = BoundedChannelFullMode.Wait
+        };
+
+        // 创建有限容量通道
+        _channel = Channel.CreateBounded<IEventSource>(boundedChannelOptions);
+
+        //_redis = redis as FullRedis;
+
+        // 创建广播消息订阅者,即所有服务器节点都能收到消息(用来发布重启、Reload配置等消息)
+        FullRedis redis = (FullRedis)cacheProvider.Cache;
+        var clusterOpt = App.GetConfig<ClusterOptions>("Cluster", true);
+        _queueBroadcast = redis.GetStream<string>(routeKey + ":broadcast");
+        _queueBroadcast.Group = clusterOpt.ServerId;//根据服务器标识分配到不同的分组里
+        _queueBroadcast.Expire = TimeSpan.FromSeconds(10);//消息10秒过期()
+        _queueBroadcast.ConsumeAsync(OnConsumeBroadcast);
+
+        // 创建队列消息订阅者,只要有一个服务节点消费了消息即可
+        _queueSingle = redis.GetQueue<ChannelEventSource>(routeKey + ":single");
+        _eventConsumer = new EventConsumer<ChannelEventSource>(_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<ChannelEventSource>(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);
+    }
+
+    /// <summary>
+    /// 将事件源写入存储器
+    /// </summary>
+    /// <param name="eventSource">事件源对象</param>
+    /// <param name="cancellationToken">取消任务 Token</param>
+    /// <returns><see cref="ValueTask"/></returns>
+    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);
+        }
+    }
+
+    /// <summary>
+    /// 从存储器中读取一条事件源
+    /// </summary>
+    /// <param name="cancellationToken">取消任务 Token</param>
+    /// <returns>事件源对象</returns>
+    public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken)
+    {
+        // 读取一条事件源
+        var eventSource = await _channel.Reader.ReadAsync(cancellationToken);
+        return eventSource;
+    }
+
+    /// <summary>
+    /// 释放非托管资源
+    /// </summary>
+    public async void Dispose()
+    {
+        await _eventConsumer.Stop();
+        GC.SuppressFinalize(this);
+    }
 }

+ 201 - 201
server/Admin.NET.Core/EventBus/RedisQueue.cs

@@ -1,202 +1,202 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using NewLife.Caching.Queues;
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// Redis 消息队列
-/// </summary>
-public static class RedisQueue
-{
-    private static ICacheProvider _cacheProvider = App.GetRequiredService<ICacheProvider>();
-
-    /// <summary>创建Redis消息队列。默认消费一次,指定消费者group时使用STREAM结构,支持多消费组共享消息</summary>
-    /// <remarks>
-    /// 使用队列时,可根据是否设置消费组来决定使用简单队列还是完整队列。 简单队列(如RedisQueue)可用作命令队列,Topic很多,但几乎没有消息。 完整队列(如RedisStream)可用作消息队列,Topic很少,但消息很多,并且支持多消费组。
-    /// </remarks>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="topic">主题</param>
-    /// <param name="group">消费组。未指定消费组时使用简单队列(如RedisQueue),指定消费组时使用完整队列(如RedisStream)</param>
-    /// <returns></returns>
-    public static IProducerConsumer<T> GetQueue<T>(String topic, String group = null)
-    {
-        // 队列需要单列
-        var key = $"myStream:{topic}";
-        if (_cacheProvider.InnerCache.TryGetValue<IProducerConsumer<T>>(key, out var queue)) return queue;
-
-        queue = _cacheProvider.GetQueue<T>(topic, group);
-        _cacheProvider.Cache.Set(key, queue);
-
-        return queue;
-    }
-
-    /// <summary>
-    /// 获取可信队列,需要确认
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="topic"></param>
-    /// <returns></returns>
-    public static RedisReliableQueue<T> GetRedisReliableQueue<T>(string topic)
-    {
-        // 队列需要单列
-        var key = $"myQueue:{topic}";
-        if (_cacheProvider.InnerCache.TryGetValue<RedisReliableQueue<T>>(key, out var queue)) return queue;
-
-        queue = (_cacheProvider.Cache as FullRedis).GetReliableQueue<T>(topic);
-        _cacheProvider.Cache.Set(key, queue);
-
-        return queue;
-    }
-
-    /// <summary>
-    /// 可信队列回滚
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="retryInterval"></param>
-    /// <returns></returns>
-    public static int RollbackAllAck(string topic, int retryInterval = 60)
-    {
-        var queue = GetRedisReliableQueue<string>(topic);
-        queue.RetryInterval = retryInterval;
-        return queue.RollbackAllAck();
-    }
-
-    /// <summary>
-    /// 发送一个数据列表到可信队列
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="value"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static int AddReliableQueueList<T>(string topic, List<T> value)
-    {
-        var queue = GetRedisReliableQueue<T>(topic);
-        var count = queue.Count;
-        var result = queue.Add(value.ToArray());
-        return result - count;
-    }
-
-    /// <summary>
-    /// 发送一条数据到可信队列
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="value"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static int AddReliableQueue<T>(string topic, T value)
-    {
-        var queue = GetRedisReliableQueue<T>(topic);
-        var count = queue.Count;
-        var result = queue.Add(value);
-        return result - count;
-    }
-
-    /// <summary>
-    /// 获取延迟队列
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static RedisDelayQueue<T> GetDelayQueue<T>(string topic)
-    {
-        // 队列需要单列
-        var key = $"myDelay:{topic}";
-        if (_cacheProvider.InnerCache.TryGetValue<RedisDelayQueue<T>>(key, out var queue)) return queue;
-
-        queue = (_cacheProvider.Cache as FullRedis).GetDelayQueue<T>(topic);
-        _cacheProvider.Cache.Set(key, queue);
-
-        return queue;
-    }
-
-    /// <summary>
-    /// 发送一条数据到延迟队列
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="value"></param>
-    /// <param name="delay">延迟时间。单位秒</param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static int AddDelayQueue<T>(string topic, T value, int delay)
-    {
-        var queue = GetDelayQueue<T>(topic);
-        return queue.Add(value, delay);
-    }
-
-    /// <summary>
-    /// 发送数据列表到延迟队列
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="value"></param>
-    /// <param name="delay"></param>
-    /// <typeparam name="T">延迟时间。单位秒</typeparam>
-    /// <returns></returns>
-    public static int AddDelayQueue<T>(string topic, List<T> value, int delay)
-    {
-        var queue = GetDelayQueue<T>(topic);
-        queue.Delay = delay;
-        return queue.Add(value.ToArray());
-    }
-
-    /// <summary>
-    /// 在可信队列获取一条数据
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static T ReliableTakeOne<T>(string topic)
-    {
-        var queue = GetRedisReliableQueue<T>(topic);
-        return queue.TakeOne(1);
-    }
-
-    /// <summary>
-    /// 异步在可信队列获取一条数据
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static async Task<T> ReliableTakeOneAsync<T>(string topic)
-    {
-        var queue = GetRedisReliableQueue<T>(topic);
-        return await queue.TakeOneAsync(1);
-    }
-
-    /// <summary>
-    /// 在可信队列获取多条数据
-    /// </summary>
-    /// <param name="topic"></param>
-    /// <param name="count"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static List<T> ReliableTake<T>(string topic, int count)
-    {
-        var queue = GetRedisReliableQueue<T>(topic);
-        return queue.Take(count).ToList();
-    }
-
-    /// <summary>
-    /// 申请分布式锁
-    /// </summary>
-    /// <param name="key">要锁定的key</param>
-    /// <param name="msTimeout">申请锁等待的时间,单位毫秒</param>
-    /// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
-    /// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
-    /// <returns></returns>
-    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;
-        }
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using NewLife.Caching.Queues;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// Redis 消息队列
+/// </summary>
+public static class RedisQueue
+{
+    private static ICacheProvider _cacheProvider = App.GetRequiredService<ICacheProvider>();
+
+    /// <summary>创建Redis消息队列。默认消费一次,指定消费者group时使用STREAM结构,支持多消费组共享消息</summary>
+    /// <remarks>
+    /// 使用队列时,可根据是否设置消费组来决定使用简单队列还是完整队列。 简单队列(如RedisQueue)可用作命令队列,Topic很多,但几乎没有消息。 完整队列(如RedisStream)可用作消息队列,Topic很少,但消息很多,并且支持多消费组。
+    /// </remarks>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="topic">主题</param>
+    /// <param name="group">消费组。未指定消费组时使用简单队列(如RedisQueue),指定消费组时使用完整队列(如RedisStream)</param>
+    /// <returns></returns>
+    public static IProducerConsumer<T> GetQueue<T>(String topic, String group = null)
+    {
+        // 队列需要单列
+        var key = $"myStream:{topic}";
+        if (_cacheProvider.InnerCache.TryGetValue<IProducerConsumer<T>>(key, out var queue)) return queue;
+
+        queue = _cacheProvider.GetQueue<T>(topic, group);
+        _cacheProvider.Cache.Set(key, queue);
+
+        return queue;
+    }
+
+    /// <summary>
+    /// 获取可信队列,需要确认
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="topic"></param>
+    /// <returns></returns>
+    public static RedisReliableQueue<T> GetRedisReliableQueue<T>(string topic)
+    {
+        // 队列需要单列
+        var key = $"myQueue:{topic}";
+        if (_cacheProvider.InnerCache.TryGetValue<RedisReliableQueue<T>>(key, out var queue)) return queue;
+
+        queue = (_cacheProvider.Cache as FullRedis).GetReliableQueue<T>(topic);
+        _cacheProvider.Cache.Set(key, queue);
+
+        return queue;
+    }
+
+    /// <summary>
+    /// 可信队列回滚
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="retryInterval"></param>
+    /// <returns></returns>
+    public static int RollbackAllAck(string topic, int retryInterval = 60)
+    {
+        var queue = GetRedisReliableQueue<string>(topic);
+        queue.RetryInterval = retryInterval;
+        return queue.RollbackAllAck();
+    }
+
+    /// <summary>
+    /// 发送一个数据列表到可信队列
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="value"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static int AddReliableQueueList<T>(string topic, List<T> value)
+    {
+        var queue = GetRedisReliableQueue<T>(topic);
+        var count = queue.Count;
+        var result = queue.Add(value.ToArray());
+        return result - count;
+    }
+
+    /// <summary>
+    /// 发送一条数据到可信队列
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="value"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static int AddReliableQueue<T>(string topic, T value)
+    {
+        var queue = GetRedisReliableQueue<T>(topic);
+        var count = queue.Count;
+        var result = queue.Add(value);
+        return result - count;
+    }
+
+    /// <summary>
+    /// 获取延迟队列
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static RedisDelayQueue<T> GetDelayQueue<T>(string topic)
+    {
+        // 队列需要单列
+        var key = $"myDelay:{topic}";
+        if (_cacheProvider.InnerCache.TryGetValue<RedisDelayQueue<T>>(key, out var queue)) return queue;
+
+        queue = (_cacheProvider.Cache as FullRedis).GetDelayQueue<T>(topic);
+        _cacheProvider.Cache.Set(key, queue);
+
+        return queue;
+    }
+
+    /// <summary>
+    /// 发送一条数据到延迟队列
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="value"></param>
+    /// <param name="delay">延迟时间。单位秒</param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static int AddDelayQueue<T>(string topic, T value, int delay)
+    {
+        var queue = GetDelayQueue<T>(topic);
+        return queue.Add(value, delay);
+    }
+
+    /// <summary>
+    /// 发送数据列表到延迟队列
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="value"></param>
+    /// <param name="delay"></param>
+    /// <typeparam name="T">延迟时间。单位秒</typeparam>
+    /// <returns></returns>
+    public static int AddDelayQueue<T>(string topic, List<T> value, int delay)
+    {
+        var queue = GetDelayQueue<T>(topic);
+        queue.Delay = delay;
+        return queue.Add(value.ToArray());
+    }
+
+    /// <summary>
+    /// 在可信队列获取一条数据
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static T ReliableTakeOne<T>(string topic)
+    {
+        var queue = GetRedisReliableQueue<T>(topic);
+        return queue.TakeOne(1);
+    }
+
+    /// <summary>
+    /// 异步在可信队列获取一条数据
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static async Task<T> ReliableTakeOneAsync<T>(string topic)
+    {
+        var queue = GetRedisReliableQueue<T>(topic);
+        return await queue.TakeOneAsync(1);
+    }
+
+    /// <summary>
+    /// 在可信队列获取多条数据
+    /// </summary>
+    /// <param name="topic"></param>
+    /// <param name="count"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static List<T> ReliableTake<T>(string topic, int count)
+    {
+        var queue = GetRedisReliableQueue<T>(topic);
+        return queue.Take(count).ToList();
+    }
+
+    /// <summary>
+    /// 申请分布式锁
+    /// </summary>
+    /// <param name="key">要锁定的key</param>
+    /// <param name="msTimeout">申请锁等待的时间,单位毫秒</param>
+    /// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
+    /// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
+    /// <returns></returns>
+    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;
+        }
+    }
 }

+ 43 - 43
server/Admin.NET.Core/EventBus/RetryEventHandlerExecutor.cs

@@ -1,44 +1,44 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 事件执行器-超时控制、失败重试熔断等等
-/// </summary>
-public class RetryEventHandlerExecutor : IEventHandlerExecutor
-{
-    public async Task ExecuteAsync(EventHandlerExecutingContext context, Func<EventHandlerExecutingContext, Task> 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}");
-        });
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 事件执行器-超时控制、失败重试熔断等等
+/// </summary>
+public class RetryEventHandlerExecutor : IEventHandlerExecutor
+{
+    public async Task ExecuteAsync(EventHandlerExecutingContext context, Func<EventHandlerExecutingContext, Task> 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}");
+        });
+    }
 }

+ 366 - 366
server/Admin.NET.Core/Extension/SqlSugarExtension.cs

@@ -1,367 +1,367 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using System.Text.Json;
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// Sqlsugar 动态查询扩展方法
-/// </summary>
-public static class SqlSugarExtension
-{
-    public static ISugarQueryable<T> SearchBy<T>(this ISugarQueryable<T> queryable, BaseFilter filter)
-    {
-        return queryable.SearchByKeyword(filter.Keyword)
-                .AdvancedSearch(filter.Search)
-                .AdvancedFilter(filter.Filter);
-    }
-
-    public static ISugarQueryable<T> SearchByKeyword<T>(this ISugarQueryable<T> queryable, string keyword)
-    {
-        return queryable.AdvancedSearch(new Search { Keyword = keyword });
-    }
-
-    public static ISugarQueryable<T> AdvancedSearch<T>(this ISugarQueryable<T> 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<T>(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<T>(propertyExpr, search.Keyword);
-
-                    right = Expression.Or(left, right);
-                }
-            }
-
-            var lambda = Expression.Lambda<Func<T, bool>>(right, paramExpr);
-
-            return queryable.Where(lambda);
-        }
-
-        return queryable;
-    }
-
-    public static ISugarQueryable<T> AdvancedFilter<T>(this ISugarQueryable<T> 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<Func<T, bool>>(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<Filter> 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<T>(
-          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 视图操作
-
-    /// <summary>
-    /// 获取映射SQL语句, 用于创建视图
-    /// </summary>
-    /// <param name="queryable"></param>
-    /// <typeparam name="T"></typeparam>
-    /// <returns></returns>
-    public static string ToMappedSqlString<T>(this ISugarQueryable<T> 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 视图操作
-
-    /// <summary>
-    /// 列表转换为树形结构
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source">列表数据</param>
-    /// <param name="childrenSelector">设置子节点列表。例如:item => item.Children</param>
-    /// <param name="parentIdSelector">设置元素的父级 Id。例如:item => item.ParentId</param>
-    /// <param name="rootParentId">根节点的父级 Id,默认为 0 </param>
-    /// <returns></returns>
-    /// <exception cref="Exception"></exception>
-    public static IEnumerable<T> ToTree<T>(
-       this IEnumerable<T> source,
-       Func<T, List<T>> childrenSelector,
-       Func<T, long> parentIdSelector,
-       int rootParentId)
-       where T : class
-    {
-        var lookup = source.ToLookup(parentIdSelector);
-        List<T> 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);
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using System.Text.Json;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// Sqlsugar 动态查询扩展方法
+/// </summary>
+public static class SqlSugarExtension
+{
+    public static ISugarQueryable<T> SearchBy<T>(this ISugarQueryable<T> queryable, BaseFilter filter)
+    {
+        return queryable.SearchByKeyword(filter.Keyword)
+                .AdvancedSearch(filter.Search)
+                .AdvancedFilter(filter.Filter);
+    }
+
+    public static ISugarQueryable<T> SearchByKeyword<T>(this ISugarQueryable<T> queryable, string keyword)
+    {
+        return queryable.AdvancedSearch(new Search { Keyword = keyword });
+    }
+
+    public static ISugarQueryable<T> AdvancedSearch<T>(this ISugarQueryable<T> 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<T>(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<T>(propertyExpr, search.Keyword);
+
+                    right = Expression.Or(left, right);
+                }
+            }
+
+            var lambda = Expression.Lambda<Func<T, bool>>(right, paramExpr);
+
+            return queryable.Where(lambda);
+        }
+
+        return queryable;
+    }
+
+    public static ISugarQueryable<T> AdvancedFilter<T>(this ISugarQueryable<T> 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<Func<T, bool>>(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<Filter> 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<T>(
+          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 视图操作
+
+    /// <summary>
+    /// 获取映射SQL语句, 用于创建视图
+    /// </summary>
+    /// <param name="queryable"></param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static string ToMappedSqlString<T>(this ISugarQueryable<T> 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 视图操作
+
+    /// <summary>
+    /// 列表转换为树形结构
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="source">列表数据</param>
+    /// <param name="childrenSelector">设置子节点列表。例如:item => item.Children</param>
+    /// <param name="parentIdSelector">设置元素的父级 Id。例如:item => item.ParentId</param>
+    /// <param name="rootParentId">根节点的父级 Id,默认为 0 </param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    public static IEnumerable<T> ToTree<T>(
+       this IEnumerable<T> source,
+       Func<T, List<T>> childrenSelector,
+       Func<T, long> parentIdSelector,
+       int rootParentId)
+       where T : class
+    {
+        var lookup = source.ToLookup(parentIdSelector);
+        List<T> 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);
+    }
 }

+ 57 - 57
server/Admin.NET.Core/Extension/SqlSugarFilterExtension.cs

@@ -1,58 +1,58 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core;
-
-public static class SqlSugarFilterExtension
-{
-    /// <summary>
-    /// 根据指定Attribute获取属性
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="type"></param>
-    /// <returns></returns>
-    private static List<string> GetPropertyNames<T>(this Type type) where T : Attribute
-    {
-        return type.GetProperties()
-            .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(T)))
-            .Select(x => x.Name).ToList();
-    }
-
-    /// <summary>
-    /// 获取过滤表达式
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="type"></param>
-    /// <param name="owners"></param>
-    /// <returns></returns>
-    public static LambdaExpression GetConditionExpression<T>(this Type type, List<long> owners) where T : Attribute
-    {
-        var fieldNames = type.GetPropertyNames<T>();
-        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);
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+public static class SqlSugarFilterExtension
+{
+    /// <summary>
+    /// 根据指定Attribute获取属性
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="type"></param>
+    /// <returns></returns>
+    private static List<string> GetPropertyNames<T>(this Type type) where T : Attribute
+    {
+        return type.GetProperties()
+            .Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(T)))
+            .Select(x => x.Name).ToList();
+    }
+
+    /// <summary>
+    /// 获取过滤表达式
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="type"></param>
+    /// <param name="owners"></param>
+    /// <returns></returns>
+    public static LambdaExpression GetConditionExpression<T>(this Type type, List<long> owners) where T : Attribute
+    {
+        var fieldNames = type.GetPropertyNames<T>();
+        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);
+    }
 }

+ 50 - 50
server/Admin.NET.Core/Job/OnlineUserJob.cs

@@ -1,51 +1,51 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using Furion.Logging.Extensions;
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 清理在线用户作业任务
-/// </summary>
-[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<ISqlSugarClient>().CopyNew();
-        await db.Deleteable<SysOnlineUser>().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<SysTenantService>().CacheTenant();
-
-        // 加载动态程序集
-        await serviceScope.ServiceProvider.GetRequiredService<SysPluginService>().LoadAllAssembly();
-        msg = $"【启动任务】加载动态程序集 {DateTime.Now}";
-        Console.WriteLine(msg);
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Furion.Logging.Extensions;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 清理在线用户作业任务
+/// </summary>
+[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<ISqlSugarClient>().CopyNew();
+        await db.Deleteable<SysOnlineUser>().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<SysTenantService>().CacheTenant();
+
+        // 加载动态程序集
+        await serviceScope.ServiceProvider.GetRequiredService<SysPluginService>().LoadAllAssembly();
+        msg = $"【启动任务】加载动态程序集 {DateTime.Now}";
+        Console.WriteLine(msg);
+    }
 }

+ 100 - 100
server/Admin.NET.Core/Logging/ElasticSearchLoggingWriter.cs

@@ -1,101 +1,101 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using Elastic.Clients.Elasticsearch;
-
-namespace Admin.NET.Core;
-
-/// <summary>
-/// ES日志写入器
-/// </summary>
-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<ElasticSearchClientContainer>()?.Logging;
-        _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
-    }
-
-    public async Task WriteAsync(LogMessage logMsg, bool flush)
-    {
-        // 是否启用操作日志
-        var sysOpLogEnabled = await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysOpLog);
-        if (!sysOpLogEnabled) return;
-
-        var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString();
-        if (string.IsNullOrWhiteSpace(jsonStr)) return;
-
-        var loggingMonitor = JSON.Deserialize<dynamic>(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);
-    }
-
-    /// <summary>
-    /// 释放服务作用域
-    /// </summary>
-    public void Dispose()
-    {
-        _serviceScope.Dispose();
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Elastic.Clients.Elasticsearch;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// ES日志写入器
+/// </summary>
+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<ElasticSearchClientContainer>()?.Logging;
+        _sysConfigService = _serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
+    }
+
+    public async Task WriteAsync(LogMessage logMsg, bool flush)
+    {
+        // 是否启用操作日志
+        var sysOpLogEnabled = await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysOpLog);
+        if (!sysOpLogEnabled) return;
+
+        var jsonStr = logMsg.Context?.Get("loggingMonitor")?.ToString();
+        if (string.IsNullOrWhiteSpace(jsonStr)) return;
+
+        var loggingMonitor = JSON.Deserialize<dynamic>(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);
+    }
+
+    /// <summary>
+    /// 释放服务作用域
+    /// </summary>
+    public void Dispose()
+    {
+        _serviceScope.Dispose();
+    }
 }

+ 1 - 1
server/Admin.NET.Core/SeedData/SysConfigSeedData.cs

@@ -25,7 +25,7 @@ public class SysConfigSeedData : ISqlSugarEntitySeedData<SysConfig>
         {
             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=1300000000106, Name="登录页默认账号", Code=ConfigConst.SysLoginDefaultAccount, Value="Demo01", SysFlag=YesNoEnum.N, Remark="登录页预填账号,可在参数配置修改", OrderNo=15, 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="新建用户/租户未填密码时的默认明文;登录页预填也读此值。修改此处不会自动更新已有用户表中的 Password,登录仍以各用户已存密文为准。", OrderNo=20, 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="新建用户/租户未填密码时的默认明文(登录页不再通过 sysInfo 预填)。修改此处不会自动更新已有用户表中的 Password,登录仍以各用户已存密文为准。", 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") },

+ 2 - 2
server/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -166,8 +166,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             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=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 },

+ 198 - 198
server/Admin.NET.Core/Service/CodeGen/Dto/CodeGenInput.cs

@@ -1,199 +1,199 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 代码生成参数类
-/// </summary>
-public class CodeGenInput : BasePageInput
-{
-    /// <summary>
-    /// 作者姓名
-    /// </summary>
-    public virtual string AuthorName { get; set; }
-
-    /// <summary>
-    /// 类名
-    /// </summary>
-    public virtual string ClassName { get; set; }
-
-    /// <summary>
-    /// 是否移除表前缀
-    /// </summary>
-    public virtual string TablePrefix { get; set; }
-
-    /// <summary>
-    /// 库定位器名
-    /// </summary>
-    public virtual string ConfigId { get; set; }
-
-    /// <summary>
-    /// 数据库名(保留字段)
-    /// </summary>
-    public virtual string DbName { get; set; }
-
-    /// <summary>
-    /// 数据库类型
-    /// </summary>
-    public virtual string DbType { get; set; }
-
-    /// <summary>
-    /// 数据库链接
-    /// </summary>
-    public virtual string ConnectionString { get; set; }
-
-    /// <summary>
-    /// 生成方式
-    /// </summary>
-    public virtual string GenerateType { get; set; }
-
-    /// <summary>
-    /// 数据库表名
-    /// </summary>
-    public virtual string TableName { get; set; }
-
-    /// <summary>
-    /// 命名空间
-    /// </summary>
-    public virtual string NameSpace { get; set; }
-
-    /// <summary>
-    /// 业务名(业务代码包名称)
-    /// </summary>
-    public virtual string BusName { get; set; }
-
-    /// <summary>
-    /// 功能名(数据库表名称)
-    /// </summary>
-    public virtual string TableComment { get; set; }
-
-    /// <summary>
-    /// 表唯一字段
-    /// </summary>
-    [Newtonsoft.Json.JsonIgnore]
-    [System.Text.Json.Serialization.JsonIgnore]
-    public virtual string TableUniqueConfig { get; set; }
-
-    /// <summary>
-    /// 表唯一字段列表
-    /// </summary>
-    public virtual List<TableUniqueConfigItem> TableUniqueList { get; set; }
-
-    /// <summary>
-    /// 菜单应用分类(应用编码)
-    /// </summary>
-    public virtual string MenuApplication { get; set; }
-
-    /// <summary>
-    /// 是否生成菜单
-    /// </summary>
-    public virtual bool GenerateMenu { get; set; }
-
-    /// <summary>
-    /// 菜单父级
-    /// </summary>
-    public virtual long? MenuPid { get; set; }
-
-    /// <summary>
-    /// 菜单图标
-    /// </summary>
-    public virtual string MenuIcon { get; set; }
-
-    /// <summary>
-    /// 页面目录
-    /// </summary>
-    public virtual string PagePath { get; set; }
-
-    /// <summary>
-    /// 支持打印类型
-    /// </summary>
-    public virtual string PrintType { get; set; }
-
-    /// <summary>
-    /// 打印模版名称
-    /// </summary>
-    public virtual string PrintName { get; set; }
-}
-
-public class AddCodeGenInput : CodeGenInput
-{
-    /// <summary>
-    /// 数据库表名
-    /// </summary>
-    [Required(ErrorMessage = "数据库表名不能为空")]
-    public override string TableName { get; set; }
-
-    /// <summary>
-    /// 业务名(业务代码包名称)
-    /// </summary>
-    [Required(ErrorMessage = "业务名不能为空")]
-    public override string BusName { get; set; }
-
-    /// <summary>
-    /// 命名空间
-    /// </summary>
-    [Required(ErrorMessage = "命名空间不能为空")]
-    public override string NameSpace { get; set; }
-
-    /// <summary>
-    /// 作者姓名
-    /// </summary>
-    [Required(ErrorMessage = "作者姓名不能为空")]
-    public override string AuthorName { get; set; }
-
-    ///// <summary>
-    ///// 类名
-    ///// </summary>
-    //[Required(ErrorMessage = "类名不能为空")]
-    //public override string ClassName { get; set; }
-
-    ///// <summary>
-    ///// 是否移除表前缀
-    ///// </summary>
-    //[Required(ErrorMessage = "是否移除表前缀不能为空")]
-    //public override string TablePrefix { get; set; }
-
-    /// <summary>
-    /// 生成方式
-    /// </summary>
-    [Required(ErrorMessage = "生成方式不能为空")]
-    public override string GenerateType { get; set; }
-
-    ///// <summary>
-    ///// 功能名(数据库表名称)
-    ///// </summary>
-    //[Required(ErrorMessage = "数据库表名不能为空")]
-    //public override string TableComment { get; set; }
-
-    /// <summary>
-    /// 是否生成菜单
-    /// </summary>
-    [Required(ErrorMessage = "是否生成菜单不能为空")]
-    public override bool GenerateMenu { get; set; }
-}
-
-public class DeleteCodeGenInput
-{
-    /// <summary>
-    /// 代码生成器Id
-    /// </summary>
-    [Required(ErrorMessage = "代码生成器Id不能为空")]
-    public long Id { get; set; }
-}
-
-public class UpdateCodeGenInput : AddCodeGenInput
-{
-    /// <summary>
-    /// 代码生成器Id
-    /// </summary>
-    [Required(ErrorMessage = "代码生成器Id不能为空")]
-    public long Id { get; set; }
-}
-
-public class QueryCodeGenInput : DeleteCodeGenInput
-{
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 代码生成参数类
+/// </summary>
+public class CodeGenInput : BasePageInput
+{
+    /// <summary>
+    /// 作者姓名
+    /// </summary>
+    public virtual string AuthorName { get; set; }
+
+    /// <summary>
+    /// 类名
+    /// </summary>
+    public virtual string ClassName { get; set; }
+
+    /// <summary>
+    /// 是否移除表前缀
+    /// </summary>
+    public virtual string TablePrefix { get; set; }
+
+    /// <summary>
+    /// 库定位器名
+    /// </summary>
+    public virtual string ConfigId { get; set; }
+
+    /// <summary>
+    /// 数据库名(保留字段)
+    /// </summary>
+    public virtual string DbName { get; set; }
+
+    /// <summary>
+    /// 数据库类型
+    /// </summary>
+    public virtual string DbType { get; set; }
+
+    /// <summary>
+    /// 数据库链接
+    /// </summary>
+    public virtual string ConnectionString { get; set; }
+
+    /// <summary>
+    /// 生成方式
+    /// </summary>
+    public virtual string GenerateType { get; set; }
+
+    /// <summary>
+    /// 数据库表名
+    /// </summary>
+    public virtual string TableName { get; set; }
+
+    /// <summary>
+    /// 命名空间
+    /// </summary>
+    public virtual string NameSpace { get; set; }
+
+    /// <summary>
+    /// 业务名(业务代码包名称)
+    /// </summary>
+    public virtual string BusName { get; set; }
+
+    /// <summary>
+    /// 功能名(数据库表名称)
+    /// </summary>
+    public virtual string TableComment { get; set; }
+
+    /// <summary>
+    /// 表唯一字段
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    public virtual string TableUniqueConfig { get; set; }
+
+    /// <summary>
+    /// 表唯一字段列表
+    /// </summary>
+    public virtual List<TableUniqueConfigItem> TableUniqueList { get; set; }
+
+    /// <summary>
+    /// 菜单应用分类(应用编码)
+    /// </summary>
+    public virtual string MenuApplication { get; set; }
+
+    /// <summary>
+    /// 是否生成菜单
+    /// </summary>
+    public virtual bool GenerateMenu { get; set; }
+
+    /// <summary>
+    /// 菜单父级
+    /// </summary>
+    public virtual long? MenuPid { get; set; }
+
+    /// <summary>
+    /// 菜单图标
+    /// </summary>
+    public virtual string MenuIcon { get; set; }
+
+    /// <summary>
+    /// 页面目录
+    /// </summary>
+    public virtual string PagePath { get; set; }
+
+    /// <summary>
+    /// 支持打印类型
+    /// </summary>
+    public virtual string PrintType { get; set; }
+
+    /// <summary>
+    /// 打印模版名称
+    /// </summary>
+    public virtual string PrintName { get; set; }
+}
+
+public class AddCodeGenInput : CodeGenInput
+{
+    /// <summary>
+    /// 数据库表名
+    /// </summary>
+    [Required(ErrorMessage = "数据库表名不能为空")]
+    public override string TableName { get; set; }
+
+    /// <summary>
+    /// 业务名(业务代码包名称)
+    /// </summary>
+    [Required(ErrorMessage = "业务名不能为空")]
+    public override string BusName { get; set; }
+
+    /// <summary>
+    /// 命名空间
+    /// </summary>
+    [Required(ErrorMessage = "命名空间不能为空")]
+    public override string NameSpace { get; set; }
+
+    /// <summary>
+    /// 作者姓名
+    /// </summary>
+    [Required(ErrorMessage = "作者姓名不能为空")]
+    public override string AuthorName { get; set; }
+
+    ///// <summary>
+    ///// 类名
+    ///// </summary>
+    //[Required(ErrorMessage = "类名不能为空")]
+    //public override string ClassName { get; set; }
+
+    ///// <summary>
+    ///// 是否移除表前缀
+    ///// </summary>
+    //[Required(ErrorMessage = "是否移除表前缀不能为空")]
+    //public override string TablePrefix { get; set; }
+
+    /// <summary>
+    /// 生成方式
+    /// </summary>
+    [Required(ErrorMessage = "生成方式不能为空")]
+    public override string GenerateType { get; set; }
+
+    ///// <summary>
+    ///// 功能名(数据库表名称)
+    ///// </summary>
+    //[Required(ErrorMessage = "数据库表名不能为空")]
+    //public override string TableComment { get; set; }
+
+    /// <summary>
+    /// 是否生成菜单
+    /// </summary>
+    [Required(ErrorMessage = "是否生成菜单不能为空")]
+    public override bool GenerateMenu { get; set; }
+}
+
+public class DeleteCodeGenInput
+{
+    /// <summary>
+    /// 代码生成器Id
+    /// </summary>
+    [Required(ErrorMessage = "代码生成器Id不能为空")]
+    public long Id { get; set; }
+}
+
+public class UpdateCodeGenInput : AddCodeGenInput
+{
+    /// <summary>
+    /// 代码生成器Id
+    /// </summary>
+    [Required(ErrorMessage = "代码生成器Id不能为空")]
+    public long Id { get; set; }
+}
+
+public class QueryCodeGenInput : DeleteCodeGenInput
+{
 }

+ 67 - 67
server/Admin.NET.Core/Service/CodeGen/Dto/ColumnOuput.cs

@@ -1,68 +1,68 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 数据库表列
-/// </summary>
-public class ColumnOuput
-{
-    /// <summary>
-    /// 字段名
-    /// </summary>
-    public string ColumnName { get; set; }
-
-    /// <summary>
-    /// 实体的Property名
-    /// </summary>
-    public string PropertyName { get; set; }
-
-    /// <summary>
-    /// 字段数据长度
-    /// </summary>
-    public int ColumnLength { get; set; }
-
-    /// <summary>
-    /// 数据库中类型
-    /// </summary>
-    public string DataType { get; set; }
-
-    /// <summary>
-    /// 字段数据默认值
-    /// </summary>
-    public string DefaultValue { get; set; }
-
-    /// <summary>
-    /// 是否为主键
-    /// </summary>
-    public bool IsPrimarykey { get; set; }
-
-    /// <summary>
-    /// 是否允许为空
-    /// </summary>
-    public bool IsNullable { get; set; }
-
-    /// <summary>
-    /// .NET字段类型
-    /// </summary>
-    public string NetType { get; set; }
-
-    /// <summary>
-    /// 字典编码
-    /// </summary>
-    public string DictTypeCode { get; set; }
-
-    /// <summary>
-    /// 字段描述
-    /// </summary>
-    public string ColumnComment { get; set; }
-
-    /// <summary>
-    /// 主外键
-    /// </summary>
-    public string ColumnKey { get; set; }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 数据库表列
+/// </summary>
+public class ColumnOuput
+{
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    public string ColumnName { get; set; }
+
+    /// <summary>
+    /// 实体的Property名
+    /// </summary>
+    public string PropertyName { get; set; }
+
+    /// <summary>
+    /// 字段数据长度
+    /// </summary>
+    public int ColumnLength { get; set; }
+
+    /// <summary>
+    /// 数据库中类型
+    /// </summary>
+    public string DataType { get; set; }
+
+    /// <summary>
+    /// 字段数据默认值
+    /// </summary>
+    public string DefaultValue { get; set; }
+
+    /// <summary>
+    /// 是否为主键
+    /// </summary>
+    public bool IsPrimarykey { get; set; }
+
+    /// <summary>
+    /// 是否允许为空
+    /// </summary>
+    public bool IsNullable { get; set; }
+
+    /// <summary>
+    /// .NET字段类型
+    /// </summary>
+    public string NetType { get; set; }
+
+    /// <summary>
+    /// 字典编码
+    /// </summary>
+    public string DictTypeCode { get; set; }
+
+    /// <summary>
+    /// 字段描述
+    /// </summary>
+    public string ColumnComment { get; set; }
+
+    /// <summary>
+    /// 主外键
+    /// </summary>
+    public string ColumnKey { get; set; }
 }

+ 2 - 2
server/Admin.NET.Core/Service/Config/SysConfigService.cs

@@ -264,7 +264,7 @@ public class SysConfigService : IDynamicApiController, ITransient
         // 与「参数配置」一致,供登录页预填;仅预填不自动登录
         var loginAccCfg = await _sysConfigRep.GetFirstAsync(u => u.Code == ConfigConst.SysLoginDefaultAccount);
         var defaultLoginAccount = loginAccCfg?.Value ?? "";
-        var defaultPassword = await GetConfigValue<string>(ConfigConst.SysPassword);
+        // 不向登录页下发「默认密码」:sys_password 仍用于服务端新建用户/租户等场景,避免在接口中暴露明文预填
         return new
         {
             tenant.Logo,
@@ -282,7 +282,7 @@ public class SysConfigService : IDynamicApiController, ITransient
             HideTenantForLogin = hideTenantForLogin,
             WayList = wayList,
             DefaultLoginAccount = defaultLoginAccount,
-            DefaultPassword = defaultPassword
+            DefaultPassword = ""
         };
     }
 

+ 53 - 53
server/Admin.NET.Core/Service/DataBase/Dto/CreateSeedDataInput.cs

@@ -1,54 +1,54 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core.Service;
-
-public class CreateSeedDataInput
-{
-    /// <summary>
-    /// 库标识
-    /// </summary>
-    public string ConfigId { get; set; }
-
-    /// <summary>
-    /// 表名
-    /// </summary>
-    /// <example>student</example>
-    public string TableName { get; set; }
-
-    /// <summary>
-    /// 实体名称
-    /// </summary>
-    /// <example>Student</example>
-    public string EntityName { get; set; }
-
-    /// <summary>
-    /// 种子名称
-    /// </summary>
-    /// <example>Student</example>
-    public string SeedDataName { get; set; }
-
-    /// <summary>
-    /// 导出位置
-    /// </summary>
-    /// <example>Web.Application</example>
-    public string Position { get; set; }
-
-    /// <summary>
-    /// 后缀
-    /// </summary>
-    /// <example>Web.Application</example>
-    public string Suffix { get; set; }
-
-    /// <summary>
-    /// 过滤已有数据
-    /// </summary>
-    /// <remarks>
-    /// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据
-    /// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。
-    /// </remarks>
-    public bool FilterExistingData { get; set; }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+public class CreateSeedDataInput
+{
+    /// <summary>
+    /// 库标识
+    /// </summary>
+    public string ConfigId { get; set; }
+
+    /// <summary>
+    /// 表名
+    /// </summary>
+    /// <example>student</example>
+    public string TableName { get; set; }
+
+    /// <summary>
+    /// 实体名称
+    /// </summary>
+    /// <example>Student</example>
+    public string EntityName { get; set; }
+
+    /// <summary>
+    /// 种子名称
+    /// </summary>
+    /// <example>Student</example>
+    public string SeedDataName { get; set; }
+
+    /// <summary>
+    /// 导出位置
+    /// </summary>
+    /// <example>Web.Application</example>
+    public string Position { get; set; }
+
+    /// <summary>
+    /// 后缀
+    /// </summary>
+    /// <example>Web.Application</example>
+    public string Suffix { get; set; }
+
+    /// <summary>
+    /// 过滤已有数据
+    /// </summary>
+    /// <remarks>
+    /// 如果数据在其它不同名的已有的种子类型的数据中出现过,就不生成这个数据
+    /// 主要用于生成菜单功能,菜单功能往往与子项目绑定,如果生成完整数据就会导致菜单项多处理重复。
+    /// </remarks>
+    public bool FilterExistingData { get; set; }
 }

+ 261 - 261
server/Admin.NET.Core/Service/Dict/SysDictTypeService.cs

@@ -1,262 +1,262 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 系统字典类型服务 🧩
-/// </summary>
-[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")]
-public class SysDictTypeService : IDynamicApiController, ITransient
-{
-    private readonly SqlSugarRepository<SysDictType> _sysDictTypeRep;
-    private readonly SysDictDataService _sysDictDataService;
-    private readonly SysCacheService _sysCacheService;
-    private readonly UserManager _userManager;
-    private readonly SysLangTextCacheService _sysLangTextCacheService;
-
-    public SysDictTypeService(SqlSugarRepository<SysDictType> sysDictTypeRep,
-        SysDictDataService sysDictDataService,
-        SysCacheService sysCacheService,
-        UserManager userManager,
-        SysLangTextCacheService sysLangTextCacheService)
-    {
-        _sysDictTypeRep = sysDictTypeRep;
-        _sysDictDataService = sysDictDataService;
-        _sysCacheService = sysCacheService;
-        _userManager = userManager;
-        _sysLangTextCacheService = sysLangTextCacheService;
-    }
-
-    /// <summary>
-    /// 获取字典类型分页列表 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取字典类型分页列表")]
-    public async Task<SqlSugarPagedList<SysDictType>> Page(PageDictTypeInput input)
-    {
-        var langCode = _userManager.LangCode;
-        var baseQuery = _sysDictTypeRep.AsQueryable()
-            .WhereIF(!_userManager.SuperAdmin, u => u.IsTenant == YesNoEnum.Y)
-            .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
-            .WhereIF(!string.IsNullOrEmpty(input.Name?.Trim()), u => u.Name.Contains(input.Name));
-        //.OrderBy(u => new { u.OrderNo, u.Code })
-        var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize);
-        var list = pageList.Items;
-        var ids = list.Select(d => d.Id).Distinct().ToList();
-        var translations = await _sysLangTextCacheService.GetTranslations(
-                               "SysDictType",
-                               "Name",
-                               ids,
-                               langCode);
-        foreach (var item in list)
-        {
-            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
-            {
-                item.Name = translatedName;
-            }
-        }
-        pageList.Items = list;
-        return pageList;
-    }
-
-    /// <summary>
-    /// 获取字典类型列表 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取字典类型列表")]
-    public async Task<List<SysDictType>> GetList()
-    {
-        var langCode = _userManager.LangCode;
-        var list = await _sysDictTypeRep.AsQueryable().OrderBy(u => new { u.OrderNo, u.Code }).ToListAsync();
-        var ids = list.Select(d => d.Id).Distinct().ToList();
-        var translations = await _sysLangTextCacheService.GetTranslations(
-                               "SysDictType",
-                               "Name",
-                               ids,
-                               langCode);
-        foreach (var item in list)
-        {
-            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
-            {
-                item.Name = translatedName;
-            }
-        }
-        return list;
-    }
-
-    /// <summary>
-    /// 获取字典类型-值列表 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [DisplayName("获取字典类型-值列表")]
-    public async Task<List<SysDictData>> GetDataList([FromQuery] GetDataDictTypeInput input)
-    {
-        var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Code == input.Code) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
-        var langCode = _userManager.LangCode;
-        var list = await _sysDictDataService.GetDictDataListByDictTypeId(dictType.Id);
-        var ids = list.Select(d => d.Id).Distinct().ToList();
-        var translations = await _sysLangTextCacheService.GetTranslations(
-                               "SysDictType",
-                               "Name",
-                               ids,
-                               langCode);
-        foreach (var item in list)
-        {
-            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
-            {
-                item.Name = translatedName;
-            }
-        }
-        return list;
-    }
-
-    /// <summary>
-    /// 添加字典类型 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [ApiDescriptionSettings(Name = "Add"), HttpPost]
-    [DisplayName("添加字典类型")]
-    public async Task AddDictType(AddDictTypeInput input)
-    {
-        if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006);
-        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
-
-        var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code);
-        if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
-
-        if (_userManager.SuperAdmin) input.IsTenant = YesNoEnum.N;  // 超级管理员添加的字典类型默认非租户级
-
-        await _sysDictTypeRep.InsertAsync(input.Adapt<SysDictType>());
-    }
-
-    /// <summary>
-    /// 更新字典类型 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [UnitOfWork]
-    [ApiDescriptionSettings(Name = "Update"), HttpPost]
-    [DisplayName("更新字典类型")]
-    public async Task UpdateDictType(UpdateDictTypeInput input)
-    {
-        var dict = await _sysDictTypeRep.GetFirstAsync(x => x.Id == input.Id);
-        if (dict.IsTenant != input.IsTenant) throw Oops.Oh(ErrorCodeEnum.D3012);
-        if (dict == null) throw Oops.Oh(ErrorCodeEnum.D3000);
-
-        if (dict.Code.ToLower().EndsWith("enum") && input.Code != dict.Code) throw Oops.Oh(ErrorCodeEnum.D3007);
-        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
-
-        var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id);
-        if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
-
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{input.Code}");
-        await _sysDictTypeRep.UpdateAsync(input.Adapt<SysDictType>());
-    }
-
-    /// <summary>
-    /// 删除字典类型 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [UnitOfWork]
-    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
-    [DisplayName("删除字典类型")]
-    public async Task DeleteDictType(DeleteDictTypeInput input)
-    {
-        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
-        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
-
-        // 删除字典值
-        await _sysDictTypeRep.DeleteAsync(dictType);
-        await _sysDictDataService.DeleteDictData(input.Id);
-    }
-
-    /// <summary>
-    /// 获取字典类型详情 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [DisplayName("获取字典类型详情")]
-    public async Task<SysDictType> GetDetail([FromQuery] DictTypeInput input)
-    {
-        return await _sysDictTypeRep.GetByIdAsync(input.Id);
-    }
-
-    /// <summary>
-    /// 修改字典类型状态 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [UnitOfWork]
-    [DisplayName("修改字典类型状态")]
-    public async Task SetStatus(DictTypeInput input)
-    {
-        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
-        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
-
-        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
-
-        dictType.Status = input.Status;
-        await _sysDictTypeRep.AsUpdateable(dictType).UpdateColumns(u => new { u.Status }, true).ExecuteCommandAsync();
-    }
-
-    /// <summary>
-    /// 获取所有字典集合 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取所有字典集合")]
-    public async Task<dynamic> GetAllDictList()
-    {
-        var langCode = _userManager.LangCode;
-        var ds = await _sysDictTypeRep.AsQueryable()
-            .InnerJoin(_sysDictDataService.VSysDictData, (u, w) => u.Id == w.DictTypeId)
-            .Select((u, w) => new DictDataOutput
-            {
-                DictDataId = w.Id, // 给翻译用
-                TypeCode = u.Code,
-                Label = w.Label,
-                Value = w.Value,
-                Code = w.Code,
-                TagType = w.TagType,
-                StyleSetting = w.StyleSetting,
-                ClassSetting = w.ClassSetting,
-                ExtData = w.ExtData,
-                Remark = w.Remark,
-                OrderNo = w.OrderNo,
-                Status = w.Status == StatusEnum.Enable && u.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable
-            })
-            .ToListAsync();
-        var ids = ds.Select(x => x.DictDataId).Distinct().ToList();
-
-        Dictionary<long, string> translations = new();
-        if (ids.Any())
-        {
-            translations = await _sysLangTextCacheService.GetTranslations(
-                "SysDictData",
-                "Label",
-                ids,
-                langCode
-            );
-        }
-        foreach (var item in ds)
-        {
-            if (translations.TryGetValue(item.DictDataId, out var translated) && !string.IsNullOrEmpty(translated))
-            {
-                item.Label = translated;
-            }
-        }
-
-        var result = ds
-            .OrderBy(u => u.OrderNo)
-            .GroupBy(u => u.TypeCode)
-            .ToDictionary(u => u.Key, u => u.ToList());
-
-        return result;
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统字典类型服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 430, Description = "系统字典类型")]
+public class SysDictTypeService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<SysDictType> _sysDictTypeRep;
+    private readonly SysDictDataService _sysDictDataService;
+    private readonly SysCacheService _sysCacheService;
+    private readonly UserManager _userManager;
+    private readonly SysLangTextCacheService _sysLangTextCacheService;
+
+    public SysDictTypeService(SqlSugarRepository<SysDictType> sysDictTypeRep,
+        SysDictDataService sysDictDataService,
+        SysCacheService sysCacheService,
+        UserManager userManager,
+        SysLangTextCacheService sysLangTextCacheService)
+    {
+        _sysDictTypeRep = sysDictTypeRep;
+        _sysDictDataService = sysDictDataService;
+        _sysCacheService = sysCacheService;
+        _userManager = userManager;
+        _sysLangTextCacheService = sysLangTextCacheService;
+    }
+
+    /// <summary>
+    /// 获取字典类型分页列表 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取字典类型分页列表")]
+    public async Task<SqlSugarPagedList<SysDictType>> Page(PageDictTypeInput input)
+    {
+        var langCode = _userManager.LangCode;
+        var baseQuery = _sysDictTypeRep.AsQueryable()
+            .WhereIF(!_userManager.SuperAdmin, u => u.IsTenant == YesNoEnum.Y)
+            .WhereIF(!string.IsNullOrEmpty(input.Code?.Trim()), u => u.Code.Contains(input.Code))
+            .WhereIF(!string.IsNullOrEmpty(input.Name?.Trim()), u => u.Name.Contains(input.Name));
+        //.OrderBy(u => new { u.OrderNo, u.Code })
+        var pageList = await baseQuery.ToPagedListAsync(input.Page, input.PageSize);
+        var list = pageList.Items;
+        var ids = list.Select(d => d.Id).Distinct().ToList();
+        var translations = await _sysLangTextCacheService.GetTranslations(
+                               "SysDictType",
+                               "Name",
+                               ids,
+                               langCode);
+        foreach (var item in list)
+        {
+            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
+            {
+                item.Name = translatedName;
+            }
+        }
+        pageList.Items = list;
+        return pageList;
+    }
+
+    /// <summary>
+    /// 获取字典类型列表 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取字典类型列表")]
+    public async Task<List<SysDictType>> GetList()
+    {
+        var langCode = _userManager.LangCode;
+        var list = await _sysDictTypeRep.AsQueryable().OrderBy(u => new { u.OrderNo, u.Code }).ToListAsync();
+        var ids = list.Select(d => d.Id).Distinct().ToList();
+        var translations = await _sysLangTextCacheService.GetTranslations(
+                               "SysDictType",
+                               "Name",
+                               ids,
+                               langCode);
+        foreach (var item in list)
+        {
+            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
+            {
+                item.Name = translatedName;
+            }
+        }
+        return list;
+    }
+
+    /// <summary>
+    /// 获取字典类型-值列表 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取字典类型-值列表")]
+    public async Task<List<SysDictData>> GetDataList([FromQuery] GetDataDictTypeInput input)
+    {
+        var dictType = await _sysDictTypeRep.GetFirstAsync(u => u.Code == input.Code) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        var langCode = _userManager.LangCode;
+        var list = await _sysDictDataService.GetDictDataListByDictTypeId(dictType.Id);
+        var ids = list.Select(d => d.Id).Distinct().ToList();
+        var translations = await _sysLangTextCacheService.GetTranslations(
+                               "SysDictType",
+                               "Name",
+                               ids,
+                               langCode);
+        foreach (var item in list)
+        {
+            if (translations.TryGetValue(item.Id, out var translatedName) && !string.IsNullOrEmpty(translatedName))
+            {
+                item.Name = translatedName;
+            }
+        }
+        return list;
+    }
+
+    /// <summary>
+    /// 添加字典类型 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    [DisplayName("添加字典类型")]
+    public async Task AddDictType(AddDictTypeInput input)
+    {
+        if (input.Code.ToLower().EndsWith("enum")) throw Oops.Oh(ErrorCodeEnum.D3006);
+        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3008);
+
+        var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
+
+        if (_userManager.SuperAdmin) input.IsTenant = YesNoEnum.N;  // 超级管理员添加的字典类型默认非租户级
+
+        await _sysDictTypeRep.InsertAsync(input.Adapt<SysDictType>());
+    }
+
+    /// <summary>
+    /// 更新字典类型 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    [DisplayName("更新字典类型")]
+    public async Task UpdateDictType(UpdateDictTypeInput input)
+    {
+        var dict = await _sysDictTypeRep.GetFirstAsync(x => x.Id == input.Id);
+        if (dict.IsTenant != input.IsTenant) throw Oops.Oh(ErrorCodeEnum.D3012);
+        if (dict == null) throw Oops.Oh(ErrorCodeEnum.D3000);
+
+        if (dict.Code.ToLower().EndsWith("enum") && input.Code != dict.Code) throw Oops.Oh(ErrorCodeEnum.D3007);
+        if (input.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
+
+        var isExist = await _sysDictTypeRep.IsAnyAsync(u => u.Code == input.Code && u.Id != input.Id);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D3001);
+
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{input.Code}");
+        await _sysDictTypeRep.UpdateAsync(input.Adapt<SysDictType>());
+    }
+
+    /// <summary>
+    /// 删除字典类型 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    [DisplayName("删除字典类型")]
+    public async Task DeleteDictType(DeleteDictTypeInput input)
+    {
+        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3010);
+
+        // 删除字典值
+        await _sysDictTypeRep.DeleteAsync(dictType);
+        await _sysDictDataService.DeleteDictData(input.Id);
+    }
+
+    /// <summary>
+    /// 获取字典类型详情 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取字典类型详情")]
+    public async Task<SysDictType> GetDetail([FromQuery] DictTypeInput input)
+    {
+        return await _sysDictTypeRep.GetByIdAsync(input.Id);
+    }
+
+    /// <summary>
+    /// 修改字典类型状态 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("修改字典类型状态")]
+    public async Task SetStatus(DictTypeInput input)
+    {
+        var dictType = await _sysDictTypeRep.GetByIdAsync(input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D3000);
+        if (dictType.SysFlag == YesNoEnum.Y && !_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D3009);
+
+        _sysCacheService.Remove($"{CacheConst.KeyDict}{dictType.Code}");
+
+        dictType.Status = input.Status;
+        await _sysDictTypeRep.AsUpdateable(dictType).UpdateColumns(u => new { u.Status }, true).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 获取所有字典集合 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取所有字典集合")]
+    public async Task<dynamic> GetAllDictList()
+    {
+        var langCode = _userManager.LangCode;
+        var ds = await _sysDictTypeRep.AsQueryable()
+            .InnerJoin(_sysDictDataService.VSysDictData, (u, w) => u.Id == w.DictTypeId)
+            .Select((u, w) => new DictDataOutput
+            {
+                DictDataId = w.Id, // 给翻译用
+                TypeCode = u.Code,
+                Label = w.Label,
+                Value = w.Value,
+                Code = w.Code,
+                TagType = w.TagType,
+                StyleSetting = w.StyleSetting,
+                ClassSetting = w.ClassSetting,
+                ExtData = w.ExtData,
+                Remark = w.Remark,
+                OrderNo = w.OrderNo,
+                Status = w.Status == StatusEnum.Enable && u.Status == StatusEnum.Enable ? StatusEnum.Enable : StatusEnum.Disable
+            })
+            .ToListAsync();
+        var ids = ds.Select(x => x.DictDataId).Distinct().ToList();
+
+        Dictionary<long, string> translations = new();
+        if (ids.Any())
+        {
+            translations = await _sysLangTextCacheService.GetTranslations(
+                "SysDictData",
+                "Label",
+                ids,
+                langCode
+            );
+        }
+        foreach (var item in ds)
+        {
+            if (translations.TryGetValue(item.DictDataId, out var translated) && !string.IsNullOrEmpty(translated))
+            {
+                item.Label = translated;
+            }
+        }
+
+        var result = ds
+            .OrderBy(u => u.OrderNo)
+            .GroupBy(u => u.TypeCode)
+            .ToDictionary(u => u.Key, u => u.ToList());
+
+        return result;
+    }
 }

+ 97 - 97
server/Admin.NET.Core/Service/Job/JobClusterServer.cs

@@ -1,98 +1,98 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 作业集群控制
-/// </summary>
-public class JobClusterServer : IJobClusterServer
-{
-    private static readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
-    private readonly Random _random = new(DateTime.Now.Millisecond);
-
-    /// <summary>
-    /// 当前作业调度器启动通知
-    /// </summary>
-    /// <param name="context">作业集群服务上下文</param>
-    public async void Start(JobClusterContext context)
-    {
-        // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting
-        if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId))
-        {
-            await _sysJobClusterRep.AsUpdateable().SetColumns(u => u.Status == ClusterStatus.Waiting).Where(u => u.ClusterId == context.ClusterId).ExecuteCommandAsync();
-        }
-        else
-        {
-            await _sysJobClusterRep.AsInsertable(new SysJobCluster { ClusterId = context.ClusterId, Status = ClusterStatus.Waiting }).ExecuteCommandAsync();
-        }
-    }
-
-    /// <summary>
-    /// 等待被唤醒
-    /// </summary>
-    /// <param name="context">作业集群服务上下文</param>
-    /// <returns><see cref="Task"/></returns>
-    public async Task WaitingForAsync(JobClusterContext context)
-    {
-        var clusterId = context.ClusterId;
-
-        while (true)
-        {
-            // 控制集群心跳频率(放在头部为了防止 IsAnyAsync continue 没sleep占用大量IO和CPU)
-            await Task.Delay(3000 + _random.Next(500, 1000)); // 错开集群同时启动
-
-            try
-            {
-                ICache cache = App.GetRequiredService<ICacheProvider>().Cache;
-                // 使用分布式锁
-                using (cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000))
-                {
-                    // 在这里查询数据库,根据以下两种情况处理
-                    // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环
-                    // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;
-                    // 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return;
-                    if (await _sysJobClusterRep.IsAnyAsync(u => u.Status == ClusterStatus.Working)) continue;
-
-                    await WorkNowAsync(clusterId);
-                    return;
-                }
-            }
-            catch { }
-        }
-    }
-
-    /// <summary>
-    /// 当前作业调度器停止通知
-    /// </summary>
-    /// <param name="context">作业集群服务上下文</param>
-    public async void Stop(JobClusterContext context)
-    {
-        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
-        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
-    }
-
-    /// <summary>
-    /// 当前作业调度器宕机
-    /// </summary>
-    /// <param name="context">作业集群服务上下文</param>
-    public async void Crash(JobClusterContext context)
-    {
-        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
-        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
-    }
-
-    /// <summary>
-    /// 指示集群可以工作
-    /// </summary>
-    /// <param name="clusterId">集群 Id</param>
-    /// <returns></returns>
-    private static async Task WorkNowAsync(string clusterId)
-    {
-        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working
-        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId);
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 作业集群控制
+/// </summary>
+public class JobClusterServer : IJobClusterServer
+{
+    private static readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep = App.GetRequiredService<SqlSugarRepository<SysJobCluster>>();
+    private readonly Random _random = new(DateTime.Now.Millisecond);
+
+    /// <summary>
+    /// 当前作业调度器启动通知
+    /// </summary>
+    /// <param name="context">作业集群服务上下文</param>
+    public async void Start(JobClusterContext context)
+    {
+        // 在作业集群表中,如果 clusterId 不存在,则新增一条(否则更新一条),并设置 status 为 ClusterStatus.Waiting
+        if (await _sysJobClusterRep.IsAnyAsync(u => u.ClusterId == context.ClusterId))
+        {
+            await _sysJobClusterRep.AsUpdateable().SetColumns(u => u.Status == ClusterStatus.Waiting).Where(u => u.ClusterId == context.ClusterId).ExecuteCommandAsync();
+        }
+        else
+        {
+            await _sysJobClusterRep.AsInsertable(new SysJobCluster { ClusterId = context.ClusterId, Status = ClusterStatus.Waiting }).ExecuteCommandAsync();
+        }
+    }
+
+    /// <summary>
+    /// 等待被唤醒
+    /// </summary>
+    /// <param name="context">作业集群服务上下文</param>
+    /// <returns><see cref="Task"/></returns>
+    public async Task WaitingForAsync(JobClusterContext context)
+    {
+        var clusterId = context.ClusterId;
+
+        while (true)
+        {
+            // 控制集群心跳频率(放在头部为了防止 IsAnyAsync continue 没sleep占用大量IO和CPU)
+            await Task.Delay(3000 + _random.Next(500, 1000)); // 错开集群同时启动
+
+            try
+            {
+                ICache cache = App.GetRequiredService<ICacheProvider>().Cache;
+                // 使用分布式锁
+                using (cache.AcquireLock("lock:JobClusterServer:WaitingForAsync", 1000))
+                {
+                    // 在这里查询数据库,根据以下两种情况处理
+                    // 1) 如果作业集群表已有 status 为 ClusterStatus.Working 则继续循环
+                    // 2) 如果作业集群表中还没有其他服务或只有自己,则插入一条集群服务或调用 await WorkNowAsync(clusterId); 之后 return;
+                    // 3) 如果作业集群表中没有 status 为 ClusterStatus.Working 的,调用 await WorkNowAsync(clusterId); 之后 return;
+                    if (await _sysJobClusterRep.IsAnyAsync(u => u.Status == ClusterStatus.Working)) continue;
+
+                    await WorkNowAsync(clusterId);
+                    return;
+                }
+            }
+            catch { }
+        }
+    }
+
+    /// <summary>
+    /// 当前作业调度器停止通知
+    /// </summary>
+    /// <param name="context">作业集群服务上下文</param>
+    public async void Stop(JobClusterContext context)
+    {
+        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
+        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
+    }
+
+    /// <summary>
+    /// 当前作业调度器宕机
+    /// </summary>
+    /// <param name="context">作业集群服务上下文</param>
+    public async void Crash(JobClusterContext context)
+    {
+        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Crashed
+        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Crashed }, u => u.ClusterId == context.ClusterId);
+    }
+
+    /// <summary>
+    /// 指示集群可以工作
+    /// </summary>
+    /// <param name="clusterId">集群 Id</param>
+    /// <returns></returns>
+    private static async Task WorkNowAsync(string clusterId)
+    {
+        // 在作业集群表中,更新 clusterId 的 status 为 ClusterStatus.Working
+        await _sysJobClusterRep.UpdateAsync(u => new SysJobCluster { Status = ClusterStatus.Working }, u => u.ClusterId == clusterId);
+    }
 }

+ 69 - 69
server/Admin.NET.Core/Utils/MiniExcelUtil.cs

@@ -1,70 +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";
-
-    /// <summary>
-    /// 导出模板Excel
-    /// </summary>
-    /// <returns></returns>
-    public static async Task<IActionResult> ExportExcelTemplate<T>(string fileName = null) where T : class, new()
-    {
-        var values = Array.Empty<T>();
-        // 在内存中当开辟空间
-        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"
-        };
-    }
-
-    /// <summary>
-    /// 获取导入数据Excel
-    /// </summary>
-    /// <param name="file"></param>
-    /// <returns></returns>
-    public static async Task<IEnumerable<T>> GetImportExcelData<T>([Required] IFormFile file) where T : class, new()
-    {
-        using MemoryStream stream = new MemoryStream();
-        await file.CopyToAsync(stream);
-        var res = await stream.QueryAsync<T>(sheetName: SheetName);
-        return res.ToArray();
-    }
-
-    /// <summary>
-    /// 获取导出数据excel地址
-    /// </summary>
-    /// <returns></returns>
-    public static async Task<string> GetExportDataExcelUrl<T>(IEnumerable<T> 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}";
-    }
+// 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";
+
+    /// <summary>
+    /// 导出模板Excel
+    /// </summary>
+    /// <returns></returns>
+    public static async Task<IActionResult> ExportExcelTemplate<T>(string fileName = null) where T : class, new()
+    {
+        var values = Array.Empty<T>();
+        // 在内存中当开辟空间
+        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"
+        };
+    }
+
+    /// <summary>
+    /// 获取导入数据Excel
+    /// </summary>
+    /// <param name="file"></param>
+    /// <returns></returns>
+    public static async Task<IEnumerable<T>> GetImportExcelData<T>([Required] IFormFile file) where T : class, new()
+    {
+        using MemoryStream stream = new MemoryStream();
+        await file.CopyToAsync(stream);
+        var res = await stream.QueryAsync<T>(sheetName: SheetName);
+        return res.ToArray();
+    }
+
+    /// <summary>
+    /// 获取导出数据excel地址
+    /// </summary>
+    /// <returns></returns>
+    public static async Task<string> GetExportDataExcelUrl<T>(IEnumerable<T> 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}";
+    }
 }

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.0</AssemblyVersion>
-    <FileVersion>1.0.0</FileVersion>
-    <Version>1.0.0</Version>
+    <AssemblyVersion>1.0.2</AssemblyVersion>
+    <FileVersion>1.0.2</FileVersion>
+    <Version>1.0.2</Version>
   </PropertyGroup>
 
   <ItemGroup>

BIN
server/Admin.NET.Web.Entry/GeoLite2-City.mmdb


BIN
server/Admin.NET.Web.Entry/ip2region.db


BIN
server/Admin.NET.Web.Entry/wwwroot/images/logo.png


BIN
server/Admin.NET.Web.Entry/wwwroot/upload/logo.png