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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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>> checkinDayMap = loadAsync( + "points.checkinCalendar", () -> loadCheckinDayMap(userId, weekStart, today), Collections.emptyMap()); + + return new OverviewData( + planStats.join(), + pointsBalance.join(), + flashcardStats.join(), + flashcardHeatmap.join(), + reviewStats.join(), + interviewHeatmap.join(), + totalLearned.join(), + ojStats.join(), + weeklyRanking.join(), + allRanking.join(), + checkinDayMap.join() + ); + } + + private CompletableFuture loadAsync(String name, Supplier supplier, T fallback) { + return CompletableFuture.supplyAsync(() -> safeCall(name, supplier, fallback), applicationIoExecutor) + .orTimeout(QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .exceptionally(error -> { + log.warn("学习驾驶舱聚合调用超时或失败: {}, reason={}", name, error.getMessage()); + return fallback; + }); + } + private LearningCockpitOverviewResponse.GrowthScore buildGrowthScore( LearningCockpitOverviewResponse.Summary summary, List modules, @@ -1102,6 +1178,21 @@ private T safeCall(String name, Supplier supplier, T fallback) { } } + private record OverviewData( + PlanStatsResponse planStats, + PointsBalanceResponse pointsBalance, + FlashcardStudyStatsVO flashcardStats, + FlashcardHeatmapVO flashcardHeatmap, + ReviewStatsResponse reviewStats, + HeatmapResponse interviewHeatmap, + Integer totalLearned, + OjStatisticsVO ojStats, + List weeklyRanking, + List allRanking, + Map> checkinDayMap + ) { + } + private static class WeeklyTargets { private final int ojTarget; private final int interviewTarget; diff --git a/xiaou-application/src/test/java/com/xiaou/web/learning/service/LearningCockpitServiceTest.java b/xiaou-application/src/test/java/com/xiaou/web/learning/service/LearningCockpitServiceTest.java new file mode 100644 index 000000000..4899ada8d --- /dev/null +++ b/xiaou-application/src/test/java/com/xiaou/web/learning/service/LearningCockpitServiceTest.java @@ -0,0 +1,103 @@ +package com.xiaou.web.learning.service; + +import com.xiaou.flashcard.service.FlashcardStudyService; +import com.xiaou.interview.service.InterviewLearnRecordService; +import com.xiaou.interview.service.InterviewMasteryService; +import com.xiaou.mockinterview.service.CareerLoopService; +import com.xiaou.oj.service.OjRankingService; +import com.xiaou.oj.service.OjSubmissionService; +import com.xiaou.plan.mapper.LearningCockpitRankSnapshotMapper; +import com.xiaou.plan.service.PlanService; +import com.xiaou.points.service.PointsService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LearningCockpitServiceTest { + + @Mock + private PlanService planService; + @Mock + private PointsService pointsService; + @Mock + private FlashcardStudyService flashcardStudyService; + @Mock + private InterviewLearnRecordService interviewLearnRecordService; + @Mock + private InterviewMasteryService interviewMasteryService; + @Mock + private OjSubmissionService ojSubmissionService; + @Mock + private OjRankingService ojRankingService; + @Mock + private CareerLoopService careerLoopService; + @Mock + private LearningCockpitRankSnapshotMapper rankSnapshotMapper; + + private ExecutorService requestExecutor; + private ExecutorService overviewExecutor; + + @BeforeEach + void setUp() { + requestExecutor = Executors.newSingleThreadExecutor(); + overviewExecutor = Executors.newFixedThreadPool(8); + } + + @AfterEach + void tearDown() { + requestExecutor.shutdownNow(); + overviewExecutor.shutdownNow(); + } + + @Test + void shouldStartIndependentOverviewQueriesInParallel() throws Exception { + CountDownLatch started = new CountDownLatch(2); + CountDownLatch release = new CountDownLatch(1); + when(planService.getStatsOverview(7L)).thenAnswer(invocation -> { + started.countDown(); + release.await(2, TimeUnit.SECONDS); + return null; + }); + when(pointsService.getPointsBalance(7L)).thenAnswer(invocation -> { + started.countDown(); + release.await(2, TimeUnit.SECONDS); + return null; + }); + when(ojRankingService.getRanking(anyString())).thenReturn(Collections.emptyList()); + + LearningCockpitService service = new LearningCockpitService( + planService, + pointsService, + flashcardStudyService, + interviewLearnRecordService, + interviewMasteryService, + ojSubmissionService, + ojRankingService, + careerLoopService, + rankSnapshotMapper, + overviewExecutor + ); + + Future request = requestExecutor.submit(() -> service.getOverview(7L, "Java", 8)); + boolean bothStarted = started.await(500, TimeUnit.MILLISECONDS); + release.countDown(); + request.get(5, TimeUnit.SECONDS); + + assertThat(bothStarted).isTrue(); + } +} diff --git a/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatRateLimitService.java b/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatRateLimitService.java index 22e00840f..7a4c92647 100644 --- a/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatRateLimitService.java +++ b/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatRateLimitService.java @@ -1,11 +1,14 @@ package com.xiaou.chat.service; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.time.Duration; + /** * 聊天室轻量限流服务,防止短时间刷屏和输入中事件风暴。 * @@ -13,11 +16,12 @@ */ @Service @RequiredArgsConstructor +@Slf4j public class ChatRateLimitService { private static final String KEY_PREFIX = "xiaou:chat:rate-limit:"; - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; @Value("${xiaou.chat.rate-limit.enabled:true}") private boolean enabled; @@ -48,13 +52,13 @@ private RateLimitResult tryAcquire(String bucket, Long userId, int limit, int wi } String key = KEY_PREFIX + bucket + ":" + userId; - long count = redisUtil.incr(key, 1); - if (count <= 0) { + long count; + try { + count = redisValueStore.increment(key, 1, Duration.ofSeconds(windowSeconds)); + } catch (RuntimeException error) { + log.warn("聊天室限流缓存不可用,按可用性策略放行: bucket={}, userId={}", bucket, userId, error); return RateLimitResult.allowed(limit); } - if (count == 1) { - redisUtil.expire(key, windowSeconds); - } if (count > limit) { return RateLimitResult.rejected(rejectMessage, windowSeconds); } diff --git a/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatWebSocketTicketService.java b/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatWebSocketTicketService.java index 2e1030c82..67456a27b 100644 --- a/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatWebSocketTicketService.java +++ b/xiaou-chat/src/main/java/com/xiaou/chat/service/ChatWebSocketTicketService.java @@ -1,10 +1,11 @@ package com.xiaou.chat.service; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.security.SecureRandom; +import java.time.Duration; import java.util.Base64; /** @@ -21,7 +22,7 @@ public class ChatWebSocketTicketService { private static final long TICKET_TTL_SECONDS = 60L; private static final int MAX_TICKET_LENGTH = 128; - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; private final SecureRandom secureRandom = new SecureRandom(); public String createTicket(Long userId) { @@ -32,7 +33,7 @@ public String createTicket(Long userId) { byte[] randomBytes = new byte[TICKET_BYTE_LENGTH]; secureRandom.nextBytes(randomBytes); String ticket = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); - redisUtil.set(buildKey(ticket), String.valueOf(userId), TICKET_TTL_SECONDS); + redisValueStore.put(buildKey(ticket), String.valueOf(userId), Duration.ofSeconds(TICKET_TTL_SECONDS)); return ticket; } @@ -41,15 +42,13 @@ public Long consumeTicket(String ticket) { return null; } - String key = buildKey(ticket); - Object cachedUserId = redisUtil.get(key); - redisUtil.del(key); + String cachedUserId = redisValueStore.take(buildKey(ticket), String.class).orElse(null); if (cachedUserId == null) { return null; } try { - return Long.parseLong(cachedUserId.toString()); + return Long.parseLong(cachedUserId); } catch (NumberFormatException e) { return null; } diff --git a/xiaou-chat/src/test/java/com/xiaou/chat/service/ChatWebSocketTicketServiceTest.java b/xiaou-chat/src/test/java/com/xiaou/chat/service/ChatWebSocketTicketServiceTest.java new file mode 100644 index 000000000..783681ade --- /dev/null +++ b/xiaou-chat/src/test/java/com/xiaou/chat/service/ChatWebSocketTicketServiceTest.java @@ -0,0 +1,38 @@ +package com.xiaou.chat.service; + +import com.xiaou.common.cache.RedisValueStore; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ChatWebSocketTicketServiceTest { + + @Test + void shouldConsumeTicketWithAtomicTake() { + RedisValueStore redisValueStore = mock(RedisValueStore.class); + when(redisValueStore.take("xiaou:chat:ws-ticket:valid_ticket", String.class)) + .thenReturn(Optional.of("42")); + ChatWebSocketTicketService service = new ChatWebSocketTicketService(redisValueStore); + + assertThat(service.consumeTicket("valid_ticket")).isEqualTo(42L); + + verify(redisValueStore).take("xiaou:chat:ws-ticket:valid_ticket", String.class); + verify(redisValueStore, never()).delete("xiaou:chat:ws-ticket:valid_ticket"); + } + + @Test + void shouldRejectMalformedTicketWithoutReadingRedis() { + RedisValueStore redisValueStore = mock(RedisValueStore.class); + ChatWebSocketTicketService service = new ChatWebSocketTicketService(redisValueStore); + + assertThat(service.consumeTicket("../invalid")).isNull(); + + verify(redisValueStore, never()).take("xiaou:chat:ws-ticket:../invalid", String.class); + } +} diff --git a/xiaou-common/pom.xml b/xiaou-common/pom.xml index a31d4a070..6bda550a9 100644 --- a/xiaou-common/pom.xml +++ b/xiaou-common/pom.xml @@ -190,6 +190,12 @@ fastjson2 2.0.43 + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/xiaou-common/src/main/java/com/xiaou/common/cache/RedisValueStore.java b/xiaou-common/src/main/java/com/xiaou/common/cache/RedisValueStore.java new file mode 100644 index 000000000..722072edf --- /dev/null +++ b/xiaou-common/src/main/java/com/xiaou/common/cache/RedisValueStore.java @@ -0,0 +1,115 @@ +package com.xiaou.common.cache; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RAtomicLong; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * Typed Redis value operations. Cache misses are returned as empty values; + * type mismatches and Redis failures remain explicit to the caller. + */ +@Component +@RequiredArgsConstructor +public class RedisValueStore { + + private final RedissonClient redissonClient; + + public Optional find(String key, Class type) { + return cast(key, type, bucket(key).get()); + } + + public Optional take(String key, Class type) { + return cast(key, type, bucket(key).getAndDelete()); + } + + public void put(String key, Object value) { + bucket(key).set(value); + } + + public void put(String key, Object value, Duration ttl) { + requirePositive(ttl); + bucket(key).set(value, ttl); + } + + public boolean delete(String... keys) { + if (keys == null || keys.length == 0) { + return false; + } + if (keys.length == 1) { + return bucket(keys[0]).delete(); + } + return redissonClient.getKeys().delete(keys) > 0; + } + + public boolean exists(String key) { + return bucket(key).isExists(); + } + + public boolean expire(String key, Duration ttl) { + requirePositive(ttl); + return bucket(key).expire(ttl); + } + + public long increment(String key, long delta) { + return redissonClient.getAtomicLong(requireKey(key)).addAndGet(delta); + } + + public long increment(String key, long delta, Duration ttlWhenCreated) { + requirePositive(ttlWhenCreated); + RAtomicLong value = redissonClient.getAtomicLong(requireKey(key)); + long result = value.addAndGet(delta); + if (result == delta) { + value.expire(ttlWhenCreated); + } + return result; + } + + public OptionalLong counter(String key) { + RAtomicLong value = redissonClient.getAtomicLong(requireKey(key)); + return value.isExists() ? OptionalLong.of(value.get()) : OptionalLong.empty(); + } + + public void setCounter(String key, long value) { + redissonClient.getAtomicLong(requireKey(key)).set(value); + } + + public Iterable keys(String pattern) { + return redissonClient.getKeys().getKeysByPattern(requireKey(pattern)); + } + + private RBucket bucket(String key) { + return redissonClient.getBucket(requireKey(key)); + } + + private Optional cast(String key, Class type, Object value) { + Objects.requireNonNull(type, "type must not be null"); + if (value == null) { + return Optional.empty(); + } + if (!type.isInstance(value)) { + throw new IllegalStateException("Redis value for key '" + key + "' is " + + value.getClass().getSimpleName() + ", expected " + type.getSimpleName()); + } + return Optional.of(type.cast(value)); + } + + private String requireKey(String key) { + if (key == null || key.isBlank()) { + throw new IllegalArgumentException("Redis key must not be blank"); + } + return key; + } + + private void requirePositive(Duration ttl) { + if (ttl == null || ttl.isZero() || ttl.isNegative()) { + throw new IllegalArgumentException("Redis TTL must be positive"); + } + } +} diff --git a/xiaou-common/src/main/java/com/xiaou/common/config/ApplicationTaskExecutorConfig.java b/xiaou-common/src/main/java/com/xiaou/common/config/ApplicationTaskExecutorConfig.java new file mode 100644 index 000000000..9192cee6d --- /dev/null +++ b/xiaou-common/src/main/java/com/xiaou/common/config/ApplicationTaskExecutorConfig.java @@ -0,0 +1,30 @@ +package com.xiaou.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * Shared bounded executor for application I/O tasks that need an explicit time budget. + */ +@Configuration +public class ApplicationTaskExecutorConfig { + + @Bean(name = "applicationIoExecutor") + public Executor applicationIoExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setMaxPoolSize(24); + executor.setQueueCapacity(200); + executor.setThreadNamePrefix("application-io-"); + executor.setKeepAliveSeconds(60); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } +} diff --git a/xiaou-common/src/main/java/com/xiaou/common/utils/ConcurrentUtils.java b/xiaou-common/src/main/java/com/xiaou/common/utils/ConcurrentUtils.java deleted file mode 100644 index ee771c846..000000000 --- a/xiaou-common/src/main/java/com/xiaou/common/utils/ConcurrentUtils.java +++ /dev/null @@ -1,1043 +0,0 @@ -package com.xiaou.common.utils; - -import lombok.extern.slf4j.Slf4j; - -import java.time.Duration; -import java.util.Map; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.LongAdder; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Supplier; - -/** - * 并发工具类 - *

- * 提供并发控制原语、限流器、熔断器、分布式锁抽象等功能 - *

- *

- * 核心特性: - *

    - *
  • 限流器:令牌桶、滑动窗口
  • - *
  • 熔断器:自动熔断与恢复
  • - *
  • 锁工具:安全锁操作、读写锁、分段锁
  • - *
  • 并发计数器:高性能计数与统计
  • - *
  • 同步屏障:CountDownLatch、CyclicBarrier封装
  • - *
  • 缓存工具:线程安全的懒加载缓存
  • - *
- *

- * - * @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 bucket = redissonClient.getBucket(key); - return bucket.remainTimeToLive() / 1000; - } catch (Exception e) { - log.error("获取过期时间失败", e); - return 0; - } - } - - /** - * 判断key是否存在 - * - * @param key 键 - * @return true 存在 false不存在 - */ - public boolean hasKey(String key) { - try { - return redissonClient.getBucket(key).isExists(); - } catch (Exception e) { - log.error("判断key是否存在失败", e); - return false; - } - } - - /** - * 删除缓存 - * - * @param key 可以传一个值 或多个 - */ - public boolean del(String... key) { - try { - if (key != null && key.length > 0) { - if (key.length == 1) { - return redissonClient.getBucket(key[0]).delete(); - } else { - return redissonClient.getKeys().delete(key) > 0; - } - } - return false; - } catch (Exception e) { - log.error("删除缓存失败", e); - return false; - } - } - - // ============================String============================= - - /** - * 普通缓存获取 - * - * @param key 键 - * @return 值 - */ - public Object get(String key) { - try { - return key == null ? null : redissonClient.getBucket(key).get(); - } catch (Exception e) { - log.error("获取缓存失败", e); - return null; - } - } - - /** - * 获取缓存并指定返回类型 - * - * @param key 键 - * @param clazz 返回类型 - * @return 值 - */ - @SuppressWarnings("unchecked") - public T get(String key, Class clazz) { - try { - Object value = get(key); - if (value == null) { - return null; - } - return (T) value; - } catch (Exception e) { - log.error("获取缓存失败", e); - return null; - } - } - - /** - * 普通缓存放入 - * - * @param key 键 - * @param value 值 - * @return true成功 false失败 - */ - public boolean set(String key, Object value) { - try { - redissonClient.getBucket(key).set(value); - return true; - } catch (Exception e) { - log.error("设置缓存失败", e); - return false; - } - } - - /** - * 普通缓存放入并设置时间 - * - * @param key 键 - * @param value 值 - * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 - * @return true成功 false 失败 - */ - public boolean set(String key, Object value, long time) { - try { - if (time > 0) { - redissonClient.getBucket(key).set(value, Duration.ofSeconds(time)); - } else { - redissonClient.getBucket(key).set(value); - } - return true; - } catch (Exception e) { - log.error("设置缓存失败", e); - return false; - } - } - - /** - * 递增 - * - * @param key 键 - * @param delta 要增加几(大于0) - * @return 增加后的值 - */ - public long incr(String key, long delta) { - try { - if (delta < 0) { - throw new RuntimeException("递增因子必须大于0"); - } - return redissonClient.getAtomicLong(key).addAndGet(delta); - } catch (Exception e) { - log.error("递增失败", e); - return 0; - } - } - - /** - * 递减 - * - * @param key 键 - * @param delta 要减少几(大于0) - * @return 减少后的值 - */ - public long decr(String key, long delta) { - try { - if (delta < 0) { - throw new RuntimeException("递减因子必须大于0"); - } - return redissonClient.getAtomicLong(key).addAndGet(-delta); - } catch (Exception e) { - log.error("递减失败", e); - return 0; - } - } - - // ================================Map================================= - - /** - * HashGet - * - * @param key 键 不能为null - * @param item 项 不能为null - * @return 值 - */ - public Object hget(String key, String item) { - try { - return redissonClient.getMap(key).get(item); - } catch (Exception e) { - log.error("Hash获取失败", e); - return null; - } - } - - /** - * 获取hashKey对应的所有键值 - * - * @param key 键 - * @return 对应的多个键值 - */ - public Map hmget(String key) { - try { - return redissonClient.getMap(key).readAllMap(); - } catch (Exception e) { - log.error("Hash获取所有键值失败", e); - return Collections.emptyMap(); - } - } - - /** - * HashSet - * - * @param key 键 - * @param map 对应多个键值 - * @return true 成功 false 失败 - */ - public boolean hmset(String key, Map map) { - try { - redissonClient.getMap(key).putAll(map); - return true; - } catch (Exception e) { - log.error("Hash设置多个键值失败", e); - return false; - } - } - - /** - * HashSet 并设置时间 - * - * @param key 键 - * @param map 对应多个键值 - * @param time 时间(秒) - * @return true成功 false失败 - */ - public boolean hmset(String key, Map map, long time) { - try { - RMap rMap = redissonClient.getMap(key); - rMap.putAll(map); - if (time > 0) { - expire(key, time); - } - return true; - } catch (Exception e) { - log.error("Hash设置多个键值并设置时间失败", e); - return false; - } - } - - /** - * 向一张hash表中放入数据,如果不存在将创建 - * - * @param key 键 - * @param item 项 - * @param value 值 - * @return true 成功 false失败 - */ - public boolean hset(String key, String item, Object value) { - try { - redissonClient.getMap(key).put(item, value); - return true; - } catch (Exception e) { - log.error("Hash设置单个键值失败", e); - return false; - } - } - - /** - * 向一张hash表中放入数据,如果不存在将创建 - * - * @param key 键 - * @param item 项 - * @param value 值 - * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 - * @return true 成功 false失败 - */ - public boolean hset(String key, String item, Object value, long time) { - try { - redissonClient.getMap(key).put(item, value); - if (time > 0) { - expire(key, time); - } - return true; - } catch (Exception e) { - log.error("Hash设置单个键值并设置时间失败", e); - return false; - } - } - - /** - * 删除hash表中的值 - * - * @param key 键 不能为null - * @param item 项 可以使多个 不能为null - */ - public boolean hdel(String key, Object... item) { - try { - return redissonClient.getMap(key).fastRemove(item) > 0; - } catch (Exception e) { - log.error("Hash删除失败", e); - return false; - } - } - - /** - * 判断hash表中是否有该项的值 - * - * @param key 键 不能为null - * @param item 项 不能为null - * @return true 存在 false不存在 - */ - public boolean hHasKey(String key, String item) { - try { - return redissonClient.getMap(key).containsKey(item); - } catch (Exception e) { - log.error("判断Hash是否存在键失败", e); - return false; - } - } - - // ============================Set============================= - - /** - * 根据key获取Set中的所有值 - * - * @param key 键 - * @return Set集合 - */ - public Set sGet(String key) { - try { - return redissonClient.getSet(key).readAll(); - } catch (Exception e) { - log.error("Set获取所有值失败", e); - return Collections.emptySet(); - } - } - - /** - * 根据value从一个set中查询,是否存在 - * - * @param key 键 - * @param value 值 - * @return true 存在 false不存在 - */ - public boolean sHasKey(String key, Object value) { - try { - return redissonClient.getSet(key).contains(value); - } catch (Exception e) { - log.error("Set判断是否存在值失败", e); - return false; - } - } - - /** - * 将数据放入set缓存 - * - * @param key 键 - * @param values 值 可以是多个 - * @return 成功个数 - */ - public long sSet(String key, Object... values) { - try { - if (values == null || values.length == 0) { - return 0; - } - RSet set = redissonClient.getSet(key); - return addSetValues(set, values); - } catch (Exception e) { - log.error("Set添加值失败", e); - return 0; - } - } - - /** - * 将set数据放入缓存 - * - * @param key 键 - * @param time 时间(秒) - * @param values 值 可以是多个 - * @return 成功个数 - */ - public long sSetAndTime(String key, long time, Object... values) { - try { - if (values == null || values.length == 0) { - return 0; - } - RSet set = redissonClient.getSet(key); - long count = addSetValues(set, values); - if (time > 0) { - expire(key, time); - } - return count; - } catch (Exception e) { - log.error("Set添加值并设置时间失败", e); - return 0; - } - } - - /** - * 获取set缓存的长度 - * - * @param key 键 - * @return 长度 - */ - public long sGetSetSize(String key) { - try { - return redissonClient.getSet(key).size(); - } catch (Exception e) { - log.error("Set获取长度失败", e); - return 0; - } - } - - /** - * 移除值为value的 - * - * @param key 键 - * @param values 值 可以是多个 - * @return 移除的个数 - */ - public long setRemove(String key, Object... values) { - try { - if (values == null || values.length == 0) { - return 0; - } - RSet set = redissonClient.getSet(key); - long removed = 0; - for (Object value : values) { - if (set.remove(value)) { - removed++; - } - } - return removed; - } catch (Exception e) { - log.error("Set移除值失败", e); - return 0; - } - } - - // ===============================List================================= - - /** - * 获取list缓存的内容 - * - * @param key 键 - * @param start 开始 - * @param end 结束 0 到 -1代表所有值 - * @return List集合 - */ - public List lGet(String key, long start, long end) { - try { - return redissonClient.getList(key).range((int) start, (int) end); - } catch (Exception e) { - log.error("List获取范围值失败", e); - return Collections.emptyList(); - } - } - - /** - * 获取list缓存的长度 - * - * @param key 键 - * @return 长度 - */ - public long lGetListSize(String key) { - try { - return redissonClient.getList(key).size(); - } catch (Exception e) { - log.error("List获取长度失败", e); - return 0; - } - } - - /** - * 通过索引 获取list中的值 - * - * @param key 键 - * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 - * @return 值 - */ - public Object lGetIndex(String key, long index) { - try { - return redissonClient.getList(key).get((int) index); - } catch (Exception e) { - log.error("List获取索引值失败", e); - return null; - } - } - - /** - * 将list放入缓存 - * - * @param key 键 - * @param value 值 - * @return true成功 false失败 - */ - public boolean lSet(String key, Object value) { - try { - redissonClient.getList(key).add(value); - return true; - } catch (Exception e) { - log.error("List添加值失败", e); - return false; - } - } - - /** - * 将list放入缓存 - * - * @param key 键 - * @param value 值 - * @param time 时间(秒) - * @return true成功 false失败 - */ - public boolean lSet(String key, Object value, long time) { - try { - redissonClient.getList(key).add(value); - if (time > 0) { - expire(key, time); - } - return true; - } catch (Exception e) { - log.error("List添加值并设置时间失败", e); - return false; - } - } - - /** - * 将list放入缓存 - * - * @param key 键 - * @param value 值 - * @return true成功 false失败 - */ - public boolean lSet(String key, List value) { - try { - redissonClient.getList(key).addAll(value); - return true; - } catch (Exception e) { - log.error("List添加集合失败", e); - return false; - } - } - - /** - * 将list放入缓存 - * - * @param key 键 - * @param value 值 - * @param time 时间(秒) - * @return true成功 false失败 - */ - public boolean lSet(String key, List value, long time) { - try { - redissonClient.getList(key).addAll(value); - if (time > 0) { - expire(key, time); - } - return true; - } catch (Exception e) { - log.error("List添加集合并设置时间失败", e); - return false; - } - } - - /** - * 根据索引修改list中的某条数据 - * - * @param key 键 - * @param index 索引 - * @param value 值 - * @return true成功 false失败 - */ - public boolean lUpdateIndex(String key, long index, Object value) { - try { - redissonClient.getList(key).set((int) index, value); - return true; - } catch (Exception e) { - log.error("List更新索引值失败", e); - return false; - } - } - - - // ===============================分布式锁================================= - - /** - * 获取分布式锁 - * - * @param lockKey 锁的key - * @return 锁对象 - */ - public RLock getLock(String lockKey) { - return redissonClient.getLock(lockKey); - } - - /** - * 获取公平锁 - * - * @param lockKey 锁的key - * @return 公平锁对象 - */ - public RLock getFairLock(String lockKey) { - return redissonClient.getFairLock(lockKey); - } - - /** - * 获取读写锁 - * - * @param lockKey 锁的key - * @return 读写锁对象 - */ - public RReadWriteLock getReadWriteLock(String lockKey) { - return redissonClient.getReadWriteLock(lockKey); - } - - // ===============================其他工具方法================================= - - /** - * 获取Redisson客户端 - * - * @return RedissonClient - */ - public RedissonClient getRedissonClient() { - return redissonClient; - } - - /** - * 清空所有缓存 - */ - public void flushAll() { - try { - redissonClient.getKeys().flushall(); - } catch (Exception e) { - log.error("清空所有缓存失败", e); - } - } - - /** - * 获取所有key - * - * @param pattern 匹配模式 - * @return key集合 - */ - public Iterable getKeys(String pattern) { - try { - return redissonClient.getKeys().getKeysByPattern(pattern); - } catch (Exception e) { - log.error("获取所有key失败", e); - return Collections.emptyList(); - } - } - - /** - * 根据模式删除key - * - * @param pattern 匹配模式 - * @return 删除的key数量 - */ - public long deleteByPattern(String pattern) { - try { - return redissonClient.getKeys().deleteByPattern(pattern); - } catch (Exception e) { - log.error("根据模式删除key失败", e); - return 0; - } - } - - // ================================ZSet(有序集合)================================= - - /** - * ZSet添加元素 - * - * @param key 键 - * @param value 值 - * @param score 分数 - * @return true成功 false失败 - */ - public boolean zAdd(String key, Object value, double score) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - return sortedSet.add(score, value); - } catch (Exception e) { - log.error("ZSet添加元素失败", e); - return false; - } - } - - /** - * ZSet获取指定范围的元素(按分数从小到大) - * - * @param key 键 - * @param start 开始位置 - * @param end 结束位置 - * @return 元素集合 - */ - public Collection zRange(String key, int start, int end) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - return sortedSet.valueRange(start, end); - } catch (Exception e) { - log.error("ZSet获取范围元素失败", e); - return Collections.emptyList(); - } - } - - /** - * ZSet获取指定范围的元素(按分数从大到小) - * - * @param key 键 - * @param start 开始位置 - * @param end 结束位置 - * @return 元素集合 - */ - public Collection zRevRange(String key, int start, int end) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - return sortedSet.valueRangeReversed(start, end); - } catch (Exception e) { - log.error("ZSet倒序获取范围元素失败", e); - return Collections.emptyList(); - } - } - - /** - * ZSet获取指定范围的元素及分数(按分数从大到小) - * - * @param key 键 - * @param start 开始位置 - * @param end 结束位置 - * @return 元素和分数的Map - */ - public Map zRevRangeWithScores(String key, int start, int end) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - Collection values = sortedSet.valueRangeReversed(start, end); - Map result = new java.util.LinkedHashMap<>(); - for (Object value : values) { - Double score = sortedSet.getScore(value); - result.put(value, score); - } - return result; - } catch (Exception e) { - log.error("ZSet倒序获取元素和分数失败", e); - return Collections.emptyMap(); - } - } - - /** - * ZSet获取元素的分数 - * - * @param key 键 - * @param value 值 - * @return 分数 - */ - public Double zScore(String key, Object value) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - return sortedSet.getScore(value); - } catch (Exception e) { - log.error("ZSet获取分数失败", e); - return null; - } - } - - /** - * ZSet获取集合大小 - * - * @param key 键 - * @return 大小 - */ - public long zSize(String key) { - try { - return redissonClient.getScoredSortedSet(key).size(); - } catch (Exception e) { - log.error("ZSet获取大小失败", e); - return 0; - } - } - - /** - * ZSet移除元素 - * - * @param key 键 - * @param values 值 - * @return 移除的个数 - */ - public long zRemove(String key, Object... values) { - try { - if (values == null || values.length == 0) { - return 0; - } - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - long removed = 0; - for (Object value : values) { - if (sortedSet.remove(value)) { - removed++; - } - } - return removed; - } catch (Exception e) { - log.error("ZSet移除元素失败", e); - return 0; - } - } - - /** - * ZSet移除指定排名范围的元素 - * - * @param key 键 - * @param start 开始排名 - * @param end 结束排名 - * @return 移除的个数 - */ - public long zRemoveRange(String key, int start, int end) { - try { - RScoredSortedSet sortedSet = redissonClient.getScoredSortedSet(key); - return sortedSet.removeRangeByRank(start, end); - } catch (Exception e) { - log.error("ZSet移除范围元素失败", e); - return 0; - } - } - - private long addSetValues(RSet set, Object... values) { - long added = 0; - for (Object value : values) { - if (set.add(value)) { - added++; - } - } - return added; - } -} diff --git a/xiaou-common/src/main/java/com/xiaou/common/utils/ThreadPoolUtils.java b/xiaou-common/src/main/java/com/xiaou/common/utils/ThreadPoolUtils.java deleted file mode 100644 index d3b0cbd47..000000000 --- a/xiaou-common/src/main/java/com/xiaou/common/utils/ThreadPoolUtils.java +++ /dev/null @@ -1,1206 +0,0 @@ -package com.xiaou.common.utils; - -import lombok.Builder; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * 线程池与并发工具类 - *

- * 提供线程池管理、异步任务执行、批量并行处理、超时控制、重试机制等功能 - *

- *

- * 核心特性: - *

    - *
  • 多种预配置线程池:通用池、IO密集型、CPU密集型、调度池
  • - *
  • 支持虚拟线程(Java 21+)
  • - *
  • 丰富的并行处理方法:parallelMap、parallelMapWithTimeout、parallelMapWithLimit
  • - *
  • 超时控制与重试机制(支持指数退避)
  • - *
  • 线程池监控与动态调参
  • - *
  • 异步回调链式操作
  • - *
  • 快速失败模式
  • - *
- *

- * - * @author xiaou - */ -@Slf4j -public class ThreadPoolUtils { - - /** - * 全局共享线程池(用于通用异步任务) - */ - private static final ExecutorService COMMON_EXECUTOR; - - /** - * 调度线程池(用于定时任务) - */ - private static final ScheduledExecutorService SCHEDULER; - - /** - * IO密集型线程池(用于IO操作) - */ - private static final ExecutorService IO_EXECUTOR; - - /** - * CPU密集型线程池(用于计算任务) - */ - private static final ExecutorService CPU_EXECUTOR; - - /** - * 自定义线程池缓存 - */ - private static final Map POOL_CACHE = new ConcurrentHashMap<>(); - - /** - * CPU核心数 - */ - private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); - - /** - * 默认超时时间(秒) - */ - private static final long DEFAULT_TIMEOUT = 30; - - /** - * 默认重试次数 - */ - private static final int DEFAULT_RETRY_TIMES = 3; - - /** - * 默认重试间隔(毫秒) - */ - private static final long DEFAULT_RETRY_DELAY = 1000; - - /** - * 虚拟线程执行器(Java 21+) - */ - private static final ExecutorService VIRTUAL_EXECUTOR; - - /** - * 是否支持虚拟线程 - */ - private static final boolean VIRTUAL_THREADS_SUPPORTED; - - static { - // 初始化通用线程池 - COMMON_EXECUTOR = new ThreadPoolExecutor( - CPU_COUNT, - CPU_COUNT * 2, - 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(1000), - new NamedThreadFactory("common"), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - - // 初始化调度线程池 - SCHEDULER = Executors.newScheduledThreadPool( - Math.max(2, CPU_COUNT / 2), - new NamedThreadFactory("scheduler") - ); - - // 初始化IO密集型线程池 - IO_EXECUTOR = new ThreadPoolExecutor( - CPU_COUNT * 2, - CPU_COUNT * 4, - 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(2000), - new NamedThreadFactory("io"), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - - // 初始化CPU密集型线程池 - CPU_EXECUTOR = new ThreadPoolExecutor( - CPU_COUNT, - CPU_COUNT, - 0L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(500), - new NamedThreadFactory("cpu"), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - - // 初始化虚拟线程执行器(Java 21+) - ExecutorService virtualExecutor = null; - boolean virtualSupported = false; - try { - // 通过反射检测是否支持虚拟线程 - Class executorsClass = Executors.class; - java.lang.reflect.Method method = executorsClass.getMethod("newVirtualThreadPerTaskExecutor"); - virtualExecutor = (ExecutorService) method.invoke(null); - virtualSupported = true; - log.info("虚拟线程已启用(Java 21+)"); - } catch (Exception e) { - log.debug("当前JDK版本不支持虚拟线程,将使用普通线程池代替"); - virtualExecutor = IO_EXECUTOR; // 回退到IO线程池 - } - VIRTUAL_EXECUTOR = virtualExecutor; - VIRTUAL_THREADS_SUPPORTED = virtualSupported; - - // 注册JVM关闭钩子 - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - log.info("JVM关闭,开始清理线程池..."); - shutdownAll(); - })); - } - - // ==================== 线程池获取 ==================== - - /** - * 获取通用线程池 - */ - public static ExecutorService getCommonExecutor() { - return COMMON_EXECUTOR; - } - - /** - * 获取IO密集型线程池 - */ - public static ExecutorService getIoExecutor() { - return IO_EXECUTOR; - } - - /** - * 获取CPU密集型线程池 - */ - public static ExecutorService getCpuExecutor() { - return CPU_EXECUTOR; - } - - /** - * 获取调度线程池 - */ - public static ScheduledExecutorService getScheduler() { - return SCHEDULER; - } - - /** - * 获取虚拟线程执行器(Java 21+,不支持时回退到IO线程池) - */ - public static ExecutorService getVirtualExecutor() { - return VIRTUAL_EXECUTOR; - } - - /** - * 是否支持虚拟线程 - */ - public static boolean isVirtualThreadsSupported() { - return VIRTUAL_THREADS_SUPPORTED; - } - - /** - * 获取或创建自定义线程池 - * - * @param name 线程池名称 - * @param coreSize 核心线程数 - * @param maxSize 最大线程数 - * @param queueSize 队列大小 - * @return ExecutorService - */ - public static ExecutorService getOrCreatePool(String name, int coreSize, int maxSize, int queueSize) { - return POOL_CACHE.computeIfAbsent(name, k -> new ThreadPoolExecutor( - coreSize, - maxSize, - 60L, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(queueSize), - new NamedThreadFactory(name), - new ThreadPoolExecutor.CallerRunsPolicy() - )); - } - - // ==================== 异步任务执行 ==================== - - /** - * 异步执行任务(无返回值) - * - * @param task 任务 - */ - public static CompletableFuture runAsync(Runnable task) { - return CompletableFuture.runAsync(task, COMMON_EXECUTOR); - } - - /** - * 异步执行任务(有返回值) - * - * @param supplier 任务 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsync(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, COMMON_EXECUTOR); - } - - /** - * 异步执行IO密集型任务 - * - * @param supplier 任务 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncIO(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, IO_EXECUTOR); - } - - /** - * 异步执行CPU密集型任务 - * - * @param supplier 任务 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncCPU(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, CPU_EXECUTOR); - } - - /** - * 使用虚拟线程异步执行任务(Java 21+,不支持时回退到IO线程池) - * - * @param supplier 任务 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncVirtual(Supplier supplier) { - return CompletableFuture.supplyAsync(supplier, VIRTUAL_EXECUTOR); - } - - // ==================== 批量并行执行 ==================== - - /** - * 并行执行多个任务并等待全部完成 - * - * @param tasks 任务列表 - * @return 结果列表 - */ - @SafeVarargs - public static List parallel(Supplier... tasks) { - List> futures = Arrays.stream(tasks) - .map(task -> CompletableFuture.supplyAsync(task, COMMON_EXECUTOR)) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - /** - * 并行处理集合中的每个元素 - * - * @param items 数据集合 - * @param processor 处理函数 - * @return 处理结果列表 - */ - public static List parallelMap(Collection items, Function processor) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - List> futures = items.stream() - .map(item -> CompletableFuture.supplyAsync(() -> processor.apply(item), COMMON_EXECUTOR)) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - /** - * 并行处理集合(使用IO线程池) - * - * @param items 数据集合 - * @param processor 处理函数 - * @return 处理结果列表 - */ - public static List parallelMapIO(Collection items, Function processor) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - List> futures = items.stream() - .map(item -> CompletableFuture.supplyAsync(() -> processor.apply(item), IO_EXECUTOR)) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - /** - * 并行处理集合,支持超时 - * - * @param items 数据集合 - * @param processor 处理函数 - * @param timeout 超时时间 - * @param unit 时间单位 - * @return 处理结果列表(超时的元素返回null) - */ - public static List parallelMapWithTimeout(Collection items, Function processor, - long timeout, TimeUnit unit) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - List> futures = items.stream() - .map(item -> CompletableFuture.supplyAsync(() -> processor.apply(item), COMMON_EXECUTOR) - .orTimeout(timeout, unit) - .exceptionally(ex -> { - log.warn("并行任务超时或异常: {}", ex.getMessage()); - return null; - })) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - /** - * 执行所有任务并等待完成(忽略返回值) - * - * @param tasks 任务列表 - */ - public static void runAll(Runnable... tasks) { - CompletableFuture[] futures = Arrays.stream(tasks) - .map(task -> CompletableFuture.runAsync(task, COMMON_EXECUTOR)) - .toArray(CompletableFuture[]::new); - CompletableFuture.allOf(futures).join(); - } - - /** - * 执行任务列表(限制并发数) - * - * @param items 数据集合 - * @param processor 处理函数 - * @param concurrency 最大并发数 - * @return 处理结果列表 - */ - public static List parallelMapWithLimit(Collection items, Function processor, - int concurrency) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - Semaphore semaphore = new Semaphore(concurrency); - List> futures = items.stream() - .map(item -> CompletableFuture.supplyAsync(() -> { - try { - semaphore.acquire(); - return processor.apply(item); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } finally { - semaphore.release(); - } - }, COMMON_EXECUTOR)) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - /** - * 并行处理集合(快速失败模式:任一任务失败立即终止) - * - * @param items 数据集合 - * @param processor 处理函数 - * @return 处理结果列表 - * @throws RuntimeException 如果任一任务失败 - */ - public static List parallelMapFailFast(Collection items, Function processor) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - List> futures = new ArrayList<>(); - AtomicInteger failedFlag = new AtomicInteger(0); - - for (T item : items) { - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - // 检查是否已有任务失败 - if (failedFlag.get() > 0) { - throw new CancellationException("其他任务已失败,取消执行"); - } - try { - return processor.apply(item); - } catch (Exception e) { - failedFlag.incrementAndGet(); - throw e; - } - }, COMMON_EXECUTOR); - futures.add(future); - } - - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } catch (CompletionException e) { - // 取消所有未完成的任务 - futures.forEach(f -> f.cancel(true)); - throw new RuntimeException("并行任务执行失败", e.getCause()); - } - } - - /** - * 并行处理集合(使用虚拟线程,适合大量IO密集型任务) - * - * @param items 数据集合 - * @param processor 处理函数 - * @return 处理结果列表 - */ - public static List parallelMapVirtual(Collection items, Function processor) { - if (items == null || items.isEmpty()) { - return Collections.emptyList(); - } - - List> futures = items.stream() - .map(item -> CompletableFuture.supplyAsync(() -> processor.apply(item), VIRTUAL_EXECUTOR)) - .collect(Collectors.toList()); - - return futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - } - - // ==================== 超时控制 ==================== - - /** - * 带超时的异步执行 - * - * @param supplier 任务 - * @param timeout 超时时间 - * @param unit 时间单位 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncWithTimeout(Supplier supplier, - long timeout, TimeUnit unit) { - return CompletableFuture.supplyAsync(supplier, COMMON_EXECUTOR) - .orTimeout(timeout, unit); - } - - /** - * 带超时和默认值的异步执行 - * - * @param supplier 任务 - * @param timeout 超时时间 - * @param unit 时间单位 - * @param defaultValue 超时默认值 - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncWithDefault(Supplier supplier, - long timeout, TimeUnit unit, - T defaultValue) { - return CompletableFuture.supplyAsync(supplier, COMMON_EXECUTOR) - .orTimeout(timeout, unit) - .exceptionally(ex -> { - log.warn("任务执行超时或异常,返回默认值: {}", ex.getMessage()); - return defaultValue; - }); - } - - /** - * 同步执行任务(带超时) - * - * @param supplier 任务 - * @param timeout 超时时间 - * @param unit 时间单位 - * @return 执行结果 - */ - public static T executeWithTimeout(Supplier supplier, long timeout, TimeUnit unit) { - try { - return CompletableFuture.supplyAsync(supplier, COMMON_EXECUTOR) - .get(timeout, unit); - } catch (TimeoutException e) { - log.warn("任务执行超时"); - throw new RuntimeException("任务执行超时", e); - } catch (Exception e) { - log.error("任务执行异常", e); - throw new RuntimeException("任务执行异常", e); - } - } - - // ==================== 重试机制 ==================== - - /** - * 带重试的任务执行 - * - * @param supplier 任务 - * @param retryTimes 重试次数 - * @param delayMs 重试间隔(毫秒) - * @return 执行结果 - */ - public static T executeWithRetry(Supplier supplier, int retryTimes, long delayMs) { - int attempts = 0; - Exception lastException = null; - - while (attempts <= retryTimes) { - try { - return supplier.get(); - } catch (Exception e) { - lastException = e; - attempts++; - if (attempts <= retryTimes) { - log.warn("任务执行失败,第{}次重试,错误: {}", attempts, e.getMessage()); - try { - Thread.sleep(delayMs); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException(ie); - } - } - } - } - - log.error("任务重试{}次后仍然失败", retryTimes); - throw new RuntimeException("任务执行失败", lastException); - } - - /** - * 带指数退避重试的任务执行 - *

- * 重试间隔按指数增长:initialDelayMs, initialDelayMs*2, initialDelayMs*4... - * 适合网络请求等可能因临时过载失败的场景 - *

- * - * @param supplier 任务 - * @param retryTimes 重试次数 - * @param initialDelayMs 初始重试间隔(毫秒) - * @param maxDelayMs 最大重试间隔(毫秒) - * @return 执行结果 - */ - public static T executeWithExponentialBackoff(Supplier supplier, int retryTimes, - long initialDelayMs, long maxDelayMs) { - int attempts = 0; - Exception lastException = null; - long currentDelay = initialDelayMs; - - while (attempts <= retryTimes) { - try { - return supplier.get(); - } catch (Exception e) { - lastException = e; - attempts++; - if (attempts <= retryTimes) { - // 添加随机抖动,避免雷同重试 - long jitter = (long) (currentDelay * 0.2 * Math.random()); - long sleepTime = Math.min(currentDelay + jitter, maxDelayMs); - log.warn("任务执行失败,第{}次重试,等待{}ms,错误: {}", attempts, sleepTime, e.getMessage()); - try { - Thread.sleep(sleepTime); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException(ie); - } - currentDelay = Math.min(currentDelay * 2, maxDelayMs); - } - } - } - - log.error("任务重试{}次后仍然失败", retryTimes); - throw new RuntimeException("任务执行失败", lastException); - } - - /** - * 带指数退避重试的任务执行(使用默认参数) - * - * @param supplier 任务 - * @return 执行结果 - */ - public static T executeWithExponentialBackoff(Supplier supplier) { - return executeWithExponentialBackoff(supplier, DEFAULT_RETRY_TIMES, 500, 10000); - } - - /** - * 带条件重试的任务执行 - *

- * 可以指定哪些异常需要重试,哪些异常直接抛出 - *

- * - * @param supplier 任务 - * @param retryTimes 重试次数 - * @param delayMs 重试间隔(毫秒) - * @param retryCondition 重试条件(返回true表示需要重试) - * @return 执行结果 - */ - public static T executeWithConditionalRetry(Supplier supplier, int retryTimes, - long delayMs, Predicate retryCondition) { - int attempts = 0; - Exception lastException = null; - - while (attempts <= retryTimes) { - try { - return supplier.get(); - } catch (Exception e) { - lastException = e; - // 检查是否需要重试 - if (!retryCondition.test(e)) { - throw new RuntimeException("不可重试的异常", e); - } - attempts++; - if (attempts <= retryTimes) { - log.warn("任务执行失败,第{}次重试,错误: {}", attempts, e.getMessage()); - try { - Thread.sleep(delayMs); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException(ie); - } - } - } - } - - log.error("任务重试{}次后仍然失败", retryTimes); - throw new RuntimeException("任务执行失败", lastException); - } - - /** - * 带重试的任务执行(使用默认参数) - * - * @param supplier 任务 - * @return 执行结果 - */ - public static T executeWithRetry(Supplier supplier) { - return executeWithRetry(supplier, DEFAULT_RETRY_TIMES, DEFAULT_RETRY_DELAY); - } - - /** - * 带重试的异步任务执行 - * - * @param supplier 任务 - * @param retryTimes 重试次数 - * @param delayMs 重试间隔(毫秒) - * @return CompletableFuture - */ - public static CompletableFuture supplyAsyncWithRetry(Supplier supplier, - int retryTimes, long delayMs) { - return CompletableFuture.supplyAsync(() -> executeWithRetry(supplier, retryTimes, delayMs), COMMON_EXECUTOR); - } - - // ==================== 定时调度 ==================== - - /** - * 延迟执行任务 - * - * @param task 任务 - * @param delay 延迟时间 - * @param unit 时间单位 - * @return ScheduledFuture - */ - public static ScheduledFuture schedule(Runnable task, long delay, TimeUnit unit) { - return SCHEDULER.schedule(task, delay, unit); - } - - /** - * 延迟执行任务(带返回值) - * - * @param callable 任务 - * @param delay 延迟时间 - * @param unit 时间单位 - * @return ScheduledFuture - */ - public static ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { - return SCHEDULER.schedule(callable, delay, unit); - } - - /** - * 固定频率执行任务 - * - * @param task 任务 - * @param initialDelay 初始延迟 - * @param period 执行间隔 - * @param unit 时间单位 - * @return ScheduledFuture - */ - public static ScheduledFuture scheduleAtFixedRate(Runnable task, long initialDelay, - long period, TimeUnit unit) { - return SCHEDULER.scheduleAtFixedRate(task, initialDelay, period, unit); - } - - /** - * 固定延迟执行任务 - * - * @param task 任务 - * @param initialDelay 初始延迟 - * @param delay 延迟时间 - * @param unit 时间单位 - * @return ScheduledFuture - */ - public static ScheduledFuture scheduleWithFixedDelay(Runnable task, long initialDelay, - long delay, TimeUnit unit) { - return SCHEDULER.scheduleWithFixedDelay(task, initialDelay, delay, unit); - } - - // ==================== 竞态任务 ==================== - - /** - * 执行多个任务,返回最先完成的结果 - * - * @param tasks 任务列表 - * @return 最先完成的结果 - */ - @SafeVarargs - public static T anyOf(Supplier... tasks) { - if (tasks == null || tasks.length == 0) { - throw new IllegalArgumentException("任务列表不能为空"); - } - - CompletableFuture[] futures = Arrays.stream(tasks) - .map(task -> CompletableFuture.supplyAsync(task, COMMON_EXECUTOR)) - .toArray(CompletableFuture[]::new); - - try { - @SuppressWarnings("unchecked") - T result = (T) CompletableFuture.anyOf(futures).get(DEFAULT_TIMEOUT, TimeUnit.SECONDS); - return result; - } catch (Exception e) { - throw new RuntimeException("获取最快任务结果失败", e); - } - } - - // ==================== 线程池状态监控 ==================== - - /** - * 获取线程池状态信息 - * - * @param executor 线程池 - * @return 状态信息Map - */ - public static Map getPoolStats(ExecutorService executor) { - Map stats = new LinkedHashMap<>(); - - if (executor instanceof ThreadPoolExecutor) { - ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; - stats.put("corePoolSize", pool.getCorePoolSize()); - stats.put("maximumPoolSize", pool.getMaximumPoolSize()); - stats.put("activeCount", pool.getActiveCount()); - stats.put("poolSize", pool.getPoolSize()); - stats.put("largestPoolSize", pool.getLargestPoolSize()); - stats.put("taskCount", pool.getTaskCount()); - stats.put("completedTaskCount", pool.getCompletedTaskCount()); - stats.put("queueSize", pool.getQueue().size()); - stats.put("queueRemainingCapacity", pool.getQueue().remainingCapacity()); - stats.put("isShutdown", pool.isShutdown()); - stats.put("isTerminated", pool.isTerminated()); - } - - return stats; - } - - /** - * 获取通用线程池状态 - */ - public static Map getCommonPoolStats() { - return getPoolStats(COMMON_EXECUTOR); - } - - /** - * 获取IO线程池状态 - */ - public static Map getIoPoolStats() { - return getPoolStats(IO_EXECUTOR); - } - - /** - * 获取CPU线程池状态 - */ - public static Map getCpuPoolStats() { - return getPoolStats(CPU_EXECUTOR); - } - - /** - * 获取所有线程池状态 - */ - public static Map> getAllPoolStats() { - Map> allStats = new LinkedHashMap<>(); - allStats.put("common", getCommonPoolStats()); - allStats.put("io", getIoPoolStats()); - allStats.put("cpu", getCpuPoolStats()); - allStats.put("virtualThreadsSupported", Collections.singletonMap("supported", VIRTUAL_THREADS_SUPPORTED)); - - // 添加自定义线程池状态 - POOL_CACHE.forEach((name, pool) -> allStats.put("custom_" + name, getPoolStats(pool))); - - return allStats; - } - - // ==================== 动态调整线程池参数 ==================== - - /** - * 动态调整线程池核心线程数 - * - * @param executor 线程池 - * @param coreSize 新的核心线程数 - */ - public static void setCorePoolSize(ExecutorService executor, int coreSize) { - if (executor instanceof ThreadPoolExecutor) { - ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; - pool.setCorePoolSize(coreSize); - log.info("线程池核心线程数已调整为: {}", coreSize); - } - } - - /** - * 动态调整线程池最大线程数 - * - * @param executor 线程池 - * @param maxSize 新的最大线程数 - */ - public static void setMaxPoolSize(ExecutorService executor, int maxSize) { - if (executor instanceof ThreadPoolExecutor) { - ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; - pool.setMaximumPoolSize(maxSize); - log.info("线程池最大线程数已调整为: {}", maxSize); - } - } - - /** - * 预热线程池(提前创建核心线程) - * - * @param executor 线程池 - */ - public static void prestartAllCoreThreads(ExecutorService executor) { - if (executor instanceof ThreadPoolExecutor) { - ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; - int count = pool.prestartAllCoreThreads(); - log.info("线程池已预热,创建核心线程数: {}", count); - } - } - - /** - * 预热所有内置线程池 - */ - public static void prestartAllPools() { - prestartAllCoreThreads(COMMON_EXECUTOR); - prestartAllCoreThreads(IO_EXECUTOR); - prestartAllCoreThreads(CPU_EXECUTOR); - log.info("所有内置线程池已预热"); - } - - // ==================== 异步回调链 ==================== - - /** - * 创建异步任务构建器 - * - * @param supplier 初始任务 - * @return AsyncChain - */ - public static AsyncChain asyncChain(Supplier supplier) { - return new AsyncChain<>(CompletableFuture.supplyAsync(supplier, COMMON_EXECUTOR)); - } - - /** - * 异步任务链式构建器 - */ - public static class AsyncChain { - private final CompletableFuture future; - - private AsyncChain(CompletableFuture future) { - this.future = future; - } - - /** - * 链式转换 - */ - public AsyncChain thenApply(Function fn) { - return new AsyncChain<>(future.thenApplyAsync(fn, COMMON_EXECUTOR)); - } - - /** - * 链式异步转换 - */ - public AsyncChain thenCompose(Function> fn) { - return new AsyncChain<>(future.thenComposeAsync(fn, COMMON_EXECUTOR)); - } - - /** - * 链式消费 - */ - public AsyncChain thenAccept(Consumer action) { - return new AsyncChain<>(future.thenApplyAsync(t -> { - action.accept(t); - return t; - }, COMMON_EXECUTOR)); - } - - /** - * 异常处理 - */ - public AsyncChain onError(Function handler) { - return new AsyncChain<>(future.exceptionally(handler)); - } - - /** - * 设置超时 - */ - public AsyncChain timeout(long timeout, TimeUnit unit) { - return new AsyncChain<>(future.orTimeout(timeout, unit)); - } - - /** - * 获取Future - */ - public CompletableFuture toFuture() { - return future; - } - - /** - * 阻塞获取结果 - */ - public T get() { - return future.join(); - } - - /** - * 带超时获取结果 - */ - public T get(long timeout, TimeUnit unit) throws TimeoutException { - try { - return future.get(timeout, unit); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - } - - // ==================== 线程池管理 ==================== - - /** - * 关闭指定线程池 - * - * @param executor 线程池 - * @param awaitSeconds 等待时间(秒) - */ - public static void shutdown(ExecutorService executor, long awaitSeconds) { - if (executor == null || executor.isShutdown()) { - return; - } - - executor.shutdown(); - try { - if (!executor.awaitTermination(awaitSeconds, TimeUnit.SECONDS)) { - executor.shutdownNow(); - if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("线程池未能正常关闭"); - } - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - /** - * 关闭所有线程池 - */ - public static void shutdownAll() { - log.info("开始关闭所有线程池..."); - - // 关闭自定义线程池 - POOL_CACHE.values().forEach(pool -> shutdown(pool, 10)); - POOL_CACHE.clear(); - - // 关闭内置线程池 - shutdown(COMMON_EXECUTOR, 30); - shutdown(IO_EXECUTOR, 30); - shutdown(CPU_EXECUTOR, 30); - shutdown(SCHEDULER, 10); - - // 关闭虚拟线程执行器(如果是独立的) - if (VIRTUAL_THREADS_SUPPORTED && VIRTUAL_EXECUTOR != IO_EXECUTOR) { - shutdown(VIRTUAL_EXECUTOR, 10); - } - - log.info("所有线程池已关闭"); - } - - /** - * 移除并关闭自定义线程池 - * - * @param name 线程池名称 - */ - public static void removePool(String name) { - ExecutorService pool = POOL_CACHE.remove(name); - if (pool != null) { - shutdown(pool, 10); - log.info("自定义线程池已移除: {}", name); - } - } - - // ==================== 辅助类 ==================== - - /** - * 命名线程工厂 - */ - public static class NamedThreadFactory implements ThreadFactory { - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String namePrefix; - private final boolean daemon; - - public NamedThreadFactory(String name) { - this(name, false); - } - - public NamedThreadFactory(String name, boolean daemon) { - this.namePrefix = "pool-" + name + "-thread-"; - this.daemon = daemon; - } - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); - t.setDaemon(daemon); - if (t.getPriority() != Thread.NORM_PRIORITY) { - t.setPriority(Thread.NORM_PRIORITY); - } - return t; - } - } - - /** - * 任务执行结果包装器 - */ - public static class TaskResult { - private final boolean success; - private final T data; - private final String errorMessage; - private final long executionTime; - - private TaskResult(boolean success, T data, String errorMessage, long executionTime) { - this.success = success; - this.data = data; - this.errorMessage = errorMessage; - this.executionTime = executionTime; - } - - public static TaskResult success(T data, long executionTime) { - return new TaskResult<>(true, data, null, executionTime); - } - - public static TaskResult failure(String errorMessage, long executionTime) { - return new TaskResult<>(false, null, errorMessage, executionTime); - } - - public boolean isSuccess() { - return success; - } - - public T getData() { - return data; - } - - public String getErrorMessage() { - return errorMessage; - } - - public long getExecutionTime() { - return executionTime; - } - } - - /** - * 带执行时间统计的任务执行 - * - * @param supplier 任务 - * @return TaskResult - */ - public static TaskResult executeWithStats(Supplier supplier) { - long startTime = System.currentTimeMillis(); - try { - T result = supplier.get(); - return TaskResult.success(result, System.currentTimeMillis() - startTime); - } catch (Exception e) { - return TaskResult.failure(e.getMessage(), System.currentTimeMillis() - startTime); - } - } - - // ==================== 配置类 ==================== - - /** - * 线程池配置 - */ - @Data - @Builder - public static class PoolConfig { - /** 核心线程数 */ - @Builder.Default - private int coreSize = CPU_COUNT; - /** 最大线程数 */ - @Builder.Default - private int maxSize = CPU_COUNT * 2; - /** 队列大小 */ - @Builder.Default - private int queueSize = 1000; - /** 线程存活时间(秒) */ - @Builder.Default - private long keepAliveSeconds = 60; - /** 线程名前缀 */ - @Builder.Default - private String threadNamePrefix = "custom"; - /** 是否守护线程 */ - @Builder.Default - private boolean daemon = false; - - /** - * 根据配置创建线程池 - */ - public ThreadPoolExecutor build() { - return new ThreadPoolExecutor( - coreSize, - maxSize, - keepAliveSeconds, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(queueSize), - new NamedThreadFactory(threadNamePrefix, daemon), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - } - } - - /** - * 重试配置 - */ - @Data - @Builder - public static class RetryConfig { - /** 重试次数 */ - @Builder.Default - private int retryTimes = 3; - /** 初始延迟(毫秒) */ - @Builder.Default - private long initialDelayMs = 1000; - /** 最大延迟(毫秒) */ - @Builder.Default - private long maxDelayMs = 30000; - /** 是否使用指数退避 */ - @Builder.Default - private boolean exponentialBackoff = false; - /** 重试条件 */ - private Predicate retryCondition; - - /** - * 执行带重试的任务 - */ - public T execute(Supplier supplier) { - if (exponentialBackoff) { - return executeWithExponentialBackoff(supplier, retryTimes, initialDelayMs, maxDelayMs); - } else if (retryCondition != null) { - return executeWithConditionalRetry(supplier, retryTimes, initialDelayMs, retryCondition); - } else { - return executeWithRetry(supplier, retryTimes, initialDelayMs); - } - } - } - - /** - * 私有构造函数,防止实例化 - */ - private ThreadPoolUtils() { - throw new UnsupportedOperationException("工具类不允许实例化"); - } -} diff --git a/xiaou-common/src/test/java/com/xiaou/common/cache/RedisValueStoreTest.java b/xiaou-common/src/test/java/com/xiaou/common/cache/RedisValueStoreTest.java new file mode 100644 index 000000000..eb1510fb2 --- /dev/null +++ b/xiaou-common/src/test/java/com/xiaou/common/cache/RedisValueStoreTest.java @@ -0,0 +1,83 @@ +package com.xiaou.common.cache; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.redisson.api.RAtomicLong; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RedisValueStoreTest { + + @Mock + private RedissonClient redissonClient; + @Mock + private RBucket bucket; + @Mock + private RAtomicLong atomicLong; + + private RedisValueStore store; + + @BeforeEach + void setUp() { + store = new RedisValueStore(redissonClient); + } + + @Test + void shouldReturnTypedCacheHit() { + when(redissonClient.getBucket("key")).thenReturn(bucket); + when(bucket.get()).thenReturn("value"); + + assertThat(store.find("key", String.class)).contains("value"); + } + + @Test + void shouldRejectUnexpectedCachedType() { + when(redissonClient.getBucket("key")).thenReturn(bucket); + when(bucket.get()).thenReturn(42L); + + assertThatThrownBy(() -> store.find("key", String.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("key") + .hasMessageContaining("String"); + } + + @Test + void shouldConsumeValueAtomically() { + when(redissonClient.getBucket("ticket")).thenReturn(bucket); + when(bucket.getAndDelete()).thenReturn("17"); + + assertThat(store.take("ticket", String.class)).contains("17"); + verify(bucket).getAndDelete(); + } + + @Test + void shouldPropagateRedisFailureInsteadOfReturningCacheMiss() { + when(redissonClient.getBucket("key")).thenReturn(bucket); + when(bucket.get()).thenThrow(new IllegalStateException("redis unavailable")); + + assertThatThrownBy(() -> store.find("key", String.class)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("redis unavailable"); + } + + @Test + void shouldApplyTtlWhenCounterIsCreated() { + Duration ttl = Duration.ofSeconds(10); + when(redissonClient.getAtomicLong("rate")).thenReturn(atomicLong); + when(atomicLong.addAndGet(1)).thenReturn(1L); + + assertThat(store.increment("rate", 1, ttl)).isEqualTo(1L); + verify(atomicLong).expire(ttl); + } +} diff --git a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityAiSummaryServiceImpl.java b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityAiSummaryServiceImpl.java index 65dc15768..099bffc9d 100644 --- a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityAiSummaryServiceImpl.java +++ b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityAiSummaryServiceImpl.java @@ -3,9 +3,9 @@ import com.xiaou.ai.dto.community.PostSummaryResult; import com.xiaou.ai.service.AiCommunityService; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.common.exception.BusinessException; import com.xiaou.common.utils.JsonUtils; -import com.xiaou.common.utils.RedisUtil; import com.xiaou.community.config.CommunityProperties; import com.xiaou.community.domain.CommunityPost; import com.xiaou.community.mapper.CommunityPostMapper; @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.time.Duration; /** * 社区AI摘要Service实现类 @@ -32,7 +33,7 @@ public class CommunityAiSummaryServiceImpl implements CommunityAiSummaryService private final CommunityPostMapper communityPostMapper; private final CommunityProperties communityProperties; private final AiCommunityService aiCommunityService; - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; private final CommunityCacheService communityCacheService; private static final String SUMMARY_CACHE_KEY = "community:post:summary:"; @@ -55,7 +56,7 @@ public Map generateSummary(Long postId, boolean forceRefresh) { // 如果不是强制刷新,先从缓存获取 if (!forceRefresh) { - String cachedData = redisUtil.get(cacheKey, String.class); + String cachedData = getCachedSummary(cacheKey); if (cachedData != null) { log.info("从缓存获取帖子摘要,帖子ID: {}", postId); return JsonUtils.parseMap(cachedData); @@ -69,8 +70,7 @@ public Map generateSummary(Long postId, boolean forceRefresh) { post.getAiKeywords().split(",") : new String[0]); // 回填缓存 - redisUtil.set(cacheKey, JsonUtils.toJsonString(result), - communityProperties.getAi().getSummaryCacheTtl()); + cacheSummary(cacheKey, result); log.info("从数据库获取帖子摘要,帖子ID: {}", postId); return result; @@ -106,8 +106,7 @@ public Map generateSummary(Long postId, boolean forceRefresh) { result.put("summary", summary); result.put("keywords", keywords); - redisUtil.set(cacheKey, JsonUtils.toJsonString(result), - communityProperties.getAi().getSummaryCacheTtl()); + cacheSummary(cacheKey, result); log.info("AI摘要生成成功并已清除帖子缓存,帖子ID: {}", postId); return result; @@ -123,7 +122,7 @@ public Map getSummary(Long postId) { String cacheKey = SUMMARY_CACHE_KEY + postId; // 先从缓存获取 - String cachedData = redisUtil.get(cacheKey, String.class); + String cachedData = getCachedSummary(cacheKey); if (cachedData != null) { return JsonUtils.parseMap(cachedData); } @@ -140,10 +139,30 @@ public Map getSummary(Long postId) { post.getAiKeywords().split(",") : new String[0]); // 回填缓存 - redisUtil.set(cacheKey, JsonUtils.toJsonString(result), - communityProperties.getAi().getSummaryCacheTtl()); + cacheSummary(cacheKey, result); return result; } + + private String getCachedSummary(String cacheKey) { + try { + return redisValueStore.find(cacheKey, String.class).orElse(null); + } catch (RuntimeException error) { + log.warn("读取帖子摘要缓存失败,key={}", cacheKey, error); + return null; + } + } + + private void cacheSummary(String cacheKey, Map value) { + try { + redisValueStore.put( + cacheKey, + JsonUtils.toJsonString(value), + Duration.ofSeconds(communityProperties.getAi().getSummaryCacheTtl()) + ); + } catch (RuntimeException error) { + log.warn("写入帖子摘要缓存失败,key={}", cacheKey, error); + } + } } diff --git a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityCacheServiceImpl.java b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityCacheServiceImpl.java index 630a79def..944e8f991 100644 --- a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityCacheServiceImpl.java +++ b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityCacheServiceImpl.java @@ -1,7 +1,7 @@ package com.xiaou.community.service.impl; import com.xiaou.common.utils.JsonUtils; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.community.config.CommunityProperties; import com.xiaou.community.domain.CommunityCategory; import com.xiaou.community.domain.CommunityPost; @@ -9,9 +9,14 @@ import com.xiaou.community.service.CommunityCacheService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RScoredSortedSet; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; +import java.time.Duration; import java.util.*; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -24,7 +29,8 @@ @RequiredArgsConstructor public class CommunityCacheServiceImpl implements CommunityCacheService { - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; + private final RedissonClient redissonClient; private final CommunityProperties communityProperties; private static final String POST_DETAIL_KEY = "community:post:detail:"; @@ -40,15 +46,19 @@ public class CommunityCacheServiceImpl implements CommunityCacheService { @Override public void cachePost(CommunityPost post) { String key = POST_DETAIL_KEY + post.getId(); - redisUtil.set(key, JsonUtils.toJsonString(post), - communityProperties.getCache().getPostDetailTtl()); + cacheWrite("cache post " + post.getId(), () -> redisValueStore.put( + key, + JsonUtils.toJsonString(post), + Duration.ofSeconds(communityProperties.getCache().getPostDetailTtl()) + )); log.debug("缓存帖子详情,帖子ID: {}", post.getId()); } @Override public CommunityPost getCachedPost(Long postId) { String key = POST_DETAIL_KEY + postId; - String cachedData = redisUtil.get(key, String.class); + String cachedData = cacheRead("read post " + postId, + () -> redisValueStore.find(key, String.class).orElse(null), null); if (cachedData != null) { log.debug("从缓存获取帖子详情,帖子ID: {}", postId); return JsonUtils.parseObject(cachedData, CommunityPost.class); @@ -59,20 +69,24 @@ public CommunityPost getCachedPost(Long postId) { @Override public void evictPost(Long postId) { String key = POST_DETAIL_KEY + postId; - redisUtil.del(key); + cacheWrite("evict post " + postId, () -> redisValueStore.delete(key)); log.debug("清除帖子缓存,帖子ID: {}", postId); } @Override public void cacheCategories(List categories) { - redisUtil.set(CATEGORIES_KEY, JsonUtils.toJsonString(categories), - communityProperties.getCache().getTagsTtl()); + cacheWrite("cache categories", () -> redisValueStore.put( + CATEGORIES_KEY, + JsonUtils.toJsonString(categories), + Duration.ofSeconds(communityProperties.getCache().getTagsTtl()) + )); log.debug("缓存分类列表,数量: {}", categories.size()); } @Override public List getCachedCategories() { - String cachedData = redisUtil.get(CATEGORIES_KEY, String.class); + String cachedData = cacheRead("read categories", + () -> redisValueStore.find(CATEGORIES_KEY, String.class).orElse(null), null); if (cachedData != null) { log.debug("从缓存获取分类列表"); return JsonUtils.parseArray(cachedData, CommunityCategory.class); @@ -82,20 +96,24 @@ public List getCachedCategories() { @Override public void evictCategories() { - redisUtil.del(CATEGORIES_KEY); + cacheWrite("evict categories", () -> redisValueStore.delete(CATEGORIES_KEY)); log.debug("清除分类缓存"); } @Override public void cacheTags(List tags) { - redisUtil.set(TAGS_KEY, JsonUtils.toJsonString(tags), - communityProperties.getCache().getTagsTtl()); + cacheWrite("cache tags", () -> redisValueStore.put( + TAGS_KEY, + JsonUtils.toJsonString(tags), + Duration.ofSeconds(communityProperties.getCache().getTagsTtl()) + )); log.debug("缓存标签列表,数量: {}", tags.size()); } @Override public List getCachedTags() { - String cachedData = redisUtil.get(TAGS_KEY, String.class); + String cachedData = cacheRead("read tags", + () -> redisValueStore.find(TAGS_KEY, String.class).orElse(null), null); if (cachedData != null) { log.debug("从缓存获取标签列表"); return JsonUtils.parseArray(cachedData, CommunityTag.class); @@ -105,25 +123,29 @@ public List getCachedTags() { @Override public void evictTags() { - redisUtil.del(TAGS_KEY); + cacheWrite("evict tags", () -> redisValueStore.delete(TAGS_KEY)); log.debug("清除标签缓存"); } @Override public void cacheUserLikeStatus(Long userId, Set likedPostIds) { String key = USER_LIKE_KEY + userId; - redisUtil.del(key); - if (likedPostIds != null && !likedPostIds.isEmpty()) { - redisUtil.sSetAndTime(key, communityProperties.getCache().getUserInfoTtl(), - likedPostIds.toArray()); - } + cacheWrite("cache user likes " + userId, () -> { + redisValueStore.delete(key); + if (likedPostIds != null && !likedPostIds.isEmpty()) { + RSet likes = redissonClient.getSet(key); + likedPostIds.forEach(likes::add); + likes.expire(Duration.ofSeconds(communityProperties.getCache().getUserInfoTtl())); + } + }); log.debug("缓存用户点赞状态,用户ID: {}, 点赞数: {}", userId, likedPostIds != null ? likedPostIds.size() : 0); } @Override public Set getUserLikedPostIds(Long userId) { String key = USER_LIKE_KEY + userId; - Set members = redisUtil.sGet(key); + Set members = cacheRead("read user likes " + userId, + () -> redissonClient.getSet(key).readAll(), null); if (members != null) { return members.stream() .map(obj -> Long.valueOf(obj.toString())) @@ -135,31 +157,38 @@ public Set getUserLikedPostIds(Long userId) { @Override public void addUserLikedPost(Long userId, Long postId) { String key = USER_LIKE_KEY + userId; - redisUtil.sSet(key, postId); - redisUtil.expire(key, communityProperties.getCache().getUserInfoTtl()); + cacheWrite("add user like " + userId, () -> { + RSet likes = redissonClient.getSet(key); + likes.add(postId); + likes.expire(Duration.ofSeconds(communityProperties.getCache().getUserInfoTtl())); + }); } @Override public void removeUserLikedPost(Long userId, Long postId) { String key = USER_LIKE_KEY + userId; - redisUtil.setRemove(key, postId); + cacheWrite("remove user like " + userId, () -> redissonClient.getSet(key).remove(postId)); } @Override public void cacheUserCollectStatus(Long userId, Set collectedPostIds) { String key = USER_COLLECT_KEY + userId; - redisUtil.del(key); - if (collectedPostIds != null && !collectedPostIds.isEmpty()) { - redisUtil.sSetAndTime(key, communityProperties.getCache().getUserInfoTtl(), - collectedPostIds.toArray()); - } + cacheWrite("cache user collections " + userId, () -> { + redisValueStore.delete(key); + if (collectedPostIds != null && !collectedPostIds.isEmpty()) { + RSet collections = redissonClient.getSet(key); + collectedPostIds.forEach(collections::add); + collections.expire(Duration.ofSeconds(communityProperties.getCache().getUserInfoTtl())); + } + }); log.debug("缓存用户收藏状态,用户ID: {}, 收藏数: {}", userId, collectedPostIds != null ? collectedPostIds.size() : 0); } @Override public Set getUserCollectedPostIds(Long userId) { String key = USER_COLLECT_KEY + userId; - Set members = redisUtil.sGet(key); + Set members = cacheRead("read user collections " + userId, + () -> redissonClient.getSet(key).readAll(), null); if (members != null) { return members.stream() .map(obj -> Long.valueOf(obj.toString())) @@ -171,14 +200,17 @@ public Set getUserCollectedPostIds(Long userId) { @Override public void addUserCollectedPost(Long userId, Long postId) { String key = USER_COLLECT_KEY + userId; - redisUtil.sSet(key, postId); - redisUtil.expire(key, communityProperties.getCache().getUserInfoTtl()); + cacheWrite("add user collection " + userId, () -> { + RSet collections = redissonClient.getSet(key); + collections.add(postId); + collections.expire(Duration.ofSeconds(communityProperties.getCache().getUserInfoTtl())); + }); } @Override public void removeUserCollectedPost(Long userId, Long postId) { String key = USER_COLLECT_KEY + userId; - redisUtil.setRemove(key, postId); + cacheWrite("remove user collection " + userId, () -> redissonClient.getSet(key).remove(postId)); } @Override @@ -191,14 +223,15 @@ public void recordSearchKeyword(String keyword) { try { // 获取当前分数,如果不存在则为0 - Double currentScore = redisUtil.zScore(HOT_SEARCH_KEY, keyword); + RScoredSortedSet hotSearches = redissonClient.getScoredSortedSet(HOT_SEARCH_KEY); + Double currentScore = hotSearches.getScore(keyword); double newScore = (currentScore != null ? currentScore : 0) + 1; // 使用Sorted Set记录搜索词,score为搜索次数 - redisUtil.zAdd(HOT_SEARCH_KEY, keyword, newScore); + hotSearches.add(newScore, keyword); // 设置过期时间 - redisUtil.expire(HOT_SEARCH_KEY, HOT_SEARCH_TTL); + hotSearches.expire(Duration.ofSeconds(HOT_SEARCH_TTL)); log.debug("记录搜索关键词: {}, 搜索次数: {}", keyword, newScore); } catch (Exception e) { log.error("记录搜索关键词失败: {}", keyword, e); @@ -213,7 +246,8 @@ public List getHotSearchKeywords(Integer limit) { try { // 获取score最高的N个关键词(倒序) - Collection keywords = redisUtil.zRevRange(HOT_SEARCH_KEY, 0, limit - 1); + Collection keywords = redissonClient.getScoredSortedSet(HOT_SEARCH_KEY) + .valueRangeReversed(0, limit - 1); if (keywords != null && !keywords.isEmpty()) { List result = keywords.stream() .map(Object::toString) @@ -231,11 +265,28 @@ public List getHotSearchKeywords(Integer limit) { @Override public void clearHotSearchKeywords() { try { - redisUtil.del(HOT_SEARCH_KEY); + redisValueStore.delete(HOT_SEARCH_KEY); log.debug("清除热门搜索词缓存"); } catch (Exception e) { log.error("清除热门搜索词缓存失败", e); } } + + private void cacheWrite(String operation, Runnable action) { + try { + action.run(); + } catch (RuntimeException error) { + log.warn("社区缓存写入失败: {}", operation, error); + } + } + + private T cacheRead(String operation, Supplier action, T fallback) { + try { + return action.get(); + } catch (RuntimeException error) { + log.warn("社区缓存读取失败: {}", operation, error); + return fallback; + } + } } diff --git a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityHotPostServiceImpl.java b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityHotPostServiceImpl.java index de3b10da7..65cfe73e4 100644 --- a/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityHotPostServiceImpl.java +++ b/xiaou-community/src/main/java/com/xiaou/community/service/impl/CommunityHotPostServiceImpl.java @@ -1,6 +1,5 @@ package com.xiaou.community.service.impl; -import com.xiaou.common.utils.RedisUtil; import com.xiaou.community.config.CommunityProperties; import com.xiaou.community.domain.CommunityPost; import com.xiaou.community.dto.CommunityPostResponse; @@ -8,8 +7,11 @@ import com.xiaou.community.service.CommunityHotPostService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RScoredSortedSet; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; +import java.time.Duration; import java.util.*; import java.util.stream.Collectors; @@ -25,7 +27,7 @@ public class CommunityHotPostServiceImpl implements CommunityHotPostService { private final CommunityPostMapper communityPostMapper; private final CommunityProperties communityProperties; - private final RedisUtil redisUtil; + private final RedissonClient redissonClient; private static final String HOT_POSTS_KEY = "community:hot:posts"; @@ -37,14 +39,14 @@ public List getHotPosts(Integer limit) { try { // 从Redis获取热门帖子ID列表(带分数) - Map hotPostMap = redisUtil.zRevRangeWithScores(HOT_POSTS_KEY, 0, limit - 1); + Map hotPostMap = getHotPostScores(limit); if (hotPostMap == null || hotPostMap.isEmpty()) { // 缓存为空,刷新缓存 log.info("热门帖子缓存为空,开始刷新"); refreshHotPosts(); // 重新获取 - hotPostMap = redisUtil.zRevRangeWithScores(HOT_POSTS_KEY, 0, limit - 1); + hotPostMap = getHotPostScores(limit); } if (hotPostMap == null || hotPostMap.isEmpty()) { @@ -102,26 +104,27 @@ public void refreshHotPosts() { } // 清空旧缓存 - redisUtil.del(HOT_POSTS_KEY); + RScoredSortedSet hotPosts = redissonClient.getScoredSortedSet(HOT_POSTS_KEY); + hotPosts.clear(); // 计算热度分数并存入Redis for (CommunityPost post : posts) { Double hotScore = calculateHotScoreInternal(post); if (hotScore >= communityProperties.getHot().getMinScore()) { - redisUtil.zAdd(HOT_POSTS_KEY, post.getId(), hotScore); + hotPosts.add(hotScore, post.getId()); } } // 保留TOP N - long size = redisUtil.zSize(HOT_POSTS_KEY); + long size = hotPosts.size(); if (size > communityProperties.getHot().getLimit()) { - redisUtil.zRemoveRange(HOT_POSTS_KEY, 0, (int)(size - communityProperties.getHot().getLimit() - 1)); + hotPosts.removeRangeByRank(0, (int) (size - communityProperties.getHot().getLimit() - 1)); } // 设置过期时间 - redisUtil.expire(HOT_POSTS_KEY, communityProperties.getCache().getHotPostsTtl()); + hotPosts.expire(Duration.ofSeconds(communityProperties.getCache().getHotPostsTtl())); - log.info("热门帖子缓存刷新完成,共{}个帖子", redisUtil.zSize(HOT_POSTS_KEY)); + log.info("热门帖子缓存刷新完成,共{}个帖子", hotPosts.size()); } catch (Exception e) { log.error("刷新热门帖子缓存失败", e); @@ -159,6 +162,15 @@ private Double calculateHotScoreInternal(CommunityPost post) { double finalScore = score - timeDecay; return Math.max(finalScore, 0.0); } + + private Map getHotPostScores(int limit) { + RScoredSortedSet hotPosts = redissonClient.getScoredSortedSet(HOT_POSTS_KEY); + Map result = new LinkedHashMap<>(); + for (Object value : hotPosts.valueRangeReversed(0, limit - 1)) { + result.put(value, hotPosts.getScore(value)); + } + return result; + } /** * 简化的转换方法(不包含用户状态) diff --git a/xiaou-mock-interview/src/main/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImpl.java b/xiaou-mock-interview/src/main/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImpl.java index 8699d9a1b..4317d7855 100644 --- a/xiaou-mock-interview/src/main/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImpl.java +++ b/xiaou-mock-interview/src/main/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImpl.java @@ -11,7 +11,6 @@ import com.xiaou.common.core.domain.PageResult; import com.xiaou.common.exception.BusinessException; import com.xiaou.common.utils.PageHelper; -import com.xiaou.common.utils.ThreadPoolUtils; import com.xiaou.mockinterview.domain.JobBattleMatchRecord; import com.xiaou.mockinterview.domain.JobBattlePlanRecord; import com.xiaou.mockinterview.dto.request.CareerLoopEventRequest; @@ -28,8 +27,8 @@ import com.xiaou.mockinterview.mapper.JobBattlePlanRecordMapper; import com.xiaou.mockinterview.service.CareerLoopService; import com.xiaou.mockinterview.service.JobBattleService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -44,6 +43,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -54,7 +54,6 @@ */ @Slf4j @Service -@RequiredArgsConstructor public class JobBattleServiceImpl implements JobBattleService { private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @@ -63,6 +62,21 @@ public class JobBattleServiceImpl implements JobBattleService { private final JobBattlePlanRecordMapper planRecordMapper; private final JobBattleMatchRecordMapper matchRecordMapper; private final CareerLoopService careerLoopService; + private final Executor applicationIoExecutor; + + public JobBattleServiceImpl( + AiJobBattleService aiJobBattleService, + JobBattlePlanRecordMapper planRecordMapper, + JobBattleMatchRecordMapper matchRecordMapper, + CareerLoopService careerLoopService, + @Qualifier("applicationIoExecutor") Executor applicationIoExecutor + ) { + this.aiJobBattleService = aiJobBattleService; + this.planRecordMapper = planRecordMapper; + this.matchRecordMapper = matchRecordMapper; + this.careerLoopService = careerLoopService; + this.applicationIoExecutor = applicationIoExecutor; + } @Value("${job-battle.ai.timeout-seconds:4}") private long aiTimeoutSeconds; @@ -259,7 +273,7 @@ public JobBattleInterviewReviewResult reviewInterview(Long userId, JobBattleInte private T runAiWithFallback(String operation, Supplier aiSupplier, Supplier fallbackSupplier) { long timeoutSeconds = Math.max(1L, aiTimeoutSeconds); try { - T result = CompletableFuture.supplyAsync(aiSupplier, ThreadPoolUtils.getIoExecutor()) + T result = CompletableFuture.supplyAsync(aiSupplier, applicationIoExecutor) .completeOnTimeout(null, timeoutSeconds, TimeUnit.SECONDS) .exceptionally(ex -> { log.warn("求职作战台AI调用失败,operation={}, reason={}", operation, ex.getMessage()); diff --git a/xiaou-mock-interview/src/test/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImplTest.java b/xiaou-mock-interview/src/test/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImplTest.java index 6699b4084..07b80a2de 100644 --- a/xiaou-mock-interview/src/test/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImplTest.java +++ b/xiaou-mock-interview/src/test/java/com/xiaou/mockinterview/service/impl/JobBattleServiceImplTest.java @@ -16,10 +16,11 @@ import com.xiaou.mockinterview.mapper.JobBattleMatchRecordMapper; import com.xiaou.mockinterview.mapper.JobBattlePlanRecordMapper; import com.xiaou.mockinterview.service.CareerLoopService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -28,6 +29,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -54,9 +57,27 @@ class JobBattleServiceImplTest { @Mock private CareerLoopService careerLoopService; - @InjectMocks private JobBattleServiceImpl jobBattleService; + private ExecutorService applicationIoExecutor; + + @BeforeEach + void setUp() { + applicationIoExecutor = Executors.newCachedThreadPool(); + jobBattleService = new JobBattleServiceImpl( + aiJobBattleService, + planRecordMapper, + matchRecordMapper, + careerLoopService, + applicationIoExecutor + ); + } + + @AfterEach + void tearDown() { + applicationIoExecutor.shutdownNow(); + } + @Test void shouldRunMatchEngineUsingAnalyzeTargetAndRankTargets() { when(aiJobBattleService.analyzeTarget(any(), any(), any(), any(), any(), any(), any())) diff --git a/xiaou-moment/pom.xml b/xiaou-moment/pom.xml index ae2b48051..77d4622a3 100644 --- a/xiaou-moment/pom.xml +++ b/xiaou-moment/pom.xml @@ -33,6 +33,12 @@ xiaou-sensitive-api ${revision} + + + org.springframework.boot + spring-boot-starter-test + test + - \ No newline at end of file + diff --git a/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentFavoriteMapper.java b/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentFavoriteMapper.java index 385a1a48f..aedd512a7 100644 --- a/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentFavoriteMapper.java +++ b/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentFavoriteMapper.java @@ -16,6 +16,13 @@ public interface MomentFavoriteMapper { * 插入收藏记录 */ int insert(MomentFavorite favorite); + + /** + * Insert a favorite unless the user already favorited the moment. + * + * @return 1 when inserted, 0 when the unique key already exists + */ + int insertIfAbsent(MomentFavorite favorite); /** * 删除收藏记录 diff --git a/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentLikeMapper.java b/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentLikeMapper.java index 99c7ca80b..f3799b3fa 100644 --- a/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentLikeMapper.java +++ b/xiaou-moment/src/main/java/com/xiaou/moment/mapper/MomentLikeMapper.java @@ -16,6 +16,13 @@ public interface MomentLikeMapper { * 插入点赞 */ int insert(MomentLike momentLike); + + /** + * Insert a like unless the user already liked the moment. + * + * @return 1 when inserted, 0 when the unique key already exists + */ + int insertIfAbsent(MomentLike momentLike); /** * 根据ID删除点赞 @@ -44,4 +51,4 @@ public interface MomentLikeMapper { * @return 用户已点赞的动态ID列表 */ List selectLikedMomentIds(@Param("momentIds") List momentIds, @Param("userId") Long userId); -} \ No newline at end of file +} diff --git a/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentServiceImpl.java b/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentServiceImpl.java index 0b936641a..44a1d4342 100644 --- a/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentServiceImpl.java +++ b/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentServiceImpl.java @@ -145,15 +145,20 @@ public Boolean toggleLike(Long momentId) { if (existingLike != null) { // 取消点赞 - momentLikeMapper.deleteByMomentIdAndUserId(momentId, currentUserId); - momentMapper.decrementLikeCount(momentId); + int deleted = momentLikeMapper.deleteByMomentIdAndUserId(momentId, currentUserId); + if (deleted > 0) { + momentMapper.decrementLikeCount(momentId); + } return false; } else { // 点赞 MomentLike momentLike = new MomentLike(); momentLike.setMomentId(momentId); momentLike.setUserId(currentUserId); - momentLikeMapper.insert(momentLike); + int inserted = momentLikeMapper.insertIfAbsent(momentLike); + if (inserted == 0) { + return true; + } momentMapper.incrementLikeCount(momentId); // 发送消息通知:通知动态作者 @@ -702,15 +707,20 @@ public Boolean toggleFavorite(Long momentId) { if (existingFavorite != null) { // 取消收藏 - momentFavoriteMapper.delete(momentId, currentUserId); - momentMapper.decrementFavoriteCount(momentId); + int deleted = momentFavoriteMapper.delete(momentId, currentUserId); + if (deleted > 0) { + momentMapper.decrementFavoriteCount(momentId); + } return false; } else { // 收藏 MomentFavorite favorite = new MomentFavorite(); favorite.setMomentId(momentId); favorite.setUserId(currentUserId); - momentFavoriteMapper.insert(favorite); + int inserted = momentFavoriteMapper.insertIfAbsent(favorite); + if (inserted == 0) { + return true; + } momentMapper.incrementFavoriteCount(momentId); // 发送消息通知:通知动态作者 @@ -897,4 +907,4 @@ private CommentResponse convertToCommentResponseWithUserInfo(MomentComment comme return response; } -} \ No newline at end of file +} diff --git a/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentViewServiceImpl.java b/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentViewServiceImpl.java index d42f38ff5..71a63539b 100644 --- a/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentViewServiceImpl.java +++ b/xiaou-moment/src/main/java/com/xiaou/moment/service/impl/MomentViewServiceImpl.java @@ -1,12 +1,14 @@ package com.xiaou.moment.service.impl; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.moment.mapper.MomentMapper; import com.xiaou.moment.service.MomentViewService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.Duration; + /** * 动态浏览统计Service实现 */ @@ -15,7 +17,7 @@ @RequiredArgsConstructor public class MomentViewServiceImpl implements MomentViewService { - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; private final MomentMapper momentMapper; /** @@ -38,19 +40,18 @@ public void recordView(Long momentId, Long userId) { // 构建用户浏览记录Key String userViewKey = USER_VIEW_KEY_PREFIX + userId + ":" + momentId; - // 检查用户是否在5分钟内已经浏览过 - if (redisUtil.hasKey(userViewKey)) { - // 已经统计过,不重复统计 - return; - } - try { + // 检查用户是否在5分钟内已经浏览过 + if (redisValueStore.exists(userViewKey)) { + return; + } + // 增加浏览数 String viewCountKey = VIEW_COUNT_KEY_PREFIX + momentId; - redisUtil.incr(viewCountKey, 1); + redisValueStore.increment(viewCountKey, 1); // 记录用户浏览,5分钟过期(转换为秒) - redisUtil.set(userViewKey, "1", USER_VIEW_EXPIRE_MINUTES * 60); + redisValueStore.put(userViewKey, "1", Duration.ofMinutes(USER_VIEW_EXPIRE_MINUTES)); log.debug("记录动态浏览: momentId={}, userId={}", momentId, userId); } catch (Exception e) { @@ -66,10 +67,8 @@ public Integer getViewCount(Long momentId) { try { String viewCountKey = VIEW_COUNT_KEY_PREFIX + momentId; - Object count = redisUtil.get(viewCountKey); - if (count != null) { - return Integer.parseInt(count.toString()); - } + var count = redisValueStore.counter(viewCountKey); + return count.isPresent() ? Math.toIntExact(count.getAsLong()) : 0; } catch (Exception e) { log.error("获取动态浏览数失败: momentId={}, error={}", momentId, e.getMessage()); } @@ -83,7 +82,7 @@ public void syncViewCountToDatabase() { try { // 获取所有浏览数Key - Iterable keys = redisUtil.getKeys(VIEW_COUNT_KEY_PREFIX + "*"); + Iterable keys = redisValueStore.keys(VIEW_COUNT_KEY_PREFIX + "*"); if (keys == null) { log.info("没有需要同步的浏览数据"); return; @@ -97,12 +96,12 @@ public void syncViewCountToDatabase() { Long momentId = Long.parseLong(momentIdStr); // 获取Redis中的浏览数 - Object countObj = redisUtil.get(key); - if (countObj == null) { + var count = redisValueStore.counter(key); + if (count.isEmpty()) { continue; } - Integer viewCount = Integer.parseInt(countObj.toString()); + Integer viewCount = Math.toIntExact(count.getAsLong()); if (viewCount <= 0) { continue; } @@ -113,7 +112,7 @@ public void syncViewCountToDatabase() { } // 同步成功后,清空Redis中的计数(保留Key,值设为0) - redisUtil.set(key, 0, 0); + redisValueStore.setCounter(key, 0); syncCount++; log.debug("同步动态浏览数: momentId={}, count={}", momentId, viewCount); diff --git a/xiaou-moment/src/main/java/com/xiaou/moment/task/HotMomentCalculateTask.java b/xiaou-moment/src/main/java/com/xiaou/moment/task/HotMomentCalculateTask.java index 0b06218b4..65c291e44 100644 --- a/xiaou-moment/src/main/java/com/xiaou/moment/task/HotMomentCalculateTask.java +++ b/xiaou-moment/src/main/java/com/xiaou/moment/task/HotMomentCalculateTask.java @@ -1,15 +1,16 @@ package com.xiaou.moment.task; -import com.xiaou.common.utils.RedisUtil; import com.xiaou.moment.domain.Moment; import com.xiaou.moment.mapper.MomentMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RScoredSortedSet; +import org.redisson.api.RedissonClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; +import java.time.Duration; /** * 热门动态计算定时任务 @@ -20,7 +21,7 @@ @RequiredArgsConstructor public class HotMomentCalculateTask { - private final RedisUtil redisUtil; + private final RedissonClient redissonClient; private final MomentMapper momentMapper; /** @@ -51,8 +52,7 @@ public void calculateHotMoments() { } // 2. 获取Redisson的Sorted Set - RScoredSortedSet hotMomentsSet = redisUtil.getRedissonClient() - .getScoredSortedSet(HOT_MOMENTS_KEY); + RScoredSortedSet hotMomentsSet = redissonClient.getScoredSortedSet(HOT_MOMENTS_KEY); // 3. 清空旧数据 hotMomentsSet.clear(); @@ -65,7 +65,7 @@ public void calculateHotMoments() { } // 5. 设置过期时间(10分钟) - redisUtil.expire(HOT_MOMENTS_KEY, CACHE_EXPIRE_SECONDS); + hotMomentsSet.expire(Duration.ofSeconds(CACHE_EXPIRE_SECONDS)); log.info("定时任务:热门动态计算完成,共计算 {} 条热门动态", hotMoments.size()); } catch (Exception e) { diff --git a/xiaou-moment/src/main/resources/mapper/MomentFavoriteMapper.xml b/xiaou-moment/src/main/resources/mapper/MomentFavoriteMapper.xml index 3a770c8b7..b7adf215d 100644 --- a/xiaou-moment/src/main/resources/mapper/MomentFavoriteMapper.xml +++ b/xiaou-moment/src/main/resources/mapper/MomentFavoriteMapper.xml @@ -16,6 +16,11 @@ VALUES (#{momentId}, #{userId}, NOW()) + + INSERT IGNORE INTO moment_favorites (moment_id, user_id, create_time) + VALUES (#{momentId}, #{userId}, NOW()) + + DELETE FROM moment_favorites diff --git a/xiaou-moment/src/main/resources/mapper/MomentLikeMapper.xml b/xiaou-moment/src/main/resources/mapper/MomentLikeMapper.xml index 773048559..75925a0d7 100644 --- a/xiaou-moment/src/main/resources/mapper/MomentLikeMapper.xml +++ b/xiaou-moment/src/main/resources/mapper/MomentLikeMapper.xml @@ -18,6 +18,11 @@ VALUES (#{momentId}, #{userId}) + + INSERT IGNORE INTO moment_likes (moment_id, user_id) + VALUES (#{momentId}, #{userId}) + + DELETE FROM moment_likes WHERE id = #{id} @@ -50,4 +55,4 @@ - \ No newline at end of file + diff --git a/xiaou-moment/src/test/java/com/xiaou/moment/service/impl/MomentServiceImplTest.java b/xiaou-moment/src/test/java/com/xiaou/moment/service/impl/MomentServiceImplTest.java new file mode 100644 index 000000000..001bacc08 --- /dev/null +++ b/xiaou-moment/src/test/java/com/xiaou/moment/service/impl/MomentServiceImplTest.java @@ -0,0 +1,156 @@ +package com.xiaou.moment.service.impl; + +import com.xiaou.common.satoken.StpUserUtil; +import com.xiaou.moment.domain.Moment; +import com.xiaou.moment.domain.MomentFavorite; +import com.xiaou.moment.domain.MomentLike; +import com.xiaou.moment.mapper.MomentCommentMapper; +import com.xiaou.moment.mapper.MomentFavoriteMapper; +import com.xiaou.moment.mapper.MomentLikeMapper; +import com.xiaou.moment.mapper.MomentMapper; +import com.xiaou.moment.service.MomentViewService; +import com.xiaou.user.api.UserInfoApiService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MomentServiceImplTest { + + private static final long MOMENT_ID = 11L; + private static final long USER_ID = 22L; + + @Mock + private MomentMapper momentMapper; + @Mock + private MomentLikeMapper momentLikeMapper; + @Mock + private MomentCommentMapper momentCommentMapper; + @Mock + private MomentFavoriteMapper momentFavoriteMapper; + @Mock + private UserInfoApiService userInfoApiService; + @Mock + private MomentViewService momentViewService; + + private MomentServiceImpl service; + + @BeforeEach + void setUp() { + service = new MomentServiceImpl( + momentMapper, + momentLikeMapper, + momentCommentMapper, + momentFavoriteMapper, + userInfoApiService, + momentViewService + ); + } + + @Test + void shouldIncrementLikeCountOnlyWhenLikeRowWasInserted() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentLikeMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(null); + when(momentLikeMapper.insertIfAbsent(any(MomentLike.class))).thenReturn(0); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleLike(MOMENT_ID)).isTrue(); + } + + verify(momentMapper, never()).incrementLikeCount(MOMENT_ID); + } + + @Test + void shouldDecrementLikeCountOnlyWhenLikeRowWasDeleted() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentLikeMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(new MomentLike()); + when(momentLikeMapper.deleteByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(0); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleLike(MOMENT_ID)).isFalse(); + } + + verify(momentMapper, never()).decrementLikeCount(MOMENT_ID); + } + + @Test + void shouldUpdateLikeCountWhenRowMutationSucceeds() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentLikeMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(null); + when(momentLikeMapper.insertIfAbsent(any(MomentLike.class))).thenReturn(1); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleLike(MOMENT_ID)).isTrue(); + } + + verify(momentMapper).incrementLikeCount(MOMENT_ID); + } + + @Test + void shouldIncrementFavoriteCountOnlyWhenFavoriteRowWasInserted() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentFavoriteMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(null); + when(momentFavoriteMapper.insertIfAbsent(any(MomentFavorite.class))).thenReturn(0); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleFavorite(MOMENT_ID)).isTrue(); + } + + verify(momentMapper, never()).incrementFavoriteCount(MOMENT_ID); + } + + @Test + void shouldDecrementFavoriteCountOnlyWhenFavoriteRowWasDeleted() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentFavoriteMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(new MomentFavorite()); + when(momentFavoriteMapper.delete(MOMENT_ID, USER_ID)).thenReturn(0); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleFavorite(MOMENT_ID)).isFalse(); + } + + verify(momentMapper, never()).decrementFavoriteCount(MOMENT_ID); + } + + @Test + void shouldUpdateFavoriteCountWhenRowMutationSucceeds() { + Moment moment = moment(); + when(momentMapper.selectById(MOMENT_ID)).thenReturn(moment); + when(momentFavoriteMapper.selectByMomentIdAndUserId(MOMENT_ID, USER_ID)).thenReturn(new MomentFavorite()); + when(momentFavoriteMapper.delete(MOMENT_ID, USER_ID)).thenReturn(1); + + try (MockedStatic login = loggedInUser()) { + assertThat(service.toggleFavorite(MOMENT_ID)).isFalse(); + } + + verify(momentMapper).decrementFavoriteCount(MOMENT_ID); + } + + private MockedStatic loggedInUser() { + MockedStatic login = mockStatic(StpUserUtil.class); + login.when(StpUserUtil::getLoginIdAsLong).thenReturn(USER_ID); + return login; + } + + private Moment moment() { + Moment moment = new Moment(); + moment.setId(MOMENT_ID); + moment.setUserId(USER_ID); + return moment; + } +} diff --git a/xiaou-moyu/src/main/java/com/xiaou/moyu/service/impl/HotTopicServiceImpl.java b/xiaou-moyu/src/main/java/com/xiaou/moyu/service/impl/HotTopicServiceImpl.java index 791a44fb8..56a1d6551 100644 --- a/xiaou-moyu/src/main/java/com/xiaou/moyu/service/impl/HotTopicServiceImpl.java +++ b/xiaou-moyu/src/main/java/com/xiaou/moyu/service/impl/HotTopicServiceImpl.java @@ -2,15 +2,14 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; -import com.xiaou.common.utils.RedisUtil; -import com.xiaou.common.utils.ThreadPoolUtils; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.moyu.domain.HotTopicData; import com.xiaou.moyu.domain.HotTopicCategory; import com.xiaou.moyu.domain.HotTopicResponse; import com.xiaou.moyu.enums.HotTopicEnum; import com.xiaou.moyu.service.HotTopicService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -21,8 +20,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -32,12 +35,22 @@ */ @Slf4j @Service -@RequiredArgsConstructor public class HotTopicServiceImpl implements HotTopicService { - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; private final RestTemplate restTemplate; + private final Executor hotTopicExecutor; private final AtomicBoolean refreshing = new AtomicBoolean(false); + + public HotTopicServiceImpl( + RedisValueStore redisValueStore, + RestTemplate restTemplate, + @Qualifier("hotTopicExecutor") Executor hotTopicExecutor + ) { + this.redisValueStore = redisValueStore; + this.restTemplate = restTemplate; + this.hotTopicExecutor = hotTopicExecutor; + } /** * 热榜API基础URL @@ -108,10 +121,9 @@ public HotTopicData getHotTopicData(String platform) { @Override public Map getAllHotTopicData() { - // 使用ThreadPoolUtils并行获取所有平台数据 List platforms = Arrays.asList(HotTopicEnum.values()); - List> results = ThreadPoolUtils.parallelMapWithTimeout( + List> results = parallelMapWithTimeout( platforms, platform -> { try { @@ -141,10 +153,9 @@ public void refreshHotTopicData() { int totalCount = HotTopicEnum.values().length; try { - // 使用ThreadPoolUtils并行刷新所有平台数据,支持超时控制 List platforms = Arrays.asList(HotTopicEnum.values()); - List results = ThreadPoolUtils.parallelMapWithTimeout( + List results = parallelMapWithTimeout( platforms, platform -> { try { @@ -191,7 +202,7 @@ public void initializeHotTopicDataIfNeeded() { String[] checkPlatforms = {"weibo", "zhihu", "douyin", "kuaishou"}; for (String platform : checkPlatforms) { String cacheKey = CACHE_KEY_PREFIX + "data:" + platform; - if (redisUtil.hasKey(cacheKey)) { + if (redisValueStore.exists(cacheKey)) { hasCache = true; break; } @@ -213,7 +224,7 @@ public void initializeHotTopicDataIfNeeded() { try { String cacheKey = CACHE_KEY_PREFIX + "data:" + platform.getCode(); // 再次检查单个平台缓存,防止在循环过程中其他地方设置了缓存 - if (!redisUtil.hasKey(cacheKey)) { + if (!redisValueStore.exists(cacheKey)) { String url = baseUrl + "/" + platform.getCode(); HotTopicData data = restTemplate.getForObject(url, HotTopicData.class); @@ -241,13 +252,12 @@ public void initializeHotTopicDataIfNeeded() { } private String getCachedData(String cacheKey) { - Object cachedData = redisUtil.get(cacheKey); - return cachedData instanceof String ? (String) cachedData : null; + return redisValueStore.find(cacheKey, String.class).orElse(null); } private void cacheHotTopicData(String cacheKey, String jsonData) { - redisUtil.set(cacheKey, jsonData, cacheExpireSeconds()); - redisUtil.set(toStaleCacheKey(cacheKey), jsonData, staleCacheExpireSeconds()); + redisValueStore.put(cacheKey, jsonData, Duration.ofSeconds(cacheExpireSeconds())); + redisValueStore.put(toStaleCacheKey(cacheKey), jsonData, Duration.ofSeconds(staleCacheExpireSeconds())); } private T getStaleData(String cacheKey, Class clazz) { @@ -274,6 +284,23 @@ private long staleCacheExpireSeconds() { return Math.max(cacheExpireMinutes, staleCacheExpireMinutes) * 60; } + private List parallelMapWithTimeout( + List items, + Function processor, + long timeout, + TimeUnit unit + ) { + List> futures = items.stream() + .map(item -> CompletableFuture.supplyAsync(() -> processor.apply(item), hotTopicExecutor) + .orTimeout(timeout, unit) + .exceptionally(error -> { + log.warn("热榜并行任务超时或失败: {}", error.getMessage()); + return null; + })) + .toList(); + return futures.stream().map(CompletableFuture::join).toList(); + } + private HotTopicResponse buildLocalCategories() { Map allTypes = new LinkedHashMap<>(); Map> groupedApis = new LinkedHashMap<>(); diff --git a/xiaou-points/src/main/java/com/xiaou/points/cache/LotteryCacheWarmer.java b/xiaou-points/src/main/java/com/xiaou/points/cache/LotteryCacheWarmer.java index fb07e3a30..9d7f073e5 100644 --- a/xiaou-points/src/main/java/com/xiaou/points/cache/LotteryCacheWarmer.java +++ b/xiaou-points/src/main/java/com/xiaou/points/cache/LotteryCacheWarmer.java @@ -1,6 +1,6 @@ package com.xiaou.points.cache; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.points.domain.LotteryPrizeConfig; import com.xiaou.points.mapper.LotteryPrizeConfigMapper; import lombok.RequiredArgsConstructor; @@ -9,6 +9,7 @@ import org.springframework.stereotype.Component; import java.util.List; +import java.time.Duration; /** * 抽奖缓存预热器 @@ -22,7 +23,7 @@ public class LotteryCacheWarmer implements CommandLineRunner { private final LotteryPrizeConfigMapper prizeConfigMapper; - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; /** * 缓存Key前缀 @@ -62,13 +63,13 @@ private void warmUpPrizeConfig() { for (LotteryPrizeConfig prize : prizes) { // 缓存单个奖品配置,1小时 = 3600秒 String prizeKey = PRIZE_CONFIG_KEY + prize.getId(); - redisUtil.set(prizeKey, prize, 3600); + redisValueStore.put(prizeKey, prize, Duration.ofHours(1)); log.debug("预热奖品配置:{} - {}", prize.getId(), prize.getPrizeName()); } // 缓存奖品列表,1小时 = 3600秒 - redisUtil.set(PRIZE_LIST_KEY, prizes, 3600); + redisValueStore.put(PRIZE_LIST_KEY, prizes, Duration.ofHours(1)); log.info("预热奖品配置完成,奖品数量:{}", prizes.size()); } @@ -89,7 +90,7 @@ private void warmUpPrizeStock() { // 缓存奖品库存 String stockKey = PRIZE_STOCK_KEY + prize.getId(); Integer currentStock = prize.getCurrentStock() != null ? prize.getCurrentStock() : prize.getTotalStock(); - redisUtil.set(stockKey, currentStock); + redisValueStore.put(stockKey, currentStock); count++; log.debug("预热奖品库存:{} - {} (库存:{})", prize.getId(), prize.getPrizeName(), currentStock); diff --git a/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryEmergencyServiceImpl.java b/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryEmergencyServiceImpl.java index 11020bc0b..3d1cf8f5e 100644 --- a/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryEmergencyServiceImpl.java +++ b/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryEmergencyServiceImpl.java @@ -1,11 +1,13 @@ package com.xiaou.points.service.impl; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.points.service.LotteryEmergencyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.time.Duration; + /** * 抽奖应急服务实现 * @@ -16,7 +18,7 @@ @RequiredArgsConstructor public class LotteryEmergencyServiceImpl implements LotteryEmergencyService { - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; private static final String CIRCUIT_BREAK_KEY = "lottery:emergency:circuit_break"; private static final String DEGRADATION_KEY = "lottery:emergency:degradation"; @@ -24,36 +26,35 @@ public class LotteryEmergencyServiceImpl implements LotteryEmergencyService { @Override public void manualCircuitBreak(String reason) { log.warn("手动触发熔断,原因:{}", reason); - redisUtil.set(CIRCUIT_BREAK_KEY, reason, 3600); // 1小时 = 3600秒 + redisValueStore.put(CIRCUIT_BREAK_KEY, reason, Duration.ofHours(1)); } @Override public void resumeService() { log.info("恢复抽奖服务"); - redisUtil.del(CIRCUIT_BREAK_KEY); - redisUtil.del(DEGRADATION_KEY); + redisValueStore.delete(CIRCUIT_BREAK_KEY, DEGRADATION_KEY); } @Override public void enableDegradation() { log.warn("启用降级模式"); - redisUtil.set(DEGRADATION_KEY, true, 3600); // 1小时 = 3600秒 + redisValueStore.put(DEGRADATION_KEY, true, Duration.ofHours(1)); } @Override public void disableDegradation() { log.info("禁用降级模式"); - redisUtil.del(DEGRADATION_KEY); + redisValueStore.delete(DEGRADATION_KEY); } @Override public boolean isCircuitBroken() { - return redisUtil.hasKey(CIRCUIT_BREAK_KEY); + return redisValueStore.exists(CIRCUIT_BREAK_KEY); } @Override public boolean isDegraded() { - return redisUtil.hasKey(DEGRADATION_KEY); + return redisValueStore.exists(DEGRADATION_KEY); } } diff --git a/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryStockServiceImpl.java b/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryStockServiceImpl.java index 2cfd83fa8..8d8abf5b7 100644 --- a/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryStockServiceImpl.java +++ b/xiaou-points/src/main/java/com/xiaou/points/service/impl/LotteryStockServiceImpl.java @@ -1,6 +1,5 @@ package com.xiaou.points.service.impl; -import com.xiaou.common.utils.RedisUtil; import com.xiaou.points.domain.LotteryPrizeConfig; import com.xiaou.points.mapper.LotteryPrizeConfigMapper; import com.xiaou.points.service.LotteryStockService; @@ -8,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import org.redisson.api.RAtomicLong; import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,7 +25,7 @@ public class LotteryStockServiceImpl implements LotteryStockService { private final LotteryPrizeConfigMapper prizeConfigMapper; - private final RedisUtil redisUtil; + private final RedissonClient redissonClient; /** * 库存缓存Key @@ -54,7 +54,7 @@ public boolean deductStock(Long prizeId) { // 获取分布式锁 String lockKey = STOCK_LOCK_KEY + prizeId; - RLock lock = redisUtil.getLock(lockKey); + RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁,最多等待3秒 @@ -188,7 +188,7 @@ public void syncStockToDatabase() { } private RAtomicLong getStockCounter(Long prizeId) { - return redisUtil.getRedissonClient().getAtomicLong(STOCK_KEY + prizeId); + return redissonClient.getAtomicLong(STOCK_KEY + prizeId); } private void initializeStockIfAbsent(RAtomicLong stockCounter, LotteryPrizeConfig prize) { diff --git a/xiaou-user/src/main/java/com/xiaou/user/service/impl/CaptchaServiceImpl.java b/xiaou-user/src/main/java/com/xiaou/user/service/impl/CaptchaServiceImpl.java index ef73debeb..64cf5c00e 100644 --- a/xiaou-user/src/main/java/com/xiaou/user/service/impl/CaptchaServiceImpl.java +++ b/xiaou-user/src/main/java/com/xiaou/user/service/impl/CaptchaServiceImpl.java @@ -3,7 +3,7 @@ import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.LineCaptcha; import cn.hutool.core.util.IdUtil; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import com.xiaou.user.service.CaptchaService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -11,7 +11,7 @@ import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.time.Duration; /** * 验证码服务实现类 @@ -23,7 +23,7 @@ @RequiredArgsConstructor public class CaptchaServiceImpl implements CaptchaService { - private final RedisUtil redisUtil; + private final RedisValueStore redisValueStore; /** * 验证码在Redis中的key前缀 @@ -74,7 +74,7 @@ public Map generateCaptcha() { // 将验证码存储到Redis中,保持原样(验证时忽略大小写) String redisKey = CAPTCHA_KEY_PREFIX + captchaKey; - redisUtil.set(redisKey, captchaCode, CAPTCHA_EXPIRE_MINUTES * 60); // 转换为秒 + redisValueStore.put(redisKey, captchaCode, Duration.ofMinutes(CAPTCHA_EXPIRE_MINUTES)); // 获取验证码图片的Base64编码 String captchaImage = lineCaptcha.getImageBase64(); @@ -103,7 +103,7 @@ public boolean verifyCaptcha(String captchaKey, String captcha) { try { String redisKey = CAPTCHA_KEY_PREFIX + captchaKey; - String storedCaptcha = (String) redisUtil.get(redisKey); + String storedCaptcha = redisValueStore.find(redisKey, String.class).orElse(null); if (storedCaptcha == null) { log.warn("验证码已过期或不存在,key: {}", captchaKey); @@ -133,7 +133,7 @@ public boolean verifyCaptcha(String captchaKey, String captcha) { public void deleteCaptcha(String captchaKey) { if (captchaKey != null) { String redisKey = CAPTCHA_KEY_PREFIX + captchaKey; - redisUtil.del(redisKey); + redisValueStore.delete(redisKey); log.debug("删除验证码成功,key: {}", captchaKey); } } diff --git a/xiaou-user/src/test/java/com/xiaou/user/service/impl/CaptchaServiceImplTest.java b/xiaou-user/src/test/java/com/xiaou/user/service/impl/CaptchaServiceImplTest.java index 2ffb96c57..074763a0e 100644 --- a/xiaou-user/src/test/java/com/xiaou/user/service/impl/CaptchaServiceImplTest.java +++ b/xiaou-user/src/test/java/com/xiaou/user/service/impl/CaptchaServiceImplTest.java @@ -4,15 +4,16 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; -import com.xiaou.common.utils.RedisUtil; +import com.xiaou.common.cache.RedisValueStore; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.slf4j.LoggerFactory; import java.util.stream.Collectors; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -21,16 +22,15 @@ class CaptchaServiceImplTest { @Test void generateCaptchaShouldNotLogPlainTextCaptchaCode() { - RedisUtil redisUtil = mock(RedisUtil.class); - when(redisUtil.set(anyString(), anyString(), anyLong())).thenReturn(true); - CaptchaServiceImpl service = new CaptchaServiceImpl(redisUtil); + RedisValueStore redisValueStore = mock(RedisValueStore.class); + CaptchaServiceImpl service = new CaptchaServiceImpl(redisValueStore); ListAppender appender = attachListAppender(); try { service.generateCaptcha(); ArgumentCaptor captchaCaptor = ArgumentCaptor.forClass(Object.class); - org.mockito.Mockito.verify(redisUtil).set(anyString(), captchaCaptor.capture(), anyLong()); + org.mockito.Mockito.verify(redisValueStore).put(anyString(), captchaCaptor.capture(), any()); String captchaCode = String.valueOf(captchaCaptor.getValue()); String logs = renderedLogs(appender); @@ -44,9 +44,9 @@ void generateCaptchaShouldNotLogPlainTextCaptchaCode() { @Test void failedVerificationShouldNotLogInputOrStoredCaptchaCode() { - RedisUtil redisUtil = mock(RedisUtil.class); - when(redisUtil.get("user:captcha:captcha-key")).thenReturn("ABCD"); - CaptchaServiceImpl service = new CaptchaServiceImpl(redisUtil); + RedisValueStore redisValueStore = mock(RedisValueStore.class); + when(redisValueStore.find("user:captcha:captcha-key", String.class)).thenReturn(Optional.of("ABCD")); + CaptchaServiceImpl service = new CaptchaServiceImpl(redisValueStore); ListAppender appender = attachListAppender(); try { From 9169c6d437d626d79f26823ff17900f9f0884485 Mon Sep 17 00:00:00 2001 From: lzf <3153566913@qq.com> Date: Wed, 15 Jul 2026 09:13:47 +0800 Subject: [PATCH 2/3] docs: reorganize and harden documentation site --- AI-DOCS/SUMMARY.md | 159 ++---- docs-site/.vitepress/config.mts | 494 ++++++++++-------- docs-site/.vitepress/theme/DocContext.vue | 34 ++ docs-site/.vitepress/theme/custom.css | 245 +++++++++ docs-site/.vitepress/theme/index.ts | 14 + docs-site/api/index.md | 41 ++ docs-site/architecture/backend-modules.md | 10 +- docs-site/architecture/database.md | 10 +- docs-site/architecture/frontend-apps.md | 2 +- docs-site/architecture/index.md | 28 + docs-site/architecture/overview.md | 16 +- docs-site/guide/changelog.md | 65 +++ docs-site/guide/ci-cd-production.md | 12 +- docs-site/guide/documentation-maintenance.md | 269 ++++------ docs-site/guide/git-log-release-notes.md | 2 +- docs-site/guide/index.md | 46 ++ docs-site/guide/learning-paths.md | 2 +- docs-site/guide/local-dev.md | 9 +- docs-site/guide/onboarding-tasks.md | 2 +- docs-site/guide/quick-start.md | 6 +- docs-site/guide/release-verification.md | 2 +- docs-site/guide/security-policy.md | 6 +- .../guide/version-release-handoff-template.md | 4 +- docs-site/index.md | 140 ++--- docs-site/manuals/core-workflows.md | 2 +- docs-site/manuals/index.md | 23 + docs-site/modules/common.md | 4 +- docs-site/modules/version-history.md | 123 +++-- docs-site/operations/index.md | 33 ++ docs-site/package.json | 3 +- docs-site/public/favicon.ico | Bin 0 -> 766 bytes docs-site/reference/ai-schemas.md | 93 ++-- docs-site/reference/database-tables.md | 9 +- docs-site/reference/docs-sync-baseline.md | 262 ++-------- docs-site/reference/env-vars.md | 184 +------ docs-site/reference/feature-coverage.md | 2 +- docs-site/reference/index.md | 39 ++ .../reference/module-regression-matrix.md | 2 +- docs-site/reference/source-map.md | 6 +- docs-site/roadmap/index.md | 18 + docs-site/roadmap/v2.2.0-docs-plan.md | 4 + docs-site/roadmap/v2.2.1-docs-plan.md | 4 + docs-site/scripts/audit-docs.mjs | 111 ++++ .../scripts/generate-docs-sync-baseline.mjs | 40 +- 44 files changed, 1465 insertions(+), 1115 deletions(-) create mode 100644 docs-site/.vitepress/theme/DocContext.vue create mode 100644 docs-site/.vitepress/theme/custom.css create mode 100644 docs-site/.vitepress/theme/index.ts create mode 100644 docs-site/api/index.md create mode 100644 docs-site/architecture/index.md create mode 100644 docs-site/guide/index.md create mode 100644 docs-site/manuals/index.md create mode 100644 docs-site/operations/index.md create mode 100644 docs-site/public/favicon.ico create mode 100644 docs-site/reference/index.md create mode 100644 docs-site/roadmap/index.md create mode 100644 docs-site/scripts/audit-docs.mjs diff --git a/AI-DOCS/SUMMARY.md b/AI-DOCS/SUMMARY.md index ad83168f5..d4bf783e2 100644 --- a/AI-DOCS/SUMMARY.md +++ b/AI-DOCS/SUMMARY.md @@ -1,107 +1,52 @@ -# AI-DOCS 文档结构优化完成报告 - -## 1. 项目概述 - -已成功创建标准化的AI-DOCS文档结构,并完成现有文档的迁移。 - -## 2. 目录结构 - -``` -AI-DOCS/ -├── PRD/ # 产品需求文档 (32个文件) -├── Technical/ # 技术文档 (14个文件) -├── Testing/ # 测试文档 (待填充) -├── Deployment/ # 部署文档 -├── User/ # 用户文档 -├── Development/ # 开发文档 -├── Archive/ # 归档文档 (包含原docs文件夹) -└── assets/ # 公共资源 (图片、模板) -``` - -## 3. 迁移统计 - -- **总文件数**: 553个 -- **PRD文档**: 32个 -- **技术文档**: 14个 -- **归档文档**: 包含原docs文件夹所有内容 -- **图片资源**: 包含所有手册截图 - -## 4. 文档规范 - -### 4.1 命名规范 -- PRD文档: `{模块名称}PRD-v{版本号}.md` -- 技术文档: `{主题名称}.md` -- 测试文档: `{模块名称}测试{文档类型}-v{版本号}.md` - -### 4.2 版本管理 -- 使用语义化版本号 -- 旧版本文档移至Archive目录 -- 保持文档与代码同步 - -## 5. 模板创建 - -已创建以下文档模板: -- PRD模板 -- 技术文档模板 -- 测试文档模板 -- 部署文档模板 -- 用户文档模板 -- 开发文档模板 - -## 6. 链接更新 - -已更新以下文件中的文档链接: -- README.md -- CONTRIBUTING.md -- SKILL.md -- docs-site/ (多个文件) - -## 7. 后续建议 - -### 7.1 文档维护 -1. 定期更新文档,保持与代码同步 -2. 废弃文档及时归档 -3. 使用版本控制管理文档变更 - -### 7.2 文档审查 -1. PRD需要产品经理审查 -2. 技术文档需要架构师审查 -3. 测试文档需要测试负责人审查 - -### 7.3 文档工具 -1. 支持Markdown编写 -2. 支持图表 (Mermaid, PlantUML) -3. 支持代码高亮 - -## 8. 注意事项 - -1. 原docs文件夹已移动至 `AI-DOCS/Archive/docs-original` -2. 所有文档链接已更新指向AI-DOCS -3. 文档模板位于 `AI-DOCS/assets/templates/` -4. 每个目录都有README.md说明文档规范 - -## 9. 文件清单 - -### 主要目录 -- `AI-DOCS/PRD/` - 产品需求文档 -- `AI-DOCS/Technical/` - 技术文档 -- `AI-DOCS/Testing/` - 测试文档 -- `AI-DOCS/Deployment/` - 部署文档 -- `AI-DOCS/User/` - 用户文档 -- `AI-DOCS/Development/` - 开发文档 -- `AI-DOCS/Archive/` - 归档文档 -- `AI-DOCS/assets/` - 公共资源 - -### 模板文件 -- `AI-DOCS/assets/templates/PRD-template.md` -- `AI-DOCS/assets/templates/Technical-template.md` -- `AI-DOCS/assets/templates/Testing-template.md` -- `AI-DOCS/assets/templates/Deployment-template.md` -- `AI-DOCS/assets/templates/User-template.md` -- `AI-DOCS/assets/templates/Development-template.md` - ---- - -**完成时间**: 2026-05-29 -**执行者**: AI Assistant -**状态**: 已完成 \ No newline at end of file +# AI-DOCS 分类索引 + +`AI-DOCS` 保存产品需求、技术专题、测试证据、部署材料和历史归档。与当前代码直接对应的架构、模块、API、操作和运维文档统一维护在 `docs-site`。 + +## 当前规模 + +截至 2026-07-14,`AI-DOCS` 共 177 个 Markdown 文件: + +| 分类 | 文件数 | 定位 | +| --- | ---: | --- | +| `PRD` | 32 | 产品目标、范围、流程和验收标准 | +| `Technical` | 22 | 技术专题、面试亮点和设计说明 | +| `Development` | 8 | 环境、规范和贡献材料 | +| `Testing` | 5 | 测试计划、用例和报告 | +| `User` | 4 | 用户材料和 FAQ | +| `Deployment` | 3 | 部署专题材料 | +| `assets` | 7 | 模板和文档资源 | +| `Archive` | 95 | 历史版本、废弃方案和原始文档快照 | +| 根索引 | 1 | 本页 | + +## 分类入口 + +- [PRD](./PRD/README.md):需求和验收边界。 +- [技术专题](./Technical/README.md):架构专题、技术亮点和专项分析。 +- [开发材料](./Development/README.md):环境、规范和贡献流程。 +- [测试材料](./Testing/README.md):测试计划、用例和结果证据。 +- [用户材料](./User/README.md):用户手册与 FAQ 原始资料。 +- [部署材料](./Deployment/README.md):部署专题和交付说明。 +- [历史归档](./Archive/README.md):不再作为当前实现依据的文档。 +- [资源与模板](./assets/README.md):PRD、技术、测试和交付模板。 + +## 与 VitePress 的关系 + +| 内容 | 默认入口 | +| --- | --- | +| 当前系统架构、模块、API 和运维事实 | `docs-site` | +| 产品需求和设计背景 | `AI-DOCS/PRD`、`AI-DOCS/Technical` | +| 测试过程和交付证据 | `AI-DOCS/Testing` | +| 历史方案和旧版本副本 | `AI-DOCS/Archive` | + +当一份专题材料影响当前开发或运维流程时,在 VitePress 中写一份简洁、可执行的现行说明,并链接到这里的背景材料。不要直接把归档文档加入主导航。 + +## 维护规则 + +1. 新文档进入最具体的分类目录,不放在 `AI-DOCS` 根目录。 +2. 文件名说明主题;需要独立版本管理的 PRD 使用语义化版本号。 +3. 当前实现发生变化时,优先更新 `docs-site`,再更新相关专题材料。 +4. 废弃方案移动到 `Archive`,并在标题或开头标明状态。 +5. 每个分类的 `README.md` 维护入口和新增规则。 +6. 不在两处维护相同的接口表、模块数、数据库表数或环境变量清单。 + +VitePress 的分类和校验规则见 `docs-site/guide/documentation-maintenance.md`。 diff --git a/docs-site/.vitepress/config.mts b/docs-site/.vitepress/config.mts index cca91e604..123bc9f02 100644 --- a/docs-site/.vitepress/config.mts +++ b/docs-site/.vitepress/config.mts @@ -1,170 +1,245 @@ -import { defineConfig, DefaultTheme } from 'vitepress' +import { defineConfig, type DefaultTheme } from 'vitepress' type SidebarItem = DefaultTheme.SidebarItem -const guideGettingStarted: SidebarItem[] = [ - { text: '快速开始', link: '/guide/quick-start' }, - { text: '本地开发', link: '/guide/local-dev' }, - { text: '本地完整启动剧本', link: '/guide/startup-playbook' }, - { text: '常见问题(FAQ)', link: '/guide/faq' } -] - -const guideLearningPaths: SidebarItem[] = [ - { text: '学习路线', link: '/guide/learning-paths' }, - { text: '按模块学习路线', link: '/guide/module-learning-paths' }, - { text: '模块学习任务模板', link: '/guide/module-learning-workbook' }, - { text: '模块学习评审表', link: '/guide/module-learning-rubric' }, - { text: '模块结业案例', link: '/guide/module-learning-capstone' } -] - -const guideOnboarding: SidebarItem[] = [ - { text: '带教执行剧本', link: '/guide/onboarding-execution-playbook' }, - { text: '带教交付模板包', link: '/guide/onboarding-deliverable-templates' }, - { text: '角色上手任务包', link: '/guide/onboarding-tasks' }, - { text: '首个真实任务接入', link: '/guide/first-real-task' }, - { text: '首单任务题库', link: '/guide/first-task-backlog' } -] - -const guideModuleOwnership: SidebarItem[] = [ - { text: '模块接手升级路线', link: '/guide/module-ownership-ladder' }, - { text: '模块接手检查清单', link: '/guide/module-takeover-checklist' }, - { text: '模块接手样例包', link: '/guide/module-takeover-examples' }, - { text: '模块值守与回归手册', link: '/guide/module-owner-playbook' } -] - -const guideDevelopment: SidebarItem[] = [ - { text: '功能开发流程', link: '/guide/feature-development' }, - { text: '常见二开场景', link: '/guide/extension-scenarios' }, - { text: '测试与回归', link: '/guide/testing-regression' }, - { text: '开发者规范', link: '/guide/developer-standards' } -] - -const guideRelease: SidebarItem[] = [ - { text: 'Git Log 版更新记录', link: '/guide/git-log-release-notes' }, - { text: '生产 CI/CD 自动发布', link: '/guide/ci-cd-production' }, - { text: '版本公告与交接模板', link: '/guide/version-release-handoff-template' }, - { text: '版本交付实战样例', link: '/guide/version-release-worked-examples' }, - { text: '发布前验证', link: '/guide/release-verification' } -] - -const guideSecurity: SidebarItem[] = [ - { text: '权限与安全边界', link: '/guide/security-boundaries' }, - { text: '独立部署', link: '/guide/deploy' }, - { text: '文档维护规范', link: '/guide/documentation-maintenance' } -] - -const guideApi: SidebarItem[] = [ - { text: 'API 认证指南', link: '/guide/api-auth' }, - { text: '数据库迁移指南', link: '/guide/database-migration' } -] - -const guideCommunity: SidebarItem[] = [ - { text: '贡献指南', link: '/guide/contributing' }, - { text: '行为准则', link: '/guide/code-of-conduct' }, - { text: '安全策略', link: '/guide/security-policy' }, - { text: '发布流程', link: '/guide/release-process' }, - { text: '变更日志', link: '/guide/changelog' } +const guideSidebar: SidebarItem[] = [ + { text: '指南总览', link: '/guide/' }, + { + text: '上手与环境', + items: [ + { text: '快速开始', link: '/guide/quick-start' }, + { text: '本地开发', link: '/guide/local-dev' }, + { text: '本地完整启动剧本', link: '/guide/startup-playbook' }, + { text: '常见问题(FAQ)', link: '/guide/faq' } + ] + }, + { + text: '学习与带教', + items: [ + { text: '学习路线', link: '/guide/learning-paths' }, + { text: '按模块学习路线', link: '/guide/module-learning-paths' }, + { text: '模块学习任务模板', link: '/guide/module-learning-workbook' }, + { text: '模块学习评审表', link: '/guide/module-learning-rubric' }, + { text: '模块结业案例', link: '/guide/module-learning-capstone' }, + { text: '带教执行剧本', link: '/guide/onboarding-execution-playbook' }, + { text: '带教交付模板包', link: '/guide/onboarding-deliverable-templates' }, + { text: '角色上手任务包', link: '/guide/onboarding-tasks' }, + { text: '首个真实任务接入', link: '/guide/first-real-task' }, + { text: '首单任务题库', link: '/guide/first-task-backlog' } + ] + }, + { + text: '模块接手', + items: [ + { text: '模块接手升级路线', link: '/guide/module-ownership-ladder' }, + { text: '模块接手检查清单', link: '/guide/module-takeover-checklist' }, + { text: '模块接手样例包', link: '/guide/module-takeover-examples' }, + { text: '模块值守与回归手册', link: '/guide/module-owner-playbook' } + ] + }, + { + text: '开发与质量', + items: [ + { text: '功能开发流程', link: '/guide/feature-development' }, + { text: '常见二开场景', link: '/guide/extension-scenarios' }, + { text: '测试与回归', link: '/guide/testing-regression' }, + { text: '开发者规范', link: '/guide/developer-standards' }, + { text: '权限与安全边界', link: '/guide/security-boundaries' }, + { text: 'API 认证指南', link: '/guide/api-auth' }, + { text: '数据库迁移指南', link: '/guide/database-migration' } + ] + }, + { + text: '发布与部署', + items: [ + { text: '生产 CI/CD 自动发布', link: '/guide/ci-cd-production' }, + { text: '版本公告与交接模板', link: '/guide/version-release-handoff-template' }, + { text: '版本交付实战样例', link: '/guide/version-release-worked-examples' }, + { text: '发布前验证', link: '/guide/release-verification' }, + { text: '独立部署', link: '/guide/deploy' }, + { text: '发布流程', link: '/guide/release-process' }, + { text: 'Git Log 版更新记录', link: '/guide/git-log-release-notes' }, + { text: '变更日志', link: '/guide/changelog' } + ] + }, + { + text: '协作与治理', + items: [ + { text: '文档维护规范', link: '/guide/documentation-maintenance' }, + { text: '贡献指南', link: '/guide/contributing' }, + { text: '行为准则', link: '/guide/code-of-conduct' }, + { text: '安全策略', link: '/guide/security-policy' } + ] + } ] -const architecture = [ - { text: '架构总览', link: '/architecture/overview' }, +const architectureSidebar: SidebarItem[] = [ + { text: '架构导航', link: '/architecture/' }, + { text: '整体架构', link: '/architecture/overview' }, { text: '后端模块', link: '/architecture/backend-modules' }, { text: '前端应用', link: '/architecture/frontend-apps' }, { text: '数据库与脚本', link: '/architecture/database' } ] -const modulesCore: SidebarItem[] = [ +const modulesSidebar: SidebarItem[] = [ { text: '模块总览', link: '/modules/' }, - { text: '公共底座', link: '/modules/common' }, - { text: '工具类详解', link: '/modules/utils' }, - { text: '鉴权与用户体系', link: '/modules/auth' }, - { text: '用户账户与个人中心', link: '/modules/user-account' }, - { text: 'AI Runtime', link: '/modules/ai-runtime' } -] - -const modulesLearning: SidebarItem[] = [ - { text: '题库与成长闭环', link: '/modules/interview-and-growth' }, - { text: '面试题库', link: '/modules/interview' }, - { text: '模拟面试与求职作战台', link: '/modules/mock-interview-job-battle' }, - { text: '学习资产', link: '/modules/learning-assets' }, - { text: '闪卡', link: '/modules/flashcard' }, - { text: '计划与学习小组', link: '/modules/plan-team' }, - { text: '知识图谱', link: '/modules/knowledge' }, - { text: 'SQL 优化工作台', link: '/modules/sql-optimizer' }, - { text: 'OJ 判题系统', link: '/modules/oj' } -] - -const modulesContent: SidebarItem[] = [ - { text: '社区与内容矩阵', link: '/modules/community-content' }, - { text: '社区帖子', link: '/modules/community' }, - { text: '动态广场', link: '/modules/moments' }, - { text: '博客', link: '/modules/blog' }, - { text: '代码工坊', link: '/modules/codepen' }, - { text: 'IM 聊天室', link: '/modules/chat' }, - { text: '简历系统', link: '/modules/resume' } -] - -const modulesPlatform: SidebarItem[] = [ - { text: '积分与抽奖', link: '/modules/points' }, - { text: '文件存储', link: '/modules/file-storage' }, - { text: '通知中心', link: '/modules/notification' }, - { text: '敏感词风控', link: '/modules/sensitive' } -] - -const modulesTools: SidebarItem[] = [ - { text: '工具、摸鱼与版本', link: '/modules/tools-moyu-version' }, - { text: '开发者工具', link: '/modules/dev-tools' }, - { text: '摸鱼工具', link: '/modules/moyu' }, - { text: '版本历史', link: '/modules/version-history' }, - { text: '仪表盘与日志', link: '/modules/dashboard-logs' }, - { text: '系统运营后台', link: '/modules/system-ops' } -] - -const operations = [ - { text: 'Docker 与服务部署', link: '/operations/docker' }, - { text: '环境变量总表', link: '/operations/env-vars' }, - { text: '监控与观测', link: '/operations/monitoring' }, - { text: '线上接口业务正确性测试(2026-06-18)', link: '/operations/online-api-business-correctness-2026-06-18' }, - { text: '告警 Runbook', link: '/operations/alert-runbooks' }, - { text: '事故响应', link: '/operations/incident-response' }, - { text: '问题定位流程', link: '/operations/diagnosis-flow' }, - { text: '常见问题排查', link: '/operations/troubleshooting' } -] - -const referenceApi: SidebarItem[] = [ - { text: '术语表', link: '/reference/glossary' }, - { text: 'API 路由索引', link: '/reference/api-routes' }, - { text: 'API 调用示例', link: '/reference/api-examples' }, - { text: '前端路由索引', link: '/reference/frontend-routes' }, - { text: '响应体与错误码', link: '/reference/response-errors' }, - { text: 'WebSocket 协议', link: '/reference/websocket' } + { + text: '核心与公共', + items: [ + { text: '公共底座', link: '/modules/common' }, + { text: '工具与基础设施', link: '/modules/utils' }, + { text: '鉴权与用户体系', link: '/modules/auth' }, + { text: '用户账户与个人中心', link: '/modules/user-account' }, + { text: 'AI Runtime', link: '/modules/ai-runtime' } + ] + }, + { + text: '学习成长', + items: [ + { text: '题库与成长闭环', link: '/modules/interview-and-growth' }, + { text: '面试题库', link: '/modules/interview' }, + { text: '模拟面试与求职作战台', link: '/modules/mock-interview-job-battle' }, + { text: '学习资产', link: '/modules/learning-assets' }, + { text: '闪卡', link: '/modules/flashcard' }, + { text: '计划与学习小组', link: '/modules/plan-team' }, + { text: '知识图谱', link: '/modules/knowledge' }, + { text: 'SQL 优化工作台', link: '/modules/sql-optimizer' }, + { text: 'OJ 判题系统', link: '/modules/oj' } + ] + }, + { + text: '内容与社交', + items: [ + { text: '社区与内容矩阵', link: '/modules/community-content' }, + { text: '社区帖子', link: '/modules/community' }, + { text: '动态广场', link: '/modules/moments' }, + { text: '博客', link: '/modules/blog' }, + { text: '代码工坊', link: '/modules/codepen' }, + { text: 'IM 聊天室', link: '/modules/chat' }, + { text: '简历系统', link: '/modules/resume' } + ] + }, + { + text: '平台能力', + items: [ + { text: '积分与抽奖', link: '/modules/points' }, + { text: '文件存储', link: '/modules/file-storage' }, + { text: '通知中心', link: '/modules/notification' }, + { text: '敏感词风控', link: '/modules/sensitive' } + ] + }, + { + text: '工具与运营', + items: [ + { text: '工具、摸鱼与版本', link: '/modules/tools-moyu-version' }, + { text: '开发者工具', link: '/modules/dev-tools' }, + { text: '摸鱼工具', link: '/modules/moyu' }, + { text: '版本历史', link: '/modules/version-history' }, + { text: '仪表盘与日志', link: '/modules/dashboard-logs' }, + { text: '系统运营后台', link: '/modules/system-ops' } + ] + } ] -const referenceData: SidebarItem[] = [ - { text: '数据表索引', link: '/reference/database-tables' }, - { text: '数据库字段阅读指南', link: '/reference/database-field-guide' }, - { text: '源码地图', link: '/reference/source-map' }, - { text: '模块依赖地图', link: '/reference/module-dependencies' } +const apiSidebar: SidebarItem[] = [ + { text: 'API 导航', link: '/api/' }, + { + text: '用户端 API', + items: [ + { text: '账号与系统', link: '/api/account-system' }, + { text: '学习成长', link: '/api/learning-growth' }, + { text: 'OJ 判题', link: '/api/oj' }, + { text: '内容社区', link: '/api/content-community' }, + { text: '平台能力', link: '/api/platform' }, + { text: '工具与运营', link: '/api/tools-operations' } + ] + }, + { + text: '管理端 API', + items: [ + { text: '积分与抽奖', link: '/api/admin-points-lottery' }, + { text: '内容社区', link: '/api/admin-content-community' }, + { text: '平台能力', link: '/api/admin-platform' }, + { text: 'AI Runtime', link: '/api/admin-ai-runtime' } + ] + } ] -const referenceSystem: SidebarItem[] = [ - { text: 'AI Schema 与治理', link: '/reference/ai-schemas' }, - { text: '权限注解与角色边界索引', link: '/reference/permission-boundaries' }, - { text: '异常路径与失败态索引', link: '/reference/failure-paths' }, - { text: '事件、通知与回流索引', link: '/reference/event-backflow-index' }, - { text: '统计、排行与计数口径索引', link: '/reference/statistics-ranking-counts' }, - { text: '幂等、回滚与补偿索引', link: '/reference/idempotency-rollbacks-compensation' }, - { text: '前端渲染安全', link: '/reference/frontend-rendering-security' }, - { text: '模块状态机与生命周期索引', link: '/reference/module-state-machines' } +const operationsSidebar: SidebarItem[] = [ + { text: '运维导航', link: '/operations/' }, + { + text: '部署与配置', + items: [ + { text: 'Docker 与服务部署', link: '/operations/docker' }, + { text: '环境变量总表', link: '/operations/env-vars' } + ] + }, + { + text: '观测与响应', + items: [ + { text: '监控与观测', link: '/operations/monitoring' }, + { text: '告警 Runbook', link: '/operations/alert-runbooks' }, + { text: '事故响应', link: '/operations/incident-response' }, + { text: '问题定位流程', link: '/operations/diagnosis-flow' }, + { text: '常见问题排查', link: '/operations/troubleshooting' } + ] + }, + { + text: '验证记录', + items: [ + { text: '线上接口业务正确性测试(2026-06-18)', link: '/operations/online-api-business-correctness-2026-06-18' } + ] + } ] -const referenceQuality: SidebarItem[] = [ - { text: '模块最小回归矩阵', link: '/reference/module-regression-matrix' }, - { text: '全功能覆盖矩阵', link: '/reference/feature-coverage' }, - { text: '文档同步基线', link: '/reference/docs-sync-baseline' } +const referenceSidebar: SidebarItem[] = [ + { text: '参考总览', link: '/reference/' }, + { + text: 'API 与协议', + items: [ + { text: '术语表', link: '/reference/glossary' }, + { text: 'API 路由索引', link: '/reference/api-routes' }, + { text: 'API 调用示例', link: '/reference/api-examples' }, + { text: '前端路由索引', link: '/reference/frontend-routes' }, + { text: '响应体与错误码', link: '/reference/response-errors' }, + { text: 'WebSocket 协议', link: '/reference/websocket' } + ] + }, + { + text: '数据与源码', + items: [ + { text: '数据表索引', link: '/reference/database-tables' }, + { text: '数据库字段阅读指南', link: '/reference/database-field-guide' }, + { text: '环境变量索引', link: '/reference/env-vars' }, + { text: '源码地图', link: '/reference/source-map' }, + { text: '模块依赖地图', link: '/reference/module-dependencies' } + ] + }, + { + text: '系统设计', + items: [ + { text: 'AI Schema 与治理', link: '/reference/ai-schemas' }, + { text: '权限注解与角色边界', link: '/reference/permission-boundaries' }, + { text: '异常路径与失败态', link: '/reference/failure-paths' }, + { text: '事件、通知与回流', link: '/reference/event-backflow-index' }, + { text: '统计、排行与计数口径', link: '/reference/statistics-ranking-counts' }, + { text: '幂等、回滚与补偿', link: '/reference/idempotency-rollbacks-compensation' }, + { text: '前端渲染安全', link: '/reference/frontend-rendering-security' }, + { text: '模块状态机与生命周期', link: '/reference/module-state-machines' } + ] + }, + { + text: '质量保障', + items: [ + { text: '模块最小回归矩阵', link: '/reference/module-regression-matrix' }, + { text: '全功能覆盖矩阵', link: '/reference/feature-coverage' }, + { text: '文档同步基线', link: '/reference/docs-sync-baseline' } + ] + } ] -const manuals = [ +const manualsSidebar: SidebarItem[] = [ + { text: '手册导航', link: '/manuals/' }, { text: '用户端操作手册', link: '/manuals/user-operations' }, { text: '管理端操作手册', link: '/manuals/admin-operations' }, { text: '端到端业务链路图', link: '/manuals/business-flow-map' }, @@ -172,16 +247,23 @@ const manuals = [ { text: '验证记录与已知问题', link: '/manuals/verified-scenarios' } ] +const roadmapSidebar: SidebarItem[] = [ + { text: '版本与历史', link: '/roadmap/' }, + { text: 'v2.2.1 文档计划(历史)', link: '/roadmap/v2.2.1-docs-plan' }, + { text: 'v2.2.0 文档计划(历史)', link: '/roadmap/v2.2.0-docs-plan' } +] + +const base = process.env.VITEPRESS_BASE || '/' +const sitemapHostname = process.env.SITEMAP_HOSTNAME + export default defineConfig({ lang: 'zh-CN', title: 'Code Nest 文档', - description: 'Code Nest v2.3.1 文档工作线', - base: process.env.VITEPRESS_BASE || '/', + description: 'Code Nest v2.4.1 工程文档中心:架构、模块、API、操作手册与运维参考。', + base, cleanUrls: true, lastUpdated: true, - sitemap: { - hostname: process.env.SITEMAP_HOSTNAME || 'https://code-nest.example.com' // 替换为实际域名 - }, + sitemap: sitemapHostname ? { hostname: sitemapHostname } : undefined, markdown: { lineNumbers: true }, @@ -204,91 +286,51 @@ export default defineConfig({ } }, head: [ - ['meta', { name: 'theme-color', content: '#2563eb' }], - ['link', { rel: 'icon', type: 'image/svg+xml', href: '/code-nest-mark.svg' }], + ['meta', { name: 'theme-color', content: '#0f766e' }], + ['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}code-nest-mark.svg` }], ['meta', { name: 'author', content: 'Code Nest Team' }], ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'Code Nest 文档' }], - ['meta', { property: 'og:title', content: 'Code Nest - 面向开发者成长社区的全量开发与运维手册' }], - ['meta', { property: 'og:description', content: '后端 15 个模块、前端双端、文档站独立部署的完整技术文档' }], - ['meta', { name: 'twitter:card', content: 'summary_large_image' }], - ['meta', { name: 'twitter:title', content: 'Code Nest - 面向开发者成长社区的全量开发与运维手册' }], - ['meta', { name: 'twitter:description', content: '后端 15 个模块、前端双端、文档站独立部署的完整技术文档' }] + ['meta', { property: 'og:title', content: 'Code Nest 工程文档中心' }], + ['meta', { property: 'og:description', content: '覆盖 28 个 Maven 子模块、双前端、API、部署与运维的工程手册。' }], + ['meta', { name: 'twitter:card', content: 'summary' }], + ['meta', { name: 'twitter:title', content: 'Code Nest 工程文档中心' }], + ['meta', { name: 'twitter:description', content: '按角色、任务和系统边界组织的 Code Nest 文档。' }] ], themeConfig: { logo: '/code-nest-mark.svg', nav: [ - { text: '指南', link: '/guide/quick-start' }, - { text: '架构', link: '/architecture/overview' }, + { text: '开始', link: '/guide/' }, + { text: '架构', link: '/architecture/' }, { text: '模块', link: '/modules/' }, - { text: '参考', link: '/reference/api-routes' }, - { text: '手册', link: '/manuals/user-operations' }, - { text: '运维', link: '/operations/docker' }, - { text: 'v2.2.1 计划', link: '/roadmap/v2.2.1-docs-plan' } - ], - sidebar: { - '/guide/': [ - { text: '入门指南', items: guideGettingStarted }, - { text: '学习路线', items: guideLearningPaths }, - { text: '带教与首单', items: guideOnboarding }, - { text: '模块接手', items: guideModuleOwnership }, - { text: '开发流程', items: guideDevelopment }, - { text: '版本发布', items: guideRelease }, - { text: '安全与部署', items: guideSecurity }, - { text: 'API 与数据库', items: guideApi }, - { text: '社区与治理', items: guideCommunity } - ], - '/architecture/': [{ text: '系统架构', items: architecture }], - '/modules/': [ - { text: '核心与公共', items: modulesCore }, - { text: '学习成长', items: modulesLearning }, - { text: '内容与社交', items: modulesContent }, - { text: '平台能力', items: modulesPlatform }, - { text: '工具与运营', items: modulesTools } - ], - '/reference/': [ - { text: 'API 与协议', items: referenceApi }, - { text: '数据与源码', items: referenceData }, - { text: '系统设计', items: referenceSystem }, - { text: '质量保障', items: referenceQuality } - ], - '/api/': [ + { text: 'API', link: '/api/' }, + { text: '运维', link: '/operations/' }, { - text: '用户端 API', + text: '资料', items: [ - { text: '账号与系统', link: '/api/account-system' }, - { text: '学习成长', link: '/api/learning-growth' }, - { text: 'OJ 判题', link: '/api/oj' }, - { text: '内容社区', link: '/api/content-community' }, - { text: '平台能力', link: '/api/platform' }, - { text: '工具与运营', link: '/api/tools-operations' } - ] - }, - { - text: '管理端 API', - items: [ - { text: '积分与抽奖', link: '/api/admin-points-lottery' }, - { text: '内容社区', link: '/api/admin-content-community' }, - { text: '平台能力', link: '/api/admin-platform' }, - { text: 'AI Runtime', link: '/api/admin-ai-runtime' } + { text: '操作手册', link: '/manuals/' }, + { text: '参考索引', link: '/reference/' }, + { text: '版本与历史', link: '/roadmap/' } ] } ], - '/manuals/': [{ text: '操作手册', items: manuals }], - '/operations/': [{ text: '部署运维', items: operations }], - '/roadmap/': [ - { - text: '文档路线图', - items: [ - { text: 'v2.2.1 文档计划', link: '/roadmap/v2.2.1-docs-plan' }, - { text: 'v2.2.0 文档计划', link: '/roadmap/v2.2.0-docs-plan' } - ] - } - ] - }, + sidebar: { + '/guide/': guideSidebar, + '/architecture/': architectureSidebar, + '/modules/': modulesSidebar, + '/api/': apiSidebar, + '/operations/': operationsSidebar, + '/reference/': referenceSidebar, + '/manuals/': manualsSidebar, + '/roadmap/': roadmapSidebar + }, search: { provider: 'local' }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/xiaou61/Code-Nest' } + ], + externalLinkIcon: true, editLink: { pattern: 'https://github.com/xiaou61/Code-Nest/edit/master/docs-site/:path', text: '在 GitHub 上编辑此页' @@ -308,8 +350,14 @@ export default defineConfig({ timeStyle: 'short' } }, + sidebarMenuLabel: '文档目录', + returnToTopLabel: '返回顶部', + darkModeSwitchLabel: '外观', + lightModeSwitchTitle: '切换到浅色模式', + darkModeSwitchTitle: '切换到深色模式', + skipToContentLabel: '跳到正文', footer: { - message: 'Code Nest v2.3.1 documentation workline.', + message: 'Code Nest v2.4.1 engineering documentation.', copyright: 'MIT Licensed.' } } diff --git a/docs-site/.vitepress/theme/DocContext.vue b/docs-site/.vitepress/theme/DocContext.vue new file mode 100644 index 000000000..72ed601ee --- /dev/null +++ b/docs-site/.vitepress/theme/DocContext.vue @@ -0,0 +1,34 @@ + + + diff --git a/docs-site/.vitepress/theme/custom.css b/docs-site/.vitepress/theme/custom.css new file mode 100644 index 000000000..d43fed4fd --- /dev/null +++ b/docs-site/.vitepress/theme/custom.css @@ -0,0 +1,245 @@ +:root { + --vp-c-brand-1: #0f766e; + --vp-c-brand-2: #0b625c; + --vp-c-brand-3: #094f4a; + --vp-c-brand-soft: rgba(15, 118, 110, 0.12); + --vp-c-bg: #fbfcfd; + --vp-c-bg-alt: #f3f5f7; + --vp-c-bg-elv: #ffffff; + --vp-c-bg-soft: #f0f3f5; + --vp-c-text-1: #20252b; + --vp-c-text-2: #59636d; + --vp-c-text-3: #77818a; + --vp-c-divider: #d7dde3; + --vp-c-gutter: #e7ebef; + --vp-c-tip-1: #0f766e; + --vp-c-tip-2: #0b625c; + --vp-c-tip-3: #094f4a; + --vp-c-tip-soft: rgba(15, 118, 110, 0.1); + --vp-c-warning-1: #986600; + --vp-c-warning-2: #7a5200; + --vp-c-warning-soft: rgba(152, 102, 0, 0.11); + --vp-c-danger-1: #c24156; + --vp-c-danger-2: #9f3145; + --vp-c-danger-soft: rgba(194, 65, 86, 0.1); + --cn-focus-ring: #986600; + --vp-font-family-base: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", sans-serif; + --vp-font-family-mono: "Cascadia Code", "SFMono-Regular", Consolas, monospace; + --vp-layout-max-width: 1560px; + --vp-sidebar-width: 288px; +} + +.dark { + --vp-c-brand-1: #56c8bb; + --vp-c-brand-2: #79d8ce; + --vp-c-brand-3: #a0e5de; + --vp-c-brand-soft: rgba(86, 200, 187, 0.14); + --vp-c-bg: #15191d; + --vp-c-bg-alt: #1a1f24; + --vp-c-bg-elv: #20262c; + --vp-c-bg-soft: #20262c; + --vp-c-text-1: #edf1f3; + --vp-c-text-2: #b8c1c8; + --vp-c-text-3: #8f9aa3; + --vp-c-divider: #343c43; + --vp-c-gutter: #252c32; + --vp-c-warning-1: #f0bd58; + --vp-c-danger-1: #f08a9a; + --cn-focus-ring: #f0bd58; +} + +html { + scroll-padding-top: 88px; +} + +body { + letter-spacing: 0; +} + +:where(a, button, input, textarea, select, summary):focus-visible { + outline: 3px solid var(--cn-focus-ring); + outline-offset: 3px; + border-radius: 4px; +} + +.VPNav { + border-bottom: 1px solid var(--vp-c-divider); +} + +.VPNavBarTitle .title { + font-family: "Segoe UI Variable Display", "Microsoft YaHei UI", sans-serif; + font-weight: 720; +} + +.VPNavBarMenuLink, +.VPNavBarMenuGroup .button, +.VPSidebarItem .link, +.VPDocFooter .pager-link { + min-height: 32px; +} + +.VPSidebarItem.level-0 > .item > .text { + font-size: 13px; + font-weight: 700; + color: var(--vp-c-text-1); +} + +.VPSidebarItem.level-1 .text { + line-height: 1.45; +} + +.VPDoc.has-aside .content-container, +.VPDoc:not(.has-aside) .content-container { + max-width: 860px; +} + +.vp-doc h1, +.vp-doc h2, +.vp-doc h3 { + font-family: "Segoe UI Variable Display", "Microsoft YaHei UI", sans-serif; + letter-spacing: 0; +} + +.vp-doc h1 { + font-size: 2.15rem; + line-height: 1.18; +} + +.vp-doc h2 { + margin-top: 2.5rem; + padding-top: 1rem; + border-top: 1px solid var(--vp-c-divider); +} + +.vp-doc p, +.vp-doc li { + line-height: 1.78; +} + +.vp-doc table { + display: block; + width: 100%; + overflow-x: auto; + border-collapse: collapse; + font-size: 14px; +} + +.vp-doc th { + color: var(--vp-c-text-1); + background: var(--vp-c-bg-alt); + white-space: nowrap; +} + +.vp-doc th, +.vp-doc td { + padding: 10px 12px; + vertical-align: top; +} + +.vp-doc div[class*='language-'], +.vp-doc .custom-block, +.VPDocFooter .pager-link, +.VPHomeFeatures .VPFeature { + border-radius: 6px; +} + +.vp-doc code { + border: 1px solid color-mix(in srgb, var(--vp-c-divider) 74%, transparent); +} + +.vp-doc div[class*='language-'] code { + border: 0; +} + +.VPHomeHero .name, +.VPHomeHero .text { + letter-spacing: 0; +} + +.VPHomeHero .name { + color: var(--vp-c-text-1); +} + +.VPHomeHero .text { + max-width: 760px; + font-size: 2.25rem; +} + +.VPHomeHero .tagline { + max-width: 720px; + color: var(--vp-c-text-2); +} + +.VPHomeFeatures .VPFeature { + border-color: var(--vp-c-divider); + background: var(--vp-c-bg-elv); +} + +.VPHomeFeatures .VPFeature:hover { + border-color: var(--vp-c-brand-1); +} + +.cn-doc-context { + display: flex; + align-items: center; + min-height: 32px; + margin-bottom: 18px; + color: var(--vp-c-text-2); + font-size: 13px; + font-weight: 650; +} + +.cn-doc-context__section { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 9px; + border: 1px solid color-mix(in srgb, var(--vp-c-brand-1) 42%, var(--vp-c-divider)); + border-radius: 4px; + color: var(--vp-c-brand-1); + background: var(--vp-c-brand-soft); + text-decoration: none; +} + +.cn-doc-context__divider { + width: 24px; + height: 1px; + margin: 0 8px; + background: var(--vp-c-divider); +} + +.cn-doc-context__version { + font-family: var(--vp-font-family-mono); + font-size: 12px; + color: var(--vp-c-text-3); +} + +@media (max-width: 767px) { + .vp-doc h1 { + font-size: 1.8rem; + } + + .VPHomeHero .text { + font-size: 1.85rem; + } + + .vp-doc table { + margin-right: 0; + margin-left: 0; + } + + .cn-doc-context { + margin-bottom: 14px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/docs-site/.vitepress/theme/index.ts b/docs-site/.vitepress/theme/index.ts new file mode 100644 index 000000000..462d96dcf --- /dev/null +++ b/docs-site/.vitepress/theme/index.ts @@ -0,0 +1,14 @@ +import { h } from 'vue' +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import DocContext from './DocContext.vue' +import './custom.css' + +const theme: Theme = { + extends: DefaultTheme, + Layout: () => h(DefaultTheme.Layout, null, { + 'doc-before': () => h(DocContext) + }) +} + +export default theme diff --git a/docs-site/api/index.md b/docs-site/api/index.md new file mode 100644 index 000000000..d0b48ed0e --- /dev/null +++ b/docs-site/api/index.md @@ -0,0 +1,41 @@ +# API 导航 + +API 文档按“调用端 + 业务域”分类。先确认认证域,再进入对应接口分组;需要完整路由、示例和错误语义时使用参考索引。 + +## 调用前先确认 + +| 项 | 用户端 | 管理端 | +| --- | --- | --- | +| 路径前缀 | `/api/user/**` | `/api/admin/**` | +| 登录工具 | `StpUserUtil` | `StpAdminUtil` | +| 主要调用方 | 用户端 Web / Electron | 管理端 Web / Electron | +| 权限重点 | 登录态、资源归属 | 登录态、角色与 `@RequireAdmin` | + +认证细节见 [API 认证指南](/guide/api-auth),统一响应和错误码见 [响应体与错误码](/reference/response-errors)。 + +## 用户端 API + +| 分组 | 内容 | +| --- | --- | +| [账号与系统](/api/account-system) | 注册登录、用户资料、系统公共接口 | +| [学习成长](/api/learning-growth) | 题库、计划、小组、闪卡、模拟面试和学习资产 | +| [OJ 判题](/api/oj) | 题目、提交、判题、赛事和排行榜 | +| [内容社区](/api/content-community) | 社区、动态、博客、代码工坊和评论互动 | +| [平台能力](/api/platform) | 文件、通知、聊天、积分和简历 | +| [工具与运营](/api/tools-operations) | 开发工具、摸鱼、版本和公共运营数据 | + +## 管理端 API + +| 分组 | 内容 | +| --- | --- | +| [积分与抽奖](/api/admin-points-lottery) | 规则、活动、奖品、风控和统计 | +| [内容社区](/api/admin-content-community) | 社区、动态、博客和内容审核 | +| [平台能力](/api/admin-platform) | 用户、文件、通知、敏感词和系统运营 | +| [AI Runtime](/api/admin-ai-runtime) | 模型配置、Prompt、RAG、Schema 和回归治理 | + +## 常用参考 + +- [API 路由索引](/reference/api-routes):按 Controller 和路径查询。 +- [API 调用示例](/reference/api-examples):查看请求头、分页和错误处理。 +- [权限注解与角色边界](/reference/permission-boundaries):判断接口属于公开、用户态还是管理态。 +- [WebSocket 协议](/reference/websocket):查看聊天室握手、票据和消息类型。 diff --git a/docs-site/architecture/backend-modules.md b/docs-site/architecture/backend-modules.md index c1d865615..9d77842b6 100644 --- a/docs-site/architecture/backend-modules.md +++ b/docs-site/architecture/backend-modules.md @@ -1,6 +1,6 @@ # 后端模块详解 -Code-Nest 后端由 24 个 Maven 子模块组成,按职责分为**基础设施模块**、**业务模块**和**API 契约模块**三类。 +Code-Nest 后端由 28 个 Maven 子模块组成,按职责分为**基础设施模块**、**业务模块**、**API 契约模块**和**启动聚合模块**四类。 ## 模块分类与依赖方向 @@ -149,8 +149,8 @@ com.xiaou.sensitive.api | Controller | ChatUserController, ChatAdminController | | 核心功能 | 一对一/群组聊天、消息收发、历史记录、消息撤回、WebSocket 实时推送 | | 包结构 | `controller/`, `service/`, `dto/`, `domain/`, `mapper/`, `config/`, `websocket/` | -| 特色 | STOMP + WebSocket 实时通信,聊天记录分页查询 | -| 关键配置 | WebSocketConfig (STOMP 端点 `/ws`) | +| 特色 | 原生 JSON WebSocket、一次性票据、心跳和聊天记录分页查询 | +| 关键配置 | `WebSocketConfig` 注册 `/ws/chat`,`SaTokenWebSocketInterceptor` 消费短期票据 | #### xiaou-plan — 学习计划 @@ -296,7 +296,7 @@ com.xiaou.{module} | 管理认证 | `/api/auth` | `/auth` | 无 | 管理员登录 | | 验证码 | `/api/captcha` | `/captcha` | 无 | 图形验证码 | | 平台公共能力 | `/api/file`、`/api/notification`、`/api/version` | `/file`、`/notification`、`/version` | 按方法判断 | 上传需登录,读取可匿名 | -| WebSocket | `/ws` | — | STOMP | 聊天实时通信 | +| WebSocket | `/ws/chat` | — | 原生 WebSocket JSON 消息 | 聊天实时通信 | > 各模块的详细端点列表见 [API 路由索引](/reference/api-routes)。下表只列每个模块的主前缀。 @@ -334,7 +334,7 @@ CodeNestApplication.java (@SpringBootApplication) | 路径 | 说明 | |------|------| -| `pom.xml` | 根 POM,定义 24 个子模块 | +| `pom.xml` | 根 POM,定义 28 个子模块 | | `xiaou-application/pom.xml` | 聚合所有模块的依赖 | | `xiaou-application/.../CodeNestApplication.java` | Spring Boot 启动类 | | `xiaou-application/.../application.yml` | 主配置 (端口/MyBatis/Sa-Token) | diff --git a/docs-site/architecture/database.md b/docs-site/architecture/database.md index c2f50b1f8..cac06d724 100644 --- a/docs-site/architecture/database.md +++ b/docs-site/architecture/database.md @@ -1,6 +1,6 @@ # 数据库设计 -Code-Nest 使用单一 MySQL 数据库 `code_nest`,当前基线包含 **136 张表**,加上版本增量脚本后共 **142 张表**,采用"基线 + 版本增量"的脚本组织方式。 +Code-Nest 使用单一 MySQL 数据库 `code_nest`。当前 `sql/MySql/code_nest.sql` 已合并为 **145 张表**的完整结构基线;`sql/v*` 下的版本脚本用于旧环境升级和演进追溯,不需要在全新数据库上重复执行。 ## 设计原则 @@ -28,7 +28,7 @@ Code-Nest 使用单一 MySQL 数据库 `code_nest`,当前基线包含 **136 | `file_` | xiaou-filestorage | 5 | 文件信息/存储/访问/迁移/系统设置 | | `flashcard` | xiaou-flashcard | 9 | 闪卡/卡组/学习记录/复习日志/统计 | | `interview_` | xiaou-interview | 8 | 面试题/分类/收藏/掌握度/题单/学习记录 | -| `job_battle_` | xiaou-mock-interview | 1 | 求职对战 | +| `job_battle_` | xiaou-mock-interview | 2 | 求职计划与岗位匹配分析 | | `knowledge_` | xiaou-knowledge | 2 | 知识图谱节点/关系 | | `lottery_` | xiaou-points | 4 | 抽奖配置/记录/调整历史/统计 | | `mock_interview_` | xiaou-mock-interview | 4 | 模拟面试/方向/QA/统计 | @@ -41,7 +41,7 @@ Code-Nest 使用单一 MySQL 数据库 `code_nest`,当前基线包含 **136 | `sql_monitor_` | xiaou-sql-optimizer | 1 | SQL 监控日志 | | `storage_` | xiaou-filestorage | 1 | 存储配置 | | `study_team_` | xiaou-team | 10 | 小组/成员/签到/讨论/任务/申请/统计 | -| `sys_` | xiaou-system | 7 | 管理员/角色/权限/日志 | +| `sys_` | xiaou-system | 9 | 管理员、角色、权限、日志和智能体审计 | | `user_` | xiaou-user/points/plan | 10 | 用户信息/积分/计划/签到/抽奖限制/日历 | | `growth_autopilot_` | xiaou-plan | 3 | 成长自动导航目标/任务/事件 | | `learning_cockpit_` | xiaou-application | 1 | 学习驾驶舱排名快照 | @@ -243,7 +243,7 @@ Redis 使用多个 database 索引隔离不同数据: ```text sql/ ├── MySql/ -│ ├── code_nest.sql ← 完整结构基线 (136 表) +│ ├── code_nest.sql ← 当前完整结构基线 (145 表) │ └── code_nest_data.sql ← 初始化数据 ├── v1.2.0/ ← 初始版本 ├── v1.2.1/ ← 敏感词/知识图谱增量 @@ -298,6 +298,6 @@ sql/ | --- | --- | | [架构总览](/architecture/overview) | 整体架构和部署拓扑 | | [后端模块](/architecture/backend-modules) | 后端 Maven 子模块详解 | -| [数据表索引](/reference/database-tables) | 全部 142 张表清单 | +| [数据表索引](/reference/database-tables) | 当前 145 张表清单 | | [数据库字段阅读指南](/reference/database-field-guide) | 命名规范和类型说明 | | [模块总览](/modules/) | 各模块的数据表分布 | diff --git a/docs-site/architecture/frontend-apps.md b/docs-site/architecture/frontend-apps.md index d7e8ee092..c099c652f 100644 --- a/docs-site/architecture/frontend-apps.md +++ b/docs-site/architecture/frontend-apps.md @@ -138,7 +138,7 @@ server: { |------|------|----------| | AI 对话 | `/ai` | 流式响应 (SSE)、Markdown 渲染 | | 代码工坊 | `/codepen` | CodeMirror 6 编辑器、多语言切换 | -| 聊天室 | `/chat` | WebSocket + STOMP、乐观消息、心跳 | +| 聊天室 | `/chat` | 原生 JSON WebSocket、一次性票据、乐观消息、心跳 | | OJ 判题 | `/oj` | 代码提交、实时状态轮询 | | 学习小组 | `/team` | 签到日历、讨论区、任务板 | | 抽奖 | `/lottery` | 转盘动画、概率展示 | diff --git a/docs-site/architecture/index.md b/docs-site/architecture/index.md new file mode 100644 index 000000000..fabd000bf --- /dev/null +++ b/docs-site/architecture/index.md @@ -0,0 +1,28 @@ +# 架构导航 + +架构文档回答四个问题:系统如何部署、后端如何拆分、前端如何组织、数据如何演进。模块内部细节放在 [模块总览](/modules/),接口明细放在 [API 导航](/api/)。 + +## 架构阅读顺序 + +1. [整体架构](/architecture/overview):理解单体模块化边界、运行形态和基础设施。 +2. [后端模块](/architecture/backend-modules):查看 28 个 Maven 子模块的职责与依赖。 +3. [前端应用](/architecture/frontend-apps):理解用户端、管理端、共享设计系统和 Electron 构建。 +4. [数据库与脚本](/architecture/database):理解主库基线、版本脚本和迁移原则。 + +## 按问题定位 + +| 问题 | 先看 | 再看 | +| --- | --- | --- | +| 一个请求经过哪些层 | [整体架构](/architecture/overview) | [源码地图](/reference/source-map) | +| 修改公共模块会影响谁 | [后端模块](/architecture/backend-modules) | [模块依赖地图](/reference/module-dependencies) | +| 双前端如何共享组件 | [前端应用](/architecture/frontend-apps) | [前端路由索引](/reference/frontend-routes) | +| 新表应该放在哪里 | [数据库与脚本](/architecture/database) | [数据表索引](/reference/database-tables) | +| AI、WebSocket 或缓存的边界 | [整体架构](/architecture/overview) | 对应模块页和协议参考 | + +## 架构约束 + +- 后端保持单体模块化部署,由 `xiaou-application` 聚合启动。 +- 模块间通过明确依赖和 API 契约协作,避免循环依赖。 +- 用户端与管理端共享设计系统,但保留独立路由、权限和构建产物。 +- 数据库以 `sql/MySql/code_nest.sql` 为当前基线,增量脚本用于版本演进。 +- 基础设施失败策略由调用方明确决定,不由通用工具静默吞错。 diff --git a/docs-site/architecture/overview.md b/docs-site/architecture/overview.md index 257eb78e9..8c2375590 100644 --- a/docs-site/architecture/overview.md +++ b/docs-site/architecture/overview.md @@ -1,6 +1,6 @@ # 整体架构 -Code-Nest 是一个基于 Spring Boot 3.4.4 + Vue 3 的全栈学习成长平台,采用**单体模块化架构**(Modular Monolith),将业务拆分为 24 个独立 Maven 子模块,最终由 `xiaou-application` 聚合启动为一个 JAR。 +Code-Nest 是一个基于 Spring Boot 3.4.4 + Vue 3 的全栈学习成长平台,采用**单体模块化架构**(Modular Monolith)。根 POM 声明 28 个 Maven 子模块,最终由 `xiaou-application` 聚合启动为一个 JAR。 ## 技术栈总览 @@ -9,12 +9,12 @@ Code-Nest 是一个基于 Spring Boot 3.4.4 + Vue 3 的全栈学习成长平台 | 运行时 | Java | 17 | | 后端框架 | Spring Boot | 3.4.4 | | ORM | MyBatis-Plus | 3.5.x | -| 数据库 | MySQL | 8.0+,共 142 张表 | +| 数据库 | MySQL | 8.0+,当前主库基线 145 张表 | | 缓存 | Redis | 7.x + Redisson | | 鉴权 | Sa-Token | 多端鉴权 (User + Admin) | -| AI 接入 | Spring AI + LangGraph4j | Prompt 编排 / RAG / 图执行 | +| AI 接入 | LangChain4j + LangGraph4j + LlamaIndex | Prompt、图执行、RAG 和结构化输出 | | 文件存储 | 本地磁盘 / S3 兼容 | 可切换 | -| 实时通信 | Spring WebSocket | STOMP 协议 | +| 实时通信 | Spring WebSocket | 原生 JSON 消息协议 + 一次性票据 | | 前端 (用户端) | Vue 3 + Vite | Element Plus | | 前端 (管理端) | Vue 3 + Vite | Element Plus | | 文档站 | VitePress | docs-site/ | @@ -93,10 +93,10 @@ java -jar code-nest.jar --spring.profiles.active=prod |------|------| | 部署方式 | 单 JAR + 外置配置 | | 前端 | Nginx 托管静态资源,反向代理后端 API | -| 数据库 | 单 MySQL 实例,共 142 张表 | +| 数据库 | 单 MySQL 实例,当前主库基线 145 张表 | | 缓存 | Redis db3(业务缓存 + Redisson)+ db4(Sa-Token 会话) | | 文件 | 本地磁盘存储(默认),可切换 S3 / MinIO | -| WebSocket | 与 HTTP 共用 9999 端口,STOMP 协议,Nginx 需单独配置 Upgrade 代理 | +| WebSocket | 与 HTTP 共用 9999 端口,端点 `/ws/chat`,Nginx 需配置 Upgrade 代理 | ## 双端鉴权架构 @@ -245,7 +245,7 @@ xiaou-application (聚合) ## Maven 模块清单 -Code-Nest 当前包含 24 个 Maven 子模块,最终由 `xiaou-application` 聚合启动: +Code-Nest 当前包含 28 个 Maven 子模块,最终由 `xiaou-application` 聚合启动: | 分组 | 模块 | 说明 | |------|------|------| @@ -348,7 +348,7 @@ curl http://localhost:9999/api/actuator/prometheus | 单体 vs 微服务 | 模块化单体 | 团队规模小,模块化足以隔离,避免分布式复杂度 | | 多端鉴权 | Sa-Token 多 Stp | 用户端与管理端完全隔离,Token 不互通,Redis db4 统一存储 | | ORM 选择 | MyBatis-Plus | 灵活 SQL + 代码生成,适合复杂查询场景 | -| AI 接入 | Spring AI + LangGraph4j | Prompt 编排 + RAG + 图执行,支持复杂 AI 流程 | +| AI 接入 | LangChain4j + LangGraph4j + LlamaIndex | Prompt 编排 + RAG + 图执行,支持复杂 AI 流程 | | AI 降级 | 自动降级 | AI 不可用时返回默认提示,不影响其他业务 | | 文件存储 | 抽象接口 + 本地默认 | 最小依赖,可扩展 S3/OSS/MinIO | | API 模块 | 接口与实现分离 | 防止循环依赖,明确模块间契约 | diff --git a/docs-site/guide/changelog.md b/docs-site/guide/changelog.md index 3f7496ace..71a8b67f6 100644 --- a/docs-site/guide/changelog.md +++ b/docs-site/guide/changelog.md @@ -12,6 +12,9 @@ | 版本 | 日期 | 主要变更 | |------|------|---------| +| v2.4.1(开发中) | 2026-07-14 | 共享设计系统、缓存与线程池治理、热点链路优化、文档中心重构 | +| v2.4.0 | 2026-07-14 | 管理员 Agent 统一运行时、权限审计与全站质量门禁 | +| v2.3.2 | 2026-06-14 | 版本分页与用户端首页首屏修复 | | v2.3.1 | 2026-06-14 | 统一 CI、生产自动部署、服务器备份与回滚 | | v2.3.0 | 2026-06-14 | 双端 UI 壳层优化、聊天室连接修复、面试分类管理修复 | | v2.2.2 | 2026-05-27 | GitHub 协作治理文档、文档站完善 | @@ -23,6 +26,68 @@ --- +## v2.4.1(开发中,2026-07-14) + +### Added + +- 新增独立的 `code-nest-design-system` 包,统一承载用户端和管理端共享组件、主题、设计令牌与组合式函数 +- 新增 `RedisValueStore` 和 Spring 托管的 `applicationIoExecutor`,为缓存值、计数器和通用 I/O 异步任务提供明确边界 +- 为学习驾驶舱、WebSocket 票据、Redis 值存储和朋友圈核心服务补充聚焦回归测试 +- 重构 VitePress 文档中心,新增分类首页、工程手册主题、文档上下文标识、自动同步基线和导航完整性审计 + +### Changed + +- 双前端改为复用同一份设计系统源码,移除两套长期漂移的重复实现 +- 学习驾驶舱、社区热点、朋友圈、摸鱼工具、抽奖和验证码等热点链路收敛缓存访问、批量查询与并发执行方式 +- 删除静态 `RedisUtil`、`ThreadPoolUtils` 和 `ConcurrentUtils`,改用依赖注入、模块专用执行器以及领域库原生能力 +- 文档导航重组为“开始、架构、模块、API、运维、资料”,历史路线图与现行维护入口分离 + +### Notes + +- `v2.4.1` 当前是开发分支,最近正式发布 tag 仍为 `v2.4.0` +- 文档站构建前会刷新 [文档同步基线](/reference/docs-sync-baseline) 并运行文档审计 + +--- + +## v2.4.0 (2026-07-14) + +### Added + +- 新增管理员端统一自然语言入口和后端 Agent Runtime,覆盖规划、工具注册、权限策略、预览、确认、执行与审计 +- 新增 26 个生产工具的统一 Registry、会话上下文、幂等控制、工具指标、readiness、dry-run 和恢复分析能力 +- 新增分层全站评测脚本和 CI 门禁,覆盖后端、双前端、AI、RAG、仓库卫生与发布构建 + +### Changed + +- 管理端只保留聊天抽屉、结果展示和确认交互,规划、权限和执行职责统一收口到后端 +- AI completion 增加全局预算和场景级覆盖能力,管理员 planner 使用独立输出预算 +- 后端 Maven、双前端和文档站发布版本统一到 `v2.4.0` + +### Fixed + +- 修复本地文件路径越界、聊天消息契约、抽奖风控和 OJ 判题边界问题 + +### Security + +- Agent 写操作必须经过后端权限策略、精确确认、审计状态机与幂等校验 +- 会话上下文按管理员隔离,真实 AI 密钥只通过运行环境注入 + +--- + +## v2.3.2 (2026-06-14) + +### Fixed + +- 修复版本时间轴与管理端版本列表分页总数不准确的问题 +- 修复用户端首页首屏因 reveal 状态未解除而大面积空白的问题 + +### Changed + +- 根据 Git 提交和版本分支重建线上版本历史 +- 后端 Maven、双前端和文档站发布版本统一到 `v2.3.2` + +--- + ## v2.3.1 (2026-06-14) ### Added diff --git a/docs-site/guide/ci-cd-production.md b/docs-site/guide/ci-cd-production.md index fc256b201..9508c1830 100644 --- a/docs-site/guide/ci-cd-production.md +++ b/docs-site/guide/ci-cd-production.md @@ -61,15 +61,15 @@ CODE_NEST_HEALTH_URL=http://127.0.0.1:9999/api/actuator/health 推送 `v*` 分支或 tag 会触发 `Deploy Production`: ```bash -git checkout -b v2.3.1 -git push -u origin v2.3.1 +git checkout -b vX.Y.Z +git push -u origin vX.Y.Z ``` 也可以在合并后创建 tag: ```bash -git tag -a v2.3.1 -m "Code Nest v2.3.1" -git push origin v2.3.1 +git tag -a vX.Y.Z -m "Code Nest vX.Y.Z" +git push origin vX.Y.Z ``` ### 手动部署指定 ref @@ -92,7 +92,7 @@ python scripts/deploy-production.py \ --host 36.140.150.167 \ --user root \ --identity ~/.ssh/code_nest_github_actions_ed25519 \ - --version v2.3.1 + --version vX.Y.Z ``` 如果已经提前构建过后端 Jar 和两个前端 `dist`,可以跳过构建: @@ -102,7 +102,7 @@ python scripts/deploy-production.py \ --host 36.140.150.167 \ --user root \ --identity ~/.ssh/code_nest_github_actions_ed25519 \ - --version v2.3.1 \ + --version vX.Y.Z \ --skip-build ``` diff --git a/docs-site/guide/documentation-maintenance.md b/docs-site/guide/documentation-maintenance.md index 267b8de5b..c1e78da04 100644 --- a/docs-site/guide/documentation-maintenance.md +++ b/docs-site/guide/documentation-maintenance.md @@ -1,215 +1,124 @@ # 文档维护规范 -本页说明 Code Nest 文档站(`docs-site`)的日常维护流程,包括目录结构、各类型页面的定位和更新原则、校验方法,以及变更时必须同步检查的关联页面。 - -文档站的目标是让新人快速定位代码入口、让开发者在改功能时顺便补文档、让页面之间的导航链路不会断。 - -## 目录结构 - -```text -docs-site/ -├── .vitepress/ -│ └── config.mts # 侧栏和导航配置 -├── guide/ # 使用和流程指南 -│ ├── quick-start.md # 新人快速开始 -│ ├── local-dev.md # 本地开发环境搭建 -│ ├── deploy.md # 独立部署 -│ ├── documentation-maintenance.md # 本页 -│ ├── learning-paths.md # 学习路径推荐 -│ ├── onboarding-tasks.md # 入职打卡任务 -│ ├── feature-development.md # 功能开发流程 -│ ├── release-verification.md # 发布前验证 -│ ├── testing-regression.md # 测试回归 -│ └── git-log-release-notes.md # Git 日志和发布说明 -├── modules/ # 按功能拆分的模块教程 -│ ├── index.md # 模块总览与导航 -│ ├── auth.md # 鉴权 -│ ├── blog.md # 博客 -│ ├── chat.md # 聊天 -│ ├── codepen.md # 代码工坊 -│ ├── community-content.md # 社区内容矩阵 -│ ├── community.md # 社区互动 -│ ├── dashboard-logs.md # 仪表盘与日志 -│ ├── dev-tools.md # 开发工具 -│ ├── interview-and-growth.md # 面试与成长 -│ ├── interview.md # 面试题库 -│ ├── flashcard.md # 闪卡 -│ ├── knowledge.md # 知识库 -│ ├── learning-assets.md # 学习资产 -│ ├── mock-interview-job-battle.md # 模拟面试与求职 -│ ├── moments.md # 动态 -│ ├── moyu.md # 摸鱼 -│ ├── notification.md # 通知 -│ ├── oj.md # OJ 判题 -│ ├── plan-team.md # 计划与学习小组 -│ ├── points.md # 积分与抽奖 -│ ├── resume.md # 简历 -│ ├── sensitive.md # 敏感词风控 -│ ├── sql-optimizer.md # SQL 优化 -│ ├── system-ops.md # 系统运营后台 -│ ├── tools-moyu-version.md # 开发工具/摸鱼/版本 -│ ├── user-account.md # 用户账户 -│ ├── file-storage.md # 文件存储 -│ └── ai-runtime.md # AI Runtime -├── reference/ # 可查询的索引和参考 -│ ├── api-routes.md # API 路由 -│ ├── database-tables.md # 数据表 -│ ├── frontend-routes.md # 前端路由 -│ ├── source-map.md # 源码地图 -│ ├── ai-schemas.md # AI Schema -│ ├── websocket.md # WebSocket 协议 -│ ├── response-errors.md # 响应码与错误 -│ ├── permission-boundaries.md # 权限边界 -│ ├── feature-coverage.md # 功能覆盖矩阵 -│ ├── module-regression-matrix.md # 回归验证矩阵 -│ ├── module-state-machines.md # 状态机索引 -│ ├── statistics-ranking-counts.md # 统计与排名口径 -│ ├── docs-sync-baseline.md # 文档同步基线 -│ ├── frontend-rendering-security.md # 前端渲染安全 -│ └── glossary.md # 术语表 -├── architecture/ # 架构总览 -│ ├── overview.md -│ ├── backend-modules.md -│ ├── frontend-apps.md -│ └── database.md -├── operations/ # 运维操作 -│ ├── incident-response.md -│ └── troubleshooting.md -├── roadmap/ # 版本计划 -└── index.md # 首页 -``` +Code Nest 的文档分为两层:`docs-site` 是与当前代码同步的工程文档,`AI-DOCS` 保存 PRD、专题材料、测试证据和历史归档。新增内容前先判断它属于哪一层,避免同一事实维护两份。 -## 页面类型和定位 +## 文档分类 -| 类型 | 目录 | 写给谁看 | 更新时机 | +| 分类 | 目录 | 主要读者 | 写什么 | | --- | --- | --- | --- | -| 指南页 | `guide/` | 新人或操作者 | 流程变更时 | -| 模块页 | `modules/` | 开发者 | 功能新增或修改时 | -| 参考页 | `reference/` | 查表用 | API/路由/表结构变更时 | -| 架构页 | `architecture/` | 架构师或新人 | 大范围重构时 | -| 运维页 | `operations/` | 值班和运维 | 故障复盘或流程调整时 | -| 路线图 | `roadmap/` | 团队 | 版本迭代时 | +| 开始与开发 | `guide/` | 新人、开发者、发布负责人 | 环境、流程、学习、测试、发布和协作规范 | +| 系统架构 | `architecture/` | 维护者、架构评审者 | 系统边界、模块拆分、前端结构、数据库演进 | +| 功能模块 | `modules/` | 模块负责人 | 定位、源码入口、流程、数据、风险和验证 | +| API | `api/` | 前后端联调人员 | 按调用端和业务域组织的接口说明 | +| 操作手册 | `manuals/` | 用户、运营、验收人员 | 实际入口、操作步骤、预期结果和失败处理 | +| 部署运维 | `operations/` | 运维、值班人员 | 部署、配置、观测、告警、事故和排障 | +| 参考索引 | `reference/` | 需要快速查事实的人 | 路由、表、源码、协议、状态、权限和质量矩阵 | +| 版本历史 | `roadmap/` | 需要追溯的人 | 已完成的文档建设计划,不承载当前待办 | -## 变更检查矩阵 +每个一级目录必须有 `index.md`,负责说明这一类文档解决什么问题、读者如何选择页面。 -当你改了一类页面,要同步检查的关联页面: +## docs-site 与 AI-DOCS 的边界 -| 改动位置 | 必须检查 | +| 内容 | 放置位置 | | --- | --- | -| 模块页 | 源码地图、API 路由、前端路由、数据库表、术语表、模块总览 | -| 新增 API | API 路由、权限边界、对应模块页 | -| 新增前端路由 | 前端路由、对应模块页 | -| 新增数据表 | 数据库表、对应模块页 | -| 新增状态值 | 术语表、状态机索引 | -| 新增权限 | 权限边界、鉴权模块页 | -| 修改部署配置 | 部署指南、本地开发 | -| 修改 WebSocket 协议 | WebSocket 协议页、聊天模块页 | -| 修改 AI Schema | AI Schema 页、AI Runtime 模块页 | -| 新增 v-html 使用 | 前端渲染安全 | +| 当前代码的架构、接口、模块和运维事实 | `docs-site` | +| 产品需求、技术专题、测试报告和交付材料 | `AI-DOCS` 对应分类 | +| 已废弃方案、旧版本副本和历史快照 | `AI-DOCS/Archive` | +| 项目简介和最短启动路径 | 根 `README.md` | -## VitePress 构建和校验 +如果 `AI-DOCS` 中的专题已经影响当前开发流程,应在 `docs-site` 写一份可执行的现行说明,并链接回原始材料;不要直接把历史长文挂到主导航。 -### 构建 +## 页面结构 -```bash -cd docs-site -npm ci -npm run build -``` +### 模块页 -构建会检查: +推荐顺序: -1. Markdown 链接是否存在目标文件。 -2. Vue 模板语法是否正确。 -3. 侧栏配置中引用的页面是否存在。 +1. 功能定位。 +2. 用户端、管理端和后端入口。 +3. 推荐阅读顺序。 +4. 源码地图。 +5. 接口与权限边界。 +6. 核心流程和状态。 +7. 主要数据表。 +8. 常见问题和失败态。 +9. 验证清单。 +10. 相关模块。 -### 常见构建错误 +### 指南页 -| 错误 | 原因 | 修复 | -| --- | --- | --- | -| 死链接 | 链接指向不存在的页或标题 | 检查路径和文件名 | -| Vue 模板编译失败 | 正文里有未转义尖括号 | 用 `<` 和 `>` 替换 | -| 侧栏找不到文件 | config.mts 引用的路径与实际文件不一致 | 对齐路径 | +指南必须围绕一个可完成的任务组织,优先使用“前置条件 → 操作步骤 → 验证结果 → 失败处理”。不要把多个目标堆进同一页。 -### 尖括号转义 +### 参考页 -VitePress 用 Vue 编译 Markdown,尖括号会被当作 HTML 标签。以下内容必须转义: +参考页强调可查询性,优先使用稳定字段的表格。背景解释保持简短,复杂原因链接到架构或模块页。 -| 场景 | 错误写法 | 正确写法 | -| --- | --- | --- | -| Java 泛型 | `List<String>` | `List<String>` | -| 路由参数 | `/dev-tools/<toolName>` | `/dev-tools/<toolName>` | -| HTML 标签提及 | `<script>` | 用中文"脚本标签"或 `<script>` | -| Vue 组件名 | `<KeepAlive>` | `<KeepAlive>` | +### 操作手册 -### localhost 链接 +从使用者可见的入口和动作开始,不要求读者理解 Controller、Mapper 或内部类名。技术排查链接到运维和模块页。 -VitePress 会把 Markdown 代码块中的 localhost URL 也当作链接去验证。如果写示例命令,用环境变量替代: +## 事实来源 -| 不推荐 | 推荐 | +| 事实 | 权威来源 | | --- | --- | -| `curl http://localhost:9999/api/health` | `curl ${BACKEND_HOST}/api/health`,再在注释中说明默认值 | - -## 写作约定 - -### 文件名和标题 - -- 文件名使用 kebab-case,例如 `plan-team.md`。 -- 页面标题用中文,首行 `#` 后面直接跟标题,不加编号。 - -### 模块页模板 - -每个模块页建议包含以下段落: +| Maven 版本与模块数 | 根 `pom.xml` | +| API 路由 | Controller 注解和请求前缀 | +| 前端路由 | 双前端 Router 配置 | +| 数据表和字段 | `sql/MySql/code_nest.sql` 与版本 SQL | +| 环境变量 | `application*.yml`、`.env*` 和 `@Value` 注入点 | +| 构建命令 | `package.json`、根脚本和 CI 工作流 | +| 当前提交 | [文档同步基线](/reference/docs-sync-baseline) | -1. **定位** — 一段话说清模块做什么、给谁用。 -2. **源码入口** — 控制器、服务、Mapper、前端组件的路径表。 -3. **API 一览** — 用户端和管理端的主要接口。 -4. **核心流程** — 关键业务的状态流转或调用链。 -5. **数据表** — 主要表名和字段概要。 -6. **常见坑** — 开发时容易踩的雷。 -7. **验证清单** — 改完代码后怎么验证。 +不要从旧文档复制数字后直接更新。模块数、表数、端点数和版本号必须回到源码重新统计。 -### 表格约定 - -- 状态值表格至少列出 code 和含义。 -- API 表格至少列出方法、路径和用途。 -- 状态机图用文本表格或缩进列表,不用 Mermaid。 - -### 交叉链接 - -- 模块页之间用相对路径,例如 `[鉴权](/modules/auth)`。 -- 同一页面的标题链接用 `[标题](#标题锚点)`。 -- 不使用绝对 URL,避免 VitePress 死链接检查误报。 - -## 同步脚本 +## 写作约定 -`sync:baseline` 脚本在 dev/build/preview 前自动执行,它会刷新: +- 文件名使用 kebab-case,页面首个一级标题使用中文任务名或模块名。 +- 一段只表达一个结论,避免“完整、全面、强大”等无证据形容词。 +- 命令、路径、配置键和类名使用行内代码。 +- API 表至少包含方法、路径、认证域和用途。 +- 状态或统计表必须说明口径和空值行为。 +- 历史内容明确标记版本和状态,不能伪装成当前推荐做法。 +- 图片必须有能说明信息的替代文本,不使用“图片”作为 alt。 +- 超过 1200 行的页面应考虑按业务域拆分。 -- 源码地图中的文件列表 -- API 路由索引 -- 前端路由索引 -- 数据库表索引 +## 变更检查矩阵 -如果脚本执行失败,构建也会失败。常见原因是 Java 源码路径变更后脚本中的 glob 模式没有更新。 +| 代码改动 | 同步检查 | +| --- | --- | +| 新增或删除 Maven 模块 | 架构总览、后端模块、模块总览、源码地图 | +| 新增 API | API 分组、路由索引、权限边界、对应模块页 | +| 新增前端页面 | 前端应用、前端路由索引、操作手册 | +| 新增表或字段 | 数据库架构、数据表索引、字段指南、模块页 | +| 修改 WebSocket | 协议页、聊天模块、失败路径和前端调用说明 | +| 修改 AI Schema | AI Runtime、Schema 索引、回归矩阵 | +| 修改缓存或计数语义 | 统计口径、幂等补偿、对应模块页 | +| 修改部署配置 | 环境变量、Docker、独立部署、启动剧本 | +| 修改核心用户流程 | 操作手册、核心链路教程、验证记录 | + +## 校验命令 -## 新增页面检查清单 +```bash +cd docs-site +npm run audit +npm run build +``` -当你新增一个文档页面时: +构建前会自动刷新同步基线并执行文档审计。审计失败时先修导航覆盖和已知旧事实,再处理 VitePress 编译错误。 -1. 确认页面放在正确的目录下(guide/modules/reference/architecture/operations)。 -2. 在 `.vitepress/config.mts` 的侧栏配置中添加条目。 -3. 在 `modules/index.md` 的总览表或对应索引页添加一行。 -4. 构建通过且无死链接。 -5. 搜索相关术语表,确保新术语已收录。 -6. 如果涉及新 API、新路由或新表,同步更新对应的参考页。 -7. 如果涉及新状态值,同步更新术语表的状态速查。 +## 新增页面清单 +1. 放入正确分类目录。 +2. 添加一个明确的 `#` 标题和开场定位。 +3. 在 `.vitepress/config.mts` 中加入侧栏。 +4. 在对应分类的 `index.md` 增加入口。 +5. 补充必要的跨页链接。 +6. 运行 `npm run audit` 和 `npm run build`。 +7. 在桌面和移动端检查导航、表格和代码块。 ## 相关文档 -| 文档 | 说明 | -| --- | --- | -| [文档同步基线](/reference/docs-sync-baseline) | 文档版本追踪 | -| [Git Log 版更新记录](/guide/git-log-release-notes) | 更新日志 | -| [版本公告与交接模板](/guide/version-release-handoff-template) | 版本交接 | -| [功能开发流程](/guide/feature-development) | 开发流程 | +- [文档同步基线](/reference/docs-sync-baseline) +- [功能开发流程](/guide/feature-development) +- [测试与回归](/guide/testing-regression) +- [版本公告与交接模板](/guide/version-release-handoff-template) diff --git a/docs-site/guide/git-log-release-notes.md b/docs-site/guide/git-log-release-notes.md index 20172f091..4316926de 100644 --- a/docs-site/guide/git-log-release-notes.md +++ b/docs-site/guide/git-log-release-notes.md @@ -166,7 +166,7 @@ git log --oneline --decorate --grep="docs: update" - [版本历史](/modules/version-history):讲已经整理好的主版本演进和产品内版本墙实现。 - [版本公告与发版交接模板](/guide/version-release-handoff-template):讲怎么把证据落成可交付文本。 - [版本公告与交接实战样例](/guide/version-release-worked-examples):讲真实版本是怎么一步步写出来的。 -- [v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan):讲当前阶段还在补什么。 +- [路线图归档](/roadmap/):追溯文档中心从 v2.2.0 到 v2.2.1 的建设过程。 - [文档同步基线](/reference/docs-sync-baseline):讲当前文档和代码同步到哪个提交。 可以把职责简单理解成: diff --git a/docs-site/guide/index.md b/docs-site/guide/index.md new file mode 100644 index 000000000..f59d45bd4 --- /dev/null +++ b/docs-site/guide/index.md @@ -0,0 +1,46 @@ +# 开始与开发指南 + +这里按任务组织文档。第一次接触项目先完成环境和启动;开始改代码后,再进入开发、测试、接手和发布流程。 + +## 按角色进入 + +| 角色 | 建议入口 | 目标 | +| --- | --- | --- | +| 新加入开发者 | [快速开始](/guide/quick-start) → [本地完整启动剧本](/guide/startup-playbook) | 在本机跑通后端、双前端和文档站 | +| 模块维护者 | [模块接手升级路线](/guide/module-ownership-ladder) → [模块值守与回归手册](/guide/module-owner-playbook) | 能定位代码、判断影响面并完成回归 | +| 功能开发者 | [功能开发流程](/guide/feature-development) → [测试与回归](/guide/testing-regression) | 从需求走到可验证的实现 | +| 发布负责人 | [发布前验证](/guide/release-verification) → [生产 CI/CD 自动发布](/guide/ci-cd-production) | 形成可回滚、可交接的版本 | +| 文档维护者 | [文档维护规范](/guide/documentation-maintenance) | 保证页面分类、事实和导航持续同步 | + +## 上手与环境 + +- [快速开始](/guide/quick-start):最短路径完成依赖安装和基础运行。 +- [本地开发](/guide/local-dev):解释服务依赖、数据库和常用命令。 +- [本地完整启动剧本](/guide/startup-playbook):按顺序启动整套系统。 +- [常见问题](/guide/faq):处理高频环境和构建问题。 + +## 学习与带教 + +- [学习路线](/guide/learning-paths):按目标选择阅读顺序。 +- [按模块学习路线](/guide/module-learning-paths):围绕单个业务域深入。 +- [角色上手任务包](/guide/onboarding-tasks):将阅读转成可验收任务。 +- [首个真实任务接入](/guide/first-real-task):从练习过渡到真实改动。 + +## 开发与质量 + +- [功能开发流程](/guide/feature-development) +- [常见二开场景](/guide/extension-scenarios) +- [测试与回归](/guide/testing-regression) +- [开发者规范](/guide/developer-standards) +- [权限与安全边界](/guide/security-boundaries) + +## 发布与协作 + +- [发布前验证](/guide/release-verification) +- [版本公告与交接模板](/guide/version-release-handoff-template) +- [贡献指南](/guide/contributing) +- [变更日志](/guide/changelog) + +## 下一步 + +完成环境启动后,进入 [架构导航](/architecture/) 建立系统全局认知,或直接从 [模块总览](/modules/) 选择负责的业务域。 diff --git a/docs-site/guide/learning-paths.md b/docs-site/guide/learning-paths.md index efc033ef5..22f2093cc 100644 --- a/docs-site/guide/learning-paths.md +++ b/docs-site/guide/learning-paths.md @@ -147,7 +147,7 @@ Code Nest 的文档已经按模块拆得很细,但第一次接手时不要从 | 7 | [核心链路教程](/manuals/core-workflows) | 用户视角的端到端流程 | | 8 | [端到端业务链路图](/manuals/business-flow-map) | 全局业务链路和横切能力如何串联 | | 9 | [验证记录与已知问题](/manuals/verified-scenarios) | 真实验证结果和已知缺口 | -| 10 | [v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan) | 当前阶段补什么、为什么补、接下来怎么继续 | +| 10 | [文档同步基线](/reference/docs-sync-baseline) | 当前文档与分支、提交、模块和数据库基线是否一致 | | 11 | [按 Git Log 重写版本更新记录](/guide/git-log-release-notes) | 新版版本说明如何基于提交、README 和真实证据重写 | | 12 | [版本公告与发版交接模板](/guide/version-release-handoff-template) | 把版本证据落成 README、产品公告和团队交接单 | | 13 | [版本公告与交接实战样例](/guide/version-release-worked-examples) | 看 `v2.2.0`、`v2.1.2` 是怎样从证据一路写成交付物的 | diff --git a/docs-site/guide/local-dev.md b/docs-site/guide/local-dev.md index 1639c7365..03a48d0c0 100644 --- a/docs-site/guide/local-dev.md +++ b/docs-site/guide/local-dev.md @@ -30,11 +30,12 @@ CREATE DATABASE code_nest DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ mysql -u root -p code_nest < sql/MySql/code_nest.sql ``` -基础脚本包含 136 张表,加上增量脚本后共 142 张表,是系统运行的最低要求。 +当前基础脚本已经包含 145 张表,是新环境初始化的完整结构基线。 -### 3. 导入增量脚本(按版本顺序) +### 3. 旧数据库升级时导入增量脚本 ```bash +# 仅用于从旧版本数据库升级;全新数据库跳过此步骤 # 按版本号从小到大依次执行 for ver in v1.2.0 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1 v1.8.2 v1.8.3 v1.8.4; do if [ -f "sql/$ver/$ver.sql" ]; then @@ -43,14 +44,14 @@ for ver in v1.2.0 v1.3.0 v1.4.0 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1 v1.8.2 done ``` -> **提示**:增量脚本中有些是 ALTER TABLE,如果基础脚本已包含最新结构则可能报"列已存在",可安全忽略。 +> **提示**:全新数据库只执行 `sql/MySql/code_nest.sql`。增量脚本只用于旧环境升级,重复执行可能出现列或索引已存在错误。 ### 4. 确认表数量 ```sql USE code_nest; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'code_nest'; --- 预期:142 张表左右(视版本可能有差异) +-- 预期:145 张表 ``` ## Redis 配置 diff --git a/docs-site/guide/onboarding-tasks.md b/docs-site/guide/onboarding-tasks.md index c90596010..d365186b6 100644 --- a/docs-site/guide/onboarding-tasks.md +++ b/docs-site/guide/onboarding-tasks.md @@ -185,7 +185,7 @@ Mapper: | 项目 | 内容 | | --- | --- | | 目标 | 能让新增代码、文档索引、操作手册和验证记录保持一致 | -| 先读 | [文档维护规范](/guide/documentation-maintenance)、[全功能覆盖矩阵](/reference/feature-coverage)、[核心链路教程](/manuals/core-workflows)、[v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan) | +| 先读 | [文档维护规范](/guide/documentation-maintenance)、[全功能覆盖矩阵](/reference/feature-coverage)、[核心链路教程](/manuals/core-workflows)、[文档同步基线](/reference/docs-sync-baseline) | | 推荐任务 | 任选一个模块页,检查它是否有入口、源码、API、表、流程、风险和验证清单 | | 输出 | 一份文档缺口清单,按“必须补、可以后补、无需补”分类 | diff --git a/docs-site/guide/quick-start.md b/docs-site/guide/quick-start.md index 3d6cecf4c..2cc69d021 100644 --- a/docs-site/guide/quick-start.md +++ b/docs-site/guide/quick-start.md @@ -27,7 +27,7 @@ ## 第 1 步:克隆仓库 ```powershell -git clone https://github.com/your-org/Code-Nest.git +git clone https://github.com/xiaou61/Code-Nest.git cd Code-Nest ``` @@ -35,7 +35,7 @@ cd Code-Nest ```powershell git branch -# 应该看到 * v2.3.1 或对应版本分支 +# 当前文档工作线应看到 * v2.4.1,其他版本按实际分支判断 ``` ## 第 2 步:初始化数据库 @@ -67,7 +67,7 @@ dir /s /b sql\MySql\code_nest.sql ```sql USE code_nest; SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'code_nest'; --- 预期:142 张表左右(视版本可能有差异) +-- 预期:145 张表 ``` ## 第 3 步:配置后端 diff --git a/docs-site/guide/release-verification.md b/docs-site/guide/release-verification.md index aa0c34afc..1b9a1f24e 100644 --- a/docs-site/guide/release-verification.md +++ b/docs-site/guide/release-verification.md @@ -347,7 +347,7 @@ mvn -pl xiaou-ai -am "-Dtest=AiSceneRegressionEvalTest" "-Dsurefire.failIfNoSpec ```text 验证范围:用户端聊天 ws-ticket 和消息发送 日期:2025-xx-xx -版本:v2.3.1 +版本:vX.Y.Z 已执行:用户端 build、后端 package、登录后进入 /chat、发送文本消息 通过项:ws-ticket 获取成功,WebSocket CONNECT 成功,消息 ACK 成功 未验证:多实例广播、禁言到期恢复 diff --git a/docs-site/guide/security-policy.md b/docs-site/guide/security-policy.md index ee6d7210e..db9103a42 100644 --- a/docs-site/guide/security-policy.md +++ b/docs-site/guide/security-policy.md @@ -10,9 +10,9 @@ Code Nest 涉及用户账户、内容发布、文件上传、WebSocket、AI Runt | 版本 | 支持状态 | |------|---------| -| v2.2.x | ✅ 完整支持 | -| v2.1.x | ⚠️ 仅安全修复 | -| < v2.1.0 | ❌ 不支持 | +| v2.4.x | 完整支持 | +| v2.3.x | 仅安全修复 | +| < v2.3.0 | 不支持 | ## 报告安全漏洞 diff --git a/docs-site/guide/version-release-handoff-template.md b/docs-site/guide/version-release-handoff-template.md index 2b601dd98..1dcb6a117 100644 --- a/docs-site/guide/version-release-handoff-template.md +++ b/docs-site/guide/version-release-handoff-template.md @@ -81,7 +81,7 @@ 需要同步的文档: - [版本历史](/modules/version-history) -- [当前路线图](/roadmap/v2.2.1-docs-plan) +- [路线图归档](/roadmap/) - 其他: 后续补测 / 跟进项: @@ -274,7 +274,7 @@ README 一般不必保留: - [按 Git Log 重写版本更新记录](/guide/git-log-release-notes) - [发布前验证](/guide/release-verification) - [文档维护规范](/guide/documentation-maintenance) -- [v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan) +- [路线图归档](/roadmap/) 它们的职责可以简单理解成: diff --git a/docs-site/index.md b/docs-site/index.md index c777815e5..c80d536b8 100644 --- a/docs-site/index.md +++ b/docs-site/index.md @@ -3,91 +3,95 @@ layout: home hero: name: Code Nest - text: v2.3.1 项目文档 - tagline: 面向开发者成长社区的全量开发与运维手册 — 后端 15 个模块、前端双端、文档站独立部署。 + text: v2.4.1 工程文档中心 + tagline: 按角色、任务和系统边界组织的开发、接口、操作与运维手册。 image: src: /code-nest-mark.svg - alt: Code Nest + alt: Code Nest 标志 actions: - theme: brand - text: 快速开始 - link: /guide/quick-start + text: 开始使用 + link: /guide/ - theme: alt - text: 架构总览 - link: /architecture/overview + text: 系统架构 + link: /architecture/ - theme: alt - text: 查看模块 - link: /modules/ + text: 运维入口 + link: /operations/ features: - - title: 学习与成长 - details: 面试题库 + 掌握度追踪、计划打卡、学习小组(排行/贡献/讨论)、闪卡、知识图谱、成长自动驾驶 - - title: OJ 判题 - details: 多语言支持、go-judge 沙箱隔离、ACM/IOI 赛制、竞赛排名、首次 AC 积分奖励 - - title: AI Runtime - details: 结构化 AI 对话、RAG 知识库增强、多模型切换、Schema 校验回归、Token 用量与成本监控 - - title: 内容与创作 - details: Markdown 博客、社区帖子、动态广场、代码工坊(Fork/收益/模板)、简历系统 - - title: 积分与抽奖 - details: 多来源积分发放与流水、概率可调抽奖、库存与限制规则、熔断降级、风险检测与黑名单 - - title: 平台运营 - details: Sa-Token 双登录域隔离、@RequireAdmin 权限、操作/登录日志审计、管理端仪表盘、文件存储、敏感词风控 + - title: 开始与开发 + details: 环境搭建、学习路线、模块接手、功能开发、测试与发布流程。 + link: /guide/ + - title: 系统架构 + details: 单体模块化边界、28 个 Maven 子模块、双前端和数据演进方式。 + link: /architecture/ + - title: 功能模块 + details: 按学习成长、内容社交、平台能力和工具运营定位源码与影响面。 + link: /modules/ + - title: API + details: 用户端与管理端接口分组、认证方式、协议和错误语义。 + link: /api/ + - title: 操作手册 + details: 面向用户端、管理端和端到端业务链路的实际操作说明。 + link: /manuals/ + - title: 运维与参考 + details: 部署配置、监控告警、事故响应、源码索引和质量矩阵。 + link: /operations/ --- -## 项目技术栈 +## 按角色进入 -| 层 | 技术 | 版本 | +| 你是谁 | 第一入口 | 接下来 | | --- | --- | --- | -| 后端框架 | Spring Boot | 3.4.4 | -| 运行时 | JDK | 17 | -| ORM | MyBatis-Plus | 3.5.x | -| 鉴权 | Sa-Token | 1.39.x | -| 缓存 | Redis 7.x + Redisson | — | -| 数据库 | MySQL | 8.0+ | -| 前端 | Vue 3 + Vite + Element Plus | — | -| 文档站 | VitePress 2.x | — | -| 判题沙箱 | go-judge | 独立 Docker 容器 | -| 监控 | Prometheus + Grafana | Docker Compose | +| 第一次运行项目 | [开始与开发指南](/guide/) | [本地完整启动剧本](/guide/startup-playbook) | +| 接手一个模块 | [模块总览](/modules/) | [模块接手检查清单](/guide/module-takeover-checklist) | +| 开发或联调接口 | [API 导航](/api/) | [响应体与错误码](/reference/response-errors) | +| 使用用户端或管理端 | [操作手册](/manuals/) | [核心链路教程](/manuals/core-workflows) | +| 部署或处理线上问题 | [部署与运维导航](/operations/) | [问题定位流程](/operations/diagnosis-flow) | +| 查代码、表或配置 | [参考索引](/reference/) | [源码地图](/reference/source-map) | -## 文档导航 +## 当前系统基线 -| 你想做什么 | 去哪里 | +| 维度 | 当前值 | 权威来源 | +| --- | --- | --- | +| 后端 | Spring Boot 3.4.4、JDK 17、28 个 Maven 子模块 | 根 `pom.xml` | +| 数据库 | MySQL 8.0+、主库基线 145 张表 | `sql/MySql/code_nest.sql` | +| 缓存与会话 | Redis 7.x + Redisson,业务缓存和 Sa-Token 分库 | 应用配置 | +| 前端 | Vue 3 用户端、管理端、共享设计系统、Electron 构建 | 双前端工程 | +| AI | LangChain4j + LangGraph4j + LlamaIndex Sidecar | `xiaou-ai`、`llamaindex-service` | +| 文档 | VitePress 2.x,120+ Markdown 页面 | `docs-site` | + +实时分支、提交和统计见 [文档同步基线](/reference/docs-sync-baseline)。 + +## 常用任务 + +| 任务 | 文档 | | --- | --- | -| 从零搭建本地开发环境 | [快速开始](/guide/quick-start) | -| 了解整体架构和模块划分 | [架构总览](/architecture/overview) | -| 按模块深入学习 | [模块总览](/modules/) | -| 理解双登录域和权限模型 | [鉴权与用户体系](/modules/auth) | -| 查看 API 接口清单 | [API 路由索引](/reference/api-routes) | -| 查看数据库表结构 | [数据表索引](/reference/database-tables) | -| 排查线上或本地问题 | [问题定位流程](/operations/diagnosis-flow) | -| 处理线上事故 | [事故响应](/operations/incident-response) | -| 理解安全边界 | [权限与安全边界](/guide/security-boundaries) | -| 查看环境变量清单 | [环境变量](/operations/env-vars) | -| 查看用户端操作接口 | [用户端操作手册](/manuals/user-operations) | -| 查看管理端操作接口 | [管理端操作手册](/manuals/admin-operations) | -| 走一遍核心业务链路 | [核心链路教程](/manuals/core-workflows) | -| 部署到生产环境 | [独立部署](/guide/deploy) | -| 开发一个新功能 | [功能开发流程](/guide/feature-development) | +| 启动后端和双前端 | [快速开始](/guide/quick-start) | +| 理解请求如何穿过系统 | [整体架构](/architecture/overview) | +| 找到某个业务模块的源码入口 | [模块总览](/modules/) | +| 查看接口前缀和认证域 | [API 导航](/api/) | +| 查数据库表和字段 | [数据表索引](/reference/database-tables) | +| 修改环境变量和密钥 | [环境变量总表](/operations/env-vars) | +| 选择最低回归范围 | [模块最小回归矩阵](/reference/module-regression-matrix) | +| 发布前做统一验证 | [发布前验证](/guide/release-verification) | -## 关键端口速查 +## 关键端口 -| 组件 | 端口 | 说明 | +| 组件 | 默认端口 | 说明 | | --- | --- | --- | -| 后端 API | 9999 | `server.port`,context-path `/api` | -| 用户端 dev | 3001 | `vue3-user-front` 开发服务器 | -| 管理端 dev | 3000 | `vue3-admin-front` 开发服务器 | -| 文档站 dev | 5175 | `docs-site` VitePress 开发服务器 | -| MySQL | 3306 | 数据库 `code_nest`,142 张表 | -| Redis | 6379 | db3 业务缓存 + db4 Sa-Token 会话 | -| go-judge | 5050 | OJ 判题沙箱(Docker 容器) | -| RAG Sidecar | 18080 | AI 知识库检索(可选) | - -## 使用检查 +| 后端 API | 9999 | context-path `/api` | +| 用户端开发服务 | 3001 | `vue3-user-front` | +| 管理端开发服务 | 3000 | `vue3-admin-front` | +| 文档站开发服务 | 5175 | `docs-site` | +| MySQL | 3306 | 数据库 `code_nest` | +| Redis | 6379 | 业务缓存与会话分库 | +| go-judge | 5050 | OJ 判题沙箱 | +| RAG Sidecar | 18080 | 可选知识库服务 | -第一次打开文档站时,建议先确认以下 5 个入口能正常使用: +## 文档边界 -1. [快速开始](/guide/quick-start) — 能启动并构建文档站。 -2. [模块总览](/modules/) — 能看到所有业务模块并点击进入。 -3. [API 路由索引](/reference/api-routes) — 能找到核心模块的接口清单。 -4. [问题定位流程](/operations/diagnosis-flow) — 遇到问题能找到排查入口。 -5. [文档同步基线](/reference/docs-sync-baseline) — 能看出当前文档站同步到了哪个 commit。 +- `docs-site`:当前代码对应的公开工程文档,是开发和运维的默认入口。 +- `AI-DOCS`:PRD、技术专题、测试材料和历史归档,不替代现行模块/API 文档。 +- `README.md`:项目简介和最短启动入口,不承载完整技术细节。 diff --git a/docs-site/manuals/core-workflows.md b/docs-site/manuals/core-workflows.md index 48e7a10af..070bdc585 100644 --- a/docs-site/manuals/core-workflows.md +++ b/docs-site/manuals/core-workflows.md @@ -8,7 +8,7 @@ 执行以下链路前,确保: -1. MySQL 已启动,`code_nest` 数据库已导入基础表结构(142 张表左右)。 +1. MySQL 已启动,`code_nest` 数据库已导入当前基础表结构(145 张表)。 2. Redis 已启动,`redis-cli ping` 返回 `PONG`。 3. 后端已启动,`curl http://localhost:9999/api/actuator/health` 返回 `{"status":"UP"}`。 4. 前端至少启动了一端(用户端 3001 或管理端 3000)。 diff --git a/docs-site/manuals/index.md b/docs-site/manuals/index.md new file mode 100644 index 000000000..573dd6d55 --- /dev/null +++ b/docs-site/manuals/index.md @@ -0,0 +1,23 @@ +# 操作手册导航 + +操作手册面向实际使用系统的人,重点说明入口、操作顺序、预期结果和失败处理。开发细节请查看模块页和 API 文档。 + +## 按使用端选择 + +| 使用端 | 文档 | 适合场景 | +| --- | --- | --- | +| 用户端 | [用户端操作手册](/manuals/user-operations) | 注册登录、学习、内容、工具和个人数据 | +| 管理端 | [管理端操作手册](/manuals/admin-operations) | 运营配置、审核、统计和系统治理 | + +## 按业务流程选择 + +- [端到端业务链路图](/manuals/business-flow-map):快速判断一个动作会经过哪些页面和模块。 +- [核心链路教程](/manuals/core-workflows):按步骤完成高频业务闭环。 +- [验证记录与已知问题](/manuals/verified-scenarios):确认哪些场景已验证、哪些问题仍需注意。 + +## 使用方式 + +1. 先确认当前使用的是用户端还是管理端。 +2. 按功能名称搜索对应章节。 +3. 操作失败时记录页面、请求、错误提示和时间。 +4. 进入 [问题定位流程](/operations/diagnosis-flow) 或对应模块页继续排查。 diff --git a/docs-site/modules/common.md b/docs-site/modules/common.md index 9b77720c8..fc5d127e1 100644 --- a/docs-site/modules/common.md +++ b/docs-site/modules/common.md @@ -172,7 +172,7 @@ RuntimeException | 常量 | 值 | 说明 | | --- | --- | --- | | `SYSTEM_NAME` | `"Code-Nest"` | 系统名称 | -| `SYSTEM_VERSION` | `"1.0.0"` | 版本号(已过时,项目已到 v2.2.x) | +| `SYSTEM_VERSION` | `"1.0.0"` | 版本号(已过时,当前开发线已到 v2.4.x) | | `DEFAULT_PASSWORD` | `"123456"` | 默认密码 | | `TOKEN_EXPIRE_TIME` | `7200L` | Token 有效期 2 小时 | | `CACHE_PREFIX` | `"code_nest:"` | Redis 缓存前缀 | @@ -519,7 +519,7 @@ getIpAddress(request): | 常量 | 当前值 | 问题 | 建议 | | --- | --- | --- | --- | -| `SYSTEM_VERSION` | `"1.0.0"` | 项目已到 v2.2.x,常量严重过时 | 从 pom.xml 读取或删除此常量 | +| `SYSTEM_VERSION` | `"1.0.0"` | 当前开发线已到 v2.4.x,常量严重过时 | 从 pom.xml 读取或删除此常量 | | `DEFAULT_PASSWORD` | `"123456"` | 安全风险:默认密码太弱 | 至少改为随机生成或强制首次修改 | | `TOKEN_EXPIRE_TIME` | `7200L` | 2 小时可能对管理端太长 | 管理端考虑缩短到 30 分钟 | | `REGEX_PASSWORD` | 需要 `小写+大写+数字,≥8位` | 不要求特殊字符 | 考虑增加特殊字符要求 | diff --git a/docs-site/modules/version-history.md b/docs-site/modules/version-history.md index da1bb6dc9..aef7bbefa 100644 --- a/docs-site/modules/version-history.md +++ b/docs-site/modules/version-history.md @@ -1,84 +1,81 @@ # 版本历史 -这页建议分成两个视角来读: +本页同时回答两个问题:仓库版本线如何演进,以及产品内版本墙如何维护。仓库版本以 Git、发布配置和 [文档同步基线](/reference/docs-sync-baseline) 为证据;用户可见公告由后台 `/system/version` 和用户端 `/version-history` 管理。 -1. **Git 演进视角**:项目真实是怎么从 `v1.8.x`、`v2.0.x` 一路演进到 `v2.2.x` 的。 -2. **产品内版本墙视角**:后台 `/system/version` 和用户端 `/version-history` 这套功能本身是怎么实现、怎么发布、怎么维护的。 +## 推荐阅读顺序 -如果你只是想给用户写一条版本公告,看下面的“产品内版本墙”部分就够了。 -如果你想回答“这个仓库最近几版到底做了什么、哪些是大版本、哪些只是补强批次”,先看下面的 Git 演进视角。 +1. 先看“版本线怎么看”,分清开发分支、发布版本和 tag。 +2. 再看 `v2.0.0` 到 `v2.4.x` 的关键节点,理解 AI Runtime、工程治理、生产交付和平台化优化的演进。 +3. 需要写发布说明时,继续看 [变更日志](/guide/changelog) 和 [版本公告与发版交接模板](/guide/version-release-handoff-template)。 +4. 需要维护产品内版本墙时,直接跳到“产品内版本墙视角”。 -## 推荐学习顺序 - -1. 先看“版本线怎么看”,建立整个仓库的演进脑图。 -2. 再看 `v2.0.0` 到 `v2.2.x` 的关键版本节点,理解这条主线是怎么从 AI Runtime 重构一路走到工程质量与文档治理。 -3. 然后看“当前状态怎么判断”,理解 tag、分支、README 版本段和文档基线之间的关系。 -4. 最后再看产品内版本墙的接口、表结构、状态流转和写作规范。 - -## 先看结论 - -如果只用一句话概括这条版本线,可以这样理解: +## 版本主线概览 | 阶段 | 关键词 | 主要变化 | | --- | --- | --- | | `v1.8.1` ~ `v1.8.2` | 学习闭环与求职闭环 | 学习资产、Career Loop、内容到学习资产沉淀 | | `v2.0.0` | AI Runtime 重构 | 移除 Coze,统一到 `LangChain4j + LangGraph4j + LlamaIndex` | -| `v2.0.1` | 重构后的功能补齐 | AI 治理总览、OJ 赛事增强、文件预览、积分导出 | -| `v2.1.0` | Growth Intelligence | 用户成长驾驶舱 + 管理端 AI 治理中心 | -| `v2.1.1` ~ `v2.1.2` | 安全与聊天室稳定性 | 文件权限、WebSocket 票据、限流、失败态闭环 | -| `v2.2.0` | 工程质量与文档治理 | 异常出口、参数校验、Mapper SQL、前端规范、独立 VitePress 文档站 | +| `v2.0.1` ~ `v2.1.2` | 功能补齐与稳定化 | AI 治理、成长驾驶舱、文件权限、WebSocket 票据与限流 | +| `v2.2.0` ~ `v2.2.2` | 工程质量与文档治理 | Mapper 硬化、前端规范、独立 VitePress 文档站与协作治理 | +| `v2.3.0` ~ `v2.3.2` | 生产交付与体验修复 | 双端 UI、统一 CI/CD、自动部署、版本分页和首页首屏修复 | +| `v2.4.0` | Agent 平台化 | 管理员 Agent Runtime、工具注册、权限确认、审计与质量门禁 | +| `v2.4.1`(开发中) | 架构与热点链路优化 | 共享设计系统、缓存与线程池治理、批量查询、回归测试与文档中心重构 | ## 版本线怎么看 -这个仓库的“版本记录”不只存在于一个地方,而是分散在四类证据里: +同一个“当前版本”在不同语境下含义不同,判断时要同时看以下证据: -| 来源 | 用途 | 适合回答什么问题 | +| 来源 | 用途 | 当前示例 | | --- | --- | --- | -| Git tag | 正式切过哪些版本点 | “仓库明确打过哪些 tag” | -| Git commit / PR merge | 某个版本批次真实是怎么落地的 | “这一版实际改了什么” | -| README 更新日志 | 面向读者的版本摘要 | “这一版对外怎么描述” | -| 产品内版本墙 | 面向用户的产品公告 | “用户在前台能看到什么” | - -当前看到的实际情况是: +| Git 分支 | 正在开发哪条工作线 | `v2.4.1` | +| Maven `revision` 与前端 `package.json` | 当前构建产物声明的版本 | 发布前仍可能保持 `v2.4.0` | +| Git tag | 已形成的正式发布锚点 | `v2.4.0` | +| Git commit / PR merge | 某个版本实际落地了什么 | `bce58a7` 等提交 | +| README 与变更日志 | 面向维护者和读者的版本摘要 | [变更日志](/guide/changelog) | +| 产品内版本墙 | 面向最终用户的公告 | `/version-history` | -- 正式 tag 一直打到 `V2.0.0` -- `v2.0.1`、`v2.1.0`、`v2.1.2` 之后更多通过 **release commit / PR merge / README 版本段** 来表达 -- `v2.2.0` 则明显已经进入 **工程质量 + 文档治理批次持续推进** 的状态 +因此不要仅凭分支名宣布发布,也不要因为 package 版本尚未提升就忽略正在进行的开发线。更稳的表达是:**当前开发线为 `v2.4.1`,最近正式发布版本为 `v2.4.0`。** -所以更稳的读法不是只看 tag,而是: +## Git 演进视角 -**tag 看“官方锚点”,git log 看“真实演进过程”,README 看“对外摘要”。** +下面按真实提交和发布锚点整理;每次构建文档站时,[文档同步基线](/reference/docs-sync-baseline) 会记录当时的分支与提交。 -## Git 演进视角 +### `v2.4.1` 当前开发线 -下面这部分按照真实提交来整理,不是按 PRD 想象出来的时间线。 +`v2.4.1` 从 `v2.4.0` 发布点继续推进,重点不是堆叠新页面,而是减少重复基础设施、修正热点链路和提升可验证性。 -### `v2.2.0` 当前工作线 +| 日期 | 提交或批次 | 含义 | +| --- | --- | --- | +| 2026-07-14 | `bce58a7` `refactor: complete v2.4.1 optimizations` | 抽取共享设计系统,治理缓存与线程池,优化学习、社区、朋友圈等热点链路并补测试 | +| 2026-07-14 | 文档中心重构批次 | 重组分类与导航,新增工程手册主题、自动同步基线和导航审计 | -这是当前仓库最明显的主线,但它和 `v2.0.0` 不一样,**不是一个单点 release commit 就结束**,而是一串持续推进的治理批次。 +这一工作线的发布状态和详细变化以 [变更日志](/guide/changelog) 为准。 -关键提交: +### `v2.4.0` Agent 平台化发布 -| 日期 | 提交 | 含义 | +| 日期 | 提交或 tag | 含义 | | --- | --- | --- | -| 2026-04-26 | `857eacf` `chore: harden code quality for v2.2.0` | 明确把版本主题收敛到工程质量治理 | -| 2026-05-02 | `9551452` `chore: continue v2.2.0 mapper hardening` | 继续推进 Mapper / SQL 级别硬化 | -| 2026-05-16 | `0dd1819` `docs: add standalone VitePress site` | 独立文档站起点 | -| 2026-05-16 ~ 2026-05-20 | 一系列 `docs:` 提交 | 持续补模块文档、索引、运维、回归、交接、事故响应等 | +| 2026-07-14 | `8166255` `feat(agent): add unified backend runtime and quality gates` | 建立后端 Agent Runtime 与质量门禁 | +| 2026-07-14 | `5bb48f8` `chore(release): prepare v2.4.0` | 同步发布版本与发布资料 | +| 2026-07-14 | tag `v2.4.0` | 当前最近的正式发布锚点 | -这一版真正的关键词不是“新增某个业务功能”,而是: +这一版将 Agent 规划、权限、确认、执行、审计与幂等能力收口到后端,管理端只负责交互和结果展示。 -1. 统一异常出口和参数校验。 -2. 收敛 Mapper SQL 和类型安全。 -3. 补齐前端构建、Lint、样板治理。 -4. 建立独立 VitePress 文档站和维护闭环。 +### `v2.3.x` 生产交付与体验修复 -对应资料: +`v2.3.0` 建立双端 UI 优化和部署脚本,`v2.3.1` 接入统一 CI/CD、生产备份和失败回滚,`v2.3.2` 修复版本分页与首页首屏空白。`V2.3.1` 与 `v2.4.0` 都可以从 tag 追溯。 + +### `v2.2.x` 工程质量与文档治理 + +这一阶段从单点业务功能转向持续治理: + +| 日期 | 提交 | 含义 | +| --- | --- | --- | +| 2026-04-26 | `857eacf` `chore: harden code quality for v2.2.0` | 将版本主题收敛到工程质量治理 | +| 2026-05-02 | `9551452` `chore: continue v2.2.0 mapper hardening` | 继续推进 Mapper / SQL 级别硬化 | +| 2026-05-16 | `0dd1819` `docs: add standalone VitePress site` | 建立独立文档站 | -- [README 更新日志](D:/onenodes/githubprojectstart/Code-Nest/README.md) -- [v2.2.0 工程质量计划](D:/onenodes/githubprojectstart/Code-Nest/AI-DOCS/Archive/历史版本/2026-04-26-v2.2.0-code-quality-hardening.md) -- [v2.2.0 文档计划](/roadmap/v2.2.0-docs-plan) -- [文档同步基线](/reference/docs-sync-baseline) +历史建设过程保留在 [路线图归档](/roadmap/),现行维护规则见 [文档维护规范](/guide/documentation-maintenance)。 ### `v2.1.2` 聊天室防刷增强 @@ -99,8 +96,8 @@ 对应资料: -- [README v2.1.2 段落](D:/onenodes/githubprojectstart/Code-Nest/README.md) -- [v2.1.2 审计记录](D:/onenodes/githubprojectstart/Code-Nest/AI-DOCS/Development/Code-Nest优化审计报告.md) +- [仓库 README](https://github.com/xiaou61/Code-Nest/blob/master/README.md) 的 `v2.1.2` 段落 +- 仓库内 `AI-DOCS/Development/Code-Nest优化审计报告.md` ### `v2.1.1` 安全加固稳定批次 @@ -138,9 +135,9 @@ 对应资料: -- [README v2.1.0 段落](D:/onenodes/githubprojectstart/Code-Nest/README.md) -- [v2.1.0 Growth Intelligence 设计](D:/onenodes/githubprojectstart/Code-Nest/AI-DOCS/Archive/历史版本/2026-04-26-v2.1.0-growth-intelligence-design.md) -- [v2.1.0 Growth Intelligence 实施计划](D:/onenodes/githubprojectstart/Code-Nest/AI-DOCS/Archive/历史版本/2026-04-26-v2.1.0-growth-intelligence.md) +- [仓库 README](https://github.com/xiaou61/Code-Nest/blob/master/README.md) 的 `v2.1.0` 段落 +- 仓库内 `AI-DOCS/Archive/历史版本/2026-04-26-v2.1.0-growth-intelligence-design.md` +- 仓库内 `AI-DOCS/Archive/历史版本/2026-04-26-v2.1.0-growth-intelligence.md` ### `v2.0.1` 重构后功能补齐 @@ -213,15 +210,15 @@ | --- | --- | | 仓库当前工作线是什么 | `pom.xml`、前端 `package.json`、README 版本 badge | | 文档和代码同步到哪一批 | [文档同步基线](/reference/docs-sync-baseline) | -| 当前大版本主题是什么 | README 的最新版本段 + [v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan) | +| 当前大版本主题是什么 | README 最新版本段 + [变更日志](/guide/changelog) | | 产品内给用户展示什么版本公告 | `/version-history` 和后台 `/system/version` | -当前仓库的状态更接近: +当前仓库状态: -- **代码主线版本**:`v2.2.0` -- **最近清晰 release 锚点**:`v2.1.2` -- **最近正式 tag 锚点**:`V2.0.0` -- **当前文档治理主线**:`v2.2.1` 持续维护与版本记录方法补强 +- **当前开发线**:`v2.4.1` +- **当前构建产物版本**:`v2.4.0`,以 Maven `revision` 和各前端 `package.json` 为准 +- **最近正式 tag**:`v2.4.0` +- **当前文档基线**:由 [文档同步基线](/reference/docs-sync-baseline) 在构建前动态刷新 ## 产品内版本墙视角 diff --git a/docs-site/operations/index.md b/docs-site/operations/index.md new file mode 100644 index 000000000..30495d99a --- /dev/null +++ b/docs-site/operations/index.md @@ -0,0 +1,33 @@ +# 部署与运维导航 + +运维文档按“部署配置、日常观测、故障响应、验证记录”组织。线上问题先止损,再定位;不要从长篇模块文档开始搜索。 + +## 部署配置 + +| 任务 | 文档 | +| --- | --- | +| 使用 Docker 启动依赖和应用 | [Docker 与服务部署](/operations/docker) | +| 配置数据库、Redis、AI、RAG 和安全密钥 | [环境变量总表](/operations/env-vars) | +| 独立部署前后端和文档站 | [独立部署](/guide/deploy) | +| 使用 CI/CD 发布生产版本 | [生产 CI/CD 自动发布](/guide/ci-cd-production) | + +## 观测与故障响应 + +| 场景 | 第一入口 | 补充材料 | +| --- | --- | --- | +| 指标异常或服务抖动 | [监控与观测](/operations/monitoring) | [告警 Runbook](/operations/alert-runbooks) | +| 线上事故 | [事故响应](/operations/incident-response) | [问题定位流程](/operations/diagnosis-flow) | +| 本地或线上功能异常 | [常见问题排查](/operations/troubleshooting) | 对应模块页 | +| 发布前确认 | [发布前验证](/guide/release-verification) | [模块最小回归矩阵](/reference/module-regression-matrix) | + +## 响应原则 + +1. 先确认影响范围和用户可见症状。 +2. 记录时间线、版本、配置和关键日志。 +3. 优先止损,避免在事故中直接做大范围重构。 +4. 修复后补回归项、Runbook 和已知问题记录。 + +## 验证记录 + +- [线上接口业务正确性测试(2026-06-18)](/operations/online-api-business-correctness-2026-06-18) +- [验证记录与已知问题](/manuals/verified-scenarios) diff --git a/docs-site/package.json b/docs-site/package.json index 44369c700..4fcfd113b 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -5,10 +5,11 @@ "type": "module", "scripts": { "sync:baseline": "node ./scripts/generate-docs-sync-baseline.mjs", + "audit": "node ./scripts/audit-docs.mjs", "postinstall": "npm run sync:baseline", "predev": "npm run sync:baseline", "dev": "vitepress dev . --host 0.0.0.0", - "prebuild": "npm run sync:baseline", + "prebuild": "npm run sync:baseline && npm run audit", "build": "vitepress build .", "prepreview": "npm run sync:baseline", "preview": "vitepress preview . --host 0.0.0.0" diff --git a/docs-site/public/favicon.ico b/docs-site/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3292e5fa281c8f1568412064d5e5fb4794aaee5a GIT binary patch literal 766 zcmc(du@S;B3`GUU094c|8G}hEjk|Pd(wG0Y?;^5d zG|l!X;JOysvF?e6I1nq@FpDI(&q$CkDV2qpEs}RBcBvti%+)g43HyzP%Ro z{*rfjB=4qL)T|*yq9ALpU1+t6L>;4^;q4IU^%jlG6v0V3Xa;R@@x=`k6)0;n13OsT z>4hqaE@K8&(5q(P!Y)8B+I@Y+WKiU*V%F#V+^ppdNIK$ZUKi#W{(?EhfV(Y`&&%v* Jdwz$%;sIn56ukfd literal 0 HcmV?d00001 diff --git a/docs-site/reference/ai-schemas.md b/docs-site/reference/ai-schemas.md index 0ad2c47a0..f3eeb066d 100644 --- a/docs-site/reference/ai-schemas.md +++ b/docs-site/reference/ai-schemas.md @@ -1,27 +1,32 @@ # AI 结构化输出 Schema 索引 -本页索引 AI 模块所有结构化输出 DTO(Structured Output)的 Schema 定义。AI 模块使用 Spring AI 的 `BeanOutputConverter` + JSON Schema 驱动,将大模型输出反序列化为强类型 Java 对象。每条 Schema 都绑定一个 Prompt 模板,构成「Prompt + Schema = 任务单元」的组合。 +本页索引 AI 模块的结构化输出契约(Structured Output)。当前实现使用 LangChain4j 调用模型,由 `AiStructuredOutputSpec` 显式绑定 Prompt、根类型和字段约束;同一份契约既用于运行时校验,也能导出 JSON Schema。场景服务解析模型 JSON 后再映射到业务 DTO,避免 Prompt、校验规则和返回结构长期分叉。 ## 架构总览 ``` -AiStructuredOutputSpec (枚举注册) - ├── scene → Prompt 模板 ID - ├── outputClass → Schema DTO 类 - └── converter → BeanOutputConverter<T> (自动生成 JSON Schema) - -AiPromptSpec (枚举注册) - ├── id → Prompt 模板 ID - ├── systemPrompt → 系统提示词 - └── userPromptTemplate → 用户提示词模板 (Velocity) +AiStructuredOutputCatalog + └── AiStructuredOutputSpec + ├── promptSpec → AiPromptSpec + ├── rootType → OBJECT / ARRAY + ├── field contract → 类型、必填、范围和枚举约束 + ├── validateObject / validateArray → 运行时校验 + └── jsonSchema → Draft 2020-12 Schema + +AiPromptCatalog + └── AiPromptSpec (record) + ├── key + version → promptId + ├── systemPrompt → 系统提示词 + ├── userTemplate → LangChain4j PromptTemplate + └── maxCompletionTokens → 可选场景预算 ``` **调用链路:** -1. 前端请求 → Controller → `AiSceneService` -2. `AiSceneService` 从 `AiStructuredOutputSpec` 获取 `outputClass` + `converter` -3. 从 `AiPromptSpec` 获取 Prompt 模板,注入变量 -4. 调用 LLM,输出 JSON → `converter.convert()` 反序列化 → 强类型 DTO -5. DTO 返回给调用方 +1. 前端请求进入 Controller,再转到对应的 Scene Support 或业务服务。 +2. 服务从领域 Prompt holder 获取 `AiPromptSpec`,用 LangChain4j `PromptTemplate` 注入命名变量。 +3. `AiModelFactory` 调用模型并返回文本结果。 +4. `AiJsonResponseParser` 提取 JSON,领域 `AiStructuredOutputSpec` 校验根类型、字段类型、范围和枚举值。 +5. 校验通过后由场景服务映射为业务 DTO;失败时进入该场景定义的降级或错误处理。 ## 面试模块 Schema @@ -531,51 +536,59 @@ AI 模块使用 LangGraph4j 实现多轮对话的状态机,每个场景有独 ## Prompt 模板注册机制 -所有 Prompt 通过 `AiPromptSpec` 枚举注册,运行时自动加载: +Prompt 以不可变的 `AiPromptSpec` record 表达,按领域放在静态 holder 中,再由 `AiPromptCatalog` 汇总: ```java -public enum AiPromptSpec { - INTERVIEW_GENERATE("interview.generate", systemPrompt, userPromptTemplate), - INTERVIEW_EVALUATE("interview.evaluate", systemPrompt, userPromptTemplate), - OJ_CODE_REVIEW("oj.codeReview", systemPrompt, userPromptTemplate), - // ... - ; +public final class InterviewPromptSpecs { + public static final AiPromptSpec EVALUATE_ANSWER = AiPromptSpec.of( + "mock_interview.evaluate_answer", + "v1", + "你是资深技术面试官……", + "面试方向:{{direction}}\n候选人回答:{{answer}}" + ); } ``` -每个枚举值对应: -- **id** — 全局唯一标识,与前端请求中的 scene 参数一致 -- **systemPrompt** — 系统角色设定 -- **userPromptTemplate** — Velocity 模板,支持 ${variable} 占位符 +每个 Prompt 规范包含: + +- **key + version**:共同组成全局唯一的 `promptId` +- **systemPrompt**:系统角色、任务和输出约束 +- **userTemplate**:LangChain4j 模板,使用双花括号包裹的命名占位符,例如 `direction` +- **maxCompletionTokens**:可选的场景级输出预算;未设置时使用全局预算 ## Schema 验证与容错 AI 模块的 Schema 验证链路: -1. **输出约束** — `BeanOutputConverter` 将 DTO 类自动转为 JSON Schema 注入 Prompt -2. **格式校验** — LLM 返回 JSON 后,Jackson 反序列化时校验字段类型 -3. **降级处理** — 反序列化失败时返回 `Result.error("AI 输出解析失败")` -4. **AI 回归测试** — 18 条用例覆盖所有 Schema 的解析正确性 +1. **输出约束**:系统 Prompt 明确要求只返回符合场景格式的 JSON。 +2. **JSON 提取**:`AiJsonResponseParser` 清理代码块、提取对象并处理少量兼容包装。 +3. **契约校验**:`AiStructuredOutputSpec` 对对象或数组执行必填、类型、范围、枚举与嵌套结构校验。 +4. **Schema 导出**:`AiStructuredJsonSchemaBuilder` 从同一份契约生成 Draft 2020-12 JSON Schema,避免校验和文档两套定义。 +5. **容错与回归**:场景服务处理解析或校验失败,并由 Prompt、Schema、Graph 和场景回归测试覆盖关键结构。 ## 维护检查清单 新增 AI Schema 时: -1. 在 `xiaou-ai/src/main/java/com/xiaou/ai/dto/` 下创建 DTO 类 -2. 在 `AiStructuredOutputSpec` 枚举中注册 scene → outputClass 映射 -3. 在 `AiPromptSpec` 枚举中注册对应的 Prompt 模板 -4. 如果是多轮对话场景,在对应 `graph/` 包下创建 State 类 -5. 在 `AiSceneService` 中添加场景路由逻辑 -6. 编写 AI 回归测试用例 -7. 更新本页 Schema 索引 -8. 更新对应的模块功能文档 +1. 在对应 `prompt/{domain}/*PromptSpecs.java` 中定义 `AiPromptSpec`。 +2. 在对应 `structured/{domain}/*StructuredOutputSpecs.java` 中定义对象或数组契约。 +3. 确认新的 holder 或规范已进入 `AiPromptCatalog` 和 `AiStructuredOutputCatalog`。 +4. 在场景服务中接入模板渲染、模型调用、JSON 解析、契约校验和 DTO 映射。 +5. 如果是多轮场景,在对应 `graph/` 包补 State、节点和失败分支。 +6. 编写 Prompt、Schema、解析、Graph 和场景回归测试。 +7. 更新本页、对应模块页和回归说明。 ## 源码导航 | 文件 | 说明 | | --- | --- | -| `xiaou-ai/.../structured/AiStructuredOutputSpec.java` | Schema 注册枚举 | -| `xiaou-ai/.../prompt/AiPromptSpec.java` | Prompt 模板注册枚举 | +| `xiaou-ai/.../structured/AiStructuredOutputSpec.java` | Prompt 与结构化输出契约绑定 | +| `xiaou-ai/.../structured/AiStructuredOutputCatalog.java` | 结构化输出规范清单 | +| `xiaou-ai/.../structured/AiStructuredJsonSchemaBuilder.java` | 从契约生成 JSON Schema | +| `xiaou-ai/.../structured/AiStructuredOutputValidator.java` | JSON 对象和数组运行时校验 | +| `xiaou-ai/.../prompt/AiPromptSpec.java` | 不可变 Prompt 规范 | +| `xiaou-ai/.../prompt/AiPromptCatalog.java` | 分领域 Prompt 汇总清单 | +| `xiaou-ai/.../util/AiJsonResponseParser.java` | 模型 JSON 文本提取与兼容解析 | | `xiaou-ai/.../dto/interview/` | 面试模块 DTO | | `xiaou-ai/.../dto/jobbattle/` | 求职作战 DTO | | `xiaou-ai/.../dto/oj/` | OJ 模块 DTO | diff --git a/docs-site/reference/database-tables.md b/docs-site/reference/database-tables.md index eba9ad4fe..eec41dd94 100644 --- a/docs-site/reference/database-tables.md +++ b/docs-site/reference/database-tables.md @@ -6,13 +6,13 @@ ## 表统计总览 -基线脚本包含 136 张表,加上增量脚本后共 142 张表,按业务域分布: +当前主库基线包含 145 张表,按业务域分布: | 业务域 | 表数量 | 模块 | 表前缀 | | --- | --- | --- | --- | -| 账号与权限 | 8 | xiaou-user, xiaou-system | `user_info`, `sys_*` | +| 账号、权限与智能体审计 | 10 | xiaou-user, xiaou-system | `user_info`, `sys_*` | | 面试题库 | 8 | xiaou-interview | `interview_*` | -| 模拟面试与求职 | 9 | xiaou-mock-interview | `mock_interview_*`, `career_loop_*`, `job_battle_*` | +| 模拟面试与求职 | 10 | xiaou-mock-interview | `mock_interview_*`, `career_loop_*`, `job_battle_*` | | 成长与计划 | 7 | xiaou-plan | `growth_autopilot_*`, `user_plan`, `plan_*`, `learning_cockpit_*` | | 学习小组 | 11 | xiaou-team | `study_team_*` | | 闪卡 | 8 | xiaou-flashcard | `flashcard_*` | @@ -87,6 +87,8 @@ | 角色-权限 | `sys_role_permission` | role_id, permission_id | xiaou-system | | 登录日志 | `sys_login_log` | username, ip, location, browser, status | xiaou-system | | 操作日志 | `sys_operation_log` | module, operation, method, params, user_id | xiaou-system | +| 智能体审计 | `sys_agent_audit` | audit_id, action_id, risk_category, status, operator_id | xiaou-system | +| 智能体会话上下文 | `sys_agent_session_context` | session_id, turns_json, updated_time | xiaou-system | ## 学习成长 @@ -116,6 +118,7 @@ | `career_loop_action` | session_id, title, type, deadline, status | xiaou-mock-interview | | `career_loop_snapshot` | session_id, stage, snapshot_json | xiaou-mock-interview | | `job_battle_plan_record` | user_id, jd_text, plan_json, status | xiaou-mock-interview | +| `job_battle_match_record` | user_id, target_count, best_score, fallback_count, result_json | xiaou-mock-interview | ### 成长与计划 diff --git a/docs-site/reference/docs-sync-baseline.md b/docs-site/reference/docs-sync-baseline.md index f04d80b2a..bd0c62916 100644 --- a/docs-site/reference/docs-sync-baseline.md +++ b/docs-site/reference/docs-sync-baseline.md @@ -1,222 +1,68 @@ -# 文档同步基线 - -本页记录文档站与源码之间的同步基线。当你怀疑某个模块文档已经和源码不一致时,可以按照本页的步骤快速定位漂移。 - -## 基线版本 - -| 项 | 值 | -| --- | --- | -| 项目版本 | v2.3.1 | -| 基线提交 | `v2.3.1 CI/CD release PR` | -| 文档站提交 | 同上 | -| 文档页数量 | 约 45 个 .md 文件 | -| 源码模块数 | 24 个 xiaou-* 模块 | -| Controller 数 | 100 个 | -| REST 端点数 | 约 757 个 | -| 数据库表数 | 142 张表(基线 136 + 增量 6) | - -## 文档页与源码对应关系 - -### 模块文档(docs-site/modules/) - -| 文档页 | 对应模块 | 核心源码目录 | 基线检查要点 | -| --- | --- | --- | --- | -| auth.md | xiaou-user, xiaou-system | `xiaou-user/.../controller/user/AuthController.java`、`xiaou-system/.../controller/admin/AuthAdminController.java` | 登录注册接口、Sa-Token 配置 | -| user-account.md | xiaou-user | `xiaou-user/.../controller/user/UserController.java`、`xiaou-user/.../controller/admin/AdminUserController.java` | 用户 CRUD、个人中心 | -| system-ops.md | xiaou-system | `xiaou-system/.../controller/admin/` | 仪表盘、日志、角色权限 | -| dashboard-logs.md | xiaou-system | `xiaou-system/.../controller/admin/DashboardController.java`、`LogController.java` | 统计接口、日志 CRUD | -| interview.md | xiaou-interview | `xiaou-interview/.../controller/` | 题库、分类、学习记录 | -| interview-and-growth.md | xiaou-interview, xiaou-plan | 驾驶舱、成长自动驾驶 | 驾驶舱接口、成长分数公式 | -| mock-interview-job-battle.md | xiaou-mock-interview | `xiaou-mock-interview/.../controller/` | 模拟面试、求职闭环、岗位匹配 | -| plan-team.md | xiaou-plan, xiaou-team | `xiaou-plan/.../controller/user/`、`xiaou-team/.../controller/` | 计划打卡、小组 CRUD | -| flashcard.md | xiaou-flashcard | `xiaou-flashcard/.../controller/` | 卡组、学习、SM-2 | -| oj.md | xiaou-oj | `xiaou-oj/.../controller/` | 题目、判题、赛事 | -| community.md | xiaou-community | `xiaou-community/.../controller/` | 帖子、评论、分类 | -| community-content.md | xiaou-community, xiaou-moment, xiaou-blog | 三模块用户端+管理端 Controller | 社区、动态、博客交叉 | -| moments.md | xiaou-moment | `xiaou-moment/.../controller/` | 动态发布、互动 | -| blog.md | xiaou-blog | `xiaou-blog/.../controller/` | 博客文章、分类标签 | -| codepen.md | xiaou-codepen | `xiaou-codepen/.../controller/` | 代码工坊、模板 | -| resume.md | xiaou-resume | `xiaou-resume/.../controller/` | 简历模板、分析 | -| file-storage.md | xiaou-filestorage | `xiaou-filestorage/.../controller/` | 上传、下载、存储配置、迁移 | -| notification.md | xiaou-notification | `xiaou-notification/.../controller/` | 通知 CRUD、模板 | -| chat.md | xiaou-chat | `xiaou-chat/.../controller/`、`xiaou-chat/.../websocket/` | REST + WebSocket | -| points.md | xiaou-points | `xiaou-points/.../controller/` | 积分、签到、抽奖 | -| sensitive.md | xiaou-sensitive | `xiaou-sensitive/.../controller/` | 敏感词、白名单、版本 | -| version-history.md | xiaou-version | `xiaou-version/.../controller/` | 版本发布 | -| tools-moyu-version.md | xiaou-moyu, xiaou-version | 摸鱼工具 + 版本历史 | 日历、薪资、Bug 商店 | -| knowledge.md | xiaou-knowledge | `xiaou-knowledge/.../controller/` | 图谱、节点 | -| learning-assets.md | xiaou-learning-asset | `xiaou-learning-asset/.../controller/` | 资产记录、审核 | -| sql-optimizer.md | xiaou-sql-optimizer | `xiaou-sql-optimizer/.../controller/` | SQL 监控、优化 | -| ai-runtime.md | xiaou-ai, xiaou-system(AI Config) | `xiaou-ai/.../controller/`、`xiaou-system/.../AiConfigController.java` | AI 配置、治理 | -| dev-tools.md | 前端本地能力 | 无后端 | JSON 工具、文本 Diff | -| user-operations.md | 全模块用户端 | 截图目录 | 操作手册 | -| admin-operations.md | 全模块管理端 | 截图目录 | 操作手册 | - -### 参考文档(docs-site/reference/) - -| 文档页 | 数据来源 | 基线检查要点 | -| --- | --- | --- | -| permission-boundaries.md | `xiaou-common/.../aspect/AdminAuthAspect.java`、`StpInterfaceImpl.java`、`SaTokenConfig.java` | @RequireAdmin 切面逻辑、白名单路径、StpInterface 实现 | -| response-errors.md | `xiaou-common/.../domain/Result.java`、`GlobalExceptionHandler.java`、`ResultCode.java` | 错误码枚举、异常处理映射、NotLoginException 子类型 | -| websocket.md | `xiaou-chat/.../websocket/`、`ChatWebSocketTicketService.java`、`ChatRateLimitService.java` | ws-ticket 参数、限流配置、消息类型 | -| database-tables.md | `sql/MySql/code_nest.sql`、各模块 Mapper XML | 表数量、索引、字段约定 | -| frontend-rendering-security.md | `vue3-user-front/src/utils/markdown.js`、`vue3-admin-front/src/utils/markdown.js` | MarkdownIt 配置、DOMPurify 白名单、highlight.js 语言 | -| feature-coverage.md | 全模块 Controller 源码 | API 端点数、@RequireAdmin 方法数、Controller 分布 | -| env-vars.md | `xiaou-application/src/main/resources/application*.yml`、`@Value` 注入 | 环境变量、profile 差异、安全密钥 | -| docs-sync-baseline.md | 本页 | 基线版本、漂移检测 | - -### 架构文档(docs-site/architecture/) - -| 文档页 | 数据来源 | 基线检查要点 | -| --- | --- | --- | -| overview.md | 项目整体结构 | 模块清单、技术栈版本 | -| database.md | SQL 脚本、MyBatis 配置 | 表前缀映射、索引策略 | -| frontend.md | Vue3 前端项目 | 路由结构、状态管理 | -| ai-architecture.md | AI 模块源码 | AI 配置、RAG、回归测试 | - -## 漂移检测步骤 - -当怀疑文档与源码不一致时,按以下步骤排查: + -# 检查是否有人偷偷用了 @SaCheckRole 或 @SaCheckPermission -grep -r "@SaCheckRole\|@SaCheckPermission" xiaou-*/src/main/java/ | wc -l -# 预期结果:0 - -# 检查是否有人用了 @SaIgnore(项目约定只用 SaTokenConfig 白名单) -grep -r "@SaIgnore" xiaou-*/src/main/java/ | wc -l -# 预期结果:0 -``` - -### 4. 错误码漂移 - -```bash -# 检查 ResultCode 枚举值数量 -grep "^[[:space:]]*[A-Z_]*(" xiaou-common/src/main/java/com/xiaou/common/core/domain/ResultCode.java | wc -l -# 基线值:27 - -# 检查 GlobalExceptionHandler 处理的异常类型数量 -grep "@ExceptionHandler" xiaou-common/src/main/java/com/xiaou/common/exception/GlobalExceptionHandler.java | wc -l -# 基线值:18 -``` +# 文档同步基线 -### 5. 配置项漂移 +本页数据在启动或构建 VitePress 前自动生成,用于判断当前文档对应哪个源码提交,以及关键规模数据是否已经漂移。 -```bash -# 检查 @Value 注入点数量 -grep -r "@Value" xiaou-*/src/main/java/ | wc -l +## 当前基线 -# 检查 YAML 配置项数量 -grep -r "^[a-z]" xiaou-application/src/main/resources/application.yml | wc -l -``` +| 项 | 当前值 | +| --- | --- | +| Maven 项目版本 | `{{ baseline.projectVersion }}` | +| 文档工作线 | `{{ baseline.branch }}` | +| 当前提交 | `{{ baseline.shortSha }}` | +| 提交说明 | {{ baseline.subject }} | +| 上游分支 | `{{ upstream }}` | +| 工作区状态 | {{ baseline.workingTree }} | +| Markdown 页面 | {{ baseline.documentPages }} 页 | +| Maven 子模块 | {{ baseline.mavenModules }} 个 | +| 主库基线表 | {{ baseline.databaseTables }} 张 | +| 生成时间 | {{ generatedAt }} | + +> {{ baseline.note }} + +## 数据来源 + +| 指标 | 权威来源 | +| --- | --- | +| 项目版本、模块数 | 根 `pom.xml` | +| 文档页数 | `docs-site` 下的 Markdown 文件,排除 `.vitepress` 和 `node_modules` | +| 数据库表数 | `sql/MySql/code_nest.sql` 中的 `CREATE TABLE` | +| 分支、提交、工作区 | 当前 Git 仓库 | -### 6. 前端 v-html 漂移 +## 自动校验 ```bash -# 检查 v-html 使用量 -grep -r "v-html" vue3-user-front/src/ | wc -l -# 基线值:16 个文件 - -grep -r "v-html" vue3-admin-front/src/ | wc -l -# 基线值:3 个文件 - -# 检查是否有 v-html 没有走统一渲染链路 -grep -r "v-html" vue3-user-front/src/ | grep -v "renderMarkdown\|sanitizeHtml\|escapeHtml" +cd docs-site +npm run audit +npm run build ``` -## 同步规则 - -### 新增功能时 - -1. 新增 Controller 方法 → 更新对应模块文档的"API 端点"表 -2. 新增数据表 → 更新 `database-tables.md` 的表索引和 `code_nest.sql` 基线 -3. 新增 \`@RequireAdmin\` 方法 → 更新 \`permission-boundaries.md\` 和 \`feature-coverage.md\` 的方法分布表 -4. 新增 \`@Value\` 注入 → 更新 \`env-vars.md\` 的配置表 -5. 新增 \`v-html\` 使用 → 更新 \`frontend-rendering-security.md\` 的审计表 -6. 新增 \`ResultCode\` 枚举 → 更新 \`response-errors.md\` 的枚举表 -7. 新增 WebSocket 事件类型 → 更新 \`websocket.md\` 的事件表 - -### 修改功能时 - -1. 修改接口参数或返回值 → 更新对应模块文档的接口说明 -2. 修改 DOMPurify 白名单 → 同步更新用户端和管理端两个 \`markdown.js\`,更新 \`frontend-rendering-security.md\` -3. 修改 SaTokenConfig 白名单 → 更新 \`permission-boundaries.md\` 的白名单表 -4. 修改限流配置 → 更新 \`websocket.md\` 和 \`env-vars.md\` 的限流表 -5. 修改错误码或异常处理 → 更新 \`response-errors.md\` 的映射表 - -### 删除功能时 - -1. 删除 Controller 方法 → 从对应模块文档移除相关条目 -2. 删除数据表 → 更新 \`database-tables.md\`,添加版本增量脚本说明表已废弃 -3. 不要直接删除文档页 → 标记为"已废弃"并添加指向替代页的链接 - -## 基线数值速查 - -以下是关键数值的基线值,用于快速检测漂移: - -| 指标 | 基线值 | 检测命令 | -| --- | --- | --- | -| 数据库表数 | 142 | \`grep "CREATE TABLE" sql/MySql/code_nest.sql | wc -l\` + 增量 | -| Controller 类数 | 100 | \`find . -name "*Controller.java" | wc -l\` | -| REST 端点数 | ~757 | \`grep -r "@Get/Post/Put/Delete/PatchMapping" --include="*.java" | wc -l\` | -| @RequireAdmin 方法数 | ~270 | \`grep -r "@RequireAdmin" --include="*.java" | wc -l\` | -| @SaCheckRole 使用数 | 0 | \`grep -r "@SaCheckRole" --include="*.java" | wc -l\` | -| @SaCheckPermission 使用数 | 0 | \`grep -r "@SaCheckPermission" --include="*.java" | wc -l\` | -| @SaIgnore 使用数 | 0 | \`grep -r "@SaIgnore" --include="*.java" | wc -l\` | -| ResultCode 枚举值数 | 27 | \`grep "^[[:space:]]*[A-Z_]*(" ResultCode.java | wc -l\` | -| GlobalExceptionHandler 异常类型 | 18 | \`grep "@ExceptionHandler" GlobalExceptionHandler.java | wc -l\` | -| 用户端 v-html 文件数 | 16 | \`grep -rl "v-html" vue3-user-front/src/ | wc -l\` | -| 管理端 v-html 文件数 | 3 | \`grep -rl "v-html" vue3-admin-front/src/ | wc -l\` | -| highlight.js 注册语言数 | 14 | 检查 \`markdown.js\` 中 \`hljs.registerLanguage\` 调用数 | -| DOMPurify FORBID_TAGS 数 | 5 | 检查 \`markdown.js\` 中 \`FORBID_TAGS\` 数组长度 | -| DOMPurify FORBID_ATTR 数 | 5 | 检查 \`markdown.js\` 中 \`FORBID_ATTR\` 数组长度 | - -## 源码导航 - -| 文件 | 说明 | -| --- | --- | -| \`docs-site/reference/docs-sync-baseline.md\` | 本页 | -| \`sql/MySql/code_nest.sql\` | 数据库基线脚本 | -| \`xiaou-common/.../domain/ResultCode.java\` | 错误码枚举基线 | -| \`xiaou-common/.../exception/GlobalExceptionHandler.java\` | 异常处理基线 | -| \`xiaou-common/.../aspect/AdminAuthAspect.java\` | 权限注解基线 | -| \`xiaou-common/.../config/SaTokenConfig.java\` | 白名单配置基线 | -| \`vue3-user-front/src/utils/markdown.js\` | 前端渲染安全基线 | -| \`vue3-admin-front/src/utils/markdown.js\` | 管理端渲染安全基线 | +`npm run audit` 检查: +1. 每个 Markdown 页面是否进入 VitePress 导航。 +2. 各一级分类是否有总览页。 +3. 首页、架构页是否仍保留已知过时统计。 +4. 是否继续把原生 WebSocket 错写成 STOMP。 +5. 是否出现超过 1200 行、需要拆分的超长页面。 -## 相关文档 +## 发现漂移时怎么处理 -| 文档 | 说明 | +| 漂移类型 | 处理方式 | | --- | --- | -| [文档维护规范](/guide/documentation-maintenance) | 文档维护流程 | -| [版本公告与交接模板](/guide/version-release-handoff-template) | 版本交接 | -| [Git Log 版更新记录](/guide/git-log-release-notes) | 更新日志 | -| [架构总览](/architecture/overview) | 项目版本信息 | +| 模块数变化 | 更新架构总览、后端模块页和模块导航 | +| 数据表变化 | 更新数据库架构页、数据表索引和相关模块页 | +| 新增 Controller 或路由 | 更新 API 页面、路由索引和权限边界 | +| 新增前端页面 | 更新前端应用页和前端路由索引 | +| 配置项变化 | 更新环境变量总表和部署文档 | +| 状态机或计数口径变化 | 更新对应参考索引和最小回归矩阵 | + +详细维护规则见 [文档维护规范](/guide/documentation-maintenance)。 diff --git a/docs-site/reference/env-vars.md b/docs-site/reference/env-vars.md index 3810e1e08..28663396f 100644 --- a/docs-site/reference/env-vars.md +++ b/docs-site/reference/env-vars.md @@ -1,176 +1,16 @@ -# 环境变量与配置索引 +# 环境变量索引 -本页列出 Code Nest 后端所有可通过环境变量覆盖的配置项,以及各 profile 的差异。前端项目的环境变量见各 `vue3-*-front/` 目录下的 `.env*` 文件。 +环境变量的权威维护页已经统一到 [环境变量总表](/operations/env-vars)。本页保留为参考入口,避免旧链接失效。 -## 配置加载顺序 +## 快速入口 -```text -application.yml ← 基础配置(port、MyBatis、Sa-Token 公共项、AI、社区、OJ) - └─ application-{profile}.yml ← profile 覆盖(MySQL/Redis 地址、凭据) - └─ application-sec.yml ← 密钥覆盖(JWT secret、AI API key 等) -``` - -当前默认 profile 为 `dev`。激活方式: - -```bash -# 开发 -java -jar app.jar - -# Docker -java -jar app.jar --spring.profiles.active=docker - -# 生产 -java -jar app.jar --spring.profiles.active=prod - -# 安全密钥 -java -jar app.jar --spring.profiles.active=prod,sec -``` - -## 环境变量完整表 - -### 数据库与 Redis - -| 变量名 | 对应配置 | 默认值(Docker profile) | 说明 | -| --- | --- | --- | --- | -| `XIAOU_MYSQL_URL` | `spring.datasource.url` | `jdbc:mysql://mysql:3306/code_nest?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8` | 含主机、端口、库名、时区、字符集 | -| `XIAOU_MYSQL_USERNAME` | `spring.datasource.username` | `root` | MySQL 用户名 | -| `XIAOU_MYSQL_PASSWORD` | `spring.datasource.password` | `123456` | **生产环境必须覆盖** | -| `XIAOU_REDIS_ADDRESS` | `spring.data.redis.redisson.config` → `address` | `redis://redis:6379` | Redisson 连接地址 | -| `XIAOU_REDIS_DATABASE` | `spring.data.redis.redisson.config` → `database` | `3` | 业务数据 Redis DB | -| `XIAOU_SA_TOKEN_REDIS_HOST` | `sa-token.alone-redis.host` | `redis` | Sa-Token 独立 Redis 主机 | -| `XIAOU_SA_TOKEN_REDIS_PORT` | `sa-token.alone-redis.port` | `6379` | Sa-Token 独立 Redis 端口 | -| `XIAOU_SA_TOKEN_REDIS_DATABASE` | `sa-token.alone-redis.database` | `4` | Sa-Token Redis DB(与业务 DB 隔离) | -| `XIAOU_SA_TOKEN_REDIS_TIMEOUT` | `sa-token.alone-redis.timeout` | `10000` | Sa-Token Redis 连接超时(ms) | -| `XIAOU_SA_TOKEN_REDIS_POOL_MAX_ACTIVE` | `sa-token.alone-redis.jedis.pool.max-active` | `8` | 连接池最大连接数 | -| `XIAOU_SA_TOKEN_REDIS_POOL_MAX_IDLE` | `sa-token.alone-redis.jedis.pool.max-idle` | `8` | 连接池最大空闲 | -| `XIAOU_SA_TOKEN_REDIS_POOL_MIN_IDLE` | `sa-token.alone-redis.jedis.pool.min-idle` | `0` | 连接池最小空闲 | -| `XIAOU_SA_TOKEN_REDIS_POOL_MAX_WAIT` | `sa-token.alone-redis.jedis.pool.max-wait` | `3000` | 连接池最大等待(ms) | - -### JWT / Sa-Token 认证 - -| 变量名 | 对应配置 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `XIAOU_JWT_SECRET` | `jwt.secret` | `change-me-code-nest-jwt-secret-at-least-64-bytes-for-dev-only-2026` | **生产环境必须覆盖**,HS512 要求 ≥64 字节 | - -> 注意:Sa-Token 的 `token-name`、`timeout`、`activity-timeout` 等配置当前在 `application.yml` 中硬编码,不支持环境变量覆盖。如需调整,需修改 YAML 或新增 `@Value` 入口。 - -### AI 模块 - -| 变量名 | 对应配置 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `XIAOU_AI_PROVIDER` | `xiaou.ai.provider` | `openai-compatible` | AI 提供商标识 | -| `XIAOU_AI_BASE_URL` | `xiaou.ai.base-url` | 空 | AI API 基础 URL | -| `XIAOU_AI_API_KEY` | `xiaou.ai.api-key` | 空 | **生产环境必须覆盖** | -| `XIAOU_AI_CHAT_MODEL` | `xiaou.ai.model.chat` | `gpt-5.4` | Chat 模型名 | -| `XIAOU_AI_MAX_COMPLETION_TOKENS` | `xiaou.ai.model.max-completion-tokens` | `2048` | 单次 completion 上限 | -| `XIAOU_AI_EMBEDDING_MODEL` | `xiaou.ai.model.embedding` | `text-embedding-3-small` | Embedding 模型名 | -| `XIAOU_AI_PRICING_CURRENCY` | `xiaou.ai.pricing.currency` | `USD` | 定价货币 | -| `XIAOU_AI_INPUT_PRICE_PER_MILLION` | `xiaou.ai.pricing.input-per-million` | `0` | 输入单价(每百万 token) | -| `XIAOU_AI_OUTPUT_PRICE_PER_MILLION` | `xiaou.ai.pricing.output-per-million` | `0` | 输出单价(每百万 token) | -| `XIAOU_AI_METRICS_PERSIST_ENABLED` | `xiaou.ai.metrics.persistence.enabled` | `true` | AI 指标持久化开关 | -| `XIAOU_AI_METRICS_REDIS_KEY` | `xiaou.ai.metrics.persistence.redis-key` | `xiaou:ai:runtime:metrics` | AI 指标 Redis Key | -| `XIAOU_AI_RAG_ENABLED` | `xiaou.ai.rag.enabled` | `false` | RAG 开关 | -| `XIAOU_AI_RAG_ENDPOINT` | `xiaou.ai.rag.endpoint` | `http://localhost:18080` | RAG 服务端点 | -| `XIAOU_AI_RAG_API_KEY` | `xiaou.ai.rag.api-key` | 空 | RAG API Key | - -### @Value 注入的应用配置 - -以下配置通过 Java `@Value` 注入,支持环境变量覆盖但不在 YAML 中声明默认值: - -| 配置键 | 注入位置 | 默认值 | 说明 | -| --- | --- | --- | --- | -| `server.port` | StartupApplicationListener | `8080` | `@Value` 回退默认值,实际由 `application.yml` 覆盖为 `9999`(dev)或 `8080`(Docker/prod) | -| `server.servlet.context-path` | StartupApplicationListener | `/api` | 启动日志读取上下文路径 | -| `spring.profiles.active` | StartupApplicationListener | `default` | 启动日志读取 profile | -| `xiaou.cors.allowed-origin-patterns` | CorsConfig / WebSocketConfig | `http://localhost:3000,http://localhost:3001,...` | CORS 允许的来源 | -| `xiaou.chat.rate-limit.enabled` | ChatRateLimitService | `true` | 聊天限流开关 | -| `xiaou.chat.rate-limit.message-limit` | ChatRateLimitService | `8` | 消息限流阈值 | -| `xiaou.chat.rate-limit.message-window-seconds` | ChatRateLimitService | `10` | 消息限流窗口 | -| `xiaou.chat.rate-limit.typing-limit` | ChatRateLimitService | `12` | 输入中限流阈值 | -| `xiaou.chat.rate-limit.typing-window-seconds` | ChatRateLimitService | `10` | 输入中限流窗口 | -| `xiaou.chat.message.max-text-length` | ChatMessageServiceImpl | `1000` | 消息文本最大长度 | -| `xiaou.chat.message.max-image-url-length` | ChatMessageServiceImpl | `1024` | 图片 URL 最大长度 | -| `xiaou.sensitive.source-sync.max-retries` | SensitiveSourceSyncScheduler | `2` | 同步最大重试 | -| `xiaou.sensitive.source-sync.retry-delay-ms` | SensitiveSourceSyncScheduler | `2000` | 重试间隔(ms) | -| `xiaou.sensitive.source-sync.alert-threshold` | SensitiveSourceSyncScheduler | `3` | 告警阈值 | -| `xiaou.sensitive.source-sync.alert-webhook` | SensitiveSourceSyncScheduler | 空 | 告警 Webhook URL | -| `hot-topic.api.base-url` | HotTopicServiceImpl | `http://113.44.190.45:9996/api` | 热榜 API 地址 | - -## Profile 差异对比 - -| 配置项 | dev | docker | prod | -| --- | --- | --- | --- | -| 服务端口 | 9999 | 8080(Dockerfile) | 8080 | -| MySQL 驱动 | P6Spy 代理 | 原生驱动 | 原生驱动 | -| MySQL 主机 | `localhost:3306` | `mysql:3306` | 需覆盖 | -| MySQL 密码 | `1234` | `123456` | 需覆盖 | -| Redis 主机 | `127.0.0.1` | `redis` | 需覆盖 | -| 业务 Redis DB | 3 | 3 | 3 | -| Sa-Token Redis DB | 4 | 4 | 4 | -| Spring Security | 禁用默认用户 | 禁用默认用户 | 禁用默认用户 | -| SQL 日志 | P6Spy 接管 | 无 P6Spy | 无 P6Spy | - -## 生产部署必须覆盖的变量 - -以下变量在默认配置中使用弱密码或占位值,**上线前必须通过环境变量或 `application-sec.yml` 覆盖**: - -| 变量 | 风险等级 | 说明 | -| --- | --- | --- | -| `XIAOU_JWT_SECRET` | **高** | 默认值硬编码在 YAML 中,攻击者可伪造 Token | -| `XIAOU_MYSQL_PASSWORD` | **高** | Docker 默认 `123456`,dev 默认 `1234` | -| `XIAOU_AI_API_KEY` | **高** | 默认为空,AI 功能不可用 | -| `XIAOU_MYSQL_URL` | 中 | 生产 MySQL 地址需覆盖 | -| `XIAOU_REDIS_ADDRESS` | 中 | 生产 Redis 地址需覆盖 | -| `XIAOU_SA_TOKEN_REDIS_HOST` | 中 | 生产 Sa-Token Redis 地址需覆盖 | -| `XIAOU_AI_RAG_API_KEY` | 中 | RAG 功能需 API Key | - -## 安全密钥文件(application-sec.yml) - -`application-sec.yml` 在 `.gitignore` 中被排除,不会被提交到仓库。它通过 `spring.config.import` 加载: - -```yaml -# application-sec.yml 示例 -xiaou: - ai: - provider: openai-compatible - base-url: https://your-ai-endpoint/v1 - api-key: sk-your-real-key - model: - chat: gpt-5.4 - max-completion-tokens: 2048 -``` - -> **不要把真实密钥写在 `application-dev.yml` 或 `application-docker.yml` 中**。这些文件会进入 Git 仓库。 - -## 新增配置项规则 - -1. 需要环境变量覆盖的配置:在 YAML 中使用 `${ENV_VAR:default}` 语法。 -2. 仅需硬编码默认值的配置:直接写在 `application.yml` 或 profile 文件中。 -3. 敏感配置(密码、密钥):放在 `application-sec.yml`,不提交到仓库。 -4. 新增 `@Value` 注入时:同步更新本页的"@Value 注入的应用配置"表。 - -## 源码导航 - -| 文件 | 说明 | -| --- | --- | -| `xiaou-application/src/main/resources/application.yml` | 基础配置(端口、Sa-Token、AI、社区、OJ) | -| `xiaou-application/src/main/resources/application-dev.yml` | 开发环境 MySQL/Redis | -| `xiaou-application/src/main/resources/application-docker.yml` | Docker 环境 MySQL/Redis | -| `xiaou-application/src/main/resources/application-prod.yml` | 生产占位(需覆盖) | -| `xiaou-application/src/main/resources/application-sec.yml` | 安全密钥(不提交) | -| `xiaou-common/src/main/java/**/config/CorsConfig.java` | CORS @Value 注入 | -| `xiaou-chat/.../config/WebSocketConfig.java` | WebSocket CORS @Value | -| `xiaou-chat/.../service/ChatRateLimitService.java` | 限流 @Value 注入 | -| `xiaou-chat/.../service/impl/ChatMessageServiceImpl.java` | 消息限制 @Value | -| `xiaou-sensitive/.../scheduler/SensitiveSourceSyncScheduler.java` | 同步 @Value | -| `xiaou-moyu/.../service/impl/HotTopicServiceImpl.java` | 热榜 API @Value | - - -## 相关文档 - -| 文档 | 说明 | +| 需要查什么 | 去哪里 | | --- | --- | -| [Docker 与服务部署](/operations/docker) | 部署配置 | -| [本地完整启动剧本](/guide/startup-playbook) | 本地启动配置 | -| [架构总览](/architecture/overview) | 技术栈说明 | -| [独立部署](/guide/deploy) | 生产环境部署 | +| MySQL、Redis、Sa-Token | [环境变量总表:数据库与缓存](/operations/env-vars#数据库) | +| AI、RAG、模型和价格 | [环境变量总表:AI 配置](/operations/env-vars#ai-配置) | +| 文件存储和上传限制 | [环境变量总表:文件存储](/operations/env-vars#文件存储) | +| CORS 和 WebSocket 来源 | [环境变量总表:CORS 配置](/operations/env-vars#cors-配置) | +| Docker 完整示例 | [环境变量总表:Docker 专用变量](/operations/env-vars#docker-专用变量) | +| 生产安全检查 | [环境变量总表:安全清单](/operations/env-vars#安全清单) | + +新增或修改配置项时,只更新 `/operations/env-vars`,并在 [文档维护规范](/guide/documentation-maintenance) 的变更检查矩阵中确认关联页面。 diff --git a/docs-site/reference/feature-coverage.md b/docs-site/reference/feature-coverage.md index c139e4150..bc7ccd76d 100644 --- a/docs-site/reference/feature-coverage.md +++ b/docs-site/reference/feature-coverage.md @@ -1,6 +1,6 @@ # 全功能覆盖矩阵 -本页是 v2.2.0 文档站的"覆盖检查表"。每一行至少要能回答:用户从哪里进、后台从哪里管、后端走哪个前缀、核心数据落在哪些表、文档写在哪里。 +本页是现行文档站的功能覆盖检查表。每一行至少要能回答:用户从哪里进、后台从哪里管、后端走哪个前缀、核心数据落在哪些表、文档写在哪里。 ## API 端点统计总览 diff --git a/docs-site/reference/index.md b/docs-site/reference/index.md new file mode 100644 index 000000000..a99396e04 --- /dev/null +++ b/docs-site/reference/index.md @@ -0,0 +1,39 @@ +# 参考索引 + +参考页用于快速查事实,不承担完整教学。需要理解背景和流程时回到指南、架构或模块页。 + +## API 与协议 + +- [术语表](/reference/glossary) +- [API 路由索引](/reference/api-routes) +- [API 调用示例](/reference/api-examples) +- [前端路由索引](/reference/frontend-routes) +- [响应体与错误码](/reference/response-errors) +- [WebSocket 协议](/reference/websocket) + +## 数据与源码 + +- [数据表索引](/reference/database-tables) +- [数据库字段阅读指南](/reference/database-field-guide) +- [环境变量索引](/reference/env-vars) +- [源码地图](/reference/source-map) +- [模块依赖地图](/reference/module-dependencies) + +## 系统设计 + +| 主题 | 文档 | +| --- | --- | +| AI 输出、Prompt 与 RAG | [AI Schema 与治理](/reference/ai-schemas) | +| 登录、角色和资源归属 | [权限注解与角色边界](/reference/permission-boundaries) | +| 异常和降级 | [异常路径与失败态](/reference/failure-paths) | +| 事件和通知 | [事件、通知与回流](/reference/event-backflow-index) | +| 统计与排行 | [统计、排行与计数口径](/reference/statistics-ranking-counts) | +| 幂等和补偿 | [幂等、回滚与补偿](/reference/idempotency-rollbacks-compensation) | +| 前端富文本安全 | [前端渲染安全](/reference/frontend-rendering-security) | +| 状态流转 | [模块状态机与生命周期](/reference/module-state-machines) | + +## 质量保障 + +- [模块最小回归矩阵](/reference/module-regression-matrix) +- [全功能覆盖矩阵](/reference/feature-coverage) +- [文档同步基线](/reference/docs-sync-baseline) diff --git a/docs-site/reference/module-regression-matrix.md b/docs-site/reference/module-regression-matrix.md index cb8cd9b1b..cee161496 100644 --- a/docs-site/reference/module-regression-matrix.md +++ b/docs-site/reference/module-regression-matrix.md @@ -77,7 +77,7 @@ | --- | --- | --- | --- | --- | | [开发者工具](/modules/dev-tools) | 至少一项核心工具输入输出、复制或下载动作、路由打开 | 非法输入、空输入、超长输入 | 前端本地逻辑、浏览器 API | 模块页、[前端路由索引](/reference/frontend-routes)、操作手册 | | [摸鱼工具](/modules/moyu) | 至少一个工具页、一个数据列表或计算器、后台配置或统计 | 空数据、缓存过期、未登录限制或非法输入 | Redis、工具表、定时任务 | 模块页、[数据表索引](/reference/database-tables)、验证记录 | -| [版本历史](/modules/version-history) | 版本列表、详情、后台新增或编辑、状态流转 | 重复版本号、未发布版本公开可见、排序错误 | 版本表、后台权限 | 模块页、[数据表索引](/reference/database-tables)、[v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan)、[按 Git Log 重写版本更新记录](/guide/git-log-release-notes) | +| [版本历史](/modules/version-history) | 版本列表、详情、后台新增或编辑、状态流转 | 重复版本号、未发布版本公开可见、排序错误 | 版本表、后台权限 | 模块页、[数据表索引](/reference/database-tables)、[变更日志](/guide/changelog)、[按 Git Log 重写版本更新记录](/guide/git-log-release-notes) | ## 快速断点设置 diff --git a/docs-site/reference/source-map.md b/docs-site/reference/source-map.md index b7d7fb02f..40b21c16f 100644 --- a/docs-site/reference/source-map.md +++ b/docs-site/reference/source-map.md @@ -12,7 +12,7 @@ Code-Nest/ ├── xiaou-sensitive-api/ # 敏感词 API 模块 (跨模块调用契约) ├── xiaou-user/ # 用户管理模块 ├── xiaou-system/ # 系统管理模块 (管理员、角色、日志) -├── xiaou-ai/ # AI 功能模块 (Spring AI + LangGraph4j) +├── xiaou-ai/ # AI 功能模块 (LangChain4j + LangGraph4j + LlamaIndex) ├── xiaou-interview/ # 面试题库模块 ├── xiaou-mock-interview/ # 模拟面试与求职模块 ├── xiaou-oj/ # 在线判题模块 @@ -283,7 +283,7 @@ xiaou-{module}/ ``` vue3-user-front/ -├── package.json # code-nest-user-desktop v2.2.1 +├── package.json # 用户端包名与发布版本(以文件内容为准) ├── vite.config.js # Vite 配置 (端口 3001, 代理 /api→9999) ├── electron/ # Electron 打包配置 ├── src/ @@ -341,7 +341,7 @@ vue3-user-front/ ``` vue3-admin-front/ -├── package.json # code-nest-admin-desktop v2.2.1 +├── package.json # 管理端包名与发布版本(以文件内容为准) ├── vite.config.js # Vite 配置 (端口 3000, 代理 /api→9999) ├── electron/ # Electron 打包配置 ├── src/ diff --git a/docs-site/roadmap/index.md b/docs-site/roadmap/index.md new file mode 100644 index 000000000..e62a116f6 --- /dev/null +++ b/docs-site/roadmap/index.md @@ -0,0 +1,18 @@ +# 版本与文档历史 + +这里保存文档站建设过程和历史计划,不代表当前待办。当前功能变化以 [变更日志](/guide/changelog) 和 [文档同步基线](/reference/docs-sync-baseline) 为准。 + +## 当前入口 + +- [变更日志](/guide/changelog):查看版本能力变化。 +- [Git Log 版更新记录](/guide/git-log-release-notes):从提交历史理解演进。 +- [文档同步基线](/reference/docs-sync-baseline):确认当前页面对应的分支、提交和源码统计。 + +## 历史计划 + +| 计划 | 状态 | 用途 | +| --- | --- | --- | +| [v2.2.1 文档计划](/roadmap/v2.2.1-docs-plan) | 历史归档 | 记录持续维护机制的建设过程 | +| [v2.2.0 文档计划](/roadmap/v2.2.0-docs-plan) | 历史归档 | 记录独立 VitePress 文档中心的初始建设过程 | + +历史计划保留用于追溯,不再放在一级导航中,也不作为当前版本状态依据。 diff --git a/docs-site/roadmap/v2.2.0-docs-plan.md b/docs-site/roadmap/v2.2.0-docs-plan.md index b65b22dd6..db82c8cb0 100644 --- a/docs-site/roadmap/v2.2.0-docs-plan.md +++ b/docs-site/roadmap/v2.2.0-docs-plan.md @@ -1,5 +1,9 @@ # v2.2.0 文档计划 +::: warning 历史归档 +本页记录独立文档中心的初始建设过程,不再代表当前待办。现行入口请使用 [文档维护规范](/guide/documentation-maintenance)、[文档同步基线](/reference/docs-sync-baseline) 和 [路线图归档](/roadmap/)。 +::: + v2.2.0 的文档目标是把 Code Nest 从“能看 README”升级为“能按角色和模块持续维护的文档中心”。 ## 建设原则 diff --git a/docs-site/roadmap/v2.2.1-docs-plan.md b/docs-site/roadmap/v2.2.1-docs-plan.md index ef123ec0e..81ab12bd5 100644 --- a/docs-site/roadmap/v2.2.1-docs-plan.md +++ b/docs-site/roadmap/v2.2.1-docs-plan.md @@ -1,5 +1,9 @@ # v2.2.1 文档计划 +::: warning 历史归档 +本页记录 `v2.2.1` 时期的文档建设计划。文中的“当前”仅指当时的工作线;现行入口请使用 [文档维护规范](/guide/documentation-maintenance)、[文档同步基线](/reference/docs-sync-baseline) 和 [路线图归档](/roadmap/)。 +::: + `v2.2.1` 不是重新从零搭文档站,而是在 `v2.2.0` 已经完成“独立文档中心”之后,继续把**版本节奏、持续维护和新批次入口**补成可长期接力的形态。 如果说 `v2.2.0` 解决的是“有没有一套能用的文档中心”,那 `v2.2.1` 更关注三件事: diff --git a/docs-site/scripts/audit-docs.mjs b/docs-site/scripts/audit-docs.mjs new file mode 100644 index 000000000..641aec8cd --- /dev/null +++ b/docs-site/scripts/audit-docs.mjs @@ -0,0 +1,111 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const docsRoot = path.resolve(__dirname, '..') +const configPath = path.resolve(docsRoot, '.vitepress', 'config.mts') + +function collectMarkdown(directory) { + const files = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.vitepress') { + continue + } + const fullPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...collectMarkdown(fullPath)) + } else if (entry.isFile() && entry.name.endsWith('.md')) { + files.push(fullPath) + } + } + return files +} + +function routeFor(filePath) { + const relative = path.relative(docsRoot, filePath).replaceAll('\\', '/') + if (relative === 'index.md') { + return '/' + } + if (relative.endsWith('/index.md')) { + return `/${relative.slice(0, -'/index.md'.length)}` + } + return `/${relative.slice(0, -'.md'.length)}` +} + +const errors = [] +const warnings = [] +const markdownFiles = collectMarkdown(docsRoot) +const routes = new Set(markdownFiles.map(routeFor)) +const config = readFileSync(configPath, 'utf8') +const configuredLinks = new Set( + [...config.matchAll(/link:\s*'([^']+)'/g)] + .map(match => match[1].split('#')[0].replace(/\/$/, '') || '/') +) + +for (const route of routes) { + if (route === '/' || route === '/404') { + continue + } + if (!configuredLinks.has(route)) { + errors.push(`页面未加入 VitePress 导航: ${route}`) + } +} + +const requiredIndexes = [ + 'guide/index.md', + 'architecture/index.md', + 'modules/index.md', + 'api/index.md', + 'operations/index.md', + 'manuals/index.md', + 'reference/index.md', + 'roadmap/index.md' +] + +for (const relativePath of requiredIndexes) { + if (!existsSync(path.resolve(docsRoot, relativePath))) { + errors.push(`缺少分类总览页: ${relativePath}`) + } +} + +const staleFacts = [ + { pattern: /v2\.3\.1 项目文档/, message: '首页仍使用 v2.3.1 标题' }, + { pattern: /24 个[^。\n]{0,24}Maven 子模块/, message: '仍存在旧的 24 模块统计' }, + { pattern: /142 张表/, message: '仍存在旧的 142 张表统计' }, + { pattern: /STOMP 协议|STOMP \+ WebSocket|WebSocket \+ STOMP/, message: '聊天室仍被错误描述为 STOMP' }, + { pattern: /Spring AI \+ LangGraph4j|BeanOutputConverter/, message: 'AI Runtime 仍被描述为旧 Spring AI 实现' } +] + +for (const filePath of markdownFiles) { + const content = readFileSync(filePath, 'utf8') + const relative = path.relative(docsRoot, filePath).replaceAll('\\', '/') + for (const stale of staleFacts) { + if (stale.pattern.test(content)) { + errors.push(`${stale.message}: ${relative}`) + } + } + const lines = content.split(/\r?\n/).length + if (lines > 1200) { + warnings.push(`超长页面 (${lines} 行): ${relative}`) + } +} + +if (!config.includes('Code Nest v2.4.1 工程文档中心')) { + errors.push('VitePress description 未标记 v2.4.1 文档中心') +} + +console.log(`[docs-audit] pages=${markdownFiles.length}, configuredLinks=${configuredLinks.size}`) +for (const warning of warnings) { + console.warn(`[docs-audit] warning: ${warning}`) +} +for (const error of errors) { + console.error(`[docs-audit] error: ${error}`) +} + +if (errors.length > 0) { + process.exit(1) +} + +console.log('[docs-audit] passed') diff --git a/docs-site/scripts/generate-docs-sync-baseline.mjs b/docs-site/scripts/generate-docs-sync-baseline.mjs index b7ddbef02..db3832b27 100644 --- a/docs-site/scripts/generate-docs-sync-baseline.mjs +++ b/docs-site/scripts/generate-docs-sync-baseline.mjs @@ -1,5 +1,5 @@ import { execSync } from 'node:child_process' -import { mkdirSync, writeFileSync } from 'node:fs' +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -9,6 +9,40 @@ const docsRoot = path.resolve(__dirname, '..') const repoRoot = path.resolve(docsRoot, '..') const outputPath = path.resolve(docsRoot, '.vitepress', 'cache', 'docs-sync-baseline.json') +function countMarkdownFiles(directory) { + let count = 0 + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.vitepress') { + continue + } + const fullPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + count += countMarkdownFiles(fullPath) + } else if (entry.isFile() && entry.name.endsWith('.md')) { + count += 1 + } + } + return count +} + +function readText(filePath) { + try { + return readFileSync(filePath, 'utf8') + } catch { + return '' + } +} + +const rootPom = readText(path.resolve(repoRoot, 'pom.xml')) +const projectVersion = + rootPom.match(/\s*([^<]+)\s*<\/revision>/)?.[1]?.trim() || + rootPom.match(/\s*(v?\d+\.\d+\.\d+)\s*<\/version>/)?.[1]?.trim() || + 'unknown' +const mavenModules = [...rootPom.matchAll(/([^<]+)<\/module>/g)].length +const databaseSql = readText(path.resolve(repoRoot, 'sql', 'MySql', 'code_nest.sql')) +const databaseTables = [...databaseSql.matchAll(/^\s*CREATE TABLE\b/gim)].length +const documentPages = countMarkdownFiles(docsRoot) + function runGit(command, fallback = '') { try { return execSync(command, { @@ -32,6 +66,7 @@ const status = runGit('git status --porcelain', '') const isDirty = status.length > 0 const baseline = { + projectVersion, branch, upstream, shortSha, @@ -39,6 +74,9 @@ const baseline = { commitCount, commitTime, subject, + documentPages, + mavenModules, + databaseTables, isDirty, workingTree: isDirty ? '有未提交变更' : '工作区已清洁', generatedAt: new Date().toISOString(), From 55f9c9bfb74e20368cf4897d529228a471264c78 Mon Sep 17 00:00:00 2001 From: lzf <3153566913@qq.com> Date: Wed, 15 Jul 2026 09:26:29 +0800 Subject: [PATCH 3/3] chore(release): prepare v2.4.1 --- CHANGELOG.md | 26 +++++++++++++++++++++ README.md | 31 ++++++++++++++++++++++---- RELEASE.md | 29 ++++++++++++++++++++++++ code-nest-design-system/package.json | 2 +- docs-site/guide/changelog.md | 7 +++--- docs-site/modules/version-history.md | 19 ++++++++-------- docs-site/package-lock.json | 4 ++-- docs-site/package.json | 2 +- pom-xml-flattened | 4 ++-- pom.xml | 2 +- vue3-admin-front/package-lock.json | 6 ++--- vue3-admin-front/package.json | 2 +- vue3-user-front/package-lock.json | 6 ++--- vue3-user-front/package.json | 2 +- xiaou-ai/pom-xml-flattened | 4 ++-- xiaou-application/pom-xml-flattened | 12 ++++------ xiaou-blog/pom-xml-flattened | 4 ++-- xiaou-chat/pom-xml-flattened | 4 ++-- xiaou-codepen/pom-xml-flattened | 4 ++-- xiaou-common/pom-xml-flattened | 9 ++++++-- xiaou-community/pom-xml-flattened | 4 ++-- xiaou-filestorage/pom-xml-flattened | 4 ++-- xiaou-flashcard/pom-xml-flattened | 4 ++-- xiaou-interview/pom-xml-flattened | 4 ++-- xiaou-knowledge/pom-xml-flattened | 4 ++-- xiaou-learning-asset/pom-xml-flattened | 4 ++-- xiaou-mock-interview/pom-xml-flattened | 4 ++-- xiaou-moment/pom-xml-flattened | 9 ++++++-- xiaou-moyu/pom-xml-flattened | 4 ++-- xiaou-notification/pom-xml-flattened | 4 ++-- xiaou-oj/pom-xml-flattened | 4 ++-- xiaou-plan/pom-xml-flattened | 4 ++-- xiaou-points/pom-xml-flattened | 4 ++-- xiaou-resume/pom-xml-flattened | 4 ++-- xiaou-sensitive-api/pom-xml-flattened | 4 ++-- xiaou-sensitive/pom-xml-flattened | 4 ++-- xiaou-sql-optimizer/pom-xml-flattened | 4 ++-- xiaou-system/pom-xml-flattened | 4 ++-- xiaou-team/pom-xml-flattened | 4 ++-- xiaou-user-api/pom-xml-flattened | 4 ++-- xiaou-user/pom-xml-flattened | 4 ++-- xiaou-version/pom-xml-flattened | 4 ++-- 42 files changed, 179 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af77e926f..e347e5c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ ## [Unreleased] +## [v2.4.1] - 2026-07-15 + +### Added + +- 新增独立的 `code-nest-design-system` 包,由用户端和管理端共享组件、主题、设计令牌与组合式函数。 +- 新增可注入的 `RedisValueStore` 和 Spring 托管的有界 `applicationIoExecutor`。 +- 新增学习驾驶舱、WebSocket 票据、Redis 值存储和朋友圈核心服务的聚焦回归测试。 +- 重构 VitePress 文档中心,新增分类首页、响应式工程手册主题、动态同步基线和导航完整性审计。 + +### Changed + +- 学习驾驶舱、社区热点与摘要、朋友圈、摸鱼工具、抽奖和验证码等热点链路减少重复查询、缓存往返与无边界并发。 +- 缓存、锁、限流和执行器改为依赖注入或模块自有实现,明确基础设施与领域语义边界。 +- 129 个 Markdown 页面重组为开始、架构、模块、API、运维和资料六个一级入口。 +- 后端 Maven、共享设计系统、管理端前端、用户端前端、文档站和部署示例统一升级到 `v2.4.1`。 + +### Removed + +- 删除双前端内重复的设计系统源码,统一从 `code-nest-design-system` 引用。 +- 删除静态 `RedisUtil`、`ThreadPoolUtils` 和 `ConcurrentUtils`,避免全局状态与无边界资源管理。 + +### Migration + +- 本版本不新增数据库迁移、环境变量或对外 API 兼容性要求。 +- 前端依赖仍通过仓库内 `file:../code-nest-design-system` 安装,无需发布外部 npm 包。 + ## [v2.4.0] - 2026-07-14 ### Added diff --git a/README.md b/README.md index 5c245d693..f29e81e4a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Code Nest -![Version](https://img.shields.io/badge/version-v2.4.0-blue.svg) +![Version](https://img.shields.io/badge/version-v2.4.1-blue.svg) ![Java](https://img.shields.io/badge/java-17-orange.svg) ![Spring Boot](https://img.shields.io/badge/spring%20boot-3.4.4-brightgreen.svg) ![Vue](https://img.shields.io/badge/vue-3.x-4fc08d.svg) @@ -16,6 +16,20 @@ Code Nest 是一个面向开发者的成长型社区与知识运营平台,采 - **vue3-user-front**:面向开发者的用户端,提供刷题、简历制作、动态广场、博客阅读、代码分享、学习资产沉淀、通知消息等场景。 - **xiaou-application**:多模块聚合的 Spring Boot API,整合 `xiaou-*` 业务模块,对外暴露统一的 `/api` 网关、鉴权、日志与监控。 +## v2.4.1 Architecture And Documentation Hardening + +`v2.4.1` 聚焦共享基础设施、热点链路和工程文档治理,在不改变对外 API 与数据库结构的前提下减少重复实现,并补强可验证性。 + +### 本次版本完成 + +- **共享设计系统**:抽取独立的 `code-nest-design-system`,用户端和管理端复用同一套组件、主题、设计令牌与组合式函数。 +- **缓存边界收敛**:新增可注入的 `RedisValueStore`,移除静态 `RedisUtil`,集合、锁和业务语义继续由模块直接使用 Redisson 表达。 +- **线程池治理**:通用 I/O 任务使用 Spring 托管的有界 `applicationIoExecutor`,独立负载保留模块专用执行器,删除静态线程池与通用并发工具。 +- **热点链路优化**:学习驾驶舱、社区热点与摘要、朋友圈、摸鱼工具、抽奖和验证码等链路减少重复查询、缓存往返与无边界并发。 +- **聚焦回归测试**:补充学习驾驶舱、WebSocket 票据、Redis 值存储和朋友圈核心服务测试。 +- **文档中心重构**:129 个 Markdown 页面按开始、架构、模块、API、运维和资料重新组织,并新增响应式主题、动态同步基线与导航审计。 +- **版本基线统一**:Maven、共享设计系统、双前端、文档站、Jar 命令与 Docker 示例统一升级到 `v2.4.1`。 + ## v2.4.0 Unified Backend Agent Runtime `v2.4.0` 将管理员自然语言操作收口为后端统一 Agent 运行时。管理端只调用 `POST /admin/agent/chat`,后端负责 LLM 规划、工具注册、权限与策略、预览、强确认、执行和审计。 @@ -359,7 +373,7 @@ mvn clean package -DskipTests mvn -pl xiaou-application -am spring-boot:run # 或直接运行打包后的 jar -java -jar xiaou-application/target/xiaou-application-v2.4.0.jar --spring.profiles.active=prod +java -jar xiaou-application/target/xiaou-application-v2.4.1.jar --spring.profiles.active=prod ``` - API 根地址:`http://localhost:9999/api` @@ -527,7 +541,7 @@ management: ```bash # 构建镜像 -docker build -t code-nest:v2.4.0 -f docker/Dockerfile . +docker build -t code-nest:v2.4.1 -f docker/Dockerfile . # 运行容器 docker run -d \ @@ -535,7 +549,7 @@ docker run -d \ -p 9999:9999 \ -e SPRING_PROFILES_ACTIVE=prod \ --env-file docker/env/example.env \ - code-nest:v2.4.0 + code-nest:v2.4.1 ``` 如果要把 MySQL / Redis / Java 主服务 / `llamaindex-service` 一起编排起来,推荐使用: @@ -605,6 +619,15 @@ server { 仅列出最近版本,更多历史可查看 `git log`。 +### v2.4.1 Architecture And Documentation Hardening + +- **共享前端基础**:双前端改为复用独立设计系统包,避免组件与主题继续双份漂移。 +- **缓存与并发治理**:使用可注入缓存边界和 Spring 托管执行器替代静态全局工具。 +- **热点链路优化**:学习、社区、朋友圈、摸鱼、抽奖和验证码链路减少重复查询与缓存往返。 +- **测试补强**:为学习驾驶舱、WebSocket 票据、Redis 值存储和朋友圈服务新增回归覆盖。 +- **文档工程化**:129 个页面完成分类重组,VitePress 增加响应式主题、动态基线和自动审计。 +- **版本基线升级**:Maven、共享设计系统、双前端、文档站、Jar 与 Docker 示例统一升级到 `v2.4.1`。 + ### v2.4.0 Unified Backend Agent Runtime - **统一后端编排**:单一聊天接口完成 LLM 规划、工具注册、策略、执行和审计,前端保持薄边界。 diff --git a/RELEASE.md b/RELEASE.md index 9941663c4..ffa745135 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,34 @@ # 发布流程 +## v2.4.1 + +`v2.4.1` 是架构与文档治理补丁版本,重点收敛共享前端基础设施、缓存与线程池边界,优化热点业务链路,并把现行 Markdown 与 VitePress 文档纳入可持续审计。 + +### Highlights + +- 双前端统一复用 `code-nest-design-system`,移除长期漂移的重复组件、主题和设计令牌。 +- 使用 `RedisValueStore`、Spring 托管执行器和模块专用 Redisson 能力替代静态缓存与并发工具。 +- 学习驾驶舱、社区热点与摘要、朋友圈、摸鱼工具、抽奖和验证码等热点链路减少重复查询与缓存往返。 +- 为学习驾驶舱、WebSocket 票据、Redis 值存储和朋友圈核心服务补充聚焦回归测试。 +- 129 个 VitePress 页面完成分类重组,新增响应式主题、暗色模式、动态同步基线和导航完整性审计。 + +### Migration + +- 本版本不新增数据库脚本,不改变对外 API,也不新增生产环境变量。 +- 双前端通过仓库内 `file:../code-nest-design-system` 解析共享包,部署流程继续执行各前端的 `npm ci` 与生产构建即可。 + +### Verification + +- 本地 `scripts/code-nest-eval.ps1 -Tier release` 已通过:生成 `xiaou-application-v2.4.1.jar`,双前端与文档站构建成功,发布脚本语法检查通过。 +- 本地 `scripts/code-nest-eval.ps1 -Tier hygiene` 已通过:空白、密钥占位符和文本 NUL 字节检查无阻断项。 +- GitHub PR 必须通过后端测试与打包、双前端契约测试与构建、文档构建、RAG 测试和仓库卫生检查。 +- VitePress 已覆盖桌面、390px 移动端、暗色模式、搜索、长表格和键盘焦点浏览器验收。 + +### Risks And Rollback + +- 主要风险是共享设计系统本地包解析、缓存 key 行为和异步执行时序;发布门禁覆盖双前端构建及相关后端测试。 +- 本版本无数据库回滚动作;如需回退,使用既有发布备份恢复 `v2.4.0` 后端 Jar 和双前端静态资源。 + ## v2.4.0 `v2.4.0` 发布管理员端统一后端 Agent 运行时。本版本的范围是单一聊天入口和当前 26 个已注册生产工具,不表示尚未注册的所有业务接口都已支持自然语言操作。 diff --git a/code-nest-design-system/package.json b/code-nest-design-system/package.json index dbee9df37..f76934716 100644 --- a/code-nest-design-system/package.json +++ b/code-nest-design-system/package.json @@ -1,6 +1,6 @@ { "name": "@code-nest/design-system", - "version": "2.4.0", + "version": "2.4.1", "private": true, "type": "module", "files": [ diff --git a/docs-site/guide/changelog.md b/docs-site/guide/changelog.md index 71a8b67f6..ec9a57629 100644 --- a/docs-site/guide/changelog.md +++ b/docs-site/guide/changelog.md @@ -12,7 +12,7 @@ | 版本 | 日期 | 主要变更 | |------|------|---------| -| v2.4.1(开发中) | 2026-07-14 | 共享设计系统、缓存与线程池治理、热点链路优化、文档中心重构 | +| v2.4.1 | 2026-07-15 | 共享设计系统、缓存与线程池治理、热点链路优化、文档中心重构 | | v2.4.0 | 2026-07-14 | 管理员 Agent 统一运行时、权限审计与全站质量门禁 | | v2.3.2 | 2026-06-14 | 版本分页与用户端首页首屏修复 | | v2.3.1 | 2026-06-14 | 统一 CI、生产自动部署、服务器备份与回滚 | @@ -26,7 +26,7 @@ --- -## v2.4.1(开发中,2026-07-14) +## v2.4.1 (2026-07-15) ### Added @@ -41,10 +41,11 @@ - 学习驾驶舱、社区热点、朋友圈、摸鱼工具、抽奖和验证码等热点链路收敛缓存访问、批量查询与并发执行方式 - 删除静态 `RedisUtil`、`ThreadPoolUtils` 和 `ConcurrentUtils`,改用依赖注入、模块专用执行器以及领域库原生能力 - 文档导航重组为“开始、架构、模块、API、运维、资料”,历史路线图与现行维护入口分离 +- 后端 Maven、共享设计系统、双前端、文档站和部署示例统一升级到 `v2.4.1` ### Notes -- `v2.4.1` 当前是开发分支,最近正式发布 tag 仍为 `v2.4.0` +- 本版本不新增数据库迁移、环境变量或对外 API 兼容性要求 - 文档站构建前会刷新 [文档同步基线](/reference/docs-sync-baseline) 并运行文档审计 --- diff --git a/docs-site/modules/version-history.md b/docs-site/modules/version-history.md index aef7bbefa..5ea059a0f 100644 --- a/docs-site/modules/version-history.md +++ b/docs-site/modules/version-history.md @@ -19,7 +19,7 @@ | `v2.2.0` ~ `v2.2.2` | 工程质量与文档治理 | Mapper 硬化、前端规范、独立 VitePress 文档站与协作治理 | | `v2.3.0` ~ `v2.3.2` | 生产交付与体验修复 | 双端 UI、统一 CI/CD、自动部署、版本分页和首页首屏修复 | | `v2.4.0` | Agent 平台化 | 管理员 Agent Runtime、工具注册、权限确认、审计与质量门禁 | -| `v2.4.1`(开发中) | 架构与热点链路优化 | 共享设计系统、缓存与线程池治理、批量查询、回归测试与文档中心重构 | +| `v2.4.1` | 架构与热点链路优化 | 共享设计系统、缓存与线程池治理、批量查询、回归测试与文档中心重构 | ## 版本线怎么看 @@ -28,26 +28,27 @@ | 来源 | 用途 | 当前示例 | | --- | --- | --- | | Git 分支 | 正在开发哪条工作线 | `v2.4.1` | -| Maven `revision` 与前端 `package.json` | 当前构建产物声明的版本 | 发布前仍可能保持 `v2.4.0` | -| Git tag | 已形成的正式发布锚点 | `v2.4.0` | +| Maven `revision` 与前端 `package.json` | 当前构建产物声明的版本 | `v2.4.1` | +| Git tag | 已形成的正式发布锚点 | `v2.4.1` | | Git commit / PR merge | 某个版本实际落地了什么 | `bce58a7` 等提交 | | README 与变更日志 | 面向维护者和读者的版本摘要 | [变更日志](/guide/changelog) | | 产品内版本墙 | 面向最终用户的公告 | `/version-history` | -因此不要仅凭分支名宣布发布,也不要因为 package 版本尚未提升就忽略正在进行的开发线。更稳的表达是:**当前开发线为 `v2.4.1`,最近正式发布版本为 `v2.4.0`。** +因此不要仅凭分支名宣布发布,也不要因为 package 版本尚未提升就忽略正在进行的开发线。当前统一口径是:**开发线、构建产物和最近正式发布版本均为 `v2.4.1`。** ## Git 演进视角 下面按真实提交和发布锚点整理;每次构建文档站时,[文档同步基线](/reference/docs-sync-baseline) 会记录当时的分支与提交。 -### `v2.4.1` 当前开发线 +### `v2.4.1` 架构与文档治理发布 `v2.4.1` 从 `v2.4.0` 发布点继续推进,重点不是堆叠新页面,而是减少重复基础设施、修正热点链路和提升可验证性。 | 日期 | 提交或批次 | 含义 | | --- | --- | --- | | 2026-07-14 | `bce58a7` `refactor: complete v2.4.1 optimizations` | 抽取共享设计系统,治理缓存与线程池,优化学习、社区、朋友圈等热点链路并补测试 | -| 2026-07-14 | 文档中心重构批次 | 重组分类与导航,新增工程手册主题、自动同步基线和导航审计 | +| 2026-07-15 | `9169c6d` `docs: reorganize and harden documentation site` | 重组分类与导航,新增工程手册主题、自动同步基线和导航审计 | +| 2026-07-15 | tag `v2.4.1` | 架构、热点链路和文档治理补丁正式发布 | 这一工作线的发布状态和详细变化以 [变更日志](/guide/changelog) 为准。 @@ -215,9 +216,9 @@ 当前仓库状态: -- **当前开发线**:`v2.4.1` -- **当前构建产物版本**:`v2.4.0`,以 Maven `revision` 和各前端 `package.json` 为准 -- **最近正式 tag**:`v2.4.0` +- **当前发布线**:`v2.4.1` +- **当前构建产物版本**:`v2.4.1`,以 Maven `revision` 和各前端 `package.json` 为准 +- **最近正式 tag**:`v2.4.1` - **当前文档基线**:由 [文档同步基线](/reference/docs-sync-baseline) 在构建前动态刷新 ## 产品内版本墙视角 diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index aba779ef1..53cd2f33f 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-nest-docs-site", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-nest-docs-site", - "version": "2.4.0", + "version": "2.4.1", "devDependencies": { "vitepress": "2.0.0-alpha.17" } diff --git a/docs-site/package.json b/docs-site/package.json index 4fcfd113b..cb01bf4ae 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -1,6 +1,6 @@ { "name": "code-nest-docs-site", - "version": "2.4.0", + "version": "2.4.1", "private": true, "type": "module", "scripts": { diff --git a/pom-xml-flattened b/pom-xml-flattened index 4d54bdccf..4bb0684a1 100644 --- a/pom-xml-flattened +++ b/pom-xml-flattened @@ -4,7 +4,7 @@ 4.0.0 com.xiaou Code-Nest - v2.4.0 + v2.4.1 pom xiaou-common @@ -45,7 +45,7 @@ 5.8.35 3.4.4 1.3.0 - v2.4.0 + v2.4.1 1.13.0 1.18.36 17 diff --git a/pom.xml b/pom.xml index fc06a25ae..abfdf762d 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ - v2.4.0 + v2.4.1 UTF-8 17 diff --git a/vue3-admin-front/package-lock.json b/vue3-admin-front/package-lock.json index dbed52a2f..e6fcb0bb1 100644 --- a/vue3-admin-front/package-lock.json +++ b/vue3-admin-front/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-nest-admin-desktop", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-nest-admin-desktop", - "version": "2.4.0", + "version": "2.4.1", "dependencies": { "@antv/g6": "^4.8.24", "@code-nest/design-system": "file:../code-nest-design-system", @@ -43,7 +43,7 @@ }, "../code-nest-design-system": { "name": "@code-nest/design-system", - "version": "2.4.0", + "version": "2.4.1", "peerDependencies": { "@element-plus/icons-vue": "^2.3.1", "element-plus": "^2.4.4", diff --git a/vue3-admin-front/package.json b/vue3-admin-front/package.json index 7c77c9749..634112cb9 100644 --- a/vue3-admin-front/package.json +++ b/vue3-admin-front/package.json @@ -1,6 +1,6 @@ { "name": "code-nest-admin-desktop", - "version": "2.4.0", + "version": "2.4.1", "type": "module", "main": "out/main/index.js", "scripts": { diff --git a/vue3-user-front/package-lock.json b/vue3-user-front/package-lock.json index a77e2dca2..f3a05e6ba 100644 --- a/vue3-user-front/package-lock.json +++ b/vue3-user-front/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-nest-user-desktop", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-nest-user-desktop", - "version": "2.4.0", + "version": "2.4.1", "dependencies": { "@antv/g6": "^4.8.24", "@code-nest/design-system": "file:../code-nest-design-system", @@ -43,7 +43,7 @@ }, "../code-nest-design-system": { "name": "@code-nest/design-system", - "version": "2.4.0", + "version": "2.4.1", "peerDependencies": { "@element-plus/icons-vue": "^2.3.1", "element-plus": "^2.4.4", diff --git a/vue3-user-front/package.json b/vue3-user-front/package.json index 78f4aa999..ff35eed17 100644 --- a/vue3-user-front/package.json +++ b/vue3-user-front/package.json @@ -1,6 +1,6 @@ { "name": "code-nest-user-desktop", - "version": "2.4.0", + "version": "2.4.1", "type": "module", "main": "out/main/index.js", "scripts": { diff --git a/xiaou-ai/pom-xml-flattened b/xiaou-ai/pom-xml-flattened index ff40cea3f..bf78f32f1 100644 --- a/xiaou-ai/pom-xml-flattened +++ b/xiaou-ai/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-ai - v2.4.0 + v2.4.1 统一AI服务模块 - 基于 LangChain4j 和 LangGraph4j 的 AI 场景门面 diff --git a/xiaou-application/pom-xml-flattened b/xiaou-application/pom-xml-flattened index 626ee1261..6b77884a1 100644 --- a/xiaou-application/pom-xml-flattened +++ b/xiaou-application/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-application - v2.4.0 + v2.4.1 xiaou-application @@ -142,14 +142,10 @@ xiaou-oj ${revision} - - org.junit.jupiter - junit-jupiter - test - org.springframework.boot - spring-boot-test + spring-boot-starter-test + test com.mysql diff --git a/xiaou-blog/pom-xml-flattened b/xiaou-blog/pom-xml-flattened index e533dceef..0943f4e48 100644 --- a/xiaou-blog/pom-xml-flattened +++ b/xiaou-blog/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-blog - v2.4.0 + v2.4.1 xiaou-blog 个人博客模块 diff --git a/xiaou-chat/pom-xml-flattened b/xiaou-chat/pom-xml-flattened index 5420b8dd3..39f724a4f 100644 --- a/xiaou-chat/pom-xml-flattened +++ b/xiaou-chat/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-chat - v2.4.0 + v2.4.1 xiaou-chat IM聊天室模块 diff --git a/xiaou-codepen/pom-xml-flattened b/xiaou-codepen/pom-xml-flattened index 345b43b5d..58ff70017 100644 --- a/xiaou-codepen/pom-xml-flattened +++ b/xiaou-codepen/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-codepen - v2.4.0 + v2.4.1 xiaou-codepen 代码共享器模块 - 在线代码编辑、分享和知识变现 diff --git a/xiaou-common/pom-xml-flattened b/xiaou-common/pom-xml-flattened index fc5aa99ac..948b92904 100644 --- a/xiaou-common/pom-xml-flattened +++ b/xiaou-common/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-common - v2.4.0 + v2.4.1 xiaou-common 通用工具模块 @@ -137,5 +137,10 @@ fastjson2 2.0.43 + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/xiaou-community/pom-xml-flattened b/xiaou-community/pom-xml-flattened index 42b57b97a..0076a59a7 100644 --- a/xiaou-community/pom-xml-flattened +++ b/xiaou-community/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-community - v2.4.0 + v2.4.1 xiaou-community 社区模块 diff --git a/xiaou-filestorage/pom-xml-flattened b/xiaou-filestorage/pom-xml-flattened index 071c6f768..7395a61a9 100644 --- a/xiaou-filestorage/pom-xml-flattened +++ b/xiaou-filestorage/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-filestorage - v2.4.0 + v2.4.1 xiaou-filestorage 文件存储模块 diff --git a/xiaou-flashcard/pom-xml-flattened b/xiaou-flashcard/pom-xml-flattened index 91e59f0f6..1e3a1e7e7 100644 --- a/xiaou-flashcard/pom-xml-flattened +++ b/xiaou-flashcard/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-flashcard - v2.4.0 + v2.4.1 xiaou-flashcard 闪卡记忆系统模块 - 基于SM-2间隔重复算法 diff --git a/xiaou-interview/pom-xml-flattened b/xiaou-interview/pom-xml-flattened index cfef873d9..f6fb939a4 100644 --- a/xiaou-interview/pom-xml-flattened +++ b/xiaou-interview/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-interview - v2.4.0 + v2.4.1 面试题管理模块 diff --git a/xiaou-knowledge/pom-xml-flattened b/xiaou-knowledge/pom-xml-flattened index be075b951..484a26f5c 100644 --- a/xiaou-knowledge/pom-xml-flattened +++ b/xiaou-knowledge/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-knowledge - v2.4.0 + v2.4.1 知识图谱模块 diff --git a/xiaou-learning-asset/pom-xml-flattened b/xiaou-learning-asset/pom-xml-flattened index 622882da7..cbd295810 100644 --- a/xiaou-learning-asset/pom-xml-flattened +++ b/xiaou-learning-asset/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-learning-asset - v2.4.0 + v2.4.1 学习资产转化引擎模块 diff --git a/xiaou-mock-interview/pom-xml-flattened b/xiaou-mock-interview/pom-xml-flattened index eaa05b6d1..5e85c1b6f 100644 --- a/xiaou-mock-interview/pom-xml-flattened +++ b/xiaou-mock-interview/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-mock-interview - v2.4.0 + v2.4.1 AI模拟面试模块 diff --git a/xiaou-moment/pom-xml-flattened b/xiaou-moment/pom-xml-flattened index 92f89b4d0..23da3303a 100644 --- a/xiaou-moment/pom-xml-flattened +++ b/xiaou-moment/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-moment - v2.4.0 + v2.4.1 朋友圈模块 @@ -27,5 +27,10 @@ xiaou-sensitive-api ${revision} + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/xiaou-moyu/pom-xml-flattened b/xiaou-moyu/pom-xml-flattened index 7f037047a..12bec8c52 100644 --- a/xiaou-moyu/pom-xml-flattened +++ b/xiaou-moyu/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-moyu - v2.4.0 + v2.4.1 xiaou-moyu 摸鱼工具模块 diff --git a/xiaou-notification/pom-xml-flattened b/xiaou-notification/pom-xml-flattened index c6f837f4c..4b5f2d9a7 100644 --- a/xiaou-notification/pom-xml-flattened +++ b/xiaou-notification/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-notification - v2.4.0 + v2.4.1 xiaou-notification 消息通知模块 diff --git a/xiaou-oj/pom-xml-flattened b/xiaou-oj/pom-xml-flattened index 6c48eeed6..af9621a01 100644 --- a/xiaou-oj/pom-xml-flattened +++ b/xiaou-oj/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-oj - v2.4.0 + v2.4.1 OJ在线判题模块 diff --git a/xiaou-plan/pom-xml-flattened b/xiaou-plan/pom-xml-flattened index 6889c6d7e..25d263033 100644 --- a/xiaou-plan/pom-xml-flattened +++ b/xiaou-plan/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-plan - v2.4.0 + v2.4.1 xiaou-plan 个人计划打卡模块 diff --git a/xiaou-points/pom-xml-flattened b/xiaou-points/pom-xml-flattened index 0dd201f85..e1c9cec76 100644 --- a/xiaou-points/pom-xml-flattened +++ b/xiaou-points/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-points - v2.4.0 + v2.4.1 xiaou-points 积分系统模块 diff --git a/xiaou-resume/pom-xml-flattened b/xiaou-resume/pom-xml-flattened index 58863cfce..c602fde02 100644 --- a/xiaou-resume/pom-xml-flattened +++ b/xiaou-resume/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-resume - v2.4.0 + v2.4.1 在线简历制作后端模块 diff --git a/xiaou-sensitive-api/pom-xml-flattened b/xiaou-sensitive-api/pom-xml-flattened index f0faf3ae1..49ece224d 100644 --- a/xiaou-sensitive-api/pom-xml-flattened +++ b/xiaou-sensitive-api/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-sensitive-api - v2.4.0 + v2.4.1 xiaou-sensitive-api 敏感词模块API接口 diff --git a/xiaou-sensitive/pom-xml-flattened b/xiaou-sensitive/pom-xml-flattened index 32543fadc..45138097f 100644 --- a/xiaou-sensitive/pom-xml-flattened +++ b/xiaou-sensitive/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-sensitive - v2.4.0 + v2.4.1 敏感词过滤模块 diff --git a/xiaou-sql-optimizer/pom-xml-flattened b/xiaou-sql-optimizer/pom-xml-flattened index fcb693756..ccea4d394 100644 --- a/xiaou-sql-optimizer/pom-xml-flattened +++ b/xiaou-sql-optimizer/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-sql-optimizer - v2.4.0 + v2.4.1 慢SQL智能优化模块 diff --git a/xiaou-system/pom-xml-flattened b/xiaou-system/pom-xml-flattened index 45224cf74..8d1085e37 100644 --- a/xiaou-system/pom-xml-flattened +++ b/xiaou-system/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-system - v2.4.0 + v2.4.1 xiaou-system 系统管理模块 diff --git a/xiaou-team/pom-xml-flattened b/xiaou-team/pom-xml-flattened index 1ec0e5873..e8b622b6f 100644 --- a/xiaou-team/pom-xml-flattened +++ b/xiaou-team/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-team - v2.4.0 + v2.4.1 xiaou-team 学习小组模块 diff --git a/xiaou-user-api/pom-xml-flattened b/xiaou-user-api/pom-xml-flattened index f03d81e70..720154ce0 100644 --- a/xiaou-user-api/pom-xml-flattened +++ b/xiaou-user-api/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-user-api - v2.4.0 + v2.4.1 xiaou-user-api 用户服务API模块 - 提供用户相关接口和DTO diff --git a/xiaou-user/pom-xml-flattened b/xiaou-user/pom-xml-flattened index f06c1f741..765f901a3 100644 --- a/xiaou-user/pom-xml-flattened +++ b/xiaou-user/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-user - v2.4.0 + v2.4.1 xiaou-user 用户管理模块 diff --git a/xiaou-version/pom-xml-flattened b/xiaou-version/pom-xml-flattened index 9cba7a645..709cc0474 100644 --- a/xiaou-version/pom-xml-flattened +++ b/xiaou-version/pom-xml-flattened @@ -5,11 +5,11 @@ com.xiaou Code-Nest - v2.4.0 + v2.4.1 com.xiaou xiaou-version - v2.4.0 + v2.4.1 xiaou-version 版本更新历史模块