From bce58a76da78f88ae1541212cbc4f965e0ff31e4 Mon Sep 17 00:00:00 2001
From: lzf <3153566913@qq.com>
Date: Tue, 14 Jul 2026 13:36:28 +0800
Subject: [PATCH 1/3] refactor: complete v2.4.1 optimizations
---
...13\346\261\240\346\262\273\347\220\206.md" | 17 +-
...77\347\224\250\346\214\207\345\215\227.md" | 24 +-
code-nest-design-system/package.json | 19 +
.../src}/components/app/CnPage.vue | 0
.../src}/components/app/CnPageHeader.vue | 0
.../src}/components/app/CnSection.vue | 0
.../src}/components/data/CnDataTable.vue | 0
.../src}/components/data/CnEmptyState.vue | 0
.../src}/components/data/CnFilterForm.vue | 0
.../src}/components/data/CnStatCard.vue | 0
.../src}/components/data/CnStatusTag.vue | 0
.../src}/components/data/CnToolbar.vue | 0
.../navigation/CnCommandPalette.vue | 0
.../src}/components/navigation/CnSidebar.vue | 0
.../components/navigation/CnSidebarItem.vue | 0
.../src}/components/navigation/CnTopNav.vue | 0
.../src}/components/theme/CnThemeDrawer.vue | 0
.../src}/components/theme/CnThemeSwitch.vue | 0
.../src}/composables/useCnBreakpoints.ts | 0
.../src}/composables/useCnTable.ts | 0
.../src}/composables/useTheme.ts | 0
.../src}/index.ts | 0
.../src}/plugins/element-plus.css | 0
.../src}/themes/dark.css | 0
.../src}/themes/growth.css | 0
.../src}/themes/high-contrast.css | 0
.../src}/themes/light.css | 0
.../src}/themes/professional-blue.css | 0
.../src}/tokens/component.css | 0
.../src}/tokens/motion.css | 0
.../src}/tokens/primitive.css | 0
.../src}/tokens/semantic.css | 0
.../src}/types/components.ts | 0
.../src}/types/theme.ts | 0
docs-site/modules/common.md | 59 +-
docs-site/modules/moyu.md | 2 +-
docs-site/modules/utils.md | 55 +-
vue3-admin-front/electron.vite.config.ts | 5 +
vue3-admin-front/package-lock.json | 15 +
vue3-admin-front/package.json | 1 +
.../src/design-system/tokens/motion.css | 7 -
vue3-admin-front/tsconfig.json | 4 +
vue3-admin-front/vite.config.js | 5 +
vue3-user-front/electron.vite.config.ts | 5 +
vue3-user-front/package-lock.json | 15 +
vue3-user-front/package.json | 1 +
.../design-system/components/app/CnPage.vue | 118 --
.../components/app/CnPageHeader.vue | 168 ---
.../components/app/CnSection.vue | 142 --
.../components/data/CnDataTable.vue | 144 --
.../components/data/CnEmptyState.vue | 123 --
.../components/data/CnFilterForm.vue | 160 ---
.../components/data/CnStatCard.vue | 219 ---
.../components/data/CnStatusTag.vue | 126 --
.../components/data/CnToolbar.vue | 96 --
.../navigation/CnCommandPalette.vue | 373 -----
.../components/navigation/CnSidebar.vue | 373 -----
.../components/navigation/CnSidebarItem.vue | 35 -
.../components/navigation/CnTopNav.vue | 955 -------------
.../components/theme/CnThemeDrawer.vue | 493 -------
.../components/theme/CnThemeSwitch.vue | 219 ---
.../composables/useCnBreakpoints.ts | 53 -
.../design-system/composables/useCnTable.ts | 182 ---
.../src/design-system/composables/useTheme.ts | 150 --
vue3-user-front/src/design-system/index.ts | 66 -
.../design-system/plugins/element-plus.css | 41 -
.../src/design-system/themes/dark.css | 24 -
.../src/design-system/themes/growth.css | 22 -
.../design-system/themes/high-contrast.css | 22 -
.../src/design-system/themes/light.css | 23 -
.../themes/professional-blue.css | 22 -
.../src/design-system/tokens/component.css | 29 -
.../src/design-system/tokens/primitive.css | 47 -
.../src/design-system/tokens/semantic.css | 34 -
.../src/design-system/types/components.ts | 282 ----
.../src/design-system/types/theme.ts | 32 -
vue3-user-front/tsconfig.json | 4 +
vue3-user-front/vite.config.js | 5 +
xiaou-application/pom.xml | 9 +-
.../service/LearningCockpitService.java | 119 +-
.../service/LearningCockpitServiceTest.java | 103 ++
.../chat/service/ChatRateLimitService.java | 18 +-
.../service/ChatWebSocketTicketService.java | 13 +-
.../ChatWebSocketTicketServiceTest.java | 38 +
xiaou-common/pom.xml | 6 +
.../xiaou/common/cache/RedisValueStore.java | 115 ++
.../config/ApplicationTaskExecutorConfig.java | 30 +
.../xiaou/common/utils/ConcurrentUtils.java | 1043 --------------
.../common/utils/NotificationCacheUtil.java | 28 +-
.../com/xiaou/common/utils/RedisUtil.java | 872 ------------
.../xiaou/common/utils/ThreadPoolUtils.java | 1206 -----------------
.../common/cache/RedisValueStoreTest.java | 83 ++
.../impl/CommunityAiSummaryServiceImpl.java | 39 +-
.../impl/CommunityCacheServiceImpl.java | 125 +-
.../impl/CommunityHotPostServiceImpl.java | 32 +-
.../service/impl/JobBattleServiceImpl.java | 22 +-
.../impl/JobBattleServiceImplTest.java | 25 +-
xiaou-moment/pom.xml | 8 +-
.../moment/mapper/MomentFavoriteMapper.java | 7 +
.../xiaou/moment/mapper/MomentLikeMapper.java | 9 +-
.../service/impl/MomentServiceImpl.java | 24 +-
.../service/impl/MomentViewServiceImpl.java | 37 +-
.../moment/task/HotMomentCalculateTask.java | 10 +-
.../resources/mapper/MomentFavoriteMapper.xml | 5 +
.../resources/mapper/MomentLikeMapper.xml | 7 +-
.../service/impl/MomentServiceImplTest.java | 156 +++
.../service/impl/HotTopicServiceImpl.java | 57 +-
.../points/cache/LotteryCacheWarmer.java | 11 +-
.../impl/LotteryEmergencyServiceImpl.java | 19 +-
.../service/impl/LotteryStockServiceImpl.java | 8 +-
.../user/service/impl/CaptchaServiceImpl.java | 12 +-
.../service/impl/CaptchaServiceImplTest.java | 18 +-
112 files changed, 1174 insertions(+), 8156 deletions(-)
create mode 100644 code-nest-design-system/package.json
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/app/CnPage.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/app/CnPageHeader.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/app/CnSection.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnDataTable.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnEmptyState.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnFilterForm.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnStatCard.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnStatusTag.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/data/CnToolbar.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/navigation/CnCommandPalette.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/navigation/CnSidebar.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/navigation/CnSidebarItem.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/navigation/CnTopNav.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/theme/CnThemeDrawer.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/components/theme/CnThemeSwitch.vue (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/composables/useCnBreakpoints.ts (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/composables/useCnTable.ts (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/composables/useTheme.ts (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/index.ts (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/plugins/element-plus.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/themes/dark.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/themes/growth.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/themes/high-contrast.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/themes/light.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/themes/professional-blue.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/tokens/component.css (100%)
rename {vue3-user-front/src/design-system => code-nest-design-system/src}/tokens/motion.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/tokens/primitive.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/tokens/semantic.css (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/types/components.ts (100%)
rename {vue3-admin-front/src/design-system => code-nest-design-system/src}/types/theme.ts (100%)
delete mode 100644 vue3-admin-front/src/design-system/tokens/motion.css
delete mode 100644 vue3-user-front/src/design-system/components/app/CnPage.vue
delete mode 100644 vue3-user-front/src/design-system/components/app/CnPageHeader.vue
delete mode 100644 vue3-user-front/src/design-system/components/app/CnSection.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnDataTable.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnEmptyState.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnFilterForm.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnStatCard.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnStatusTag.vue
delete mode 100644 vue3-user-front/src/design-system/components/data/CnToolbar.vue
delete mode 100644 vue3-user-front/src/design-system/components/navigation/CnCommandPalette.vue
delete mode 100644 vue3-user-front/src/design-system/components/navigation/CnSidebar.vue
delete mode 100644 vue3-user-front/src/design-system/components/navigation/CnSidebarItem.vue
delete mode 100644 vue3-user-front/src/design-system/components/navigation/CnTopNav.vue
delete mode 100644 vue3-user-front/src/design-system/components/theme/CnThemeDrawer.vue
delete mode 100644 vue3-user-front/src/design-system/components/theme/CnThemeSwitch.vue
delete mode 100644 vue3-user-front/src/design-system/composables/useCnBreakpoints.ts
delete mode 100644 vue3-user-front/src/design-system/composables/useCnTable.ts
delete mode 100644 vue3-user-front/src/design-system/composables/useTheme.ts
delete mode 100644 vue3-user-front/src/design-system/index.ts
delete mode 100644 vue3-user-front/src/design-system/plugins/element-plus.css
delete mode 100644 vue3-user-front/src/design-system/themes/dark.css
delete mode 100644 vue3-user-front/src/design-system/themes/growth.css
delete mode 100644 vue3-user-front/src/design-system/themes/high-contrast.css
delete mode 100644 vue3-user-front/src/design-system/themes/light.css
delete mode 100644 vue3-user-front/src/design-system/themes/professional-blue.css
delete mode 100644 vue3-user-front/src/design-system/tokens/component.css
delete mode 100644 vue3-user-front/src/design-system/tokens/primitive.css
delete mode 100644 vue3-user-front/src/design-system/tokens/semantic.css
delete mode 100644 vue3-user-front/src/design-system/types/components.ts
delete mode 100644 vue3-user-front/src/design-system/types/theme.ts
create mode 100644 xiaou-application/src/test/java/com/xiaou/web/learning/service/LearningCockpitServiceTest.java
create mode 100644 xiaou-chat/src/test/java/com/xiaou/chat/service/ChatWebSocketTicketServiceTest.java
create mode 100644 xiaou-common/src/main/java/com/xiaou/common/cache/RedisValueStore.java
create mode 100644 xiaou-common/src/main/java/com/xiaou/common/config/ApplicationTaskExecutorConfig.java
delete mode 100644 xiaou-common/src/main/java/com/xiaou/common/utils/ConcurrentUtils.java
delete mode 100644 xiaou-common/src/main/java/com/xiaou/common/utils/RedisUtil.java
delete mode 100644 xiaou-common/src/main/java/com/xiaou/common/utils/ThreadPoolUtils.java
create mode 100644 xiaou-common/src/test/java/com/xiaou/common/cache/RedisValueStoreTest.java
create mode 100644 xiaou-moment/src/test/java/com/xiaou/moment/service/impl/MomentServiceImplTest.java
diff --git "a/AI-DOCS/Technical/04-\345\274\202\346\255\245\345\214\226\344\270\216\347\272\277\347\250\213\346\261\240\346\262\273\347\220\206.md" "b/AI-DOCS/Technical/04-\345\274\202\346\255\245\345\214\226\344\270\216\347\272\277\347\250\213\346\261\240\346\262\273\347\220\206.md"
index c0a12bf39..e4f335783 100644
--- "a/AI-DOCS/Technical/04-\345\274\202\346\255\245\345\214\226\344\270\216\347\272\277\347\250\213\346\261\240\346\262\273\347\220\206.md"
+++ "b/AI-DOCS/Technical/04-\345\274\202\346\255\245\345\214\226\344\270\216\347\272\277\347\250\213\346\261\240\346\262\273\347\220\206.md"
@@ -12,11 +12,11 @@
## 30 秒答法
-我在项目里把大量耗时链路做了异步化治理,一方面沉淀了 ThreadPoolUtils,把批量并发、超时控制、失败快速返回、指数退避重试、调度任务等能力统一封装;另一方面把 OJ 判题、通知发送、抽奖后置事件、计划提醒等链路接入 RabbitMQ,实现接口快速返回、削峰解耦和延迟投递,整体提升了系统吞吐和接口稳定性。
+我在项目里把耗时链路分成进程内并发和跨模块异步两层治理。进程内使用 Spring 托管的有界执行器,并在调用服务中显式设置超时和降级;具有独立容量模型的任务使用模块专用执行器。跨模块则把 OJ 判题、通知、抽奖后置事件和计划提醒接入 RabbitMQ,实现快速返回、削峰解耦和失败重试。
## 1 分钟展开答法
-项目里一开始有很多典型问题,比如接口里同步做通知、同步做判题、同步扫任务,这会导致请求耗时不稳定,而且某个下游一慢就会拖垮主链路。所以我做了两层治理。第一层是进程内并发能力,把常见异步模式统一沉淀成 ThreadPoolUtils,按 CPU 密集型、IO 密集型、调度型线程池拆分,补上超时、重试、并发限制、批量并行处理和链式异步调用能力;第二层是跨模块异步解耦,把判题、通知、抽奖后置事件和计划提醒这些链路接入 RabbitMQ,主接口只做最小必要操作,耗时任务交给消费者执行,同时支持延迟投递和失败重试。
+项目里一开始有很多同步通知、同步判题和同步扫任务,导致请求耗时不稳定,下游变慢时还会拖累主链路。第一层治理是让 Spring 统一管理线程生命周期和容量:通用 I/O 使用有界的 `applicationIoExecutor`,特殊场景使用模块命名执行器,超时与降级留在具体服务中。第二层是用 RabbitMQ 解耦跨模块任务,主接口只做最小必要操作,消费者负责延迟执行和失败重试。
## 你在项目里具体做了什么
@@ -24,12 +24,11 @@
- 很多项目喜欢在业务代码里直接 `new Thread` 或零散用 `@Async`。
- 这样的问题是线程资源不可控,参数不统一,也难做监控和治理。
-- 所以我把并发能力收口到 `ThreadPoolUtils`:
- - 通用线程池
- - IO 线程池
- - CPU 线程池
- - 调度线程池
- - 虚拟线程兼容支持
+- 所以我把线程资源收口到 Spring 容器:
+ - 通用 I/O 使用有界 `applicationIoExecutor`
+ - 热榜刷新等独立负载使用模块命名执行器
+ - 线程数、队列、拒绝策略和关闭等待统一配置
+ - 超时与降级由调用服务显式声明
你要强调:
@@ -125,4 +124,4 @@
## 一句话背诵版
-我把项目里的并发和异步能力分成了两层治理:进程内通过 ThreadPoolUtils 统一封装批量并发、超时、重试和调度能力,分布式层通过 RabbitMQ 对判题、通知、抽奖后置事件和提醒任务做削峰解耦,从而同时提升接口响应和系统稳定性。
+我把并发和异步能力分成两层治理:进程内由 Spring 管理有界执行器,并在业务调用点声明超时和降级;分布式层通过 RabbitMQ 对判题、通知、抽奖后置事件和提醒任务做削峰解耦,从而同时改善接口响应和系统稳定性。
diff --git "a/AI-DOCS/Technical/\347\272\277\347\250\213\346\261\240\344\270\216\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/AI-DOCS/Technical/\347\272\277\347\250\213\346\261\240\344\270\216\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273\344\275\277\347\224\250\346\214\207\345\215\227.md"
index ae818ae97..e62ff3559 100644
--- "a/AI-DOCS/Technical/\347\272\277\347\250\213\346\261\240\344\270\216\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273\344\275\277\347\224\250\346\214\207\345\215\227.md"
+++ "b/AI-DOCS/Technical/\347\272\277\347\250\213\346\261\240\344\270\216\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273\344\275\277\347\224\250\346\214\207\345\215\227.md"
@@ -1,15 +1,27 @@
-# 线程池与并发工具类使用指南
+# 旧版线程池与并发工具迁移指南
-本项目提供了两个核心并发工具类,用于简化多线程编程和并发控制。
+> 版本说明:`ThreadPoolUtils` 和 `ConcurrentUtils` 已在 v2.4.1 删除。本文保留旧 API 示例用于迁移审查,当前代码应注入 Spring 管理的 `applicationIoExecutor`;具有独立容量模型的任务使用模块自有执行器,锁、限流和熔断由业务模块按领域语义实现。
+
+## 当前迁移映射
+
+| 旧能力 | 当前做法 |
+| --- | --- |
+| 通用 I/O 线程池 | 构造器注入 `@Qualifier("applicationIoExecutor") Executor` |
+| 模块专用线程池 | 在模块配置中声明命名执行器,例如 `hotTopicExecutor` |
+| 超时和降级 | 调用服务内使用 `CompletableFuture` 明确声明时间预算和降级值 |
+| 锁、限流、熔断 | 由所属业务服务直接实现或使用对应基础库 |
+| Redis 值与计数器 | 使用 `RedisValueStore`;集合和锁直接使用 Redisson |
+
+以下章节均描述 v2.4.1 之前的旧实现,不应在新代码中引用。
## 目录
-- [ThreadPoolUtils - 线程池工具类](#threadpoolutils---线程池工具类)
-- [ConcurrentUtils - 并发控制工具类](#concurrentutils---并发控制工具类)
+- [ThreadPoolUtils - 旧版线程池工具类](#threadpoolutils---旧版线程池工具类)
+- [ConcurrentUtils - 旧版并发控制工具类](#concurrentutils---旧版并发控制工具类)
---
-## ThreadPoolUtils - 线程池工具类
+## ThreadPoolUtils - 旧版线程池工具类
**包路径**: `com.xiaou.common.utils.ThreadPoolUtils`
@@ -250,7 +262,7 @@ ThreadPoolExecutor executor = ThreadPoolUtils.PoolConfig.builder()
---
-## ConcurrentUtils - 并发控制工具类
+## ConcurrentUtils - 旧版并发控制工具类
**包路径**: `com.xiaou.common.utils.ConcurrentUtils`
diff --git a/code-nest-design-system/package.json b/code-nest-design-system/package.json
new file mode 100644
index 000000000..dbee9df37
--- /dev/null
+++ b/code-nest-design-system/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@code-nest/design-system",
+ "version": "2.4.0",
+ "private": true,
+ "type": "module",
+ "files": [
+ "src"
+ ],
+ "exports": {
+ ".": "./src/index.ts",
+ "./*": "./src/*"
+ },
+ "peerDependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "element-plus": "^2.4.4",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5"
+ }
+}
diff --git a/vue3-admin-front/src/design-system/components/app/CnPage.vue b/code-nest-design-system/src/components/app/CnPage.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/app/CnPage.vue
rename to code-nest-design-system/src/components/app/CnPage.vue
diff --git a/vue3-admin-front/src/design-system/components/app/CnPageHeader.vue b/code-nest-design-system/src/components/app/CnPageHeader.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/app/CnPageHeader.vue
rename to code-nest-design-system/src/components/app/CnPageHeader.vue
diff --git a/vue3-admin-front/src/design-system/components/app/CnSection.vue b/code-nest-design-system/src/components/app/CnSection.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/app/CnSection.vue
rename to code-nest-design-system/src/components/app/CnSection.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnDataTable.vue b/code-nest-design-system/src/components/data/CnDataTable.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnDataTable.vue
rename to code-nest-design-system/src/components/data/CnDataTable.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnEmptyState.vue b/code-nest-design-system/src/components/data/CnEmptyState.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnEmptyState.vue
rename to code-nest-design-system/src/components/data/CnEmptyState.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnFilterForm.vue b/code-nest-design-system/src/components/data/CnFilterForm.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnFilterForm.vue
rename to code-nest-design-system/src/components/data/CnFilterForm.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnStatCard.vue b/code-nest-design-system/src/components/data/CnStatCard.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnStatCard.vue
rename to code-nest-design-system/src/components/data/CnStatCard.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnStatusTag.vue b/code-nest-design-system/src/components/data/CnStatusTag.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnStatusTag.vue
rename to code-nest-design-system/src/components/data/CnStatusTag.vue
diff --git a/vue3-admin-front/src/design-system/components/data/CnToolbar.vue b/code-nest-design-system/src/components/data/CnToolbar.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/data/CnToolbar.vue
rename to code-nest-design-system/src/components/data/CnToolbar.vue
diff --git a/vue3-admin-front/src/design-system/components/navigation/CnCommandPalette.vue b/code-nest-design-system/src/components/navigation/CnCommandPalette.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/navigation/CnCommandPalette.vue
rename to code-nest-design-system/src/components/navigation/CnCommandPalette.vue
diff --git a/vue3-admin-front/src/design-system/components/navigation/CnSidebar.vue b/code-nest-design-system/src/components/navigation/CnSidebar.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/navigation/CnSidebar.vue
rename to code-nest-design-system/src/components/navigation/CnSidebar.vue
diff --git a/vue3-admin-front/src/design-system/components/navigation/CnSidebarItem.vue b/code-nest-design-system/src/components/navigation/CnSidebarItem.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/navigation/CnSidebarItem.vue
rename to code-nest-design-system/src/components/navigation/CnSidebarItem.vue
diff --git a/vue3-admin-front/src/design-system/components/navigation/CnTopNav.vue b/code-nest-design-system/src/components/navigation/CnTopNav.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/navigation/CnTopNav.vue
rename to code-nest-design-system/src/components/navigation/CnTopNav.vue
diff --git a/vue3-admin-front/src/design-system/components/theme/CnThemeDrawer.vue b/code-nest-design-system/src/components/theme/CnThemeDrawer.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/theme/CnThemeDrawer.vue
rename to code-nest-design-system/src/components/theme/CnThemeDrawer.vue
diff --git a/vue3-admin-front/src/design-system/components/theme/CnThemeSwitch.vue b/code-nest-design-system/src/components/theme/CnThemeSwitch.vue
similarity index 100%
rename from vue3-admin-front/src/design-system/components/theme/CnThemeSwitch.vue
rename to code-nest-design-system/src/components/theme/CnThemeSwitch.vue
diff --git a/vue3-admin-front/src/design-system/composables/useCnBreakpoints.ts b/code-nest-design-system/src/composables/useCnBreakpoints.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/composables/useCnBreakpoints.ts
rename to code-nest-design-system/src/composables/useCnBreakpoints.ts
diff --git a/vue3-admin-front/src/design-system/composables/useCnTable.ts b/code-nest-design-system/src/composables/useCnTable.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/composables/useCnTable.ts
rename to code-nest-design-system/src/composables/useCnTable.ts
diff --git a/vue3-admin-front/src/design-system/composables/useTheme.ts b/code-nest-design-system/src/composables/useTheme.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/composables/useTheme.ts
rename to code-nest-design-system/src/composables/useTheme.ts
diff --git a/vue3-admin-front/src/design-system/index.ts b/code-nest-design-system/src/index.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/index.ts
rename to code-nest-design-system/src/index.ts
diff --git a/vue3-admin-front/src/design-system/plugins/element-plus.css b/code-nest-design-system/src/plugins/element-plus.css
similarity index 100%
rename from vue3-admin-front/src/design-system/plugins/element-plus.css
rename to code-nest-design-system/src/plugins/element-plus.css
diff --git a/vue3-admin-front/src/design-system/themes/dark.css b/code-nest-design-system/src/themes/dark.css
similarity index 100%
rename from vue3-admin-front/src/design-system/themes/dark.css
rename to code-nest-design-system/src/themes/dark.css
diff --git a/vue3-admin-front/src/design-system/themes/growth.css b/code-nest-design-system/src/themes/growth.css
similarity index 100%
rename from vue3-admin-front/src/design-system/themes/growth.css
rename to code-nest-design-system/src/themes/growth.css
diff --git a/vue3-admin-front/src/design-system/themes/high-contrast.css b/code-nest-design-system/src/themes/high-contrast.css
similarity index 100%
rename from vue3-admin-front/src/design-system/themes/high-contrast.css
rename to code-nest-design-system/src/themes/high-contrast.css
diff --git a/vue3-admin-front/src/design-system/themes/light.css b/code-nest-design-system/src/themes/light.css
similarity index 100%
rename from vue3-admin-front/src/design-system/themes/light.css
rename to code-nest-design-system/src/themes/light.css
diff --git a/vue3-admin-front/src/design-system/themes/professional-blue.css b/code-nest-design-system/src/themes/professional-blue.css
similarity index 100%
rename from vue3-admin-front/src/design-system/themes/professional-blue.css
rename to code-nest-design-system/src/themes/professional-blue.css
diff --git a/vue3-admin-front/src/design-system/tokens/component.css b/code-nest-design-system/src/tokens/component.css
similarity index 100%
rename from vue3-admin-front/src/design-system/tokens/component.css
rename to code-nest-design-system/src/tokens/component.css
diff --git a/vue3-user-front/src/design-system/tokens/motion.css b/code-nest-design-system/src/tokens/motion.css
similarity index 100%
rename from vue3-user-front/src/design-system/tokens/motion.css
rename to code-nest-design-system/src/tokens/motion.css
diff --git a/vue3-admin-front/src/design-system/tokens/primitive.css b/code-nest-design-system/src/tokens/primitive.css
similarity index 100%
rename from vue3-admin-front/src/design-system/tokens/primitive.css
rename to code-nest-design-system/src/tokens/primitive.css
diff --git a/vue3-admin-front/src/design-system/tokens/semantic.css b/code-nest-design-system/src/tokens/semantic.css
similarity index 100%
rename from vue3-admin-front/src/design-system/tokens/semantic.css
rename to code-nest-design-system/src/tokens/semantic.css
diff --git a/vue3-admin-front/src/design-system/types/components.ts b/code-nest-design-system/src/types/components.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/types/components.ts
rename to code-nest-design-system/src/types/components.ts
diff --git a/vue3-admin-front/src/design-system/types/theme.ts b/code-nest-design-system/src/types/theme.ts
similarity index 100%
rename from vue3-admin-front/src/design-system/types/theme.ts
rename to code-nest-design-system/src/types/theme.ts
diff --git a/docs-site/modules/common.md b/docs-site/modules/common.md
index f943344ed..9b77720c8 100644
--- a/docs-site/modules/common.md
+++ b/docs-site/modules/common.md
@@ -1,6 +1,6 @@
# 公共底座(xiaou-common)
-公共底座是全站所有业务模块的共享层。它不是面向用户的功能模块,而是把鉴权、统一响应、分页、异常、并发工具、Redis 操作、IP 解析、密码哈希、常量定义这些"每个模块都会用但不想重复写"的东西收拢到一起。
+公共底座是全站所有业务模块的共享层。它不是面向用户的功能模块,而是把鉴权、统一响应、分页、异常、受控 I/O 执行器、类型化 Redis 值访问、IP 解析、密码哈希和常量定义等通用能力收拢到一起。
读这篇时建议带着一个问题:**业务模块依赖了公共底座的哪些能力?如果要改底座行为,影响面有多大?**
@@ -13,8 +13,8 @@
| 管理端权限 | `@RequireAdmin`、`AdminAuthAspect` | 管理端接口 → 详见 [权限注解与角色边界索引](/reference/permission-boundaries) |
| 操作日志 | `@Log`、`LogAspect` | 管理端增删改接口 |
| 异常体系 | `BusinessException`、`GlobalExceptionHandler`、AI 异常族 | 所有 Service |
-| 并发工具 | `ConcurrentUtils`、`ThreadPoolUtils` | 敏感词、通知、迁移等异步场景 |
-| 缓存工具 | `RedisUtil`、`NotificationCacheUtil` | 通知未读数、系统设置 |
+| I/O 执行器 | `ApplicationTaskExecutorConfig`、`applicationIoExecutor` | 有明确时间预算的异步 I/O 聚合 |
+| 缓存能力 | `RedisValueStore`、`NotificationCacheUtil` | 票据、计数器、通知未读数和可选缓存 |
| 分页 | `PageRequest`、`PageResult`、`ManualPageHelper` | 所有列表接口 |
| 常量 | `Constants` | 全局配置默认值 |
| 密码 | `PasswordUtil` | 用户注册、管理员登录 |
@@ -33,7 +33,7 @@
1. 先看 `Result` + `ResultCode` + `GlobalExceptionHandler`,理解全站 API 响应格式和异常如何变成错误码。
2. 再看 `StpUserUtil` + `StpAdminUtil` + `SaTokenConfig`,理解双端鉴权的分仓设计(配合 [鉴权与用户体系](/modules/auth))。
3. 接着看 `@RequireAdmin` + `AdminAuthAspect`,理解管理端权限是怎么切面拦截的(配合 [权限注解与角色边界索引](/reference/permission-boundaries))。
-4. 然后看 `ConcurrentUtils` 和 `ThreadPoolUtils`,理解全站并发工具的设计与边界。
+4. 然后看 `ApplicationTaskExecutorConfig` 和 `RedisValueStore`,理解线程资源、Redis 类型和失败策略的边界。
5. 最后看 `Constants`、`PasswordUtil`、`IPUtil`、`DateHelper` 等小工具,补齐全局认知。
## 源码地图
@@ -55,9 +55,9 @@
| `xiaou-common/src/main/java/com/xiaou/common/annotation/RequireAdmin.java` | 管理端权限注解 |
| `xiaou-common/src/main/java/com/xiaou/common/aspect/AdminAuthAspect.java` | 管理端权限切面 |
| `xiaou-common/src/main/java/com/xiaou/common/annotation/Log.java` | 操作日志注解 |
-| `xiaou-common/src/main/java/com/xiaou/common/utils/ConcurrentUtils.java` | 并发工具集 |
-| `xiaou-common/src/main/java/com/xiaou/common/utils/ThreadPoolUtils.java` | 线程池工具 |
-| `xiaou-common/src/main/java/com/xiaou/common/utils/RedisUtil.java` | Redis 操作工具 |
+| `xiaou-common/src/main/java/com/xiaou/common/config/ApplicationTaskExecutorConfig.java` | Spring 托管的有界 I/O 执行器 |
+| `xiaou-common/src/main/java/com/xiaou/common/cache/RedisValueStore.java` | 类型化 Redis 值、票据和计数器操作 |
+| `xiaou-common/src/main/java/com/xiaou/common/utils/NotificationCacheUtil.java` | 可降级的通知未读数缓存 |
| `xiaou-common/src/main/java/com/xiaou/common/utils/IPUtil.java` | IP 地址解析 |
| `xiaou-common/src/main/java/com/xiaou/common/utils/PasswordUtil.java` | 密码工具 |
| `xiaou-common/src/main/java/com/xiaou/common/utils/DateHelper.java` | 日期工具 |
@@ -214,19 +214,19 @@ RuntimeException
| --- | --- | --- |
| [鉴权与用户体系](/modules/auth) | 强依赖 | 公共底座提供 Sa-Token 双端鉴权基础设施 |
| [用户账户与个人中心](/modules/user-account) | 被依赖 | 用户上下文查询依赖用户账户数据 |
-| [敏感词风控](/modules/sensitive) | 被依赖 | 敏感词检测使用公共底座的并发工具 |
-| [通知中心](/modules/notification) | 被依赖 | 通知未读数缓存使用 RedisUtil |
+| [敏感词风控](/modules/sensitive) | 被依赖 | 敏感词检测使用公共底座的响应、异常和通用工具 |
+| [通知中心](/modules/notification) | 被依赖 | 通知未读数缓存使用 `RedisValueStore` |
| [系统运营后台](/modules/system-ops) | 被依赖 | 操作日志和权限注解由公共底座提供 |
---
## 公共底座模块深度拆解
-> 以下内容基于 `xiaou-common` 全部源码逐行拆解,覆盖 2 个并发工具类、6 个核心域对象、7 个异常类、2 个注解 + 2 个切面、4 个 Sa-Token 相关类、3 个配置类、8 个工具类、1 个常量类。
+> 以下内容包含 v2.4.1 之前的并发工具历史分析。当前实现以 Spring 托管执行器、类型化 Redis 值访问和业务模块自有并发语义为准。
-### 一、并发工具集 ConcurrentUtils 深度分析
+### 一、历史并发工具集 ConcurrentUtils 深度分析
-**源码**:`ConcurrentUtils.java`(1043 行)
+**状态**:v2.4.1 已删除;以下内容仅用于迁移前设计复盘。
这是全站最大的工具类,提供限流、熔断、锁、计数器、延迟值等并发原语。
@@ -348,9 +348,9 @@ tryLock(key, timeout, unit):
| `Once` | `AtomicInteger` + `CountDownLatch` | 只执行一次保证 |
| `computeIfAbsent` | `ConcurrentMap` 安全计算 | Null-safe 的 putIfAbsent |
-### 二、线程池工具 ThreadPoolUtils 深度分析
+### 二、历史线程池工具 ThreadPoolUtils 深度分析
-**源码**:`ThreadPoolUtils.java`(1206 行)
+**状态**:v2.4.1 已删除;当前注入 Spring 管理的 `applicationIoExecutor` 或模块专用执行器。
#### 2.1 预配置线程池
@@ -530,14 +530,14 @@ getIpAddress(request):
| 编号 | 问题 | 位置 | 影响 |
| --- | --- | --- | --- |
-| BUG-1 | RateLimiter.acquire() 使用 busy-wait | `ConcurrentUtils.RateLimiter.acquire` | CPU 空转,高并发时浪费 |
-| BUG-2 | CircuitBreaker 状态切换非原子 | `ConcurrentUtils.CircuitBreaker.onFailure` | 允许少量额外请求通过 |
-| BUG-3 | POOL_CACHE 无清理机制 | `ThreadPoolUtils.POOL_CACHE` | 自定义池永不释放 |
+| BUG-1 | 旧限流器使用 busy-wait | 历史 `ConcurrentUtils` | v2.4.1 删除旧实现后已消除 |
+| BUG-2 | 旧熔断器状态切换非原子 | 历史 `ConcurrentUtils` | v2.4.1 删除旧实现后已消除 |
+| BUG-3 | 旧自定义线程池缓存无清理机制 | 历史 `ThreadPoolUtils` | v2.4.1 改为 Spring 托管执行器后已消除 |
| BUG-4 | SaTokenUserUtil 跨端查询是 TODO 桩 | `SaTokenUserUtil.getUsernameById` 等 | 返回硬编码值而非真实数据 |
| BUG-5 | Constants.SYSTEM_VERSION 过时 | `Constants.SYSTEM_VERSION = "1.0.0"` | 与实际版本不符 |
| BUG-6 | IPUtil 与 SysAdminServiceImpl 代码重复 | 两处 `getIpAddress()` | 维护时可能改一处忘另一处 |
| BUG-7 | 用户端权限始终返回空 | `StpInterfaceImpl.getPermissionList` | 用户端无法使用 RBAC |
-| BUG-8 | AsyncChain timeout 不强制停止任务 | `ThreadPoolUtils.AsyncChain.timeout` | 超时后任务仍在后台运行 |
+| BUG-8 | 异步超时不强制停止底层任务 | `CompletableFuture` 调用点 | 仍需同时配置网络客户端超时 |
#### 8.2 设计层面的潜在风险
@@ -547,9 +547,6 @@ getIpAddress(request):
| RISK-2 | DEFAULT_PASSWORD = "123456" | 如果有批量创建账号场景,弱密码风险 |
| RISK-3 | 管理端角色/权限无缓存 | 每次请求查数据库,高频访问时有压力 |
| RISK-4 | 日志异步写入无兜底 | DB 异常时操作日志丢失,无本地文件或 MQ 备份 |
-| RISK-5 | 虚拟线程反射检测 | JVM 版本或模块系统变化可能导致检测失败 |
-| RISK-6 | RateLimiter synchronized 全局锁 | 高并发下单限流器实例的吞吐受限 |
-| RISK-7 | HALF_OPEN 直接全量恢复 | 熔断恢复无慢启动,可能立即再次熔断 |
| RISK-8 | 敏感字段过滤硬编码 | 新增敏感参数需改切面代码,不可配置 |
#### 8.3 架构设计亮点
@@ -557,12 +554,12 @@ getIpAddress(request):
| 编号 | 亮点 | 说明 |
| --- | --- | --- |
| H-1 | 双端鉴权分仓 | 用户端和管理端 Token 完全隔离,互不影响 |
-| H-2 | StripedLock 分段锁 | 不同 key 分散到不同锁,减少锁竞争 |
-| H-3 | 指数退避 + 抖动 | 防止重试惊群,分布式最佳实践 |
-| H-4 | 预配置多类型线程池 | COMMON/IO/CPU/SCHEDULER 按场景选用 |
-| H-5 | 虚拟线程兼容检测 | Java 17 正常运行,Java 21+ 自动启用虚拟线程 |
-| H-6 | CallerRunsPolicy | 拒绝策略保证任务不丢失 |
-| H-7 | parallelMapFailFast | 快速失败模式避免浪费资源 |
+| H-2 | Redis 类型显式 | 缓存未命中、类型错误和基础设施故障具有不同语义 |
+| H-3 | 原子票据消费 | `RedisValueStore.take()` 合并读取与删除,避免竞争窗口 |
+| H-4 | 有界 I/O 执行器 | Spring 统一管理线程数、队列和关闭生命周期 |
+| H-5 | 模块执行器自治 | 独立负载可使用命名执行器,不挤占通用 I/O 容量 |
+| H-6 | CallerRunsPolicy | 队列饱和时向调用方施加反压,避免静默丢任务 |
+| H-7 | 显式时间预算 | 聚合服务在调用点声明超时和降级结果 |
| H-8 | 操作日志敏感参数过滤 | 密码类字段自动替换为星号 |
| H-9 | AI 异常细分 | 5 种异常区分配置、调用、检索、图执行、输出解析 |
| H-10 | Result 统一响应 | 所有 API 响应格式一致,前端处理逻辑统一 |
@@ -571,10 +568,10 @@ getIpAddress(request):
| 想了解 | 读什么 |
| --- | --- |
-| 限流 | `ConcurrentUtils.java` — RateLimiter + SlidingWindowRateLimiter |
-| 熔断 | `ConcurrentUtils.java` — CircuitBreaker 三状态转换 |
-| 线程池 | `ThreadPoolUtils.java` — 4 个预配置池 + 自定义池 + 并行执行 |
-| 重试 | `ThreadPoolUtils.java` — 指数退避 + 条件重试 |
+| Redis 标量值和计数器 | `RedisValueStore.java` — 类型检查、TTL、原子获取并删除 |
+| 通用 I/O 执行器 | `ApplicationTaskExecutorConfig.java` — 有界线程池和关闭策略 |
+| 模块专用并发 | 对应模块配置类,例如 `MoyuConfig.hotTopicExecutor` |
+| 分布式集合和锁 | 业务服务直接使用 Redisson API,保留领域语义 |
| 双端鉴权 | `StpUserUtil.java` + `StpAdminUtil.java` + `SaTokenConfig.java` → 详见 [鉴权与用户体系](/modules/auth) |
| 权限加载 | `StpInterfaceImpl.java` — 管理端 RBAC + 用户端空列表 → 详见 [权限注解与角色边界索引](/reference/permission-boundaries) |
| 管理端拦截 | `RequireAdmin.java` + `AdminAuthAspect.java` → 详见 [权限注解与角色边界索引](/reference/permission-boundaries) |
diff --git a/docs-site/modules/moyu.md b/docs-site/modules/moyu.md
index a320efa73..aaec501ac 100644
--- a/docs-site/modules/moyu.md
+++ b/docs-site/modules/moyu.md
@@ -405,7 +405,7 @@ private String baseUrl;
| 应用启动 1 分钟后 | `initializeHotTopicDataIfNeeded` | 串行请求,100ms 间隔,只补缺失缓存 |
| 每 15 分钟 | `refreshHotTopicData` | 并行请求,30s 超时,覆盖式更新 |
-**关键发现**:初始化使用**串行请求**(避免启动并发压力),刷新使用**并行请求**(ThreadPoolUtils + 超时控制)。设计合理,但初始化在平台数量多时可能需要较长时间。
+**关键发现**:初始化使用**串行请求**(避免启动并发压力),刷新使用模块自有的 `hotTopicExecutor` 并行请求并配合超时控制。设计合理,但初始化在平台数量多时可能需要较长时间。
#### 4.4 `HotTopicEnum` 平台枚举
diff --git a/docs-site/modules/utils.md b/docs-site/modules/utils.md
index 0269570b6..ab57e2d64 100644
--- a/docs-site/modules/utils.md
+++ b/docs-site/modules/utils.md
@@ -1,14 +1,16 @@
# 工具类详解
-公共底座(`xiaou-common`)提供了 14 个工具类,覆盖并发控制、线程池管理、Redis 操作、JSON 处理、敏感词检测、通知发送、分页查询等基础能力。本文档详细解析每个工具类的功能、核心方法、使用示例和注意事项。
+公共底座(`xiaou-common`)提供统一响应、缓存、通知、分页和通用数据处理能力。v2.4.1 将线程资源改为 Spring 托管的有界执行器,并将 Redis 能力收敛为类型化值存储;集合、排序集合和分布式锁由所属业务模块直接管理。
## 工具类总览
| 工具类 | 行数 | 职责 | 核心能力 |
|--------|------|------|---------|
-| `ConcurrentUtils` | 1043 | 并发工具集 | 限流器、熔断器、分段锁、计数器、同步屏障 |
-| `ThreadPoolUtils` | 1206 | 线程池工具 | 线程池管理、异步任务、批量并行、超时控制、重试机制 |
-| `RedisUtil` | 872 | Redis 工具 | String、Hash、List、Set、Sorted Set、分布式锁 |
+| `RedisValueStore` | 112 | 类型化 Redis 值存储 | `Optional` 查询、原子获取并删除、计数器、TTL |
+| `ApplicationTaskExecutorConfig` | 30 | 应用 I/O 执行器 | Spring 生命周期、有界队列、统一拒绝和关闭策略 |
+| `ConcurrentUtils` | 1043 | 历史实现,v2.4.1 已移除 | 旧版限流、熔断和进程内并发原语 |
+| `ThreadPoolUtils` | 1206 | 历史实现,v2.4.1 已移除 | 旧版静态线程池和异步封装 |
+| `RedisUtil` | 872 | 历史实现,v2.4.1 已移除 | 旧版全功能 Redis 门面 |
| `NotificationUtil` | 378 | 通知工具 | 系统公告、个人消息、模板消息、批量发送 |
| `SensitiveWordUtils` | 374 | 敏感词工具 | 敏感词检测、替换、批量检测、缓存 |
| `JsonUtils` | 280 | JSON 工具 | 对象序列化、反序列化、类型转换 |
@@ -23,9 +25,11 @@
---
-## 一、ConcurrentUtils(并发工具集)
+## 一、ConcurrentUtils(历史 API,v2.4.1 已移除)
-**源码**:`ConcurrentUtils.java`(1043 行)
+**历史源码**:`ConcurrentUtils.java`(1043 行)
+
+> 本节保留用于理解迁移前设计,示例不能用于当前代码。限流、熔断和锁现在由实际业务模块按自身语义实现,不再通过公共静态门面共享状态。
全站最大的工具类,提供限流、熔断、锁、计数器、延迟值等并发原语。
@@ -212,9 +216,11 @@ once.run(() -> {
---
-## 二、ThreadPoolUtils(线程池工具)
+## 二、ThreadPoolUtils(历史 API,v2.4.1 已移除)
+
+**历史源码**:`ThreadPoolUtils.java`(1206 行)
-**源码**:`ThreadPoolUtils.java`(1206 行)
+> 当前代码通过构造器注入 `@Qualifier("applicationIoExecutor") Executor`。摸鱼热榜等具有独立容量模型的任务继续使用模块自有执行器。
线程池管理工具,提供多种预配置线程池、异步任务执行、批量并行处理、超时控制、重试机制等功能。
@@ -412,9 +418,11 @@ String result = ThreadPoolUtils.asyncChain(() -> fetchData())
---
-## 三、RedisUtil(Redis 工具)
+## 三、RedisUtil(历史 API,v2.4.1 已移除)
+
+**历史源码**:`RedisUtil.java`(872 行)
-**源码**:`RedisUtil.java`(872 行)
+> 当前标量值、票据和计数器使用 `RedisValueStore`;Set、Sorted Set 和分布式锁由业务模块直接使用 Redisson API,以保留清晰的领域语义和失败策略。
基于 Redisson 客户端的 Redis 工具类,封装了 String、Hash、List、Set、Sorted Set、分布式锁等操作。
@@ -1084,12 +1092,12 @@ notificationCacheUtil.decrementUnreadCount(userId);
| 场景 | 推荐工具类 |
|------|-----------|
-| 限流 | `ConcurrentUtils.RateLimiter` 或 `SlidingWindowRateLimiter` |
-| 熔断 | `ConcurrentUtils.CircuitBreaker` |
-| 分布式锁 | `RedisUtil.getLock()` 或 `ConcurrentUtils.StripedLock` |
-| 异步任务 | `ThreadPoolUtils.supplyAsync()` |
-| 批量并行 | `ThreadPoolUtils.parallelMap()` |
-| Redis 操作 | `RedisUtil` |
+| Redis 标量值、票据、计数器 | `RedisValueStore` |
+| Redis Set、Sorted Set、分布式锁 | 所属模块直接使用 `RedissonClient` |
+| 通用 I/O 异步任务 | 注入 `applicationIoExecutor` |
+| 模块专用异步任务 | 使用模块自有执行器,例如 `hotTopicExecutor` |
+| 超时与降级 | 在调用服务内使用 `CompletableFuture` 明确声明时间预算 |
+| 限流和熔断 | 在业务服务内按实际状态模型实现 |
| 敏感词检测 | `SensitiveWordUtils.checkText()` |
| 发送通知 | `NotificationUtil.sendXxx()` |
| 分页查询 | `PageHelper.doPage()` |
@@ -1097,18 +1105,19 @@ notificationCacheUtil.decrementUnreadCount(userId);
### 2. 注意事项
-- **ConcurrentUtils.RateLimiter**:`acquire()` 方法使用忙等待,高并发时可能浪费 CPU
-- **RedisUtil**:所有操作都捕获异常并返回默认值,不会抛出异常
-- **ThreadPoolUtils**:所有线程池在 JVM 关闭时自动清理
+- **RedisValueStore**:缓存未命中返回空值,类型不匹配和 Redis 故障显式抛出
+- **可选缓存**:需要降级的调用方必须在自身边界捕获 Redis 异常并记录日志
+- **applicationIoExecutor**:队列和线程数有界,队列满时使用 `CallerRunsPolicy` 施加反压
+- **超时任务**:`CompletableFuture` 超时不会自动中断底层调用,调用方仍需为网络客户端配置超时
- **SensitiveWordUtils**:检测结果会缓存 5 分钟,避免重复检测
- **NotificationUtil**:发送失败只记录日志,不抛出异常
### 3. 性能优化
-- 使用 `ThreadPoolUtils.parallelMap()` 替代循环中的串行调用
-- 使用 `RedisUtil` 的批量操作替代循环中的单个操作
+- 仅对彼此独立的 I/O 调用使用 `CompletableFuture` 并行聚合,并设置整体时间预算
+- 对一次性票据使用 `RedisValueStore.take()`,避免读取和删除之间的竞争窗口
+- 对 Set、Sorted Set 和锁保留 Redisson 原生语义,避免通用门面掩盖原子性要求
- 使用 `SensitiveWordUtils.checkTextBatch()` 替代循环中的单个检测
-- 使用 `ConcurrentUtils.StripedLock` 替代全局锁,减少锁竞争
---
@@ -1120,4 +1129,4 @@ notificationCacheUtil.decrementUnreadCount(userId);
| [鉴权与用户体系](/modules/auth) | 被依赖 | 密码工具、IP 工具被鉴权模块使用 |
| [敏感词风控](/modules/sensitive) | 被依赖 | 敏感词工具被多个内容模块使用 |
| [通知中心](/modules/notification) | 被依赖 | 通知工具被多个模块使用 |
-| [积分与抽奖](/modules/points) | 被依赖 | Redis 工具、并发工具被积分模块使用 |
+| [积分与抽奖](/modules/points) | 被依赖 | 积分模块直接管理库存计数和分布式锁语义 |
diff --git a/vue3-admin-front/electron.vite.config.ts b/vue3-admin-front/electron.vite.config.ts
index 2f646f133..4d84632ff 100644
--- a/vue3-admin-front/electron.vite.config.ts
+++ b/vue3-admin-front/electron.vite.config.ts
@@ -34,12 +34,17 @@ export default defineConfig({
},
plugins: [vue()],
resolve: {
+ preserveSymlinks: true,
alias: {
+ '@/design-system': resolve(__dirname, 'node_modules/@code-nest/design-system/src'),
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
+ fs: {
+ allow: [resolve(__dirname), resolve(__dirname, '../code-nest-design-system')]
+ },
proxy: {
'/api': {
target: 'http://localhost:9999',
diff --git a/vue3-admin-front/package-lock.json b/vue3-admin-front/package-lock.json
index 79d73a93e..dbed52a2f 100644
--- a/vue3-admin-front/package-lock.json
+++ b/vue3-admin-front/package-lock.json
@@ -9,6 +9,7 @@
"version": "2.4.0",
"dependencies": {
"@antv/g6": "^4.8.24",
+ "@code-nest/design-system": "file:../code-nest-design-system",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.2",
"d3": "^7.8.5",
@@ -40,6 +41,16 @@
"vite": "^5.0.8"
}
},
+ "../code-nest-design-system": {
+ "name": "@code-nest/design-system",
+ "version": "2.4.0",
+ "peerDependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "element-plus": "^2.4.4",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5"
+ }
+ },
"node_modules/@ant-design/colors": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-4.0.5.tgz",
@@ -748,6 +759,10 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@code-nest/design-system": {
+ "resolved": "../code-nest-design-system",
+ "link": true
+ },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
diff --git a/vue3-admin-front/package.json b/vue3-admin-front/package.json
index 459b2fe6f..7c77c9749 100644
--- a/vue3-admin-front/package.json
+++ b/vue3-admin-front/package.json
@@ -16,6 +16,7 @@
"dist:win": "electron-vite build && electron-builder --win"
},
"dependencies": {
+ "@code-nest/design-system": "file:../code-nest-design-system",
"@antv/g6": "^4.8.24",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.2",
diff --git a/vue3-admin-front/src/design-system/tokens/motion.css b/vue3-admin-front/src/design-system/tokens/motion.css
deleted file mode 100644
index aa6b4841b..000000000
--- a/vue3-admin-front/src/design-system/tokens/motion.css
+++ /dev/null
@@ -1,7 +0,0 @@
-:root {
- --cn-motion-fast: 160ms;
- --cn-motion-base: 240ms;
- --cn-motion-slow: 360ms;
- --cn-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
- --cn-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
-}
diff --git a/vue3-admin-front/tsconfig.json b/vue3-admin-front/tsconfig.json
index 7d63b9bba..b04a2f029 100644
--- a/vue3-admin-front/tsconfig.json
+++ b/vue3-admin-front/tsconfig.json
@@ -18,6 +18,8 @@
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
+ "@/design-system": ["../code-nest-design-system/src/index.ts"],
+ "@/design-system/*": ["../code-nest-design-system/src/*"],
"@/*": ["src/*"]
}
},
@@ -26,6 +28,8 @@
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
+ "../code-nest-design-system/src/**/*.ts",
+ "../code-nest-design-system/src/**/*.vue",
"electron/**/*.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
diff --git a/vue3-admin-front/vite.config.js b/vue3-admin-front/vite.config.js
index 46d4fb198..434b94607 100644
--- a/vue3-admin-front/vite.config.js
+++ b/vue3-admin-front/vite.config.js
@@ -60,13 +60,18 @@ const buildManualChunks = (id) => {
export default defineConfig({
plugins: [vue()],
resolve: {
+ preserveSymlinks: true,
alias: {
+ '@/design-system': resolve(process.cwd(), 'node_modules/@code-nest/design-system/src'),
'@': resolve(process.cwd(), 'src'),
},
},
server: {
port: 3000,
open: true,
+ fs: {
+ allow: [resolve(process.cwd()), resolve(process.cwd(), '../code-nest-design-system')],
+ },
proxy: {
'/api': {
target: 'http://localhost:9999',
diff --git a/vue3-user-front/electron.vite.config.ts b/vue3-user-front/electron.vite.config.ts
index a76401183..f967f156c 100644
--- a/vue3-user-front/electron.vite.config.ts
+++ b/vue3-user-front/electron.vite.config.ts
@@ -34,12 +34,17 @@ export default defineConfig({
},
plugins: [vue()],
resolve: {
+ preserveSymlinks: true,
alias: {
+ '@/design-system': resolve(__dirname, 'node_modules/@code-nest/design-system/src'),
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3001,
+ fs: {
+ allow: [resolve(__dirname), resolve(__dirname, '../code-nest-design-system')]
+ },
proxy: {
'/api': {
target: 'http://localhost:9999',
diff --git a/vue3-user-front/package-lock.json b/vue3-user-front/package-lock.json
index bf4334bbc..a77e2dca2 100644
--- a/vue3-user-front/package-lock.json
+++ b/vue3-user-front/package-lock.json
@@ -9,6 +9,7 @@
"version": "2.4.0",
"dependencies": {
"@antv/g6": "^4.8.24",
+ "@code-nest/design-system": "file:../code-nest-design-system",
"@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0",
"axios": "^1.6.2",
@@ -40,6 +41,16 @@
"vite": "^5.0.8"
}
},
+ "../code-nest-design-system": {
+ "name": "@code-nest/design-system",
+ "version": "2.4.0",
+ "peerDependencies": {
+ "@element-plus/icons-vue": "^2.3.1",
+ "element-plus": "^2.4.4",
+ "vue": "^3.4.0",
+ "vue-router": "^4.2.5"
+ }
+ },
"node_modules/@ant-design/colors": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-4.0.5.tgz",
@@ -654,6 +665,10 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@code-nest/design-system": {
+ "resolved": "../code-nest-design-system",
+ "link": true
+ },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
diff --git a/vue3-user-front/package.json b/vue3-user-front/package.json
index ea40247c5..78f4aa999 100644
--- a/vue3-user-front/package.json
+++ b/vue3-user-front/package.json
@@ -16,6 +16,7 @@
"dist:win": "electron-vite build && electron-builder --win"
},
"dependencies": {
+ "@code-nest/design-system": "file:../code-nest-design-system",
"@antv/g6": "^4.8.24",
"@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0",
diff --git a/vue3-user-front/src/design-system/components/app/CnPage.vue b/vue3-user-front/src/design-system/components/app/CnPage.vue
deleted file mode 100644
index d8ef874ce..000000000
--- a/vue3-user-front/src/design-system/components/app/CnPage.vue
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
-
- {{ title }}
- {{ description }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/app/CnPageHeader.vue b/vue3-user-front/src/design-system/components/app/CnPageHeader.vue
deleted file mode 100644
index ea58a0adf..000000000
--- a/vue3-user-front/src/design-system/components/app/CnPageHeader.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/app/CnSection.vue b/vue3-user-front/src/design-system/components/app/CnSection.vue
deleted file mode 100644
index 224ad1785..000000000
--- a/vue3-user-front/src/design-system/components/app/CnSection.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnDataTable.vue b/vue3-user-front/src/design-system/components/data/CnDataTable.vue
deleted file mode 100644
index 3bb449496..000000000
--- a/vue3-user-front/src/design-system/components/data/CnDataTable.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-
-
- emit('selection-change', selection)"
- @sort-change="(payload: unknown) => emit('sort-change', payload)"
- @row-click="(row: unknown, column: unknown, event: Event) => emit('row-click', row, column, event)"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnEmptyState.vue b/vue3-user-front/src/design-system/components/data/CnEmptyState.vue
deleted file mode 100644
index 8014282f7..000000000
--- a/vue3-user-front/src/design-system/components/data/CnEmptyState.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
-
- {{ icon }}
-
-
-
-
{{ title }}
-
{{ description }}
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnFilterForm.vue b/vue3-user-front/src/design-system/components/data/CnFilterForm.vue
deleted file mode 100644
index f1e3dd60a..000000000
--- a/vue3-user-front/src/design-system/components/data/CnFilterForm.vue
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
-
- setFieldValue(field.prop, value)"
- @keyup.enter="emitSearch"
- />
-
- setFieldValue(field.prop, value)"
- >
-
-
-
- setFieldValue(field.prop, value)"
- />
-
- setFieldValue(field.prop, value)"
- />
-
- setFieldValue(field.prop, value)"
- />
-
-
-
- {{ searchText }}
- {{ resetText }}
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnStatCard.vue b/vue3-user-front/src/design-system/components/data/CnStatCard.vue
deleted file mode 100644
index 6c2448e27..000000000
--- a/vue3-user-front/src/design-system/components/data/CnStatCard.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{ value }}
- {{ unit }}
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnStatusTag.vue b/vue3-user-front/src/design-system/components/data/CnStatusTag.vue
deleted file mode 100644
index d13da87ab..000000000
--- a/vue3-user-front/src/design-system/components/data/CnStatusTag.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-
-
-
-
- {{ label }}
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/data/CnToolbar.vue b/vue3-user-front/src/design-system/components/data/CnToolbar.vue
deleted file mode 100644
index 91f605292..000000000
--- a/vue3-user-front/src/design-system/components/data/CnToolbar.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/navigation/CnCommandPalette.vue b/vue3-user-front/src/design-system/components/navigation/CnCommandPalette.vue
deleted file mode 100644
index 98a3f37fc..000000000
--- a/vue3-user-front/src/design-system/components/navigation/CnCommandPalette.vue
+++ /dev/null
@@ -1,373 +0,0 @@
-
-
-
-
-
- {{ title }}
- {{ description }}
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/navigation/CnSidebar.vue b/vue3-user-front/src/design-system/components/navigation/CnSidebar.vue
deleted file mode 100644
index 872a6a404..000000000
--- a/vue3-user-front/src/design-system/components/navigation/CnSidebar.vue
+++ /dev/null
@@ -1,373 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/navigation/CnSidebarItem.vue b/vue3-user-front/src/design-system/components/navigation/CnSidebarItem.vue
deleted file mode 100644
index 463daae8f..000000000
--- a/vue3-user-front/src/design-system/components/navigation/CnSidebarItem.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
- {{ item.label }}
-
-
-
-
-
-
-
- {{ item.label }}
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/navigation/CnTopNav.vue b/vue3-user-front/src/design-system/components/navigation/CnTopNav.vue
deleted file mode 100644
index 07c08cd33..000000000
--- a/vue3-user-front/src/design-system/components/navigation/CnTopNav.vue
+++ /dev/null
@@ -1,955 +0,0 @@
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/theme/CnThemeDrawer.vue b/vue3-user-front/src/design-system/components/theme/CnThemeDrawer.vue
deleted file mode 100644
index b3d2e2dbf..000000000
--- a/vue3-user-front/src/design-system/components/theme/CnThemeDrawer.vue
+++ /dev/null
@@ -1,493 +0,0 @@
-
-
-
-
-
-
-
-
-
-
{{ resolvedThemeLabel }}
-
-
-
-
-
-
-
-
-
-
-
当前状态
-
主题状态由 design-system 统一写入文档根节点。
-
-
-
-
-
-
- 偏好模式
- - {{ activeModeLabel }}
-
-
-
- 解析主题
- - {{ resolvedThemeLabel }}
-
-
-
- 色彩方案
- - {{ isDark ? '暗色' : '亮色' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/components/theme/CnThemeSwitch.vue b/vue3-user-front/src/design-system/components/theme/CnThemeSwitch.vue
deleted file mode 100644
index e49fa1dda..000000000
--- a/vue3-user-front/src/design-system/components/theme/CnThemeSwitch.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/vue3-user-front/src/design-system/composables/useCnBreakpoints.ts b/vue3-user-front/src/design-system/composables/useCnBreakpoints.ts
deleted file mode 100644
index 883747026..000000000
--- a/vue3-user-front/src/design-system/composables/useCnBreakpoints.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
-
-export interface UseCnBreakpointsOptions {
- mobile?: number
- tablet?: number
- desktop?: number
- immediate?: boolean
-}
-
-export function useCnBreakpoints(options: UseCnBreakpointsOptions = {}) {
- const breakpoints = reactive({
- mobile: options.mobile ?? 768,
- tablet: options.tablet ?? 1024,
- desktop: options.desktop ?? 1280
- })
-
- const width = ref(getViewportWidth())
-
- const isMobile = computed(() => width.value > 0 && width.value <= breakpoints.mobile)
- const isTablet = computed(() => width.value > breakpoints.mobile && width.value <= breakpoints.tablet)
- const isDesktop = computed(() => width.value > breakpoints.tablet)
- const isWide = computed(() => width.value >= breakpoints.desktop)
-
- function update() {
- width.value = getViewportWidth()
- }
-
- onMounted(() => {
- if (options.immediate !== false) {
- update()
- }
-
- window.addEventListener('resize', update, { passive: true })
- })
-
- onBeforeUnmount(() => {
- window.removeEventListener('resize', update)
- })
-
- return {
- width,
- breakpoints,
- isMobile,
- isTablet,
- isDesktop,
- isWide,
- update
- }
-}
-
-function getViewportWidth() {
- return typeof window === 'undefined' ? 0 : window.innerWidth
-}
diff --git a/vue3-user-front/src/design-system/composables/useCnTable.ts b/vue3-user-front/src/design-system/composables/useCnTable.ts
deleted file mode 100644
index c1e0cb668..000000000
--- a/vue3-user-front/src/design-system/composables/useCnTable.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { computed, reactive, ref, toRaw, type Ref } from 'vue'
-import type { CnPagination } from '../types/components'
-
-type CnTableQuery = Record
-
-export type CnTableResultData =
- | T[]
- | {
- records?: T[]
- list?: T[]
- rows?: T[]
- total?: number
- [key: string]: unknown
- }
-
-export type CnTableFetcherResult =
- | CnTableResultData
- | {
- data?: CnTableResultData
- records?: T[]
- list?: T[]
- rows?: T[]
- total?: number
- [key: string]: unknown
- }
-
-export interface UseCnTableOptions {
- query?: Q
- page?: number
- pageSize?: number
- pageSizes?: number[]
- immediate?: boolean
- fetcher: (params: Q & { pageNum: number; pageSize: number }) => Promise>
-}
-
-export function useCnTable(
- options: UseCnTableOptions
-) {
- const loading = ref(false)
- const error = ref(null)
- const rows = ref([]) as Ref
- const query = reactive({ ...(options.query ?? {}) }) as Q
- const pagination = reactive({
- page: options.page ?? 1,
- pageSize: options.pageSize ?? 10,
- total: 0,
- pageSizes: options.pageSizes ?? [10, 20, 50, 100]
- })
-
- const tablePagination = computed(() => ({
- page: pagination.page,
- pageSize: pagination.pageSize,
- total: pagination.total,
- pageSizes: pagination.pageSizes
- }))
-
- const isEmpty = computed(() => !loading.value && rows.value.length === 0)
-
- async function reload() {
- loading.value = true
- error.value = null
-
- try {
- const params = {
- ...(toRaw(query) as Q),
- pageNum: pagination.page,
- pageSize: pagination.pageSize
- } as Q & { pageNum: number; pageSize: number }
-
- const result = await options.fetcher(params)
- const nextRows = readRows(result)
- rows.value = nextRows
- pagination.total = readTotal(result, nextRows.length)
- return result
- } catch (caughtError) {
- error.value = caughtError
- throw caughtError
- } finally {
- loading.value = false
- }
- }
-
- function setQuery(nextQuery: Partial) {
- Object.assign(query, nextQuery)
- }
-
- function search(nextQuery?: Partial) {
- if (nextQuery) {
- setQuery(nextQuery)
- }
-
- pagination.page = 1
- return reload()
- }
-
- function reset(nextQuery?: Partial) {
- for (const key of Object.keys(query as CnTableQuery)) {
- delete (query as CnTableQuery)[key]
- }
-
- Object.assign(query, options.query ?? {}, nextQuery ?? {})
- pagination.page = 1
- return reload()
- }
-
- function setPage(page: number) {
- pagination.page = page
- return reload()
- }
-
- function setPageSize(pageSize: number) {
- pagination.pageSize = pageSize
- pagination.page = 1
- return reload()
- }
-
- if (options.immediate !== false) {
- void reload().catch(() => undefined)
- }
-
- return {
- loading,
- error,
- rows,
- data: rows,
- query,
- pagination,
- tablePagination,
- isEmpty,
- reload,
- search,
- reset,
- setQuery,
- setPage,
- setPageSize
- }
-}
-
-function readRows(source: unknown): T[] {
- if (Array.isArray(source)) {
- return source as T[]
- }
-
- if (!isRecord(source)) {
- return []
- }
-
- if (Array.isArray(source.records)) {
- return source.records as T[]
- }
-
- if (Array.isArray(source.list)) {
- return source.list as T[]
- }
-
- if (Array.isArray(source.rows)) {
- return source.rows as T[]
- }
-
- return readRows(source.data)
-}
-
-function readTotal(source: unknown, fallback: number): number {
- if (Array.isArray(source)) {
- return source.length
- }
-
- if (!isRecord(source)) {
- return fallback
- }
-
- const total = Number(source.total)
- if (Number.isFinite(total)) {
- return total
- }
-
- return readTotal(source.data, fallback)
-}
-
-function isRecord(value: unknown): value is Record {
- return typeof value === 'object' && value !== null
-}
diff --git a/vue3-user-front/src/design-system/composables/useTheme.ts b/vue3-user-front/src/design-system/composables/useTheme.ts
deleted file mode 100644
index fb8652633..000000000
--- a/vue3-user-front/src/design-system/composables/useTheme.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import { computed, ref } from 'vue'
-import type { CodeNestThemeMode, CodeNestThemeName, CodeNestThemeOption } from '../types/theme'
-import { isCodeNestThemeMode } from '../types/theme'
-
-export const CODE_NEST_THEME_STORAGE_KEY = 'code-nest-theme'
-
-const themeMode = ref('system')
-const resolvedTheme = ref('light')
-let systemThemeListenerReady = false
-
-export const codeNestThemeOptions: CodeNestThemeOption[] = [
- {
- value: 'system',
- label: '跟随系统',
- description: '自动匹配系统亮色或暗色'
- },
- {
- value: 'light',
- label: '亮色',
- description: '明亮清爽的默认主题'
- },
- {
- value: 'dark',
- label: '暗色',
- description: '低亮度工作环境'
- },
- {
- value: 'professional-blue',
- label: '专业蓝',
- description: '适合管理后台的信息密集主题'
- },
- {
- value: 'growth',
- label: '成长绿',
- description: '适合学习成长场景的轻盈主题'
- },
- {
- value: 'high-contrast',
- label: '高对比',
- description: '强化文字、边框和焦点识别'
- }
-]
-
-function getSystemTheme(): CodeNestThemeName {
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
- return 'light'
- }
-
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
-}
-
-function resolveTheme(mode: CodeNestThemeMode): CodeNestThemeName {
- return mode === 'system' ? getSystemTheme() : mode
-}
-
-function syncDocumentTheme(mode: CodeNestThemeMode, theme: CodeNestThemeName) {
- if (typeof document === 'undefined') {
- return
- }
-
- const root = document.documentElement
- root.dataset.theme = theme
- root.dataset.themeMode = mode
- root.style.colorScheme = theme === 'dark' ? 'dark' : 'light'
-}
-
-function readStoredTheme(): CodeNestThemeMode {
- if (typeof window === 'undefined') {
- return 'system'
- }
-
- const storedTheme = window.localStorage.getItem(CODE_NEST_THEME_STORAGE_KEY)
- return isCodeNestThemeMode(storedTheme) ? storedTheme : 'system'
-}
-
-function writeStoredTheme(mode: CodeNestThemeMode) {
- if (typeof window === 'undefined') {
- return
- }
-
- window.localStorage.setItem(CODE_NEST_THEME_STORAGE_KEY, mode)
-}
-
-function applyTheme(mode: CodeNestThemeMode, options: { persist?: boolean } = {}) {
- themeMode.value = mode
- resolvedTheme.value = resolveTheme(mode)
- syncDocumentTheme(mode, resolvedTheme.value)
-
- if (options.persist !== false) {
- writeStoredTheme(mode)
- }
-}
-
-function registerSystemThemeListener() {
- if (
- systemThemeListenerReady ||
- typeof window === 'undefined' ||
- typeof window.matchMedia !== 'function'
- ) {
- return
- }
-
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
- const handleSystemThemeChange = () => {
- if (themeMode.value === 'system') {
- applyTheme('system', { persist: false })
- }
- }
-
- if (typeof mediaQuery.addEventListener === 'function') {
- mediaQuery.addEventListener('change', handleSystemThemeChange)
- } else {
- mediaQuery.addListener(handleSystemThemeChange)
- }
-
- systemThemeListenerReady = true
-}
-
-function initializeTheme() {
- applyTheme(readStoredTheme(), { persist: false })
- registerSystemThemeListener()
-}
-
-function setTheme(mode: CodeNestThemeMode) {
- if (!isCodeNestThemeMode(mode)) {
- return
- }
-
- applyTheme(mode)
-}
-
-function toggleDark() {
- setTheme(resolvedTheme.value === 'dark' ? 'light' : 'dark')
-}
-
-const isDark = computed(() => resolvedTheme.value === 'dark')
-const followSystem = computed(() => themeMode.value === 'system')
-
-export function useTheme() {
- return {
- themeMode,
- resolvedTheme,
- isDark,
- followSystem,
- options: codeNestThemeOptions,
- initializeTheme,
- setTheme,
- toggleDark
- }
-}
diff --git a/vue3-user-front/src/design-system/index.ts b/vue3-user-front/src/design-system/index.ts
deleted file mode 100644
index e08cafe95..000000000
--- a/vue3-user-front/src/design-system/index.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-export { useTheme } from './composables/useTheme'
-export { useCnBreakpoints } from './composables/useCnBreakpoints'
-export { useCnTable } from './composables/useCnTable'
-export { default as CnPage } from './components/app/CnPage.vue'
-export { default as CnPageHeader } from './components/app/CnPageHeader.vue'
-export { default as CnSection } from './components/app/CnSection.vue'
-export { default as CnStatCard } from './components/data/CnStatCard.vue'
-export { default as CnStatusTag } from './components/data/CnStatusTag.vue'
-export { default as CnEmptyState } from './components/data/CnEmptyState.vue'
-export { default as CnToolbar } from './components/data/CnToolbar.vue'
-export { default as CnFilterForm } from './components/data/CnFilterForm.vue'
-export { default as CnDataTable } from './components/data/CnDataTable.vue'
-export { default as CnCommandPalette } from './components/navigation/CnCommandPalette.vue'
-export { default as CnSidebar } from './components/navigation/CnSidebar.vue'
-export { default as CnTopNav } from './components/navigation/CnTopNav.vue'
-export { default as CnThemeSwitch } from './components/theme/CnThemeSwitch.vue'
-export { default as CnThemeDrawer } from './components/theme/CnThemeDrawer.vue'
-export type {
- CnBreadcrumbItem,
- CnCommandItem,
- CnCommandPaletteProps,
- CnCommandSection,
- CnDataTableProps,
- CnEmptyStateProps,
- CnFilterField,
- CnFilterFieldType,
- CnFilterFormProps,
- CnFilterOption,
- CnPageHeaderProps,
- CnPageProps,
- CnPagination,
- CnSectionProps,
- CnSize,
- CnSidebarIcon,
- CnSidebarItem,
- CnSidebarProps,
- CnSidebarSearchResult,
- CnStatCardProps,
- CnStatusTagProps,
- CnSurface,
- CnTableColumn,
- CnTableColumnType,
- CnThemeDrawerProps,
- CnTopNavDropdown,
- CnTopNavGroup,
- CnTopNavItem,
- CnTopNavMobileSection,
- CnTopNavProps,
- CnTopNavUser,
- CnTopNavUserAction,
- CnTone,
- CnToolbarProps,
- CnTrend
-} from './types/components'
-export type {
- CodeNestThemeMode,
- CodeNestThemeName,
- CodeNestThemeOption,
- ThemePreference
-} from './types/theme'
-export type {
- CnTableFetcherResult,
- CnTableResultData,
- UseCnTableOptions
-} from './composables/useCnTable'
-export type { UseCnBreakpointsOptions } from './composables/useCnBreakpoints'
diff --git a/vue3-user-front/src/design-system/plugins/element-plus.css b/vue3-user-front/src/design-system/plugins/element-plus.css
deleted file mode 100644
index 780ba9ceb..000000000
--- a/vue3-user-front/src/design-system/plugins/element-plus.css
+++ /dev/null
@@ -1,41 +0,0 @@
-:root,
-[data-theme] {
- --el-color-primary: var(--cn-color-brand-primary);
- --el-color-primary-dark-2: var(--cn-color-brand-hover);
- --el-color-primary-light-3: color-mix(in srgb, var(--cn-color-brand-primary) 70%, white);
- --el-color-primary-light-5: color-mix(in srgb, var(--cn-color-brand-primary) 50%, white);
- --el-color-primary-light-7: color-mix(in srgb, var(--cn-color-brand-primary) 30%, white);
- --el-color-primary-light-8: color-mix(in srgb, var(--cn-color-brand-primary) 20%, white);
- --el-color-primary-light-9: color-mix(in srgb, var(--cn-color-brand-primary) 12%, white);
- --el-color-success: var(--cn-color-success);
- --el-color-warning: var(--cn-color-warning);
- --el-color-danger: var(--cn-color-danger);
-
- --el-bg-color: var(--cn-color-bg-surface);
- --el-bg-color-page: var(--cn-color-bg-page);
- --el-bg-color-overlay: var(--cn-color-bg-elevated);
- --el-text-color-primary: var(--cn-color-text-primary);
- --el-text-color-regular: var(--cn-color-text-secondary);
- --el-text-color-secondary: var(--cn-color-text-tertiary);
- --el-border-color: var(--cn-color-border);
- --el-border-color-light: var(--cn-color-border-subtle);
- --el-border-color-lighter: var(--cn-color-border-subtle);
- --el-border-radius-base: var(--cn-radius-control);
- --el-box-shadow-light: var(--cn-shadow-card);
- --el-fill-color-blank: var(--cn-color-bg-surface);
- --el-fill-color-light: var(--cn-color-bg-surface-muted);
- --el-mask-color: color-mix(in srgb, var(--cn-color-text-primary) 42%, transparent);
-}
-
-[data-theme='dark'] {
- --el-color-primary-light-3: var(--cn-color-brand-hover);
- --el-color-primary-light-5: var(--cn-color-brand-primary);
- --el-color-primary-light-7: color-mix(in srgb, var(--cn-color-brand-primary) 28%, transparent);
- --el-color-primary-light-8: color-mix(in srgb, var(--cn-color-brand-primary) 20%, transparent);
- --el-color-primary-light-9: color-mix(in srgb, var(--cn-color-brand-primary) 14%, transparent);
- --el-fill-color: var(--cn-color-bg-surface-muted);
- --el-fill-color-light: var(--cn-color-bg-elevated);
- --el-fill-color-lighter: var(--cn-color-border-subtle);
- --el-fill-color-extra-light: var(--cn-color-bg-surface);
- --el-mask-color: color-mix(in srgb, black 62%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/themes/dark.css b/vue3-user-front/src/design-system/themes/dark.css
deleted file mode 100644
index 5a3c12de9..000000000
--- a/vue3-user-front/src/design-system/themes/dark.css
+++ /dev/null
@@ -1,24 +0,0 @@
-[data-theme='dark'] {
- color-scheme: dark;
- --cn-color-bg-page: #0f172a;
- --cn-color-bg-canvas: #111827;
- --cn-color-bg-surface: #111827;
- --cn-color-bg-surface-muted: #172033;
- --cn-color-bg-elevated: #1f2937;
- --cn-color-text-primary: #e5e7eb;
- --cn-color-text-secondary: #b6c2d2;
- --cn-color-text-tertiary: #8b98aa;
- --cn-color-border: #334155;
- --cn-color-border-subtle: #263244;
- --cn-color-brand-primary: #60a5fa;
- --cn-color-brand-hover: #93c5fd;
- --cn-color-brand-soft: color-mix(in srgb, var(--cn-color-brand-primary) 16%, transparent);
- --cn-color-focus-ring: color-mix(in srgb, var(--cn-color-brand-primary) 22%, transparent);
- --cn-shadow-xs: 0 2px 8px color-mix(in srgb, black 22%, transparent);
- --cn-shadow-sm: 0 8px 24px color-mix(in srgb, black 28%, transparent);
- --cn-shadow-md: 0 14px 36px color-mix(in srgb, black 36%, transparent);
- --cn-shadow-card:
- 0 1px 0 color-mix(in srgb, white 4%, transparent),
- 0 16px 34px color-mix(in srgb, black 28%, transparent);
- --cn-shadow-popover: 0 18px 42px color-mix(in srgb, black 42%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/themes/growth.css b/vue3-user-front/src/design-system/themes/growth.css
deleted file mode 100644
index 9776ec4d7..000000000
--- a/vue3-user-front/src/design-system/themes/growth.css
+++ /dev/null
@@ -1,22 +0,0 @@
-[data-theme='growth'] {
- color-scheme: light;
- --cn-color-bg-page: #f3fbf7;
- --cn-color-bg-canvas: #e4f7ed;
- --cn-color-bg-surface: #ffffff;
- --cn-color-bg-surface-muted: #f7fcfa;
- --cn-color-bg-elevated: #ffffff;
- --cn-color-text-primary: #173326;
- --cn-color-text-secondary: #4f6c5e;
- --cn-color-text-tertiary: #809387;
- --cn-color-border: #cfe3d8;
- --cn-color-border-subtle: #e4efe9;
- --cn-color-brand-primary: #16a34a;
- --cn-color-brand-hover: #15803d;
- --cn-color-brand-soft: #dcfce7;
- --cn-color-focus-ring: color-mix(in srgb, var(--cn-color-brand-primary) 18%, transparent);
- --cn-shadow-xs: 0 2px 8px color-mix(in srgb, var(--cn-color-text-primary) 4%, transparent);
- --cn-shadow-sm: 0 8px 24px color-mix(in srgb, var(--cn-color-text-primary) 6%, transparent);
- --cn-shadow-md: 0 14px 36px color-mix(in srgb, var(--cn-color-text-primary) 10%, transparent);
- --cn-shadow-card: var(--cn-shadow-xs);
- --cn-shadow-popover: 0 18px 42px color-mix(in srgb, var(--cn-color-text-primary) 16%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/themes/high-contrast.css b/vue3-user-front/src/design-system/themes/high-contrast.css
deleted file mode 100644
index d908db866..000000000
--- a/vue3-user-front/src/design-system/themes/high-contrast.css
+++ /dev/null
@@ -1,22 +0,0 @@
-[data-theme='high-contrast'] {
- color-scheme: light;
- --cn-color-bg-page: #ffffff;
- --cn-color-bg-canvas: #f3f4f6;
- --cn-color-bg-surface: #ffffff;
- --cn-color-bg-surface-muted: #f9fafb;
- --cn-color-bg-elevated: #ffffff;
- --cn-color-text-primary: #000000;
- --cn-color-text-secondary: #1f2937;
- --cn-color-text-tertiary: #374151;
- --cn-color-border: #111827;
- --cn-color-border-subtle: #4b5563;
- --cn-color-brand-primary: #0047ff;
- --cn-color-brand-hover: #002ea6;
- --cn-color-brand-soft: #e8efff;
- --cn-color-focus-ring: color-mix(in srgb, var(--cn-color-brand-primary) 28%, transparent);
- --cn-shadow-xs: 0 2px 8px color-mix(in srgb, var(--cn-color-text-primary) 4%, transparent);
- --cn-shadow-sm: 0 8px 24px color-mix(in srgb, var(--cn-color-text-primary) 6%, transparent);
- --cn-shadow-md: 0 14px 36px color-mix(in srgb, var(--cn-color-text-primary) 10%, transparent);
- --cn-shadow-card: var(--cn-shadow-xs);
- --cn-shadow-popover: 0 18px 42px color-mix(in srgb, var(--cn-color-text-primary) 16%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/themes/light.css b/vue3-user-front/src/design-system/themes/light.css
deleted file mode 100644
index e6298cfef..000000000
--- a/vue3-user-front/src/design-system/themes/light.css
+++ /dev/null
@@ -1,23 +0,0 @@
-:root,
-[data-theme='light'] {
- color-scheme: light;
- --cn-color-bg-page: #f6f8fb;
- --cn-color-bg-canvas: #eaf0f9;
- --cn-color-bg-surface: #ffffff;
- --cn-color-bg-surface-muted: #f8fbff;
- --cn-color-bg-elevated: #ffffff;
- --cn-color-text-primary: #172033;
- --cn-color-text-secondary: #536179;
- --cn-color-text-tertiary: #7b879a;
- --cn-color-border: #d8e0ec;
- --cn-color-border-subtle: #e7ecf4;
- --cn-color-brand-primary: #2563eb;
- --cn-color-brand-hover: #1d4ed8;
- --cn-color-brand-soft: #eaf1ff;
- --cn-color-focus-ring: color-mix(in srgb, var(--cn-color-brand-primary) 16%, transparent);
- --cn-shadow-xs: 0 2px 8px color-mix(in srgb, var(--cn-color-text-primary) 4%, transparent);
- --cn-shadow-sm: 0 8px 24px color-mix(in srgb, var(--cn-color-text-primary) 6%, transparent);
- --cn-shadow-md: 0 14px 36px color-mix(in srgb, var(--cn-color-text-primary) 10%, transparent);
- --cn-shadow-card: var(--cn-shadow-xs);
- --cn-shadow-popover: 0 18px 42px color-mix(in srgb, var(--cn-color-text-primary) 16%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/themes/professional-blue.css b/vue3-user-front/src/design-system/themes/professional-blue.css
deleted file mode 100644
index 1be76f488..000000000
--- a/vue3-user-front/src/design-system/themes/professional-blue.css
+++ /dev/null
@@ -1,22 +0,0 @@
-[data-theme='professional-blue'] {
- color-scheme: light;
- --cn-color-bg-page: #edf4ff;
- --cn-color-bg-canvas: #dbeafe;
- --cn-color-bg-surface: #ffffff;
- --cn-color-bg-surface-muted: #f3f8ff;
- --cn-color-bg-elevated: #ffffff;
- --cn-color-text-primary: #10233f;
- --cn-color-text-secondary: #3d587d;
- --cn-color-text-tertiary: #6e84a3;
- --cn-color-border: #bfd1ec;
- --cn-color-border-subtle: #dbe6f6;
- --cn-color-brand-primary: #1d4ed8;
- --cn-color-brand-hover: #1e40af;
- --cn-color-brand-soft: #dbeafe;
- --cn-color-focus-ring: color-mix(in srgb, var(--cn-color-brand-primary) 18%, transparent);
- --cn-shadow-xs: 0 2px 8px color-mix(in srgb, var(--cn-color-text-primary) 4%, transparent);
- --cn-shadow-sm: 0 8px 24px color-mix(in srgb, var(--cn-color-text-primary) 6%, transparent);
- --cn-shadow-md: 0 14px 36px color-mix(in srgb, var(--cn-color-text-primary) 10%, transparent);
- --cn-shadow-card: var(--cn-shadow-xs);
- --cn-shadow-popover: 0 18px 42px color-mix(in srgb, var(--cn-color-text-primary) 16%, transparent);
-}
diff --git a/vue3-user-front/src/design-system/tokens/component.css b/vue3-user-front/src/design-system/tokens/component.css
deleted file mode 100644
index b3d0b701c..000000000
--- a/vue3-user-front/src/design-system/tokens/component.css
+++ /dev/null
@@ -1,29 +0,0 @@
-:root,
-[data-theme] {
- --cn-card-bg: var(--cn-color-bg-surface);
- --cn-card-border: var(--cn-color-border-subtle);
- --cn-card-radius: var(--cn-radius-card);
- --cn-card-shadow: var(--cn-shadow-card);
-
- --cn-panel-bg: var(--cn-color-bg-elevated);
- --cn-panel-border: var(--cn-color-border-subtle);
- --cn-panel-radius: var(--cn-radius-panel);
-
- --cn-button-radius: var(--cn-radius-control);
- --cn-button-primary-bg: var(--cn-color-brand-primary);
- --cn-button-primary-hover-bg: var(--cn-color-brand-hover);
- --cn-button-primary-color: #ffffff;
-
- --cn-input-bg: var(--cn-color-bg-surface);
- --cn-input-border: var(--cn-color-border);
- --cn-input-hover-border: color-mix(in srgb, var(--cn-color-brand-primary) 36%, var(--cn-color-border));
- --cn-input-focus-border: var(--cn-color-brand-primary);
-
- --cn-table-header-bg: var(--cn-color-bg-surface-muted);
- --cn-table-row-hover-bg: var(--cn-color-brand-soft);
- --cn-table-border: var(--cn-color-border-subtle);
-
- --cn-nav-bg: color-mix(in srgb, var(--cn-color-bg-surface) 88%, transparent);
- --cn-nav-active-bg: var(--cn-color-brand-soft);
- --cn-nav-active-color: var(--cn-color-brand-primary);
-}
diff --git a/vue3-user-front/src/design-system/tokens/primitive.css b/vue3-user-front/src/design-system/tokens/primitive.css
deleted file mode 100644
index 4bfdd6218..000000000
--- a/vue3-user-front/src/design-system/tokens/primitive.css
+++ /dev/null
@@ -1,47 +0,0 @@
-:root {
- --cn-font-sans: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
- --cn-font-heading: 'Poppins', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
-
- --cn-blue-50: #eff6ff;
- --cn-blue-100: #dbeafe;
- --cn-blue-200: #bfdbfe;
- --cn-blue-300: #93c5fd;
- --cn-blue-400: #60a5fa;
- --cn-blue-500: #3b82f6;
- --cn-blue-600: #2563eb;
- --cn-blue-700: #1d4ed8;
- --cn-blue-800: #1e40af;
- --cn-blue-900: #1e3a8a;
-
- --cn-slate-50: #f8fafc;
- --cn-slate-100: #f1f5f9;
- --cn-slate-200: #e2e8f0;
- --cn-slate-300: #cbd5e1;
- --cn-slate-400: #94a3b8;
- --cn-slate-500: #64748b;
- --cn-slate-600: #475569;
- --cn-slate-700: #334155;
- --cn-slate-800: #1e293b;
- --cn-slate-900: #0f172a;
-
- --cn-green-500: #22c55e;
- --cn-green-600: #16a34a;
- --cn-amber-500: #f59e0b;
- --cn-amber-600: #d97706;
- --cn-red-500: #ef4444;
- --cn-red-600: #dc2626;
-
- --cn-space-1: 4px;
- --cn-space-2: 8px;
- --cn-space-3: 12px;
- --cn-space-4: 16px;
- --cn-space-5: 20px;
- --cn-space-6: 24px;
- --cn-space-8: 32px;
- --cn-space-10: 40px;
-
- --cn-radius-control: 8px;
- --cn-radius-card: 12px;
- --cn-radius-panel: 16px;
- --cn-radius-pill: 999px;
-}
diff --git a/vue3-user-front/src/design-system/tokens/semantic.css b/vue3-user-front/src/design-system/tokens/semantic.css
deleted file mode 100644
index c0fc6a4b6..000000000
--- a/vue3-user-front/src/design-system/tokens/semantic.css
+++ /dev/null
@@ -1,34 +0,0 @@
-:root,
-[data-theme] {
- --cn-color-success: var(--cn-green-600);
- --cn-color-success-soft: color-mix(in srgb, var(--cn-color-success) 12%, white);
- --cn-color-warning: var(--cn-amber-600);
- --cn-color-warning-soft: color-mix(in srgb, var(--cn-color-warning) 12%, white);
- --cn-color-danger: var(--cn-red-600);
- --cn-color-danger-soft: color-mix(in srgb, var(--cn-color-danger) 12%, white);
- --cn-color-info: var(--cn-blue-600);
- --cn-color-info-soft: var(--cn-blue-50);
-}
-
-:root,
-[data-theme] {
- --cn-bg-page: var(--cn-color-bg-page);
- --cn-bg-canvas: var(--cn-color-bg-canvas);
- --cn-surface-1: var(--cn-color-bg-surface);
- --cn-surface-2: var(--cn-color-bg-surface-muted);
- --cn-text-primary: var(--cn-color-text-primary);
- --cn-text-secondary: var(--cn-color-text-secondary);
- --cn-text-tertiary: var(--cn-color-text-tertiary);
- --cn-border: var(--cn-color-border);
- --cn-border-soft: var(--cn-color-border-subtle);
- --cn-primary: var(--cn-color-brand-primary);
- --cn-primary-hover: var(--cn-color-brand-hover);
- --cn-primary-soft: var(--cn-color-brand-soft);
- --cn-success: var(--cn-color-success);
- --cn-warning: var(--cn-color-warning);
- --cn-danger: var(--cn-color-danger);
- --cn-radius-sm: var(--cn-radius-control);
- --cn-radius-md: var(--cn-radius-card);
- --cn-radius-lg: var(--cn-radius-panel);
- --cn-transition: all var(--cn-motion-base) var(--cn-ease-out);
-}
diff --git a/vue3-user-front/src/design-system/types/components.ts b/vue3-user-front/src/design-system/types/components.ts
deleted file mode 100644
index a5cfb87c5..000000000
--- a/vue3-user-front/src/design-system/types/components.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-import type { Component } from 'vue'
-
-export type CnSurface = 'transparent' | 'plain' | 'panel'
-
-export type CnTone = 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'neutral'
-
-export type CnSize = 'sm' | 'md' | 'lg'
-
-export interface CnPageProps {
- title?: string
- description?: string
- dense?: boolean
- fullHeight?: boolean
- surface?: CnSurface
- maxWidth?: string
-}
-
-export interface CnBreadcrumbItem {
- label: string
- to?: string
-}
-
-export interface CnPageHeaderProps {
- title: string
- description?: string
- eyebrow?: string
- breadcrumbs?: CnBreadcrumbItem[]
- compact?: boolean
-}
-
-export interface CnSectionProps {
- title?: string
- description?: string
- compact?: boolean
- surface?: CnSurface
- divided?: boolean
-}
-
-export type CnTrend = 'up' | 'down' | 'flat'
-
-export interface CnStatCardProps {
- title: string
- value: string | number
- unit?: string
- description?: string
- trend?: CnTrend
- trendText?: string
- tone?: CnTone
- loading?: boolean
-}
-
-export interface CnStatusTagProps {
- label?: string
- type?: CnTone
- size?: CnSize
- dot?: boolean
- subtle?: boolean
-}
-
-export interface CnEmptyStateProps {
- title?: string
- description?: string
- icon?: string
- size?: CnSize
- surface?: CnSurface
-}
-
-export type CnFilterFieldType = 'input' | 'select' | 'date' | 'daterange' | 'switch' | 'custom'
-
-export interface CnFilterOption {
- label: string
- value: string | number | boolean
- disabled?: boolean
-}
-
-export interface CnFilterField {
- prop: string
- label: string
- type: CnFilterFieldType
- placeholder?: string
- options?: CnFilterOption[]
- span?: number
- clearable?: boolean
- multiple?: boolean
- disabled?: boolean
- slot?: string
-}
-
-export interface CnFilterFormProps {
- modelValue: Record
- fields: CnFilterField[]
- columns?: number
- loading?: boolean
- searchText?: string
- resetText?: string
-}
-
-export type CnTableColumnType = 'selection' | 'index' | 'expand'
-
-export interface CnTableColumn> {
- prop?: keyof T | string
- label?: string
- width?: number | string
- minWidth?: number | string
- align?: 'left' | 'center' | 'right'
- fixed?: boolean | 'left' | 'right'
- type?: CnTableColumnType
- slot?: string
- formatter?: (row: T, column: CnTableColumn, value: unknown, index: number) => string | number
- showOverflowTooltip?: boolean
- sortable?: boolean | 'custom'
-}
-
-export interface CnPagination {
- page: number
- pageSize: number
- total: number
- pageSizes?: number[]
- layout?: string
- background?: boolean
- disabled?: boolean
-}
-
-export interface CnDataTableProps> {
- columns: CnTableColumn[]
- data: T[]
- loading?: boolean
- pagination?: CnPagination | null
- rowKey?: string
- stripe?: boolean
- border?: boolean
- emptyTitle?: string
- emptyDescription?: string
- emptyIcon?: string
-}
-
-export interface CnToolbarProps {
- title?: string
- description?: string
- dense?: boolean
- align?: 'start' | 'center' | 'end'
-}
-
-export interface CnThemeDrawerProps {
- modelValue: boolean
- title?: string
- description?: string
- size?: string
- showAdvancedThemes?: boolean
-}
-
-export type CnSidebarIcon = string | Component
-
-export interface CnSidebarItem {
- label: string
- path?: string
- index?: string
- icon?: CnSidebarIcon
- children?: CnSidebarItem[]
-}
-
-export interface CnSidebarSearchResult {
- title: string
- path: string
- breadcrumb?: string
- icon?: CnSidebarIcon
-}
-
-export interface CnSidebarProps {
- items: CnSidebarItem[]
- activePath?: string
- collapsed?: boolean
- brand?: string
- collapsedBrand?: string
- searchable?: boolean
- searchKeyword?: string
- searchResults?: CnSidebarSearchResult[]
- searchPlaceholder?: string
- searchTooltip?: string
- ariaLabel?: string
- emptyText?: string
-}
-
-export interface CnCommandItem {
- path: string
- label: string
- desc?: string
- icon?: CnSidebarIcon
- matchPrefixes?: string[]
-}
-
-export interface CnCommandSection {
- key: string
- title: string
- items: CnCommandItem[]
-}
-
-export interface CnCommandPaletteProps {
- modelValue: boolean
- keyword?: string
- sections: CnCommandSection[]
- recentItems?: CnCommandItem[]
- activePath?: string
- title?: string
- description?: string
- placeholder?: string
- emptyTitle?: string
- emptyDescription?: string
- width?: string
-}
-
-export interface CnTopNavItem {
- path: string
- label: string
- desc?: string
- icon?: CnSidebarIcon
- matchPrefixes?: string[]
-}
-
-export interface CnTopNavGroup {
- title: string
- items: CnTopNavItem[]
-}
-
-export interface CnTopNavDropdown {
- key: string
- label: string
- icon?: CnSidebarIcon
- arrowIcon?: CnSidebarIcon
- groups?: CnTopNavGroup[]
- items?: CnTopNavItem[]
-}
-
-export interface CnTopNavMobileSection {
- key: string
- title: string
- items: CnTopNavItem[]
-}
-
-export interface CnTopNavUser {
- username?: string
- avatar?: string
-}
-
-export interface CnTopNavUserAction {
- command: string
- label: string
- icon?: CnSidebarIcon
- divided?: boolean
-}
-
-export interface CnTopNavProps {
- primaryItems: CnTopNavItem[]
- activePath?: string
- activeFullPath?: string
- dropdowns?: CnTopNavDropdown[]
- mobileSections?: CnTopNavMobileSection[]
- user?: CnTopNavUser | null
- userActions?: CnTopNavUserAction[]
- unreadCount?: number
- mobileOpen?: boolean
- scrolled?: boolean
- fixed?: boolean
- brand?: string
- subtitle?: string
- workspaceLabel?: string
- showWorkspace?: boolean
- fallbackLabel?: string
- userCaption?: string
- searchLabel?: string
- searchShortcut?: string
- mobileSearchLabel?: string
- mobileMenuLabel?: string
- notificationLabel?: string
- mobileSearchTriggerLabel?: string
- mobileDescription?: string
- primaryMobileTitle?: string
- closeLabel?: string
- mobileDrawerSize?: string
- ariaLabel?: string
- showThemeSwitch?: boolean
-}
diff --git a/vue3-user-front/src/design-system/types/theme.ts b/vue3-user-front/src/design-system/types/theme.ts
deleted file mode 100644
index 2207e2779..000000000
--- a/vue3-user-front/src/design-system/types/theme.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export const CODE_NEST_THEME_NAMES = [
- 'light',
- 'dark',
- 'professional-blue',
- 'growth',
- 'high-contrast'
-] as const
-
-export const CODE_NEST_THEME_MODES = ['system', ...CODE_NEST_THEME_NAMES] as const
-
-export type CodeNestThemeName = (typeof CODE_NEST_THEME_NAMES)[number]
-export type CodeNestThemeMode = (typeof CODE_NEST_THEME_MODES)[number]
-
-export interface ThemePreference {
- mode: CodeNestThemeMode
- resolvedTheme: CodeNestThemeName
- followSystem: boolean
-}
-
-export interface CodeNestThemeOption {
- value: CodeNestThemeMode
- label: string
- description?: string
-}
-
-export function isCodeNestThemeName(value: unknown): value is CodeNestThemeName {
- return typeof value === 'string' && CODE_NEST_THEME_NAMES.includes(value as CodeNestThemeName)
-}
-
-export function isCodeNestThemeMode(value: unknown): value is CodeNestThemeMode {
- return typeof value === 'string' && CODE_NEST_THEME_MODES.includes(value as CodeNestThemeMode)
-}
diff --git a/vue3-user-front/tsconfig.json b/vue3-user-front/tsconfig.json
index 7d63b9bba..b04a2f029 100644
--- a/vue3-user-front/tsconfig.json
+++ b/vue3-user-front/tsconfig.json
@@ -18,6 +18,8 @@
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
+ "@/design-system": ["../code-nest-design-system/src/index.ts"],
+ "@/design-system/*": ["../code-nest-design-system/src/*"],
"@/*": ["src/*"]
}
},
@@ -26,6 +28,8 @@
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
+ "../code-nest-design-system/src/**/*.ts",
+ "../code-nest-design-system/src/**/*.vue",
"electron/**/*.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
diff --git a/vue3-user-front/vite.config.js b/vue3-user-front/vite.config.js
index 83b9a4fa5..94b8dc744 100644
--- a/vue3-user-front/vite.config.js
+++ b/vue3-user-front/vite.config.js
@@ -60,13 +60,18 @@ const buildManualChunks = (id) => {
export default defineConfig({
plugins: [vue()],
resolve: {
+ preserveSymlinks: true,
alias: {
+ '@/design-system': resolve(process.cwd(), 'node_modules/@code-nest/design-system/src'),
'@': resolve(process.cwd(), 'src'),
},
},
server: {
port: 3001,
open: true,
+ fs: {
+ allow: [resolve(process.cwd()), resolve(process.cwd(), '../code-nest-design-system')],
+ },
proxy: {
'/api': {
target: 'http://localhost:9999',
diff --git a/xiaou-application/pom.xml b/xiaou-application/pom.xml
index d9589c08c..13e0706ba 100644
--- a/xiaou-application/pom.xml
+++ b/xiaou-application/pom.xml
@@ -192,15 +192,10 @@
${revision}
-
- org.junit.jupiter
- junit-jupiter
- test
-
-
org.springframework.boot
- spring-boot-test
+ spring-boot-starter-test
+ test
diff --git a/xiaou-application/src/main/java/com/xiaou/web/learning/service/LearningCockpitService.java b/xiaou-application/src/main/java/com/xiaou/web/learning/service/LearningCockpitService.java
index 1cf15cf3b..5253a54bd 100644
--- a/xiaou-application/src/main/java/com/xiaou/web/learning/service/LearningCockpitService.java
+++ b/xiaou-application/src/main/java/com/xiaou/web/learning/service/LearningCockpitService.java
@@ -21,8 +21,8 @@
import com.xiaou.points.dto.PointsBalanceResponse;
import com.xiaou.points.service.PointsService;
import com.xiaou.web.learning.dto.LearningCockpitOverviewResponse;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
@@ -32,6 +32,9 @@
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@@ -40,7 +43,6 @@
*/
@Slf4j
@Service
-@RequiredArgsConstructor
public class LearningCockpitService {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@@ -50,6 +52,7 @@ public class LearningCockpitService {
private static final int DEFAULT_WEEKLY_HOURS = 8;
private static final int MIN_WEEKLY_HOURS = 3;
private static final int MAX_WEEKLY_HOURS = 40;
+ private static final long QUERY_TIMEOUT_SECONDS = 3L;
private final PlanService planService;
private final PointsService pointsService;
@@ -60,6 +63,31 @@ public class LearningCockpitService {
private final OjRankingService ojRankingService;
private final CareerLoopService careerLoopService;
private final LearningCockpitRankSnapshotMapper rankSnapshotMapper;
+ private final Executor applicationIoExecutor;
+
+ public LearningCockpitService(
+ PlanService planService,
+ PointsService pointsService,
+ FlashcardStudyService flashcardStudyService,
+ InterviewLearnRecordService interviewLearnRecordService,
+ InterviewMasteryService interviewMasteryService,
+ OjSubmissionService ojSubmissionService,
+ OjRankingService ojRankingService,
+ CareerLoopService careerLoopService,
+ LearningCockpitRankSnapshotMapper rankSnapshotMapper,
+ @Qualifier("applicationIoExecutor") Executor applicationIoExecutor
+ ) {
+ this.planService = planService;
+ this.pointsService = pointsService;
+ this.flashcardStudyService = flashcardStudyService;
+ this.interviewLearnRecordService = interviewLearnRecordService;
+ this.interviewMasteryService = interviewMasteryService;
+ this.ojSubmissionService = ojSubmissionService;
+ this.ojRankingService = ojRankingService;
+ this.careerLoopService = careerLoopService;
+ this.rankSnapshotMapper = rankSnapshotMapper;
+ this.applicationIoExecutor = applicationIoExecutor;
+ }
public LearningCockpitOverviewResponse getOverview(Long userId) {
return getOverview(userId, null, null);
@@ -73,18 +101,18 @@ public LearningCockpitOverviewResponse getOverview(Long userId, String targetRol
LearningCockpitOverviewResponse.TargetProfile profile = resolveTargetProfile(userId, targetRole, weeklyHours);
WeeklyTargets targets = buildTargets(profile.getTargetRole(), profile.getWeeklyHours());
- PlanStatsResponse planStats = safeCall("plan.stats", () -> planService.getStatsOverview(userId), PlanStatsResponse.builder().build());
- PointsBalanceResponse pointsBalance = safeCall("points.balance", () -> pointsService.getPointsBalance(userId), new PointsBalanceResponse());
- FlashcardStudyStatsVO flashcardStats = safeCall("flashcard.stats", () -> flashcardStudyService.getStudyStats(userId, null), new FlashcardStudyStatsVO());
- FlashcardHeatmapVO flashcardHeatmap = safeCall("flashcard.heatmap", () -> flashcardStudyService.getHeatmap(userId, 21), new FlashcardHeatmapVO());
- ReviewStatsResponse reviewStats = safeCall("interview.reviewStats", () -> interviewMasteryService.getReviewStats(userId), new ReviewStatsResponse());
- HeatmapResponse interviewHeatmap = safeCall("interview.heatmap", () -> interviewMasteryService.getHeatmap(userId, Year.now().getValue()), new HeatmapResponse());
- Integer totalLearned = safeCall("interview.totalLearned", () -> interviewLearnRecordService.getTotalLearnedCount(userId), 0);
- OjStatisticsVO ojStats = safeCall("oj.stats", () -> ojSubmissionService.getStatistics(userId), new OjStatisticsVO());
- List weeklyRanking = safeCall("oj.weeklyRanking", () -> ojRankingService.getRanking("weekly"), Collections.emptyList());
- List allRanking = safeCall("oj.allRanking", () -> ojRankingService.getRanking("all"), Collections.emptyList());
-
- Map> checkinDayMap = loadCheckinDayMap(userId, weekStart, today);
+ OverviewData overviewData = loadOverviewData(userId, weekStart, today);
+ PlanStatsResponse planStats = overviewData.planStats();
+ PointsBalanceResponse pointsBalance = overviewData.pointsBalance();
+ FlashcardStudyStatsVO flashcardStats = overviewData.flashcardStats();
+ FlashcardHeatmapVO flashcardHeatmap = overviewData.flashcardHeatmap();
+ ReviewStatsResponse reviewStats = overviewData.reviewStats();
+ HeatmapResponse interviewHeatmap = overviewData.interviewHeatmap();
+ Integer totalLearned = overviewData.totalLearned();
+ OjStatisticsVO ojStats = overviewData.ojStats();
+ List weeklyRanking = overviewData.weeklyRanking();
+ List allRanking = overviewData.allRanking();
+ Map> checkinDayMap = overviewData.checkinDayMap();
int ojWeeklySolved = findAcceptedCount(weeklyRanking, userId);
int interviewWeeklyCount = sumInterviewCount(interviewHeatmap, weekStart, today);
@@ -167,6 +195,54 @@ public LearningCockpitOverviewResponse getOverview(Long userId, String targetRol
return response;
}
+ private OverviewData loadOverviewData(Long userId, LocalDate weekStart, LocalDate today) {
+ CompletableFuture planStats = loadAsync(
+ "plan.stats", () -> planService.getStatsOverview(userId), PlanStatsResponse.builder().build());
+ CompletableFuture pointsBalance = loadAsync(
+ "points.balance", () -> pointsService.getPointsBalance(userId), new PointsBalanceResponse());
+ CompletableFuture flashcardStats = loadAsync(
+ "flashcard.stats", () -> flashcardStudyService.getStudyStats(userId, null), new FlashcardStudyStatsVO());
+ CompletableFuture flashcardHeatmap = loadAsync(
+ "flashcard.heatmap", () -> flashcardStudyService.getHeatmap(userId, 21), new FlashcardHeatmapVO());
+ CompletableFuture reviewStats = loadAsync(
+ "interview.reviewStats", () -> interviewMasteryService.getReviewStats(userId), new ReviewStatsResponse());
+ CompletableFuture interviewHeatmap = loadAsync(
+ "interview.heatmap", () -> interviewMasteryService.getHeatmap(userId, Year.now().getValue()), new HeatmapResponse());
+ CompletableFuture totalLearned = loadAsync(
+ "interview.totalLearned", () -> interviewLearnRecordService.getTotalLearnedCount(userId), 0);
+ CompletableFuture ojStats = loadAsync(
+ "oj.stats", () -> ojSubmissionService.getStatistics(userId), new OjStatisticsVO());
+ CompletableFuture> weeklyRanking = loadAsync(
+ "oj.weeklyRanking", () -> ojRankingService.getRanking("weekly"), Collections.emptyList());
+ CompletableFuture> allRanking = loadAsync(
+ "oj.allRanking", () -> ojRankingService.getRanking("all"), Collections.emptyList());
+ CompletableFuture
- *
- * @author xiaou
- */
-@Slf4j
-public class ConcurrentUtils {
-
- /**
- * 限流器缓存
- */
- private static final Map RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
-
- /**
- * 熔断器缓存
- */
- private static final Map CIRCUIT_BREAKER_CACHE = new ConcurrentHashMap<>();
-
- /**
- * 分段锁缓存
- */
- private static final Map STRIPED_LOCK_CACHE = new ConcurrentHashMap<>();
-
- // ==================== 限流器 ====================
-
- /**
- * 获取或创建限流器
- *
- * @param name 限流器名称
- * @param permitsPerSecond 每秒许可数
- * @return RateLimiter
- */
- public static RateLimiter getRateLimiter(String name, double permitsPerSecond) {
- return RATE_LIMITER_CACHE.computeIfAbsent(name, k -> new RateLimiter(permitsPerSecond));
- }
-
- /**
- * 移除限流器
- */
- public static void removeRateLimiter(String name) {
- RATE_LIMITER_CACHE.remove(name);
- }
-
- /**
- * 令牌桶限流器
- */
- public static class RateLimiter {
- private final double permitsPerSecond;
- private final long intervalNanos;
- private final AtomicLong nextFreeTicketNanos;
- private final int maxPermits;
- private double storedPermits;
- private final Object mutex = new Object();
-
- public RateLimiter(double permitsPerSecond) {
- this(permitsPerSecond, (int) permitsPerSecond);
- }
-
- public RateLimiter(double permitsPerSecond, int maxBurstPermits) {
- this.permitsPerSecond = permitsPerSecond;
- this.intervalNanos = (long) (1_000_000_000.0 / permitsPerSecond);
- this.nextFreeTicketNanos = new AtomicLong(System.nanoTime());
- this.maxPermits = maxBurstPermits;
- this.storedPermits = maxBurstPermits;
- }
-
- /**
- * 尝试获取许可(非阻塞)
- */
- public boolean tryAcquire() {
- return tryAcquire(1);
- }
-
- /**
- * 尝试获取指定数量的许可(非阻塞)
- */
- public boolean tryAcquire(int permits) {
- synchronized (mutex) {
- long now = System.nanoTime();
- reSync(now);
- if (storedPermits >= permits) {
- storedPermits -= permits;
- return true;
- }
- return false;
- }
- }
-
- /**
- * 尝试在指定时间内获取许可
- */
- public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
- long timeoutNanos = unit.toNanos(timeout);
- long deadline = System.nanoTime() + timeoutNanos;
-
- while (System.nanoTime() < deadline) {
- if (tryAcquire(permits)) {
- return true;
- }
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return false;
- }
- }
- return false;
- }
-
- /**
- * 阻塞获取许可
- */
- public void acquire() {
- acquire(1);
- }
-
- /**
- * 阻塞获取指定数量的许可
- */
- public void acquire(int permits) {
- while (!tryAcquire(permits)) {
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException(e);
- }
- }
- }
-
- /**
- * 重新同步令牌
- */
- private void reSync(long now) {
- long lastTicket = nextFreeTicketNanos.get();
- if (now > lastTicket) {
- double newPermits = (now - lastTicket) / (double) intervalNanos;
- storedPermits = Math.min(maxPermits, storedPermits + newPermits);
- nextFreeTicketNanos.set(now);
- }
- }
-
- /**
- * 获取当前QPS配置
- */
- public double getPermitsPerSecond() {
- return permitsPerSecond;
- }
-
- /**
- * 带限流执行任务
- */
- public T executeWithLimit(Supplier supplier) {
- acquire();
- return supplier.get();
- }
-
- /**
- * 带限流执行任务(非阻塞,失败返回null)
- */
- public T tryExecuteWithLimit(Supplier supplier) {
- if (tryAcquire()) {
- return supplier.get();
- }
- return null;
- }
- }
-
- /**
- * 滑动窗口限流器
- */
- public static class SlidingWindowRateLimiter {
- private final int windowSize;
- private final long windowDurationMs;
- private final AtomicLong[] windows;
- private final AtomicInteger currentIndex;
- private final AtomicLong lastSlideTime;
- private final int maxRequests;
-
- /**
- * @param maxRequests 窗口内最大请求数
- * @param windowDurationMs 窗口时长(毫秒)
- * @param windowSize 滑动窗口分片数
- */
- public SlidingWindowRateLimiter(int maxRequests, long windowDurationMs, int windowSize) {
- this.maxRequests = maxRequests;
- this.windowDurationMs = windowDurationMs;
- this.windowSize = windowSize;
- this.windows = new AtomicLong[windowSize];
- for (int i = 0; i < windowSize; i++) {
- windows[i] = new AtomicLong(0);
- }
- this.currentIndex = new AtomicInteger(0);
- this.lastSlideTime = new AtomicLong(System.currentTimeMillis());
- }
-
- /**
- * 尝试获取许可
- */
- public boolean tryAcquire() {
- slideWindow();
- long total = 0;
- for (AtomicLong window : windows) {
- total += window.get();
- }
- if (total < maxRequests) {
- windows[currentIndex.get()].incrementAndGet();
- return true;
- }
- return false;
- }
-
- /**
- * 滑动窗口
- */
- private void slideWindow() {
- long now = System.currentTimeMillis();
- long last = lastSlideTime.get();
- long slotDuration = windowDurationMs / windowSize;
- long elapsed = now - last;
-
- if (elapsed >= slotDuration) {
- int slotsToSlide = (int) Math.min(elapsed / slotDuration, windowSize);
- if (lastSlideTime.compareAndSet(last, now)) {
- for (int i = 0; i < slotsToSlide; i++) {
- int nextIndex = (currentIndex.get() + 1) % windowSize;
- windows[nextIndex].set(0);
- currentIndex.set(nextIndex);
- }
- }
- }
- }
-
- /**
- * 获取当前窗口内的请求数
- */
- public long getCurrentCount() {
- slideWindow();
- long total = 0;
- for (AtomicLong window : windows) {
- total += window.get();
- }
- return total;
- }
- }
-
- // ==================== 熔断器 ====================
-
- /**
- * 获取或创建熔断器
- *
- * @param name 熔断器名称
- * @param failureThreshold 失败阈值
- * @param resetTimeoutMs 重置超时(毫秒)
- * @return CircuitBreaker
- */
- public static CircuitBreaker getCircuitBreaker(String name, int failureThreshold, long resetTimeoutMs) {
- return CIRCUIT_BREAKER_CACHE.computeIfAbsent(name,
- k -> new CircuitBreaker(failureThreshold, resetTimeoutMs));
- }
-
- /**
- * 移除熔断器
- */
- public static void removeCircuitBreaker(String name) {
- CIRCUIT_BREAKER_CACHE.remove(name);
- }
-
- /**
- * 熔断器
- */
- public static class CircuitBreaker {
- private final int failureThreshold;
- private final long resetTimeoutMs;
- private final AtomicInteger failureCount;
- private final AtomicLong lastFailureTime;
- private volatile State state;
-
- public enum State {
- CLOSED, // 正常状态
- OPEN, // 熔断状态
- HALF_OPEN // 半开状态
- }
-
- public CircuitBreaker(int failureThreshold, long resetTimeoutMs) {
- this.failureThreshold = failureThreshold;
- this.resetTimeoutMs = resetTimeoutMs;
- this.failureCount = new AtomicInteger(0);
- this.lastFailureTime = new AtomicLong(0);
- this.state = State.CLOSED;
- }
-
- /**
- * 执行带熔断保护的任务
- */
- public T execute(Supplier supplier) {
- if (!allowRequest()) {
- throw new CircuitBreakerOpenException("熔断器已打开,拒绝请求");
- }
-
- try {
- T result = supplier.get();
- onSuccess();
- return result;
- } catch (Exception e) {
- onFailure();
- throw e;
- }
- }
-
- /**
- * 执行带熔断保护的任务(带降级)
- */
- public T executeWithFallback(Supplier supplier, Supplier fallback) {
- if (!allowRequest()) {
- log.warn("熔断器已打开,执行降级逻辑");
- return fallback.get();
- }
-
- try {
- T result = supplier.get();
- onSuccess();
- return result;
- } catch (Exception e) {
- onFailure();
- log.warn("任务执行失败,执行降级逻辑: {}", e.getMessage());
- return fallback.get();
- }
- }
-
- /**
- * 是否允许请求
- */
- public boolean allowRequest() {
- if (state == State.CLOSED) {
- return true;
- }
-
- if (state == State.OPEN) {
- long now = System.currentTimeMillis();
- if (now - lastFailureTime.get() > resetTimeoutMs) {
- state = State.HALF_OPEN;
- log.info("熔断器进入半开状态");
- return true;
- }
- return false;
- }
-
- // HALF_OPEN状态,允许一个请求通过
- return true;
- }
-
- /**
- * 成功回调
- */
- public void onSuccess() {
- if (state == State.HALF_OPEN) {
- state = State.CLOSED;
- failureCount.set(0);
- log.info("熔断器已关闭");
- }
- }
-
- /**
- * 失败回调
- */
- public void onFailure() {
- lastFailureTime.set(System.currentTimeMillis());
- int failures = failureCount.incrementAndGet();
-
- if (state == State.HALF_OPEN) {
- state = State.OPEN;
- log.warn("熔断器从半开状态重新打开");
- } else if (failures >= failureThreshold) {
- state = State.OPEN;
- log.warn("失败次数达到阈值{},熔断器已打开", failureThreshold);
- }
- }
-
- /**
- * 获取当前状态
- */
- public State getState() {
- // 检查是否需要转换状态
- allowRequest();
- return state;
- }
-
- /**
- * 强制重置熔断器
- */
- public void reset() {
- state = State.CLOSED;
- failureCount.set(0);
- lastFailureTime.set(0);
- log.info("熔断器已重置");
- }
-
- /**
- * 获取失败次数
- */
- public int getFailureCount() {
- return failureCount.get();
- }
- }
-
- /**
- * 熔断器打开异常
- */
- public static class CircuitBreakerOpenException extends RuntimeException {
- public CircuitBreakerOpenException(String message) {
- super(message);
- }
- }
-
- // ==================== 锁工具 ====================
-
- /**
- * 安全执行锁内操作
- *
- * @param lock 锁
- * @param supplier 任务
- * @return 执行结果
- */
- public static T withLock(Lock lock, Supplier supplier) {
- lock.lock();
- try {
- return supplier.get();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * 安全执行锁内操作(无返回值)
- */
- public static void withLock(Lock lock, Runnable task) {
- lock.lock();
- try {
- task.run();
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * 尝试获取锁并执行
- *
- * @param lock 锁
- * @param timeout 超时时间
- * @param unit 时间单位
- * @param supplier 任务
- * @return 执行结果,获取锁失败返回null
- */
- public static T tryWithLock(Lock lock, long timeout, TimeUnit unit, Supplier supplier) {
- try {
- if (lock.tryLock(timeout, unit)) {
- try {
- return supplier.get();
- } finally {
- lock.unlock();
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- return null;
- }
-
- /**
- * 安全执行读锁内操作
- */
- public static T withReadLock(ReadWriteLock rwLock, Supplier supplier) {
- Lock readLock = rwLock.readLock();
- readLock.lock();
- try {
- return supplier.get();
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * 安全执行写锁内操作
- */
- public static T withWriteLock(ReadWriteLock rwLock, Supplier supplier) {
- Lock writeLock = rwLock.writeLock();
- writeLock.lock();
- try {
- return supplier.get();
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * 获取或创建分段锁
- */
- public static StripedLock getStripedLock(String name, int stripes) {
- return STRIPED_LOCK_CACHE.computeIfAbsent(name, k -> new StripedLock(stripes));
- }
-
- /**
- * 分段锁(减少锁竞争)
- */
- public static class StripedLock {
- private final Lock[] locks;
- private final int mask;
-
- public StripedLock(int stripes) {
- // 确保stripes是2的幂
- int size = 1;
- while (size < stripes) {
- size <<= 1;
- }
- this.mask = size - 1;
- this.locks = new Lock[size];
- for (int i = 0; i < size; i++) {
- locks[i] = new ReentrantLock();
- }
- }
-
- /**
- * 根据key获取锁
- */
- public Lock getLock(Object key) {
- return locks[hash(key) & mask];
- }
-
- /**
- * 使用指定key的锁执行任务
- */
- public T withLock(Object key, Supplier supplier) {
- Lock lock = getLock(key);
- lock.lock();
- try {
- return supplier.get();
- } finally {
- lock.unlock();
- }
- }
-
- private int hash(Object key) {
- int h = key.hashCode();
- return h ^ (h >>> 16);
- }
- }
-
- // ==================== 并发计数器 ====================
-
- /**
- * 高性能计数器
- */
- public static class Counter {
- private final LongAdder adder = new LongAdder();
-
- public void increment() {
- adder.increment();
- }
-
- public void add(long delta) {
- adder.add(delta);
- }
-
- public void decrement() {
- adder.decrement();
- }
-
- public long get() {
- return adder.sum();
- }
-
- public void reset() {
- adder.reset();
- }
-
- public long getAndReset() {
- return adder.sumThenReset();
- }
- }
-
- /**
- * 并发统计器
- */
- public static class Statistics {
- private final LongAdder count = new LongAdder();
- private final LongAdder sum = new LongAdder();
- private final AtomicLong min = new AtomicLong(Long.MAX_VALUE);
- private final AtomicLong max = new AtomicLong(Long.MIN_VALUE);
-
- public void record(long value) {
- count.increment();
- sum.add(value);
- updateMin(value);
- updateMax(value);
- }
-
- private void updateMin(long value) {
- long current;
- do {
- current = min.get();
- if (value >= current) {
- return;
- }
- } while (!min.compareAndSet(current, value));
- }
-
- private void updateMax(long value) {
- long current;
- do {
- current = max.get();
- if (value <= current) {
- return;
- }
- } while (!max.compareAndSet(current, value));
- }
-
- public long getCount() {
- return count.sum();
- }
-
- public long getSum() {
- return sum.sum();
- }
-
- public long getMin() {
- long val = min.get();
- return val == Long.MAX_VALUE ? 0 : val;
- }
-
- public long getMax() {
- long val = max.get();
- return val == Long.MIN_VALUE ? 0 : val;
- }
-
- public double getAverage() {
- long c = count.sum();
- return c == 0 ? 0 : (double) sum.sum() / c;
- }
-
- public void reset() {
- count.reset();
- sum.reset();
- min.set(Long.MAX_VALUE);
- max.set(Long.MIN_VALUE);
- }
-
- @Override
- public String toString() {
- return String.format("Statistics{count=%d, sum=%d, min=%d, max=%d, avg=%.2f}",
- getCount(), getSum(), getMin(), getMax(), getAverage());
- }
- }
-
- // ==================== 同步屏障 ====================
-
- /**
- * 创建倒计时器
- *
- * @param count 计数
- * @return CountDownLatch
- */
- public static CountDownLatch createLatch(int count) {
- return new CountDownLatch(count);
- }
-
- /**
- * 等待倒计时完成(带超时)
- */
- public static boolean awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) {
- try {
- return latch.await(timeout, unit);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- return false;
- }
- }
-
- /**
- * 等待倒计时完成
- */
- public static void awaitLatch(CountDownLatch latch) {
- try {
- latch.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("等待被中断", e);
- }
- }
-
- /**
- * 创建循环屏障
- *
- * @param parties 参与方数量
- * @return CyclicBarrier
- */
- public static CyclicBarrier createBarrier(int parties) {
- return new CyclicBarrier(parties);
- }
-
- /**
- * 创建循环屏障(带回调)
- */
- public static CyclicBarrier createBarrier(int parties, Runnable barrierAction) {
- return new CyclicBarrier(parties, barrierAction);
- }
-
- /**
- * 等待屏障
- */
- public static int awaitBarrier(CyclicBarrier barrier) {
- try {
- return barrier.await();
- } catch (InterruptedException | BrokenBarrierException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("等待屏障失败", e);
- }
- }
-
- /**
- * 等待屏障(带超时)
- */
- public static int awaitBarrier(CyclicBarrier barrier, long timeout, TimeUnit unit) {
- try {
- return barrier.await(timeout, unit);
- } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("等待屏障失败", e);
- }
- }
-
- // ==================== 信号量工具 ====================
-
- /**
- * 创建信号量
- */
- public static Semaphore createSemaphore(int permits) {
- return new Semaphore(permits);
- }
-
- /**
- * 创建公平信号量
- */
- public static Semaphore createFairSemaphore(int permits) {
- return new Semaphore(permits, true);
- }
-
- /**
- * 使用信号量执行任务
- */
- public static T withSemaphore(Semaphore semaphore, Supplier supplier) {
- try {
- semaphore.acquire();
- try {
- return supplier.get();
- } finally {
- semaphore.release();
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException("获取信号量被中断", e);
- }
- }
-
- /**
- * 尝试使用信号量执行任务
- */
- public static T tryWithSemaphore(Semaphore semaphore, long timeout, TimeUnit unit,
- Supplier supplier, T defaultValue) {
- try {
- if (semaphore.tryAcquire(timeout, unit)) {
- try {
- return supplier.get();
- } finally {
- semaphore.release();
- }
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- return defaultValue;
- }
-
- // ==================== 延迟初始化 ====================
-
- /**
- * 线程安全的懒加载容器
- */
- public static class Lazy {
- private volatile T value;
- private final Supplier supplier;
- private final Object lock = new Object();
-
- public Lazy(Supplier supplier) {
- this.supplier = supplier;
- }
-
- public T get() {
- T result = value;
- if (result == null) {
- synchronized (lock) {
- result = value;
- if (result == null) {
- value = result = supplier.get();
- }
- }
- }
- return result;
- }
-
- public boolean isInitialized() {
- return value != null;
- }
-
- public void reset() {
- synchronized (lock) {
- value = null;
- }
- }
- }
-
- /**
- * 创建懒加载容器
- */
- public static Lazy lazy(Supplier supplier) {
- return new Lazy<>(supplier);
- }
-
- /**
- * 带过期时间的缓存值
- */
- public static class ExpiringValue {
- private volatile T value;
- private volatile long expireTime;
- private final Supplier supplier;
- private final long ttlMs;
- private final Object lock = new Object();
-
- public ExpiringValue(Supplier supplier, long ttlMs) {
- this.supplier = supplier;
- this.ttlMs = ttlMs;
- }
-
- public T get() {
- long now = System.currentTimeMillis();
- if (value == null || now >= expireTime) {
- synchronized (lock) {
- now = System.currentTimeMillis();
- if (value == null || now >= expireTime) {
- value = supplier.get();
- expireTime = now + ttlMs;
- }
- }
- }
- return value;
- }
-
- public void invalidate() {
- synchronized (lock) {
- value = null;
- expireTime = 0;
- }
- }
-
- public boolean isExpired() {
- return System.currentTimeMillis() >= expireTime;
- }
- }
-
- /**
- * 创建带过期时间的缓存值
- */
- public static ExpiringValue expiringValue(Supplier supplier, Duration ttl) {
- return new ExpiringValue<>(supplier, ttl.toMillis());
- }
-
- // ==================== 单次执行 ====================
-
- /**
- * 只执行一次的任务容器
- */
- public static class Once {
- private final AtomicInteger executed = new AtomicInteger(0);
- private final CountDownLatch latch = new CountDownLatch(1);
-
- /**
- * 执行任务(只有第一次调用会执行)
- */
- public void run(Runnable task) {
- if (executed.compareAndSet(0, 1)) {
- try {
- task.run();
- } finally {
- latch.countDown();
- }
- } else {
- // 等待首次执行完成
- try {
- latch.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
-
- /**
- * 是否已执行
- */
- public boolean isExecuted() {
- return executed.get() > 0;
- }
- }
-
- /**
- * 创建单次执行容器
- */
- public static Once once() {
- return new Once();
- }
-
- // ==================== 并发映射工具 ====================
-
- /**
- * 原子更新Map中的值
- */
- public static V computeIfAbsent(ConcurrentMap map, K key, Supplier supplier) {
- V value = map.get(key);
- if (value == null) {
- value = supplier.get();
- V existing = map.putIfAbsent(key, value);
- if (existing != null) {
- value = existing;
- }
- }
- return value;
- }
-
- /**
- * 创建有界阻塞队列
- */
- public static BlockingQueue createBoundedQueue(int capacity) {
- return new LinkedBlockingQueue<>(capacity);
- }
-
- /**
- * 创建优先级阻塞队列
- */
- public static BlockingQueue createPriorityQueue() {
- return new PriorityBlockingQueue<>();
- }
-
- /**
- * 创建延迟队列
- */
- public static DelayQueue createDelayQueue() {
- return new DelayQueue<>();
- }
-
- // ==================== 工具方法 ====================
-
- /**
- * 安全休眠
- */
- public static void sleep(long millis) {
- try {
- Thread.sleep(millis);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-
- /**
- * 安全休眠
- */
- public static void sleep(Duration duration) {
- sleep(duration.toMillis());
- }
-
- /**
- * 自旋等待条件满足
- */
- public static boolean spinWait(Supplier condition, long timeoutMs) {
- long deadline = System.currentTimeMillis() + timeoutMs;
- while (System.currentTimeMillis() < deadline) {
- if (condition.get()) {
- return true;
- }
- Thread.onSpinWait();
- }
- return false;
- }
-
- /**
- * 获取当前线程名
- */
- public static String currentThreadName() {
- return Thread.currentThread().getName();
- }
-
- /**
- * 设置当前线程名
- */
- public static void setThreadName(String name) {
- Thread.currentThread().setName(name);
- }
-
- /**
- * 私有构造函数
- */
- private ConcurrentUtils() {
- throw new UnsupportedOperationException("工具类不允许实例化");
- }
-}
diff --git a/xiaou-common/src/main/java/com/xiaou/common/utils/NotificationCacheUtil.java b/xiaou-common/src/main/java/com/xiaou/common/utils/NotificationCacheUtil.java
index 55d983a22..3e2965f88 100644
--- a/xiaou-common/src/main/java/com/xiaou/common/utils/NotificationCacheUtil.java
+++ b/xiaou-common/src/main/java/com/xiaou/common/utils/NotificationCacheUtil.java
@@ -1,16 +1,21 @@
package com.xiaou.common.utils;
+import com.xiaou.common.cache.RedisValueStore;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
+import java.time.Duration;
+
/**
* 消息通知缓存工具类
*/
@Component
@RequiredArgsConstructor
+@Slf4j
public class NotificationCacheUtil {
- private final RedisUtil redisUtil;
+ private final RedisValueStore redisValueStore;
private static final String UNREAD_COUNT_KEY = "notification:unread:";
@@ -18,20 +23,33 @@ public class NotificationCacheUtil {
* 缓存未读数量
*/
public void cacheUnreadCount(Long userId, int count) {
- redisUtil.set(UNREAD_COUNT_KEY + userId, count, 30 * 60); // 30分钟,转换为秒
+ try {
+ redisValueStore.put(UNREAD_COUNT_KEY + userId, count, Duration.ofMinutes(30));
+ } catch (RuntimeException error) {
+ log.warn("缓存通知未读数失败,userId={}", userId, error);
+ }
}
/**
* 获取缓存的未读数量
*/
public Integer getCachedUnreadCount(Long userId) {
- return redisUtil.get(UNREAD_COUNT_KEY + userId, Integer.class);
+ try {
+ return redisValueStore.find(UNREAD_COUNT_KEY + userId, Integer.class).orElse(null);
+ } catch (RuntimeException error) {
+ log.warn("读取通知未读数缓存失败,userId={}", userId, error);
+ return null;
+ }
}
/**
* 清除未读数量缓存
*/
public void clearUnreadCountCache(Long userId) {
- redisUtil.del(UNREAD_COUNT_KEY + userId);
+ try {
+ redisValueStore.delete(UNREAD_COUNT_KEY + userId);
+ } catch (RuntimeException error) {
+ log.warn("清除通知未读数缓存失败,userId={}", userId, error);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/xiaou-common/src/main/java/com/xiaou/common/utils/RedisUtil.java b/xiaou-common/src/main/java/com/xiaou/common/utils/RedisUtil.java
deleted file mode 100644
index c805b4d3a..000000000
--- a/xiaou-common/src/main/java/com/xiaou/common/utils/RedisUtil.java
+++ /dev/null
@@ -1,872 +0,0 @@
-package com.xiaou.common.utils;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.redisson.api.*;
-import org.springframework.stereotype.Component;
-
-import java.time.Duration;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Redis工具类
- * 基于Redisson客户端实现
- *
- * @author xiaou
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class RedisUtil {
-
- private final RedissonClient redissonClient;
-
- // =============================常用操作=============================
-
- /**
- * 指定缓存失效时间
- *
- * @param key 键
- * @param time 时间(秒)
- * @return true成功 false失败
- */
- public boolean expire(String key, long time) {
- try {
- if (time > 0) {
- redissonClient.getBucket(key).expire(Duration.ofSeconds(time));
- }
- return true;
- } catch (Exception e) {
- log.error("设置过期时间失败", e);
- return false;
- }
- }
-
- /**
- * 根据key 获取过期时间
- *
- * @param key 键 不能为null
- * @return 时间(秒) 返回0代表为永久有效
- */
- public long getExpire(String key) {
- try {
- RBucket