diff --git a/Makefile b/Makefile
index 2f77d71c..43e46a50 100644
--- a/Makefile
+++ b/Makefile
@@ -170,6 +170,12 @@ build-backend: ## 构建后端
test-backend: ## 运行后端单元测试
cd server && JDK_JAVA_OPTIONS="$(BACKEND_TEST_JAVA_OPTIONS)" ./mvnw test
+build-backend-app: ## 构建 skillhub-app 及其依赖模块
+ cd server && ./mvnw -pl skillhub-app -am clean package -DskipTests
+
+test-backend-app: ## 运行 skillhub-app 及其依赖模块测试
+ cd server && JDK_JAVA_OPTIONS="$(BACKEND_TEST_JAVA_OPTIONS)" ./mvnw -pl skillhub-app -am test
+
build: build-backend build-frontend ## 完整构建前后端
test: test-backend test-frontend ## 运行前后端完整单元测试
diff --git a/README.md b/README.md
index 692be941..7ca391f1 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,20 @@ make dev-all-reset
Run `make help` to see all available commands.
+Useful backend commands:
+
+```bash
+make test
+make test-backend-app
+make build-backend-app
+```
+
+Do not run `./mvnw -pl skillhub-app clean test` directly under `server/`.
+`skillhub-app` depends on sibling modules in the same repo, and a standalone clean build
+can fall back to stale artifacts from the local Maven repository, which surfaces misleading
+`cannot find symbol` and signature-mismatch errors. Use `-am`, or the `make test-backend-app`
+and `make build-backend-app` targets above.
+
For the full development workflow (local dev → staging → PR), see [docs/dev-workflow.md](docs/dev-workflow.md).
### API Contract Sync
diff --git a/README_zh.md b/README_zh.md
index ee2e5b6d..7766464c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -110,6 +110,8 @@ make dev-web # 仅前端
```bash
make help # 显示所有可用命令
make test # 运行后端测试
+make test-backend-app # 运行 skillhub-app 及其依赖模块测试
+make build-backend-app # 构建 skillhub-app 及其依赖模块
make typecheck-web # TypeScript 类型检查
make build-web # 构建前端
make generate-api # 重新生成 OpenAPI 类型
@@ -117,6 +119,8 @@ make generate-api # 重新生成 OpenAPI 类型
./scripts/smoke-test.sh http://localhost:8080 # 运行冒烟测试
```
+说明:不要在 `server/` 下直接执行 `./mvnw -pl skillhub-app clean test`。`skillhub-app` 依赖同仓库的 sibling modules,单独 clean 构建时会回退到本地 Maven 仓库里的旧产物并出现大量 `cannot find symbol` / 签名不匹配错误。需要使用 `-am`,或者直接使用上面的 `make test-backend-app` / `make build-backend-app`。
+
### 项目结构
```
diff --git a/docs/02-domain-model.md b/docs/02-domain-model.md
index 61a82975..20e31621 100644
--- a/docs/02-domain-model.md
+++ b/docs/02-domain-model.md
@@ -359,7 +359,7 @@
| title | varchar(256) | |
| summary | varchar(512) | |
| keywords | varchar(512) | |
-| search_text | text | SKILL.md 正文 + frontmatter 拼接 |
+| search_text | text | `displayName`、`slug`、`summary`,以及 frontmatter 中除 `name` / `description` / `version` 外的字段展开结果 |
| visibility | enum | 冗余,避免搜索时 join |
| status | enum | |
| updated_at | datetime | |
diff --git a/docs/04-search-architecture.md b/docs/04-search-architecture.md
index 4b38a3eb..e40d6da4 100644
--- a/docs/04-search-architecture.md
+++ b/docs/04-search-architecture.md
@@ -68,7 +68,7 @@ WHERE (visibility = 'PUBLIC')
| title | varchar(256) | |
| summary | varchar(512) | |
| keywords | varchar(512) | |
-| search_text | text | SKILL.md 正文 + frontmatter 拼接 |
+| search_text | text | `displayName`、`slug`、`summary`,以及 frontmatter 中除 `name` / `description` / `version` 外的字段展开结果 |
| visibility | enum | 冗余,避免搜索时 join |
| status | enum | |
| updated_at | datetime | |
diff --git a/docs/17-backend-annotation-findings.md b/docs/17-backend-annotation-findings.md
new file mode 100644
index 00000000..30986a43
--- /dev/null
+++ b/docs/17-backend-annotation-findings.md
@@ -0,0 +1,295 @@
+# Backend Structure Findings During Annotation Pass
+
+This document records architecture and structure issues that became consistently visible while enriching backend comments. The goal is to preserve concrete observations discovered during code reading, not to propose a full redesign.
+
+## Status Update (2026-03-19)
+
+This document was re-checked after the refactor branch work for findings 1, 2, and 4.
+
+- Finding 1 is now handled in code.
+- Finding 2 is partially handled in code.
+- Finding 4 is now handled in code.
+
+Validation completed on the standard regression path:
+
+- `make test`
+- backend Maven tests: `208` passed
+- frontend Vitest tests: `61` passed
+
+Double-check notes:
+
+- The admin-user refactor removed an overlapping, unused application service rather than changing the controller-facing workflow owner.
+- The namespace and skill-lifecycle refactors moved orchestration out of controllers, but preserved the same downstream domain-service calls, request parameters, audit fields, response message keys, and mutation response shapes.
+- The security refactor centralized route metadata into one registry, but preserved the same route authorization rules, API-token scope behavior, and CSRF-ignore behavior.
+- `AuthContextFilter` is now scoped to API paths when projecting request attributes. This narrows unnecessary work on non-API requests, but it does not change existing business behavior because `userId` and `userNsRoles` consumers are API-side controllers and interceptors.
+
+## 1. Admin user management is split across overlapping application services
+
+Status: handled on branch `docs/backend-annotation-findings-discussion`
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserAppService.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/service/AdminUserManagementService.java`
+
+Why this stands out:
+
+- Both services sit in the application layer and are named as if they own the same capability.
+- The naming does not make the responsibility boundary obvious to a reader.
+- This increases the chance that new admin-user use cases get placed inconsistently.
+
+Suggested direction:
+
+- Either consolidate them into one application service, or split them with an explicit boundary such as query vs. command, or account governance vs. account operations.
+
+Current state:
+
+- `AdminUserManagementService` has been removed.
+- `UserManagementController` continues to use `AdminUserAppService` as the single application-service entry point.
+- Behavior review found no business-logic drift here because the deleted service had no active controller call path.
+
+## 2. Several controllers still perform orchestration that belongs in application services
+
+Status: partially handled on branch `docs/backend-annotation-findings-discussion`
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/NamespaceController.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/ReviewController.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/PromotionController.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillLifecycleController.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java`
+
+Why this stands out:
+
+- Some controllers coordinate multiple repositories, domain services, request-derived identities, and response assembly in one place.
+- The controller layer is therefore carrying request translation and business workflow orchestration at the same time.
+- This makes endpoint behavior harder to reuse, test, and document consistently.
+
+Suggested direction:
+
+- Move multi-step orchestration into dedicated application services and keep controllers focused on transport concerns.
+
+Current state:
+
+- `NamespaceController` has been slimmed down by moving orchestration into `NamespacePortalQueryAppService` and `NamespacePortalCommandAppService`.
+- `SkillLifecycleController` has been slimmed down by moving orchestration into `SkillLifecycleAppService`.
+- This branch preserved the original domain-service calls and response contracts for the refactored endpoints.
+- `ReviewController`, `PromotionController`, and `ClawHubCompatController` still exhibit the same structural issue and remain future work.
+
+## 3. Compatibility endpoints are tightly coupled to canonical domain and repository internals
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistryFacade.java`
+
+Why this stands out:
+
+- The compatibility layer pulls from repositories, domain services, and DTO-mapping concerns at the same time.
+- The layer is useful, but it is not isolated enough to act as a clean anti-corruption boundary.
+- Changes in canonical read models or publish flows are more likely to leak into compatibility code.
+
+Suggested direction:
+
+- Treat compatibility support as a dedicated adapter layer with narrower upstream contracts and fewer direct repository dependencies.
+
+## 4. Security route policy is spread across configuration and implementation classes
+
+Status: handled on branch `docs/backend-annotation-findings-discussion`
+
+Observed files:
+
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeService.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/AuthContextFilter.java`
+
+Why this stands out:
+
+- Route access rules, token-scope rules, and request-context projection are all related to request authorization, but they are not expressed from one central policy model.
+- A reader has to jump across modules to reconstruct how one API route is actually protected.
+
+Suggested direction:
+
+- Centralize route policy metadata or at least define one authoritative mapping between path patterns, authentication modes, and scope requirements.
+
+Current state:
+
+- Route metadata is now centralized in `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java`.
+- `SecurityConfig`, `ApiTokenScopeService`, and `AuthContextFilter` now depend on that shared registry instead of maintaining separate route lists.
+- Double-check review confirmed that the refactor preserved the previous access model while removing duplication.
+
+## 5. Governance behavior is distributed across multiple services without one clear workflow owner
+
+Observed files:
+
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java`
+
+Why this stands out:
+
+- Governance rules are present in the right domain areas, but the end-to-end moderation and publishing workflow is distributed.
+- Readers need to reconstruct lifecycle rules by navigating several services and controllers.
+
+Suggested direction:
+
+- Keep the domain split, but introduce a clearer workflow owner or workflow-facing facade for governance use cases.
+
+## 6. Search-related read paths are split in a way that is hard to follow at first glance
+
+Observed files:
+
+- `server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java`
+
+Why this stands out:
+
+- The codebase has a sensible separation between search, application assembly, and canonical detail reads, but the naming alone does not make their responsibilities obvious.
+- New contributors may need several passes to understand which service is the authoritative entry point for each read scenario.
+
+Suggested direction:
+
+- Clarify the boundary in naming or package-level docs, especially around "search result assembly" vs. "authoritative skill detail query."
+
+## 7. Event-driven counter maintenance is useful but not yet modeled as a distinct projection concern
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/listener/SkillStarEventListener.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/listener/SkillRatingEventListener.java`
+
+Why this stands out:
+
+- Listeners are maintaining derived counters, which is a legitimate pattern.
+- The projection/update responsibility is implicit rather than explicitly named as a read-model maintenance concern.
+
+Suggested direction:
+
+- Consider naming this area more explicitly as projection maintenance or read-model synchronization if the pattern continues to grow.
+
+## 8. Exception modeling is duplicated across application, domain, and auth layers
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/LocalizedException.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/AuthFlowException.java`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/exception/GlobalExceptionHandler.java`
+
+Why this stands out:
+
+- The codebase uses localized error codes consistently, which is good, but several layers define parallel exception abstractions with overlapping semantics.
+- The global exception handler then has to understand each branch separately.
+- This makes it harder to tell whether a new business error belongs to the app layer, the auth layer, or the shared domain exception model.
+
+Suggested direction:
+
+- Keep layer-specific exception types only where they represent a real boundary, and consider converging on a smaller shared contract for localized API-facing errors.
+
+## 9. Repository and read-model access patterns are mixed across layers
+
+Observed files:
+
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/AdminUserSearchRepository.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/*/*Repository.java`
+- `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/*`
+- `server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java`
+
+Why this stands out:
+
+- Some flows use domain repository ports, some use infra JPA repositories, and some app-layer read logic uses `EntityManager` directly.
+- This is not wrong in itself, but the conventions are not explicit, so contributors have to infer when bypassing the domain port layer is acceptable.
+- The mixed style increases the chance that query behavior and write behavior evolve under different architectural rules.
+
+Suggested direction:
+
+- Define explicit rules for when a use case should depend on domain repository ports, dedicated query repositories, or direct persistence adapters.
+
+## 10. OAuth login behavior is decomposed into many small classes without one visible flow owner
+
+Observed files:
+
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginSuccessHandler.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicyFactory.java`
+
+Why this stands out:
+
+- The current decomposition is modular, but understanding one OAuth login request still requires following state across request resolution, provider-specific claim extraction, access-policy evaluation, account provisioning, and redirect handling.
+- The extension points are good, yet the absence of one flow-oriented facade or documented orchestration path increases onboarding cost.
+
+Suggested direction:
+
+- Keep the provider-specific strategy types, but consider a clearer flow owner or a compact architecture note that names the stages of the OAuth pipeline.
+
+## 11. Some domain repository ports leak Spring Data pagination types
+
+Observed files:
+
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogQueryService.java`
+
+Why this stands out:
+
+- Several domain-facing repository contracts use `Page` and `Pageable` directly.
+- This makes the domain boundary more dependent on Spring Data semantics than on a framework-neutral query model.
+- It is workable, but it weakens the separation between domain contracts and persistence tooling.
+
+Suggested direction:
+
+- Either accept Spring Data as an intentional part of the domain boundary and document that choice, or introduce domain-oriented page/query abstractions where long-term isolation matters.
+
+## 12. The auth module follows a more direct JPA style than the business-domain modules
+
+Observed files:
+
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/ApiTokenRepository.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/IdentityBindingRepository.java`
+- `server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/RoleRepository.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java`
+- `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java`
+
+Why this stands out:
+
+- The auth area usually talks directly to Spring Data JPA repositories over auth entities.
+- The business-domain area more often exposes domain repository ports and implements them through infra adapters.
+- Both styles are valid, but using them side by side without an explicit rationale makes the overall architecture feel uneven.
+
+Suggested direction:
+
+- Decide whether auth is intentionally allowed to stay as a more direct persistence-oriented module, and document that distinction so contributors know which style to apply in new code.
+
+## 13. Many domain objects double as persistence entities instead of being isolated from JPA concerns
+
+Observed files:
+
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/Skill.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/Namespace.java`
+- `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTask.java`
+- `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java`
+- `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceMemberJpaRepository.java`
+
+Why this stands out:
+
+- The codebase often uses the same classes as both domain models and JPA persistence entities.
+- This keeps implementation compact, but it also means persistence annotations, lazy-loading behavior, and storage-driven shape decisions can leak into domain modeling concerns.
+- Combined with the repository-style differences already noted above, the codebase can feel partly domain-driven and partly persistence-driven depending on the module.
+
+Suggested direction:
+
+- If this is an intentional tradeoff, document it clearly as the project's default. Otherwise, consider introducing stronger separation only in areas where persistence concerns are starting to distort domain logic.
+
+## Priority Recommendation
+
+If only a small amount of structural cleanup is feasible, the highest-value items are:
+
+1. Reduce controller orchestration by introducing a few focused application services.
+2. Clarify the admin-user service boundary.
+3. Centralize security route policy so access behavior is easier to reason about.
diff --git a/docs/18-frontend-annotation-findings.md b/docs/18-frontend-annotation-findings.md
new file mode 100644
index 00000000..09a7ad5e
--- /dev/null
+++ b/docs/18-frontend-annotation-findings.md
@@ -0,0 +1,123 @@
+# Frontend Structure Findings During Annotation Pass
+
+This document records concrete structure and architecture issues noticed while enriching comments in the front-end codebase. The goal is to preserve observations that repeatedly affected code readability, not to prescribe a full rewrite.
+
+## 1. The shared query layer has become a cross-feature kitchen sink
+
+Observed files:
+
+- `web/src/shared/hooks/use-skill-queries.ts`
+- `web/src/features/skill/use-skill-detail.ts`
+- `web/src/features/namespace/use-namespace-detail.ts`
+
+Why this stands out:
+
+- One shared hook file currently owns search, skill detail, version reads, namespace membership, publishing, and promotion-related mutations.
+- Several feature modules then re-export pieces of that shared file, which hides the real dependency direction.
+- This makes the boundary between `shared` and `features` feel inverted.
+
+Suggested direction:
+
+- Split the file by feature slice or by backend resource area, and keep feature-facing hooks owned by their feature directories.
+
+## 2. The router is a large centralized registry with route policy mixed into route declarations
+
+Observed files:
+
+- `web/src/app/router.tsx`
+- `web/src/shared/components/role-guard.tsx`
+
+Why this stands out:
+
+- Route creation, auth guards, role checks, search validation, and lazy-loading rules are all declared in one large module.
+- This works, but it increases the cost of changing one route because all route concerns are concentrated in a single file.
+
+Suggested direction:
+
+- Keep one router entry point, but consider splitting route definitions by area such as public, dashboard, admin, and settings.
+
+## 3. Some pages still do too much orchestration instead of delegating to feature-level containers
+
+Observed files:
+
+- `web/src/pages/landing.tsx`
+- `web/src/pages/search.tsx`
+- `web/src/pages/skill-detail.tsx`
+- `web/src/pages/dashboard.tsx`
+
+Why this stands out:
+
+- Several pages coordinate multiple hooks, query invalidation, local UI state, navigation rules, and derived presentation decisions.
+- The page layer is therefore acting as route entry point and business container at the same time.
+
+Suggested direction:
+
+- Move heavier orchestration into feature containers or page-specific hooks so the page files mainly compose them.
+
+## 4. Feature boundaries are uneven across the codebase
+
+Observed files:
+
+- `web/src/features/*`
+- `web/src/shared/hooks/use-skill-queries.ts`
+- `web/src/shared/lib/*`
+
+Why this stands out:
+
+- Some concerns are organized cleanly by feature, while others remain in shared folders even though they are domain-specific.
+- This makes it harder to predict where new logic should live.
+
+Suggested direction:
+
+- Tighten the rule for what qualifies as `shared`: generic UI, generic hooks, and framework glue should stay there; business-specific query logic should usually live under `features`.
+
+## 5. Runtime configuration bootstrapping relies on a global window contract
+
+Observed files:
+
+- `web/src/bootstrap.ts`
+- `web/src/api/client.ts`
+- `web/public/runtime-config.js`
+
+Why this stands out:
+
+- The current approach is pragmatic for deploy-time configuration, but it couples startup and API behavior to a mutable global object on `window`.
+- That contract is easy to miss because its definition is spread across bootstrap and API code.
+
+Suggested direction:
+
+- Keep the mechanism if deploy-time injection is required, but document the lifecycle clearly or wrap it behind a dedicated runtime-config module.
+
+## 6. Some feature modules are only thin re-export layers over shared hooks
+
+Observed files:
+
+- `web/src/features/skill/use-skill-detail.ts`
+- `web/src/features/namespace/use-namespace-detail.ts`
+- `web/src/features/namespace/use-namespace-members.ts`
+- `web/src/features/publish/use-publish-skill.ts`
+
+Why this stands out:
+
+- These files preserve a feature-oriented import path, but they do not own the actual logic.
+- The real behavior still lives in shared modules, which weakens the meaning of the feature boundary.
+
+Suggested direction:
+
+- Either move the implementation into the feature modules or import the shared hooks directly; keeping both layers long term adds indirection without much value.
+
+## 7. Some generic dashboard widgets still contain workflow-specific routing rules
+
+Observed files:
+
+- `web/src/features/governance/governance-inbox.tsx`
+- `web/src/features/governance/governance-notifications.tsx`
+
+Why this stands out:
+
+- These components look presentation-oriented, but they still know how review, promotion, report, and skill routes map onto dashboard URLs.
+- That means route policy is now split between the central router and a few feature widgets.
+
+Suggested direction:
+
+- Consider moving item-to-route resolution into a dedicated governance navigation helper or feature hook, so the visual components stay closer to pure rendering.
diff --git a/document/docs/03-user-guide/discovery/search.md b/document/docs/03-user-guide/discovery/search.md
index 6a457ff9..01da8f73 100644
--- a/document/docs/03-user-guide/discovery/search.md
+++ b/document/docs/03-user-guide/discovery/search.md
@@ -11,8 +11,9 @@ description: 搜索和筛选技能
在搜索框输入关键词,SkillHub 会在以下字段中搜索:
- 技能名称
- 技能描述
-- SKILL.md 正文内容
-- 关键词
+- 技能 slug
+- frontmatter 中除 `name`、`description`、`version` 外的其他字段
+- `keywords` / `tags` 等关键词字段
## 筛选条件
diff --git a/document/i18n/en/docusaurus-plugin-content-docs/current/03-user-guide/discovery/search.md b/document/i18n/en/docusaurus-plugin-content-docs/current/03-user-guide/discovery/search.md
index f0c1b3d4..825ef714 100644
--- a/document/i18n/en/docusaurus-plugin-content-docs/current/03-user-guide/discovery/search.md
+++ b/document/i18n/en/docusaurus-plugin-content-docs/current/03-user-guide/discovery/search.md
@@ -11,8 +11,9 @@ description: Search and filter skills
Enter keywords in the search box, SkillHub searches in the following fields:
- Skill name
- Skill description
-- SKILL.md body content
-- Keywords
+- Skill slug
+- Frontmatter fields other than `name`, `description`, and `version`
+- Keyword-style fields such as `keywords` and `tags`
## Filter Conditions
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/SkillhubApplication.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/SkillhubApplication.java
index 38f33ec6..adb56bca 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/SkillhubApplication.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/SkillhubApplication.java
@@ -5,6 +5,9 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
+/**
+ * Main Spring Boot entry point for the SkillHub backend application.
+ */
@SpringBootApplication
@EnableConfigurationProperties(ProfileModerationProperties.class)
public class SkillhubApplication {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/BootstrapAdminProperties.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/BootstrapAdminProperties.java
index 11aadcdf..d3de35df 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/BootstrapAdminProperties.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/BootstrapAdminProperties.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.bootstrap;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
+/**
+ * Configuration properties for bootstrapping a default admin account in controlled environments.
+ */
@Component
@ConfigurationProperties(prefix = "skillhub.bootstrap.admin")
public class BootstrapAdminProperties {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/LocalDevDataInitializer.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/LocalDevDataInitializer.java
index b78a871c..6755beed 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/LocalDevDataInitializer.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/LocalDevDataInitializer.java
@@ -20,6 +20,9 @@ import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Seeds predictable users, memberships, and admin roles for the local development profile.
+ */
@Component
@Profile("local")
public class LocalDevDataInitializer implements ApplicationRunner {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/package-info.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/package-info.java
new file mode 100644
index 00000000..7a1e2f4b
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/bootstrap/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Startup initializers that prepare local development data and required system
+ * accounts before the application begins serving traffic.
+ */
+package com.iflytek.skillhub.bootstrap;
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java
index dbd179f4..e689c0c8 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubCompatController.java
@@ -43,6 +43,10 @@ import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
+/**
+ * Compatibility controller that exposes SkillHub content using ClawHub-style routes and payload
+ * shapes expected by legacy clients.
+ */
@RestController
@RequestMapping("/api/v1")
public class ClawHubCompatController {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistryFacade.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistryFacade.java
index 1a697f18..b7fac835 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistryFacade.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistryFacade.java
@@ -22,6 +22,10 @@ import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Component;
+/**
+ * Facade that assembles registry-style compatibility responses from the platform's canonical search
+ * and skill services.
+ */
@Component
public class ClawHubRegistryFacade {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistrySecurityConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistrySecurityConfig.java
index 66a632e9..bbc43dd1 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistrySecurityConfig.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/ClawHubRegistrySecurityConfig.java
@@ -7,6 +7,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
+/**
+ * Declares a dedicated stateless security chain for public compatibility endpoints used by
+ * registry-style clients.
+ */
@Configuration
public class ClawHubRegistrySecurityConfig {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/SkillCoordinate.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/SkillCoordinate.java
index f1077287..b5b462c2 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/SkillCoordinate.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/SkillCoordinate.java
@@ -1,3 +1,6 @@
package com.iflytek.skillhub.compat;
+/**
+ * Canonical namespace-and-slug pair used by compatibility adapters to address one skill.
+ */
public record SkillCoordinate(String namespace, String slug) {}
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java
index 4ddfb79d..bc505f49 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java
@@ -5,6 +5,9 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
+/**
+ * Serves well-known compatibility metadata used by external clients to discover the API base.
+ */
@RestController
public class WellKnownController {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/package-info.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/package-info.java
new file mode 100644
index 00000000..6b9d1a59
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/dto/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * DTOs dedicated to compatibility controllers so legacy response contracts do
+ * not leak into the primary application API surface.
+ */
+package com.iflytek.skillhub.compat.dto;
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/package-info.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/package-info.java
new file mode 100644
index 00000000..3b2ae1cb
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Compatibility endpoints and helpers that expose SkillHub data using
+ * conventions expected by external or legacy clients.
+ */
+package com.iflytek.skillhub.compat;
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java
index b7b9086e..8e921cd4 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/AsyncConfig.java
@@ -8,6 +8,10 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
+/**
+ * Enables asynchronous event handling and other background execution features used by the
+ * application module.
+ */
@Configuration
@EnableAsync
public class AsyncConfig {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java
index 78fa6199..7b36265f 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/DomainBeanConfig.java
@@ -8,6 +8,10 @@ import org.springframework.context.annotation.Configuration;
import java.time.Clock;
+/**
+ * Wires application-level Spring beans that adapt configurable infrastructure into domain-facing
+ * ports.
+ */
@Configuration
public class DomainBeanConfig {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/OpenApiConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/OpenApiConfig.java
index 214f7322..c285b676 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/OpenApiConfig.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/OpenApiConfig.java
@@ -8,6 +8,9 @@ import org.springframework.context.annotation.Configuration;
import java.util.List;
+/**
+ * OpenAPI metadata configuration for generated API documentation.
+ */
@Configuration
public class OpenApiConfig {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/WebMvcRateLimitConfig.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/WebMvcRateLimitConfig.java
index ca84cebd..0a7bfd93 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/WebMvcRateLimitConfig.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/WebMvcRateLimitConfig.java
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+/**
+ * Registers MVC interceptors related to request rate limiting.
+ */
@Configuration
public class WebMvcRateLimitConfig implements WebMvcConfigurer {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/package-info.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/package-info.java
new file mode 100644
index 00000000..2a70c248
--- /dev/null
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/config/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Spring configuration properties and lightweight application wiring for the
+ * web layer.
+ */
+package com.iflytek.skillhub.config;
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AccountMergeController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AccountMergeController.java
index 7e812f96..b7e1386e 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AccountMergeController.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AccountMergeController.java
@@ -16,6 +16,10 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+/**
+ * Endpoints for initiating, verifying, and confirming account merge flows
+ * across multiple identities owned by the same user.
+ */
@RestController
@RequestMapping("/api/v1/account/merge")
public class AccountMergeController extends BaseApiController {
diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java
index 76b62a92..1552f6f2 100644
--- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java
+++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java
@@ -38,6 +38,13 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Authentication-facing HTTP endpoints.
+ *
+ *
This controller keeps transport concerns at the boundary and delegates the
+ * actual authentication, session bootstrap, and direct-login workflows to
+ * dedicated application or auth services.
+ */
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController extends BaseApiController {
@@ -68,6 +75,10 @@ public class AuthController extends BaseApiController {
this.userAccountRepository = userAccountRepository;
}
+ /**
+ * Returns the current authenticated principal and refreshes the session if
+ * the persisted user state has diverged from the in-session snapshot.
+ */
@GetMapping("/me")
public ApiResponse me(@AuthenticationPrincipal PlatformPrincipal principal,
Authentication authentication,
@@ -103,18 +114,32 @@ public class AuthController extends BaseApiController {
return ok("response.success.read", AuthMeResponse.from(principal));
}
+ /**
+ * Lists browser-based authentication providers that can initiate an OAuth
+ * login flow for the current client.
+ */
@GetMapping("/providers")
public ApiResponse> providers(
@RequestParam(name = "returnTo", required = false) String returnTo) {
return ok("response.success.read", authMethodCatalog.listOAuthProviders(returnTo));
}
+ /**
+ * Lists all authentication methods exposed to the UI, including direct and
+ * OAuth-based flows.
+ */
@GetMapping("/methods")
public ApiResponse> methods(
@RequestParam(name = "returnTo", required = false) String returnTo) {
return ok("response.success.read", authMethodCatalog.listMethods(returnTo));
}
+ /**
+ * Rebuilds an authenticated session from an upstream identity assertion.
+ *
+ *
State is stored in Redis so the browser authorization step and token
+ * polling step can rendezvous without holding server-side session state.
+ */
@Service
public class DeviceAuthService {
@@ -38,6 +44,10 @@ public class DeviceAuthService {
this.verificationUri = verificationUri;
}
+ /**
+ * Starts a new device flow and returns both the polling token and the
+ * user-facing verification code.
+ */
public DeviceCodeResponse generateDeviceCode() {
String deviceCode = generateRandomDeviceCode();
String userCode = generateUserCode();
@@ -52,6 +62,9 @@ public class DeviceAuthService {
return new DeviceCodeResponse(deviceCode, userCode, verificationUri, EXPIRES_IN_SECONDS, POLL_INTERVAL_SECONDS);
}
+ /**
+ * Marks a user code as authorized by a concrete authenticated user.
+ */
public void authorizeDeviceCode(String userCode, String userId) {
String deviceCode = (String) redisTemplate.opsForValue().get(USER_CODE_PREFIX + userCode);
if (deviceCode == null) {
@@ -79,6 +92,10 @@ public class DeviceAuthService {
}
}
+ /**
+ * Polls the device code and either returns a pending response or redeems it
+ * into an API token exactly once.
+ */
public DeviceTokenResponse pollToken(String deviceCode) {
DeviceCodeData data = (DeviceCodeData) redisTemplate.opsForValue().get(DEVICE_CODE_PREFIX + deviceCode);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/package-info.java
new file mode 100644
index 00000000..ca9aba63
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/device/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Device authorization flow support used by CLI and headless clients.
+ */
+package com.iflytek.skillhub.auth.device;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/LocalDirectAuthProvider.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/LocalDirectAuthProvider.java
index 181b85a3..f0406c99 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/LocalDirectAuthProvider.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/LocalDirectAuthProvider.java
@@ -4,6 +4,9 @@ import com.iflytek.skillhub.auth.local.LocalAuthService;
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import org.springframework.stereotype.Component;
+/**
+ * Direct-auth provider that delegates username and password verification to the local auth flow.
+ */
@Component
public class LocalDirectAuthProvider implements DirectAuthProvider {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/package-info.java
new file mode 100644
index 00000000..9905d8cb
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/direct/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Pluggable direct-login abstractions used by local or enterprise login
+ * experiences that bypass OAuth browser redirects.
+ */
+package com.iflytek.skillhub.auth.direct;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/entity/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/entity/package-info.java
new file mode 100644
index 00000000..3c85a796
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/entity/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Authentication and authorization persistence entities such as tokens,
+ * identity bindings, roles, and grants.
+ */
+package com.iflytek.skillhub.auth.entity;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/AuthFlowException.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/AuthFlowException.java
index b2127c80..4190ae7e 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/AuthFlowException.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/AuthFlowException.java
@@ -2,6 +2,10 @@ package com.iflytek.skillhub.auth.exception;
import org.springframework.http.HttpStatus;
+/**
+ * Auth-layer exception that carries both an HTTP status and a localized message code for API
+ * rendering.
+ */
public class AuthFlowException extends RuntimeException {
private final HttpStatus status;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/package-info.java
new file mode 100644
index 00000000..37aa599f
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/exception/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Exceptions that model expected authentication flow failures and map cleanly
+ * to HTTP responses.
+ */
+package com.iflytek.skillhub.auth.exception;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java
index d716d4f3..2a4fae8b 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java
@@ -16,6 +16,10 @@ import java.util.UUID;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Resolves external OAuth identities to platform users, creating or updating
+ * bindings and user records as needed.
+ */
@Service
public class IdentityBindingService {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/package-info.java
new file mode 100644
index 00000000..f9e7b72f
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Services that manage the relationship between platform users and external
+ * identity-provider subjects.
+ */
+package com.iflytek.skillhub.auth.identity;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java
index 4fd3fd99..9d378e25 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalAuthService.java
@@ -21,6 +21,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Handles username-and-password registration and login for first-party local
+ * accounts.
+ */
@Service
public class LocalAuthService {
@@ -57,6 +61,10 @@ public class LocalAuthService {
this.clock = clock;
}
+ /**
+ * Registers a new local user, creates the credential record, and ensures
+ * the user is enrolled in the global namespace.
+ */
@Transactional
public PlatformPrincipal register(String username, String password, String email) {
String normalizedUsername = normalizeUsername(username);
@@ -96,6 +104,10 @@ public class LocalAuthService {
return buildPrincipal(user);
}
+ /**
+ * Authenticates a local account and returns the principal snapshot used to
+ * establish a web session.
+ */
@Transactional
public PlatformPrincipal login(String username, String password) {
String normalizedUsername = normalizeUsername(username);
@@ -124,6 +136,9 @@ public class LocalAuthService {
return buildPrincipal(user);
}
+ /**
+ * Changes the stored password for an already authenticated local account.
+ */
@Transactional
public void changePassword(String userId, String currentPassword, String newPassword) {
LocalCredential credential = credentialRepository.findByUserId(userId)
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java
index ffb65668..8346b9c2 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/LocalCredentialRepository.java
@@ -4,6 +4,9 @@ import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
+/**
+ * JPA repository for username-password credentials linked to platform user accounts.
+ */
@Repository
public interface LocalCredentialRepository extends JpaRepository {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/PasswordPolicyValidator.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/PasswordPolicyValidator.java
index 1afcb86f..aca8baf3 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/PasswordPolicyValidator.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/PasswordPolicyValidator.java
@@ -4,6 +4,9 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
+/**
+ * Validates local-account passwords against the platform's length and character diversity rules.
+ */
@Component
public class PasswordPolicyValidator {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/package-info.java
new file mode 100644
index 00000000..7aa1200b
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/local/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Username-and-password authentication support, including registration,
+ * password changes, and local credential validation.
+ */
+package com.iflytek.skillhub.auth.local;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeRequestRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeRequestRepository.java
index 8c98fb3c..8638cad5 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeRequestRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeRequestRepository.java
@@ -4,6 +4,9 @@ import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
+/**
+ * JPA repository for pending account-merge requests between two platform identities.
+ */
@Repository
public interface AccountMergeRequestRepository extends JpaRepository {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeService.java
index 92368699..20fc4080 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/AccountMergeService.java
@@ -32,6 +32,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Coordinates account merge requests and consolidates credentials, bindings,
+ * roles, memberships, and tokens into a single primary user.
+ */
@Service
public class AccountMergeService {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/package-info.java
new file mode 100644
index 00000000..61aea59e
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/merge/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Account-merge workflows for consolidating multiple authentication identities
+ * into a single platform user.
+ */
+package com.iflytek.skillhub.auth.merge;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/MockAuthFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/MockAuthFilter.java
index a0ab03ae..8d42ed5a 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/MockAuthFilter.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/MockAuthFilter.java
@@ -21,6 +21,9 @@ import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Local-development filter that can establish a session for a requested mock user header.
+ */
@Component
@Profile("local")
@ConditionalOnProperty(name = "skillhub.auth.mock.enabled", havingValue = "true")
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/package-info.java
new file mode 100644
index 00000000..a2c9e502
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/mock/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Development-only authentication helpers that simulate authenticated callers
+ * in local environments.
+ */
+package com.iflytek.skillhub.auth.mock;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java
index a2dd1b19..962165ec 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.oauth;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
+/**
+ * OAuth authentication exception raised when the mapped platform account is disabled.
+ */
public class AccountDisabledException extends OAuth2AuthenticationException {
public AccountDisabledException() {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java
index a5cbaac6..9a7ccef9 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.oauth;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
+/**
+ * OAuth authentication exception raised when the mapped platform account is pending approval.
+ */
public class AccountPendingException extends OAuth2AuthenticationException {
public AccountPendingException() {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java
index 774d4bef..2b013f47 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java
@@ -22,6 +22,10 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
+/**
+ * Spring Security OAuth user-service bridge that extracts provider claims,
+ * evaluates access policy, and maps the result to a {@link PlatformPrincipal}.
+ */
@Service
public class CustomOAuth2UserService implements OAuth2UserService {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java
index f7e807b9..51b802da 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java
@@ -12,6 +12,10 @@ import java.util.List;
import org.springframework.stereotype.Component;
import java.util.Map;
+/**
+ * Provider-specific claims extractor that enriches GitHub OAuth users with their primary verified
+ * email when necessary.
+ */
@Component
public class GitHubClaimsExtractor implements OAuthClaimsExtractor {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java
index 4f18a23a..4813e13c 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java
@@ -12,6 +12,10 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+/**
+ * Failure handler for OAuth logins that normalizes policy and account-state
+ * failures into predictable user-facing redirects.
+ */
@Component
public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginSuccessHandler.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginSuccessHandler.java
index d0a1d1b2..3df9a237 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginSuccessHandler.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginSuccessHandler.java
@@ -13,6 +13,10 @@ import org.springframework.stereotype.Component;
import java.io.IOException;
+/**
+ * Login success handler that copies the resolved platform principal into the
+ * HTTP session and then redirects to the stored return target.
+ */
@Component
public class OAuth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaims.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaims.java
index 679d1a39..78dc9b80 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaims.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaims.java
@@ -2,6 +2,10 @@ package com.iflytek.skillhub.auth.oauth;
import java.util.Map;
+/**
+ * Normalized identity claims extracted from an OAuth provider before local account decisions are
+ * made.
+ */
public record OAuthClaims(
String provider,
String subject,
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java
index 826b3c9b..64b75ec8 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.oauth;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
+/**
+ * Strategy interface for converting provider-specific OAuth user payloads into normalized claims.
+ */
public interface OAuthClaimsExtractor {
String getProvider();
OAuthClaims extract(OAuth2UserRequest request, OAuth2User oAuth2User);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java
index 6d1fd77f..feb60816 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.auth.oauth;
+/**
+ * Utility methods and constants for safely handling post-login redirect targets in OAuth flows.
+ */
public final class OAuthLoginRedirectSupport {
public static final String SESSION_RETURN_TO_ATTRIBUTE = "skillhub.oauth.returnTo";
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java
index 6b42f593..0e67b325 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/SkillHubOAuth2AuthorizationRequestResolver.java
@@ -6,6 +6,10 @@ import org.springframework.security.oauth2.client.web.DefaultOAuth2Authorization
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;
+/**
+ * OAuth2 authorization request resolver that preserves a sanitized post-login redirect target in
+ * the HTTP session.
+ */
@Component
public class SkillHubOAuth2AuthorizationRequestResolver
implements org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/package-info.java
new file mode 100644
index 00000000..00ac05bc
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * OAuth login adapters, claims extraction, and redirect coordination for
+ * browser-based third-party authentication.
+ */
+package com.iflytek.skillhub.auth.oauth;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessDecision.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessDecision.java
index 1aea1213..2c33fcd3 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessDecision.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessDecision.java
@@ -1,5 +1,9 @@
package com.iflytek.skillhub.auth.policy;
+/**
+ * Possible outcomes when evaluating whether an externally authenticated user may access the
+ * platform.
+ */
public enum AccessDecision {
ALLOW, DENY, PENDING_APPROVAL
}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicy.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicy.java
index f2049308..8ac38ea6 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicy.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicy.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.auth.policy;
import com.iflytek.skillhub.auth.oauth.OAuthClaims;
+/**
+ * Policy contract for deciding whether externally authenticated users may enter the platform.
+ */
public interface AccessPolicy {
AccessDecision evaluate(OAuthClaims claims);
}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicyFactory.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicyFactory.java
index 79602966..b647c92b 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicyFactory.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/AccessPolicyFactory.java
@@ -6,6 +6,9 @@ import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Set;
+/**
+ * Builds the active external-access policy from configuration properties.
+ */
@Configuration
@ConfigurationProperties(prefix = "skillhub.access-policy")
public class AccessPolicyFactory {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/EmailDomainAccessPolicy.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/EmailDomainAccessPolicy.java
index fe9d442f..d688f2a2 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/EmailDomainAccessPolicy.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/EmailDomainAccessPolicy.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.policy;
import com.iflytek.skillhub.auth.oauth.OAuthClaims;
import java.util.Set;
+/**
+ * Access policy that allows login only when the OAuth email belongs to an approved domain.
+ */
public class EmailDomainAccessPolicy implements AccessPolicy {
private final Set allowedDomains;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/OpenAccessPolicy.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/OpenAccessPolicy.java
index c0ef33ba..9febc325 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/OpenAccessPolicy.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/OpenAccessPolicy.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.auth.policy;
import com.iflytek.skillhub.auth.oauth.OAuthClaims;
+/**
+ * Access policy that accepts all OAuth-authenticated users.
+ */
public class OpenAccessPolicy implements AccessPolicy {
@Override
public AccessDecision evaluate(OAuthClaims claims) {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/ProviderAllowlistAccessPolicy.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/ProviderAllowlistAccessPolicy.java
index 2457f123..d44ec348 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/ProviderAllowlistAccessPolicy.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/ProviderAllowlistAccessPolicy.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.policy;
import com.iflytek.skillhub.auth.oauth.OAuthClaims;
import java.util.Set;
+/**
+ * Access policy that limits login to explicitly allowed OAuth providers.
+ */
public class ProviderAllowlistAccessPolicy implements AccessPolicy {
private final Set allowedProviders;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java
new file mode 100644
index 00000000..297de089
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistry.java
@@ -0,0 +1,205 @@
+package com.iflytek.skillhub.auth.policy;
+
+import java.util.List;
+import java.util.Set;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+
+/**
+ * Authoritative route-policy catalog shared by web security configuration,
+ * API-token scope checks, and request-context projection.
+ */
+@Component
+public class RouteSecurityPolicyRegistry {
+
+ private static final List AUTHORIZATION_POLICIES = List.of(
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/health"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/search"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/resolve/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/download/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/providers"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/methods"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/me"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/session/bootstrap"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/direct/login"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/local/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/auth/device/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/api/v1/check"),
+ RouteAuthorizationPolicy.permitAll(null, "/actuator/health"),
+ RouteAuthorizationPolicy.permitAll(null, "/v3/api-docs/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/swagger-ui/**"),
+ RouteAuthorizationPolicy.permitAll(null, "/.well-known/**"),
+ RouteAuthorizationPolicy.roles(null, "/actuator/prometheus", "SUPER_ADMIN", "AUDITOR"),
+ RouteAuthorizationPolicy.authenticated(HttpMethod.GET, "/api/v1/skills/*/star"),
+ RouteAuthorizationPolicy.authenticated(HttpMethod.GET, "/api/v1/skills/*/rating"),
+ RouteAuthorizationPolicy.authenticated(HttpMethod.GET, "/api/web/skills/*/star"),
+ RouteAuthorizationPolicy.authenticated(HttpMethod.GET, "/api/web/skills/*/rating"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/versions"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/versions/*"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/versions/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/versions/*/files"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/versions/*/file"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/resolve"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/tags"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/tags/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/tags/*/files"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/skills/*/*/tags/*/file"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/versions"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/versions/*"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/versions/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/versions/*/files"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/versions/*/file"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/resolve"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/tags"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/tags/*/download"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/tags/*/files"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/skills/*/*/tags/*/file"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/namespaces"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/v1/namespaces/*"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/namespaces"),
+ RouteAuthorizationPolicy.permitAll(HttpMethod.GET, "/api/web/namespaces/*"),
+ RouteAuthorizationPolicy.authenticated(null, "/api/v1/admin/**")
+ );
+
+ private static final List API_TOKEN_POLICIES = List.of(
+ ApiTokenPolicy.allow(null, "/api/v1/health"),
+ ApiTokenPolicy.allow(null, "/api/v1/auth/providers"),
+ ApiTokenPolicy.allow(null, "/api/v1/auth/me"),
+ ApiTokenPolicy.allow(null, "/api/v1/auth/device/**"),
+ ApiTokenPolicy.allow(null, "/api/v1/check"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/whoami"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/search"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/skills"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/skills/**"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/web/skills"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/web/skills/**"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/namespaces"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/namespaces/*"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/web/namespaces"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/web/namespaces/*"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/resolve/**"),
+ ApiTokenPolicy.allow(HttpMethod.GET, "/api/v1/download"),
+ ApiTokenPolicy.allow(null, "/.well-known/**"),
+ ApiTokenPolicy.allow(null, "/actuator/health"),
+ ApiTokenPolicy.allow(null, "/v3/api-docs/**"),
+ ApiTokenPolicy.allow(null, "/swagger-ui/**"),
+ ApiTokenPolicy.require(null, "/api/v1/tokens", "token:manage"),
+ ApiTokenPolicy.require(null, "/api/v1/tokens/**", "token:manage"),
+ ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills", "skill:publish"),
+ ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/skills/*/publish", "skill:publish"),
+ ApiTokenPolicy.require(HttpMethod.POST, "/api/web/skills/*/publish", "skill:publish"),
+ ApiTokenPolicy.require(HttpMethod.POST, "/api/v1/publish", "skill:publish")
+ );
+
+ private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+ public List authorizationPolicies() {
+ return AUTHORIZATION_POLICIES;
+ }
+
+ public ApiTokenAuthorizationDecision authorizeApiToken(String method, String path, Set tokenScopes) {
+ if (!isApiPath(path)) {
+ return ApiTokenAuthorizationDecision.allow();
+ }
+
+ for (ApiTokenPolicy policy : API_TOKEN_POLICIES) {
+ if (!policy.matches(method, path, pathMatcher)) {
+ continue;
+ }
+ if (policy.requiredScope() == null || tokenScopes.contains(policy.requiredScope())) {
+ return ApiTokenAuthorizationDecision.allow();
+ }
+ return ApiTokenAuthorizationDecision.missingScope(policy.requiredScope());
+ }
+
+ return ApiTokenAuthorizationDecision.unsupported(path);
+ }
+
+ public boolean shouldIgnoreCsrf(String path, String authorizationHeader) {
+ if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
+ return true;
+ }
+ if (path == null) {
+ return false;
+ }
+ return path.startsWith("/api/")
+ || path.equals("/api/v1/publish")
+ || path.startsWith("/api/v1/auth/device/");
+ }
+
+ public boolean shouldProjectRequestContext(String path) {
+ return path != null && (path.startsWith("/api/v1/")
+ || path.startsWith("/api/web/")
+ || path.startsWith("/api/"));
+ }
+
+ private boolean isApiPath(String path) {
+ return shouldProjectRequestContext(path);
+ }
+
+ public record ApiTokenAuthorizationDecision(boolean allowed, String requiredScope, String message) {
+ public static ApiTokenAuthorizationDecision allow() {
+ return new ApiTokenAuthorizationDecision(true, null, null);
+ }
+
+ public static ApiTokenAuthorizationDecision missingScope(String requiredScope) {
+ return new ApiTokenAuthorizationDecision(false, requiredScope, "Missing API token scope: " + requiredScope);
+ }
+
+ public static ApiTokenAuthorizationDecision unsupported(String path) {
+ return new ApiTokenAuthorizationDecision(false, null, "API token cannot access endpoint: " + path);
+ }
+ }
+
+ public enum AccessLevel {
+ PERMIT_ALL,
+ AUTHENTICATED,
+ ROLE_PROTECTED
+ }
+
+ public record RouteAuthorizationPolicy(HttpMethod method, String pattern, AccessLevel accessLevel, String[] roles) {
+ public static RouteAuthorizationPolicy permitAll(HttpMethod method, String pattern) {
+ return new RouteAuthorizationPolicy(method, pattern, AccessLevel.PERMIT_ALL, new String[0]);
+ }
+
+ public static RouteAuthorizationPolicy authenticated(HttpMethod method, String pattern) {
+ return new RouteAuthorizationPolicy(method, pattern, AccessLevel.AUTHENTICATED, new String[0]);
+ }
+
+ public static RouteAuthorizationPolicy roles(HttpMethod method, String pattern, String... roles) {
+ return new RouteAuthorizationPolicy(method, pattern, AccessLevel.ROLE_PROTECTED, roles);
+ }
+
+ public RequestMatcher toRequestMatcher() {
+ return method == null
+ ? new AntPathRequestMatcher(pattern)
+ : new AntPathRequestMatcher(pattern, method.name());
+ }
+ }
+
+ private record ApiTokenPolicy(HttpMethod method, String pattern, String requiredScope) {
+ static ApiTokenPolicy allow(HttpMethod method, String pattern) {
+ return new ApiTokenPolicy(method, pattern, null);
+ }
+
+ static ApiTokenPolicy require(HttpMethod method, String pattern, String requiredScope) {
+ return new ApiTokenPolicy(method, pattern, requiredScope);
+ }
+
+ boolean matches(String requestMethod, String requestPath, AntPathMatcher matcher) {
+ if (method != null && (requestMethod == null || !method.name().equalsIgnoreCase(requestMethod))) {
+ return false;
+ }
+ return matcher.match(pattern, requestPath);
+ }
+ }
+}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/SubjectWhitelistAccessPolicy.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/SubjectWhitelistAccessPolicy.java
index daf6acaf..0b9546f5 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/SubjectWhitelistAccessPolicy.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/SubjectWhitelistAccessPolicy.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.policy;
import com.iflytek.skillhub.auth.oauth.OAuthClaims;
import java.util.Set;
+/**
+ * Access policy that only permits a configured set of provider-subject pairs.
+ */
public class SubjectWhitelistAccessPolicy implements AccessPolicy {
private final Set whitelistedSubjects;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/package-info.java
new file mode 100644
index 00000000..7a4d3a21
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/policy/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Access policies that decide whether an external identity is allowed to sign
+ * in to the platform.
+ */
+package com.iflytek.skillhub.auth.policy;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformPrincipal.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformPrincipal.java
index 0dfdf196..67f13102 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformPrincipal.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformPrincipal.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.auth.rbac;
import java.io.Serializable;
import java.util.Set;
+/**
+ * Serializable authenticated principal shared across session, OAuth, and API-token flows.
+ */
public record PlatformPrincipal(
String userId,
String displayName,
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformRoleDefaults.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformRoleDefaults.java
index 2ccfedee..eb34da07 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformRoleDefaults.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/PlatformRoleDefaults.java
@@ -4,6 +4,9 @@ import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
+/**
+ * Utility methods for normalizing platform role sets and ensuring a baseline user role.
+ */
public final class PlatformRoleDefaults {
public static final String DEFAULT_USER_ROLE = "USER";
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/RbacService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/RbacService.java
index 8c94e2ee..9aa4a185 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/RbacService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/RbacService.java
@@ -11,6 +11,10 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Resolves platform roles and permissions for a user from persisted RBAC
+ * bindings.
+ */
@Service
public class RbacService {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/package-info.java
new file mode 100644
index 00000000..a7b73f79
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/rbac/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Role-based access control primitives and services for platform-wide
+ * authorization checks.
+ */
+package com.iflytek.skillhub.auth.rbac;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/ApiTokenRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/ApiTokenRepository.java
index 765c6946..b24c840c 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/ApiTokenRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/ApiTokenRepository.java
@@ -8,6 +8,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * JPA repository for personal API tokens and token listings scoped to one user.
+ */
@Repository
public interface ApiTokenRepository extends JpaRepository {
Optional findByTokenHash(String tokenHash);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/IdentityBindingRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/IdentityBindingRepository.java
index 12f48f8f..22499604 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/IdentityBindingRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/IdentityBindingRepository.java
@@ -5,6 +5,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
+/**
+ * JPA repository for links between platform users and external identity-provider subjects.
+ */
@Repository
public interface IdentityBindingRepository extends JpaRepository {
Optional findByProviderCodeAndSubject(String providerCode, String subject);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/RoleRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/RoleRepository.java
index 2fcc3a46..e858f067 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/RoleRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/RoleRepository.java
@@ -5,6 +5,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
+/**
+ * JPA repository for platform roles addressed by their stable code.
+ */
@Repository
public interface RoleRepository extends JpaRepository {
Optional findByCode(String code);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java
index 84e5bd37..162ed4fd 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/UserRoleBindingRepository.java
@@ -7,6 +7,9 @@ import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
+/**
+ * JPA repository for direct user-to-role assignments in the RBAC model.
+ */
@Repository
public interface UserRoleBindingRepository extends JpaRepository {
List findByUserId(String userId);
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/package-info.java
new file mode 100644
index 00000000..0225b70f
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/repository/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Repository ports for authentication persistence concerns.
+ */
+package com.iflytek.skillhub.auth.repository;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/PlatformSessionService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/PlatformSessionService.java
index 01332a6d..3908ab71 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/PlatformSessionService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/PlatformSessionService.java
@@ -10,13 +10,25 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.stereotype.Service;
+/**
+ * Synchronizes {@link PlatformPrincipal} snapshots with Spring Security's
+ * session-backed authentication context.
+ */
@Service
public class PlatformSessionService {
+ /**
+ * Establishes a new authenticated session and rotates the session id to
+ * reduce fixation risk.
+ */
public void establishSession(PlatformPrincipal principal, HttpServletRequest request) {
establishSession(principal, request, true);
}
+ /**
+ * Establishes a session for the supplied principal and optionally rotates
+ * the underlying servlet session id.
+ */
public void establishSession(PlatformPrincipal principal,
HttpServletRequest request,
boolean rotateSessionId) {
@@ -27,6 +39,10 @@ public class PlatformSessionService {
persist(principal, authentication, request, rotateSessionId);
}
+ /**
+ * Rebinds an updated principal to an already authenticated request without
+ * discarding the existing authentication object.
+ */
public void attachToAuthenticatedSession(PlatformPrincipal principal,
Authentication authentication,
HttpServletRequest request) {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/package-info.java
new file mode 100644
index 00000000..6afa5443
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/session/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Services that synchronize authenticated principals with the Spring Security
+ * session model.
+ */
+package com.iflytek.skillhub.auth.session;
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java
index 3b227842..82c0f2c1 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java
@@ -23,6 +23,10 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Authenticates bearer tokens and projects them into a Spring Security
+ * principal with both roles and token scopes.
+ */
@Component
public class ApiTokenAuthenticationFilter extends OncePerRequestFilter {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java
index aea4dfe0..8f69d5aa 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java
@@ -17,6 +17,9 @@ import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
+/**
+ * Enforces fine-grained API token scopes after token authentication has established the principal.
+ */
@Component
public class ApiTokenScopeFilter extends OncePerRequestFilter {
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeService.java
index 69c569c2..29eb6993 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeService.java
@@ -2,60 +2,30 @@ package com.iflytek.skillhub.auth.token;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
import org.springframework.stereotype.Service;
-import org.springframework.util.AntPathMatcher;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
+/**
+ * Parses token scopes and evaluates whether a token may access a given HTTP
+ * method and path combination.
+ */
@Service
public class ApiTokenScopeService {
private static final TypeReference> STRING_LIST = new TypeReference<>() {
};
- private static final List UNSCOPED_ALLOWED_RULES = List.of(
- ScopeRule.allow(null, "/api/v1/health"),
- ScopeRule.allow(null, "/api/v1/auth/providers"),
- ScopeRule.allow(null, "/api/v1/auth/me"),
- ScopeRule.allow(null, "/api/v1/auth/device/**"),
- ScopeRule.allow(null, "/api/v1/check"),
- ScopeRule.allow("GET", "/api/v1/whoami"),
- ScopeRule.allow("GET", "/api/v1/search"),
- ScopeRule.allow("GET", "/api/v1/skills"),
- ScopeRule.allow("GET", "/api/v1/skills/**"),
- ScopeRule.allow("GET", "/api/web/skills"),
- ScopeRule.allow("GET", "/api/web/skills/**"),
- ScopeRule.allow("GET", "/api/v1/namespaces"),
- ScopeRule.allow("GET", "/api/v1/namespaces/*"),
- ScopeRule.allow("GET", "/api/web/namespaces"),
- ScopeRule.allow("GET", "/api/web/namespaces/*"),
- ScopeRule.allow("GET", "/api/v1/search"),
- ScopeRule.allow("GET", "/api/v1/resolve/**"),
- ScopeRule.allow("GET", "/api/v1/whoami"),
- ScopeRule.allow("GET", "/api/v1/download"),
- ScopeRule.allow(null, "/.well-known/**"),
- ScopeRule.allow(null, "/actuator/health"),
- ScopeRule.allow(null, "/v3/api-docs/**"),
- ScopeRule.allow(null, "/swagger-ui/**")
- );
-
- private static final List REQUIRED_SCOPE_RULES = List.of(
- ScopeRule.require(null, "/api/v1/tokens", "token:manage"),
- ScopeRule.require(null, "/api/v1/tokens/**", "token:manage"),
- ScopeRule.require("POST", "/api/v1/skills", "skill:publish"),
- ScopeRule.require("POST", "/api/v1/skills/*/publish", "skill:publish"),
- ScopeRule.require("POST", "/api/web/skills/*/publish", "skill:publish"),
- ScopeRule.require("POST", "/api/v1/publish", "skill:publish"),
- ScopeRule.require("POST", "/api/v1/publish", "skill:publish")
- );
-
private final ObjectMapper objectMapper;
- private final AntPathMatcher pathMatcher = new AntPathMatcher();
+ private final RouteSecurityPolicyRegistry routeSecurityPolicyRegistry;
- public ApiTokenScopeService(ObjectMapper objectMapper) {
+ public ApiTokenScopeService(ObjectMapper objectMapper,
+ RouteSecurityPolicyRegistry routeSecurityPolicyRegistry) {
this.objectMapper = objectMapper;
+ this.routeSecurityPolicyRegistry = routeSecurityPolicyRegistry;
}
public Set parseScopes(String scopeJson) {
@@ -81,32 +51,17 @@ public class ApiTokenScopeService {
}
public AuthorizationDecision authorize(String method, String path, Set tokenScopes) {
- if (!isApiPath(path)) {
+ RouteSecurityPolicyRegistry.ApiTokenAuthorizationDecision decision =
+ routeSecurityPolicyRegistry.authorizeApiToken(method, path, tokenScopes);
+ if (decision.allowed()) {
return AuthorizationDecision.allow();
}
-
- for (ScopeRule rule : UNSCOPED_ALLOWED_RULES) {
- if (rule.matches(method, path, pathMatcher)) {
- return AuthorizationDecision.allow();
- }
+ if (decision.requiredScope() != null) {
+ return AuthorizationDecision.missingScope(decision.requiredScope());
}
-
- for (ScopeRule rule : REQUIRED_SCOPE_RULES) {
- if (rule.matches(method, path, pathMatcher)) {
- if (tokenScopes.contains(rule.requiredScope())) {
- return AuthorizationDecision.allow();
- }
- return AuthorizationDecision.missingScope(rule.requiredScope());
- }
- }
-
return AuthorizationDecision.unsupported(path);
}
- private boolean isApiPath(String path) {
- return path != null && (path.startsWith("/api/v1/") || path.startsWith("/api/web/") || path.startsWith("/api/"));
- }
-
public record AuthorizationDecision(boolean allowed, String requiredScope, String message) {
public static AuthorizationDecision allow() {
return new AuthorizationDecision(true, null, null);
@@ -120,21 +75,4 @@ public class ApiTokenScopeService {
return new AuthorizationDecision(false, null, "API token cannot access endpoint: " + path);
}
}
-
- private record ScopeRule(String method, String pattern, String requiredScope) {
- static ScopeRule allow(String method, String pattern) {
- return new ScopeRule(method, pattern, null);
- }
-
- static ScopeRule require(String method, String pattern, String requiredScope) {
- return new ScopeRule(method, pattern, requiredScope);
- }
-
- boolean matches(String requestMethod, String requestPath, AntPathMatcher matcher) {
- if (method != null && !method.equalsIgnoreCase(requestMethod)) {
- return false;
- }
- return matcher.match(pattern, requestPath);
- }
- }
}
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenService.java
index 6ffcaa23..d750a860 100644
--- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenService.java
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenService.java
@@ -25,6 +25,9 @@ import java.util.HexFormat;
import java.util.List;
import java.util.Optional;
+/**
+ * Issues, rotates, validates, and revokes API tokens for non-browser clients.
+ */
@Service
public class ApiTokenService {
@@ -42,11 +45,18 @@ public class ApiTokenService {
public record TokenCreateResult(String rawToken, ApiToken entity) {}
+ /**
+ * Creates a token without an explicit expiration timestamp.
+ */
@Transactional
public TokenCreateResult createToken(String userId, String name, String scopeJson) {
return createToken(userId, name, scopeJson, null);
}
+ /**
+ * Creates a new token and returns the raw secret exactly once to the
+ * caller.
+ */
@Transactional
public TokenCreateResult createToken(String userId, String name, String scopeJson, String expiresAt) {
String normalizedName = normalizeName(name);
@@ -78,6 +88,10 @@ public class ApiTokenService {
return rotateToken(userId, name, scopeJson, null);
}
+ /**
+ * Rotates a token name by revoking the previous active token before issuing
+ * a replacement.
+ */
@Transactional
public TokenCreateResult rotateToken(String userId, String name, String scopeJson, String expiresAt) {
String normalizedName = normalizeName(name);
@@ -89,11 +103,18 @@ public class ApiTokenService {
return createToken(userId, name, scopeJson, expiresAt);
}
+ /**
+ * Validates a raw bearer token against its hash and lifecycle timestamps.
+ */
public Optional validateToken(String rawToken) {
String hash = sha256(rawToken);
return tokenRepo.findByTokenHash(hash).filter(token -> token.isValid(currentTime()));
}
+ /**
+ * Revokes a token owned by the current user. Missing or foreign tokens are
+ * ignored to keep revocation idempotent.
+ */
@Transactional
public void revokeToken(Long tokenId, String userId) {
tokenRepo.findById(tokenId)
@@ -104,6 +125,9 @@ public class ApiTokenService {
});
}
+ /**
+ * Updates the expiration timestamp of an active token owned by the caller.
+ */
@Transactional
public ApiToken updateExpiration(Long tokenId, String userId, String expiresAt) {
ApiToken token = tokenRepo.findById(tokenId)
diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/package-info.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/package-info.java
new file mode 100644
index 00000000..4fbcc815
--- /dev/null
+++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * API token issuance, authentication, and scope enforcement for non-browser
+ * clients.
+ */
+package com.iflytek.skillhub.auth.token;
diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java
new file mode 100644
index 00000000..e97961f0
--- /dev/null
+++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java
@@ -0,0 +1,36 @@
+package com.iflytek.skillhub.auth.policy;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class RouteSecurityPolicyRegistryTest {
+
+ private final RouteSecurityPolicyRegistry registry = new RouteSecurityPolicyRegistry();
+
+ @Test
+ void authorizeApiToken_requiresPublishScopeForPublishEndpoints() {
+ var denied = registry.authorizeApiToken("POST", "/api/web/skills/global/publish", Set.of("skill:read"));
+ var allowed = registry.authorizeApiToken("POST", "/api/web/skills/global/publish", Set.of("skill:publish"));
+
+ assertFalse(denied.allowed());
+ assertEquals("skill:publish", denied.requiredScope());
+ assertTrue(allowed.allowed());
+ }
+
+ @Test
+ void shouldIgnoreCsrf_forBearerAndApiPaths() {
+ assertTrue(registry.shouldIgnoreCsrf("/api/v1/admin/users", null));
+ assertTrue(registry.shouldIgnoreCsrf("/not-api", "Bearer token"));
+ assertFalse(registry.shouldIgnoreCsrf("/ui/settings", null));
+ }
+
+ @Test
+ void shouldProjectRequestContext_onlyForApiRoutes() {
+ assertTrue(registry.shouldProjectRequestContext("/api/web/namespaces/team-a"));
+ assertFalse(registry.shouldProjectRequestContext("/assets/index.css"));
+ }
+}
diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilterTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilterTest.java
index f40e542c..e82f7a1a 100644
--- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilterTest.java
+++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilterTest.java
@@ -1,6 +1,7 @@
package com.iflytek.skillhub.auth.token;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
import com.iflytek.skillhub.auth.entity.ApiToken;
import com.iflytek.skillhub.auth.entity.Role;
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
@@ -31,7 +32,8 @@ class ApiTokenAuthenticationFilterTest {
private final ApiTokenService apiTokenService = mock(ApiTokenService.class);
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
private final UserRoleBindingRepository roleBindingRepository = mock(UserRoleBindingRepository.class);
- private final ApiTokenScopeService scopeService = new ApiTokenScopeService(new ObjectMapper());
+ private final ApiTokenScopeService scopeService =
+ new ApiTokenScopeService(new ObjectMapper(), new RouteSecurityPolicyRegistry());
private final ApiTokenAuthenticationFilter filter = new ApiTokenAuthenticationFilter(
apiTokenService,
userAccountRepository,
diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java
index 735fddfc..9156eba8 100644
--- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java
+++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java
@@ -1,6 +1,7 @@
package com.iflytek.skillhub.auth.token;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
@@ -27,7 +28,8 @@ import static org.mockito.Mockito.verify;
class ApiTokenScopeFilterTest {
- private final ApiTokenScopeService scopeService = new ApiTokenScopeService(new ObjectMapper());
+ private final ApiTokenScopeService scopeService =
+ new ApiTokenScopeService(new ObjectMapper(), new RouteSecurityPolicyRegistry());
@AfterEach
void clearSecurityContext() {
diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeServiceTest.java
index 002ec8a7..cc877bf3 100644
--- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeServiceTest.java
+++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeServiceTest.java
@@ -1,6 +1,7 @@
package com.iflytek.skillhub.auth.token;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry;
import org.junit.jupiter.api.Test;
import java.util.Set;
@@ -11,7 +12,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class ApiTokenScopeServiceTest {
- private final ApiTokenScopeService scopeService = new ApiTokenScopeService(new ObjectMapper());
+ private final ApiTokenScopeService scopeService =
+ new ApiTokenScopeService(new ObjectMapper(), new RouteSecurityPolicyRegistry());
@Test
void parseScopesShouldNormalizeJsonArray() {
@@ -74,4 +76,26 @@ class ApiTokenScopeServiceTest {
assertFalse(decision.allowed());
assertEquals("API token cannot access endpoint: /api/v1/me/skills", decision.message());
}
+
+ @Test
+ void authorizeShouldAllowPublicNamespaceReadWithoutScope() {
+ ApiTokenScopeService.AuthorizationDecision decision = scopeService.authorize(
+ "GET",
+ "/api/v1/namespaces/team-a",
+ Set.of()
+ );
+
+ assertTrue(decision.allowed());
+ }
+
+ @Test
+ void authorizeShouldPermitAuthMeWithoutScope() {
+ ApiTokenScopeService.AuthorizationDecision decision = scopeService.authorize(
+ "GET",
+ "/api/v1/auth/me",
+ Set.of()
+ );
+
+ assertTrue(decision.allowed());
+ }
}
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogQueryService.java
index b2f74ef0..a94a1d27 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogQueryService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogQueryService.java
@@ -4,6 +4,9 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
+/**
+ * Read-side service for paginating audit log entries with simple filters.
+ */
@Service
public class AuditLogQueryService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogRepository.java
index bd967a41..b3b80541 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogRepository.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.audit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+/**
+ * Domain repository contract for audit-log persistence and filtered pagination.
+ */
public interface AuditLogRepository {
AuditLog save(AuditLog auditLog);
Page search(String actorUserId, String action, Pageable pageable);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogService.java
index e477abe7..be9ef5f9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/AuditLogService.java
@@ -6,6 +6,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.Instant;
+/**
+ * Records audit log entries for administrative and security-relevant actions.
+ */
@Service
public class AuditLogService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/package-info.java
new file mode 100644
index 00000000..dd3bfe73
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/audit/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Audit-domain abstractions for recording and querying security-sensitive or
+ * governance-relevant actions.
+ */
+package com.iflytek.skillhub.domain.audit;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/event/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/event/package-info.java
new file mode 100644
index 00000000..3107b825
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/event/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Cross-domain integration events published by the core business layer.
+ */
+package com.iflytek.skillhub.domain.event;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/GovernanceNotificationService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/GovernanceNotificationService.java
index 519c84a9..b6a7b8f6 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/GovernanceNotificationService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/GovernanceNotificationService.java
@@ -8,6 +8,9 @@ import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Persists and manages governance notifications delivered to end users.
+ */
@Service
public class GovernanceNotificationService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/UserNotificationRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/UserNotificationRepository.java
index 85563adf..a6f159a8 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/UserNotificationRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/UserNotificationRepository.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.governance;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for user-facing governance notifications.
+ */
public interface UserNotificationRepository {
UserNotification save(UserNotification notification);
Optional findById(Long id);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/package-info.java
new file mode 100644
index 00000000..60f14992
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/governance/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Governance notification models and ports used to inform users about review
+ * and moderation outcomes.
+ */
+package com.iflytek.skillhub.domain.governance;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/IdempotencyRecordRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/IdempotencyRecordRepository.java
index f4c298a3..4fee46c5 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/IdempotencyRecordRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/IdempotencyRecordRepository.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.idempotency;
import java.time.Instant;
import java.util.Optional;
+/**
+ * Domain repository contract for tracking request idempotency state and cleanup operations.
+ */
public interface IdempotencyRecordRepository {
Optional findByRequestId(String requestId);
IdempotencyRecord save(IdempotencyRecord record);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/package-info.java
new file mode 100644
index 00000000..21c65499
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/idempotency/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Domain model for request idempotency tracking across retries of mutating
+ * operations.
+ */
+package com.iflytek.skillhub.domain.idempotency;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java
index 947d414f..1ef7d247 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/GlobalNamespaceMembershipService.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.namespace;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Ensures newly active users belong to the built-in global namespace.
+ */
@Service
public class GlobalNamespaceMembershipService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java
index d798a7d5..5482628a 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicy.java
@@ -2,6 +2,10 @@ package com.iflytek.skillhub.domain.namespace;
import org.springframework.stereotype.Component;
+/**
+ * Encapsulates namespace lifecycle rules that determine which management actions are currently
+ * allowed.
+ */
@Component
public class NamespaceAccessPolicy {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java
index dda4574b..c9db527f 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceGovernanceService.java
@@ -6,6 +6,10 @@ import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Applies namespace lifecycle transitions such as freeze, unfreeze, archive,
+ * and restore while recording audit history.
+ */
@Service
public class NamespaceGovernanceService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberRepository.java
index 6387dab9..a07150f9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberRepository.java
@@ -6,6 +6,9 @@ import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for namespace membership lookups and member administration.
+ */
public interface NamespaceMemberRepository {
Optional findByNamespaceIdAndUserId(Long namespaceId, String userId);
List findByUserId(String userId);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java
index 82c1c068..d8e8bc74 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceMemberService.java
@@ -8,6 +8,10 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
+/**
+ * Manages namespace membership additions, removals, and role changes under the
+ * namespace governance rules.
+ */
@Service
public class NamespaceMemberService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java
index febf4ac0..775206e9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceRepository.java
@@ -6,6 +6,9 @@ import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for namespace aggregates and management-oriented reads.
+ */
public interface NamespaceRepository {
Optional findById(Long id);
List findByIdIn(List ids);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java
index fef5ea3d..32f20fd1 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/NamespaceService.java
@@ -7,6 +7,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
+/**
+ * Domain service for namespace lifecycle and membership-gated mutations.
+ */
@Service
public class NamespaceService {
@@ -22,6 +25,10 @@ public class NamespaceService {
this.namespaceAccessPolicy = namespaceAccessPolicy;
}
+ /**
+ * Creates a team namespace and grants the creator the owner role in the
+ * same transaction.
+ */
@Transactional
public Namespace createNamespace(String slug, String displayName, String description, String creatorUserId) {
SlugValidator.validate(slug);
@@ -41,6 +48,9 @@ public class NamespaceService {
return namespace;
}
+ /**
+ * Updates mutable namespace profile fields after policy and role checks.
+ */
@Transactional
public Namespace updateNamespace(Long namespaceId, String displayName, String description, String avatarUrl,
String operatorUserId) {
@@ -63,11 +73,19 @@ public class NamespaceService {
return namespaceRepository.save(namespace);
}
+ /**
+ * Loads a namespace by slug and fails with a business exception when it is
+ * missing.
+ */
public Namespace getNamespaceBySlug(String slug) {
return namespaceRepository.findBySlug(slug)
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
}
+ /**
+ * Returns archived namespaces only to callers that already belong to them;
+ * all other callers see archived namespaces as not found.
+ */
public Namespace getNamespaceBySlugForRead(String slug, String userId, Map userNsRoles) {
Namespace namespace = getNamespaceBySlug(slug);
if (namespace.getStatus() != NamespaceStatus.ARCHIVED) {
@@ -84,6 +102,9 @@ public class NamespaceService {
.orElseThrow(() -> new DomainBadRequestException("error.namespace.id.notFound", namespaceId));
}
+ /**
+ * Ensures the caller holds an owner or admin membership in the namespace.
+ */
public void assertAdminOrOwner(Long namespaceId, String userId) {
NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
.map(NamespaceMember::getRole)
@@ -93,6 +114,9 @@ public class NamespaceService {
}
}
+ /**
+ * Ensures the caller is at least a member of the namespace.
+ */
public void assertMember(Long namespaceId, String userId) {
namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
.orElseThrow(() -> new DomainForbiddenException("error.namespace.membership.required"));
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/SlugValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/SlugValidator.java
index dd3457e7..29c27677 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/SlugValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/SlugValidator.java
@@ -5,6 +5,9 @@ import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
import java.util.Set;
import java.util.regex.Pattern;
+/**
+ * Validates and normalizes namespace-style slugs used across public identifiers.
+ */
public class SlugValidator {
private static final int MIN_LENGTH = 2;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/package-info.java
new file mode 100644
index 00000000..2700f3bc
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/namespace/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Namespace aggregate, membership rules, and governance policies that scope
+ * skills and collaboration permissions.
+ */
+package com.iflytek.skillhub.domain.namespace;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportRepository.java
index 291e3503..62349668 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportRepository.java
@@ -6,6 +6,9 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+/**
+ * Domain repository contract for abuse reports filed against skills.
+ */
public interface SkillReportRepository {
SkillReport save(SkillReport report);
Optional findById(Long id);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportService.java
index cb96f00d..52cbd871 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/SkillReportService.java
@@ -13,6 +13,10 @@ import java.time.Instant;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Handles skill abuse reports from submission through moderation outcome
+ * handling.
+ */
@Service
public class SkillReportService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/package-info.java
new file mode 100644
index 00000000..869ab7e2
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/report/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Reporting domain objects and services for user-submitted skill abuse or
+ * quality issues.
+ */
+package com.iflytek.skillhub.domain.report;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java
index 4275959b..a6dc87c9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java
@@ -4,6 +4,10 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
+/**
+ * Domain repository contract for promotion requests that copy or elevate one skill version into a
+ * target catalog entry.
+ */
public interface PromotionRequestRepository {
PromotionRequest save(PromotionRequest request);
Optional findById(Long id);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java
index 7ee3c953..f627f749 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java
@@ -22,6 +22,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+/**
+ * Handles promotion requests that copy approved skills into the global
+ * namespace.
+ *
+ *
Promotion is intentionally modeled separately from normal review because
+ * it creates or updates a distinct target skill lineage.
+ */
@Service
public class PromotionService {
@@ -55,6 +62,10 @@ public class PromotionService {
this.clock = clock;
}
+ /**
+ * Submits a promotion request for a published source version using both
+ * namespace and platform roles for authorization.
+ */
@Transactional
public PromotionRequest submitPromotion(Long sourceSkillId, Long sourceVersionId,
Long targetNamespaceId, String userId,
@@ -148,6 +159,10 @@ public class PromotionService {
return promotionRequestRepository.save(request);
}
+ /**
+ * Approves a promotion request and materializes a published copy of the
+ * source version in the target global namespace.
+ */
@Transactional
public PromotionRequest approvePromotion(Long promotionId, String reviewerId,
String comment, Set platformRoles) {
@@ -230,6 +245,9 @@ public class PromotionService {
return savedRequest;
}
+ /**
+ * Rejects a pending promotion request without changing the source skill.
+ */
@Transactional
public PromotionRequest rejectPromotion(Long promotionId, String reviewerId,
String comment, Set platformRoles) {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java
index 21991214..bc5dae42 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java
@@ -8,6 +8,9 @@ import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
+/**
+ * Centralizes review and promotion permission checks derived from namespace and platform roles.
+ */
@Component
public class ReviewPermissionChecker {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java
index 4802ac11..7133abb7 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java
@@ -29,6 +29,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+/**
+ * Coordinates the review workflow for skill versions.
+ *
+ *
This service owns transitions between draft, pending review, approved,
+ * and rejected states together with the review task record that tracks the
+ * moderation decision.
+ */
@Service
public class ReviewService {
@@ -65,6 +72,10 @@ public class ReviewService {
this.clock = clock;
}
+ /**
+ * Submits a draft version into the review queue using both namespace roles
+ * and platform roles to determine permission.
+ */
@Transactional
public ReviewTask submitReview(Long skillVersionId,
String userId,
@@ -98,6 +109,10 @@ public class ReviewService {
}
}
+ /**
+ * Legacy overload that evaluates submission rights only from namespace
+ * memberships.
+ */
@Transactional
public ReviewTask submitReview(Long skillVersionId,
String userId,
@@ -130,6 +145,10 @@ public class ReviewService {
}
}
+ /**
+ * Approves a pending review task, publishes the underlying version, and
+ * emits downstream notifications and publication events.
+ */
@Transactional
public ReviewTask approveReview(Long reviewTaskId, String reviewerId, String comment,
Map userNamespaceRoles,
@@ -202,6 +221,10 @@ public class ReviewService {
return reviewTaskRepository.findById(reviewTaskId).orElse(task);
}
+ /**
+ * Rejects a pending review task and returns the underlying version to a
+ * non-published state with reviewer metadata captured on the task.
+ */
@Transactional
public ReviewTask rejectReview(Long reviewTaskId, String reviewerId, String comment,
Map userNamespaceRoles,
@@ -244,6 +267,10 @@ public class ReviewService {
return reviewTaskRepository.findById(reviewTaskId).orElse(task);
}
+ /**
+ * Withdraws a previously submitted review request and puts the version back
+ * into draft so the owner can amend and resubmit it.
+ */
@Transactional
public SkillVersion withdrawReview(Long skillVersionId, String userId) {
ReviewTask task = reviewTaskRepository.findBySkillVersionIdAndStatus(
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java
index 9d3a3c49..5596f60a 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java
@@ -4,6 +4,9 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
+/**
+ * Domain repository contract for moderation review tasks and their state transitions.
+ */
public interface ReviewTaskRepository {
ReviewTask save(ReviewTask reviewTask);
Optional findById(Long id);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/package-info.java
new file mode 100644
index 00000000..98386091
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Review and promotion workflows that move skill versions through moderation
+ * and cross-namespace publication.
+ */
+package com.iflytek.skillhub.domain.review;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainBadRequestException.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainBadRequestException.java
index 670489fe..aab80d58 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainBadRequestException.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainBadRequestException.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.domain.shared.exception;
+/**
+ * Domain exception used when caller input violates business validation rules.
+ */
public class DomainBadRequestException extends LocalizedDomainException {
public DomainBadRequestException(String messageCode, Object... messageArgs) {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainForbiddenException.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainForbiddenException.java
index 1d5c9525..14807d15 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainForbiddenException.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainForbiddenException.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.domain.shared.exception;
+/**
+ * Domain exception used when the caller lacks permission for the requested business action.
+ */
public class DomainForbiddenException extends LocalizedDomainException {
public DomainForbiddenException(String messageCode, Object... messageArgs) {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainNotFoundException.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainNotFoundException.java
index 36b64c55..df387c69 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainNotFoundException.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/DomainNotFoundException.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.domain.shared.exception;
+/**
+ * Domain exception used when a required business entity cannot be found.
+ */
public class DomainNotFoundException extends LocalizedDomainException {
public DomainNotFoundException(String messageCode, Object... messageArgs) {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java
index c09d1919..35e55cd7 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/LocalizedDomainException.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.domain.shared.exception;
+/**
+ * Base class for domain-layer exceptions that carry a localized message code and arguments.
+ */
public abstract class LocalizedDomainException extends RuntimeException {
private final String messageCode;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/package-info.java
new file mode 100644
index 00000000..ee685b3d
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/shared/exception/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Reusable business exceptions raised by domain services and translated at the
+ * application boundary.
+ */
+package com.iflytek.skillhub.domain.shared.exception;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillFileRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillFileRepository.java
index 4cc0bd81..5a863645 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillFileRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillFileRepository.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.domain.skill;
import java.util.List;
+/**
+ * Domain repository contract for files belonging to one published or draft skill version.
+ */
public interface SkillFileRepository {
List findByVersionId(Long versionId);
SkillFile save(SkillFile file);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java
index f3080f4f..d8ca3d44 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillRepository.java
@@ -6,6 +6,9 @@ import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for loading and persisting skill aggregates and common read models.
+ */
public interface SkillRepository {
Optional findById(Long id);
List findByIdIn(List ids);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillTagRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillTagRepository.java
index f8cd1c1c..e45b09e6 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillTagRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillTagRepository.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.skill;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for persisted skill tags and tag lookups.
+ */
public interface SkillTagRepository {
Optional findBySkillIdAndTagName(Long skillId, String tagName);
List findBySkillId(Long skillId);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionRepository.java
index 6a5739c7..eaeacd13 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionRepository.java
@@ -3,6 +3,9 @@ package com.iflytek.skillhub.domain.skill;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for skill version history and publication-state queries.
+ */
public interface SkillVersionRepository {
Optional findById(Long id);
List findByIdIn(List ids);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionStatsRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionStatsRepository.java
index 28685aa2..905621f9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionStatsRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/SkillVersionStatsRepository.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.domain.skill;
import java.util.Optional;
+/**
+ * Domain repository contract for per-version counters such as download statistics.
+ */
public interface SkillVersionStatsRepository {
Optional findBySkillVersionId(Long skillVersionId);
void incrementDownloadCount(Long skillVersionId, Long skillId);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java
index 16980af1..f4632159 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/VisibilityChecker.java
@@ -4,6 +4,10 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRole;
import java.util.Map;
+/**
+ * Evaluates whether a caller may read a skill based on publication state, visibility, ownership,
+ * and namespace roles.
+ */
public class VisibilityChecker {
public boolean canAccess(Skill skill, String currentUserId, Map userNamespaceRoles) {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java
index a461d2d2..36fc1ffc 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java
@@ -6,6 +6,10 @@ import org.yaml.snakeyaml.Yaml;
import java.util.LinkedHashMap;
import java.util.Map;
+/**
+ * Parses `SKILL.md` frontmatter and body content into the normalized metadata model used by the
+ * publish pipeline.
+ */
public class SkillMetadataParser {
private static final String FRONTMATTER_DELIMITER = "---";
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/package-info.java
new file mode 100644
index 00000000..4fef333c
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Parsing and representation of metadata extracted from packaged skills.
+ */
+package com.iflytek.skillhub.domain.skill.metadata;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/package-info.java
new file mode 100644
index 00000000..6c438d5e
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Core skill aggregate model including versions, files, visibility, and
+ * repositories shared across publication and consumption flows.
+ */
+package com.iflytek.skillhub.domain.skill;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java
index bc9678b1..8e83ba1b 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillDownloadService.java
@@ -26,6 +26,12 @@ import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
+/**
+ * Domain service that delivers packaged skills to callers.
+ *
+ *
It combines visibility checks, version resolution, object-storage access,
+ * and download tracking into a single download-oriented API.
+ */
@Service
public class SkillDownloadService {
private static final Logger log = LoggerFactory.getLogger(SkillDownloadService.class);
@@ -77,6 +83,9 @@ public class SkillDownloadService {
}
}
+ /**
+ * Downloads the latest published version available to the caller.
+ */
public DownloadResult downloadLatest(
String namespaceSlug,
String skillSlug,
@@ -97,6 +106,10 @@ public class SkillDownloadService {
return downloadVersion(skill, version);
}
+ /**
+ * Downloads an explicit version when the caller has permission to access
+ * the containing skill.
+ */
public DownloadResult downloadVersion(
String namespaceSlug,
String skillSlug,
@@ -114,6 +127,9 @@ public class SkillDownloadService {
return downloadVersion(skill, version);
}
+ /**
+ * Downloads the version pointed to by a mutable tag name.
+ */
public DownloadResult downloadByTag(
String namespaceSlug,
String skillSlug,
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java
index a96ccc67..999ae53c 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java
@@ -23,6 +23,10 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Handles governance-oriented mutations on skills and versions, including
+ * hiding, archiving, restoring, and destructive cleanup.
+ */
@Service
public class SkillGovernanceService {
@@ -157,6 +161,11 @@ public class SkillGovernanceService {
objectStorageService.deleteObject(String.format("packages/%d/%d/bundle.zip", skill.getId(), version.getId()));
skillFileRepository.deleteByVersionId(version.getId());
skillVersionRepository.delete(version);
+ if (version.getId().equals(skill.getLatestVersionId())) {
+ skill.setLatestVersionId(findLatestPublishedVersionId(skill.getId()));
+ skill.setUpdatedBy(actorUserId);
+ skillRepository.save(skill);
+ }
auditLogService.record(
actorUserId,
"DELETE_SKILL_VERSION",
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java
index 73b2d0e7..e374f3b9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillLifecycleProjectionService.java
@@ -10,6 +10,10 @@ import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
+/**
+ * Builds lightweight lifecycle projections that describe which skill version should be surfaced to
+ * a given viewer.
+ */
@Service
public class SkillLifecycleProjectionService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java
index de5238b3..f43c7a25 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java
@@ -45,6 +45,12 @@ import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
+/**
+ * Publishes packaged skill artifacts into persisted skill and version records.
+ *
+ *
The service validates archive contents, parses metadata, stores files,
+ * creates review tasks when needed, and updates the skill's lifecycle pointer.
+ */
@Service
public class SkillPublishService {
@@ -100,6 +106,12 @@ public class SkillPublishService {
this.clock = clock;
}
+ /**
+ * Publishes an extracted package into the target namespace.
+ *
+ *
Super administrators may auto-publish, while regular publishers
+ * usually create a pending-review version.
+ */
@Transactional
public PublishResult publishFromEntries(
String namespaceSlug,
@@ -110,6 +122,10 @@ public class SkillPublishService {
return publishFromEntriesInternal(namespaceSlug, entries, publisherId, visibility, platformRoles, false, false);
}
+ /**
+ * Rebuilds a new version from an already published version by copying its
+ * stored files and rewriting the embedded metadata version field.
+ */
@Transactional
public PublishResult rereleasePublishedVersion(
Long skillId,
@@ -220,6 +236,9 @@ public class SkillPublishService {
return skillRepository.save(newSkill);
});
+ // Update visibility to match the latest publish request
+ skill.setVisibility(visibility);
+
if (skill.getStatus() == SkillStatus.ARCHIVED) {
throw new DomainBadRequestException("error.skill.publish.archived", skillSlug);
}
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java
index 55bf67c5..3a35e0a8 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java
@@ -31,6 +31,13 @@ import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
+/**
+ * Read-side domain service for skill detail, version browsing, and packaged
+ * file inspection.
+ *
+ *
Unlike search, this service works from the authoritative skill model and
+ * applies viewer-specific visibility rules before returning data.
+ */
@Service
public class SkillQueryService {
@@ -171,6 +178,10 @@ public class SkillQueryService {
);
}
+ /**
+ * Lists skills within a namespace after filtering out records the caller is
+ * not allowed to discover.
+ */
public Page listSkillsByNamespace(
String namespaceSlug,
String currentUserId,
@@ -193,6 +204,10 @@ public class SkillQueryService {
return new PageImpl<>(pageContent, pageable, accessibleSkills.size());
}
+ /**
+ * Returns metadata for a visible version, including the stored manifest and
+ * parsed metadata payload.
+ */
public SkillVersionDetailDTO getVersionDetail(
String namespaceSlug,
String skillSlug,
@@ -247,6 +262,10 @@ public class SkillQueryService {
return availableFiles(skillVersion.getId());
}
+ /**
+ * Opens a single file stream from object storage after verifying that the
+ * caller may inspect the requested version.
+ */
public InputStream getFileContent(
String namespaceSlug,
String skillSlug,
@@ -328,6 +347,10 @@ public class SkillQueryService {
return version.isDownloadReady();
}
+ /**
+ * Resolves a version selector such as an exact version, tag, or implicit
+ * latest reference into a concrete download target.
+ */
public ResolvedVersionDTO resolveVersion(
String namespaceSlug,
String skillSlug,
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillSlugResolutionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillSlugResolutionService.java
index e4521da3..080172b1 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillSlugResolutionService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillSlugResolutionService.java
@@ -8,6 +8,9 @@ import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
+/**
+ * Resolves ambiguous namespace-slug pairs to the most appropriate skill record for the caller.
+ */
@Service
public class SkillSlugResolutionService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java
index 07c3fdc0..6d7a801b 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillTagService.java
@@ -12,6 +12,10 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
+/**
+ * Manages named tags that resolve to skill versions while enforcing
+ * visibility and membership constraints.
+ */
@Service
public class SkillTagService {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/package-info.java
new file mode 100644
index 00000000..6450f412
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Domain services that coordinate skill publication, lifecycle, query, and
+ * download use cases across multiple repositories and infrastructure ports.
+ */
+package com.iflytek.skillhub.domain.skill.service;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
index 86dca42c..c61e420b 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java
@@ -9,6 +9,10 @@ import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+/**
+ * Default pre-publish validator that scans text-like package files for likely secrets and
+ * accidental real credentials.
+ */
@Component
public class BasicPrePublishValidator implements PrePublishValidator {
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/NoOpPrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/NoOpPrePublishValidator.java
index cd8e1ef8..44ae1cc5 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/NoOpPrePublishValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/NoOpPrePublishValidator.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.domain.skill.validation;
+/**
+ * Trivial validator used when no extra pre-publish checks are desired.
+ */
public class NoOpPrePublishValidator implements PrePublishValidator {
@Override
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/PrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/PrePublishValidator.java
index 5f64f151..deb08471 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/PrePublishValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/PrePublishValidator.java
@@ -4,6 +4,10 @@ import com.iflytek.skillhub.domain.skill.metadata.SkillMetadata;
import java.util.List;
+/**
+ * Extension point for content-aware validation that runs after package parsing but before a skill
+ * version is accepted for publishing.
+ */
public interface PrePublishValidator {
ValidationResult validate(SkillPackageContext context);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java
index c4520a54..87a952b9 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackagePolicy.java
@@ -9,6 +9,10 @@ import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Set;
+/**
+ * Shared package-policy rules for path normalization, extension allowlists, and lightweight file
+ * signature validation.
+ */
public final class SkillPackagePolicy {
public static final int MAX_FILE_COUNT = 100;
@@ -16,14 +20,14 @@ public final class SkillPackagePolicy {
public static final long MAX_TOTAL_PACKAGE_SIZE = 100 * 1024 * 1024; // 100MB
public static final String SKILL_MD_PATH = "SKILL.md";
public static final Set ALLOWED_EXTENSIONS = Set.of(
- // 文档
+ // Documentation
".md", ".txt", ".json", ".yaml", ".yml", ".html", ".css", ".csv", ".pdf",
- // 配置
+ // Configuration
".toml", ".xml", ".ini", ".cfg", ".env",
- // 脚本/语言
+ // Scripts and source code
".js", ".ts", ".py", ".sh", ".rb", ".go", ".rs", ".java", ".kt",
".lua", ".sql", ".r", ".bat", ".ps1", ".zsh", ".bash",
- // 图片
+ // Images
".png", ".jpg", ".jpeg", ".svg", ".gif", ".webp", ".ico"
);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java
index a483612c..836a9eb5 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java
@@ -10,6 +10,10 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+/**
+ * Validates uploaded skill packages against structural, metadata, and size constraints before
+ * publish-time domain processing continues.
+ */
public class SkillPackageValidator {
private static final Pattern YAML_LINE_COLUMN = Pattern.compile("line\\s+(\\d+),\\s+column\\s+(\\d+)");
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/package-info.java
new file mode 100644
index 00000000..fd6dc62e
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Validation policies for uploaded skill packages before they become persisted
+ * skill versions.
+ */
+package com.iflytek.skillhub.domain.skill.validation;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingRepository.java
index b29f5a4b..b628e598 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingRepository.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.domain.social;
import java.util.Optional;
+/**
+ * Domain repository contract for per-user ratings and rating aggregates on one skill.
+ */
public interface SkillRatingRepository {
SkillRating save(SkillRating rating);
Optional findBySkillIdAndUserId(Long skillId, String userId);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java
index 7d8a6235..95e67b0c 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillRatingService.java
@@ -10,6 +10,10 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
+/**
+ * Domain service for creating or updating user ratings on skills and emitting
+ * the corresponding social event.
+ */
@Service
public class SkillRatingService {
private final SkillRatingRepository ratingRepository;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarRepository.java
index 0cd9f1ab..2eabe344 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarRepository.java
@@ -4,6 +4,9 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+/**
+ * Domain repository contract for skill star relationships and starred-skill pagination.
+ */
public interface SkillStarRepository {
SkillStar save(SkillStar star);
Optional findBySkillIdAndUserId(Long skillId, String userId);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarService.java
index b1dd49bc..e9a5485c 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarService.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/SkillStarService.java
@@ -8,6 +8,9 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Domain service for starring and unstarring skills in an idempotent manner.
+ */
@Service
public class SkillStarService {
private final SkillStarRepository starRepository;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/event/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/event/package-info.java
new file mode 100644
index 00000000..0a2ae09e
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/event/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Domain events emitted by social interactions so counters and projections can
+ * react asynchronously.
+ */
+package com.iflytek.skillhub.domain.social.event;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/package-info.java
new file mode 100644
index 00000000..f9631312
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/social/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Social interactions such as starring and rating that enrich the skill
+ * marketplace experience.
+ */
+package com.iflytek.skillhub.domain.social;
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/ProfileChangeRequestRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/ProfileChangeRequestRepository.java
index 0672f9b5..b3380ebb 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/ProfileChangeRequestRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/ProfileChangeRequestRepository.java
@@ -6,7 +6,6 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
-
/**
* Repository for {@link ProfileChangeRequest} entities.
* Implementations are provided by the infra layer (JPA).
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/UserAccountRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/UserAccountRepository.java
index d2163c56..c0f4f295 100644
--- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/UserAccountRepository.java
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/UserAccountRepository.java
@@ -6,6 +6,9 @@ import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Optional;
+/**
+ * Domain repository contract for user-account identity lookups and administrative searches.
+ */
public interface UserAccountRepository {
Optional findById(String id);
List findByIdIn(List ids);
diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/package-info.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/package-info.java
new file mode 100644
index 00000000..e9e49d57
--- /dev/null
+++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/user/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * User account and profile moderation domain objects that back identity and
+ * profile management flows.
+ */
+package com.iflytek.skillhub.domain.user;
diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java
index 572138b6..ba24d46d 100644
--- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java
+++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceServiceTest.java
@@ -239,6 +239,35 @@ class SkillGovernanceServiceTest {
verify(skillVersionRepository, never()).delete(any());
}
+
+ @Test
+ void deleteVersion_updatesLatestVersionPointerWhenDeletingArchivedSkillsLatestDraft() {
+ Skill skill = new Skill(1L, "demo", "owner", com.iflytek.skillhub.domain.skill.SkillVisibility.PUBLIC);
+ setField(skill, "id", 1L);
+ skill.setStatus(SkillStatus.ARCHIVED);
+ skill.setLatestVersionId(2L);
+
+ SkillVersion draftVersion = new SkillVersion(1L, "2.0.0-rc1", "owner");
+ setField(draftVersion, "id", 2L);
+ draftVersion.setStatus(SkillVersionStatus.DRAFT);
+
+ SkillVersion publishedVersion = new SkillVersion(1L, "1.0.0", "owner");
+ setField(publishedVersion, "id", 3L);
+ publishedVersion.setStatus(SkillVersionStatus.PUBLISHED);
+ publishedVersion.setPublishedAt(Instant.parse("2026-03-17T10:00:00Z"));
+
+ given(skillVersionRepository.findBySkillId(1L)).willReturn(java.util.List.of(draftVersion, publishedVersion));
+ given(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED))
+ .willReturn(java.util.List.of(publishedVersion));
+ given(skillRepository.save(skill)).willReturn(skill);
+ given(skillFileRepository.findByVersionId(2L)).willReturn(java.util.List.of());
+
+ service.deleteVersion(skill, draftVersion, "owner", Map.of(), "127.0.0.1", "JUnit");
+
+ assertThat(skill.getLatestVersionId()).isEqualTo(3L);
+ verify(skillRepository).save(skill);
+ }
+
private void setField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java
index 97043f98..8580ff0b 100644
--- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java
+++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java
@@ -732,6 +732,49 @@ class SkillPublishServiceTest {
verify(skillVersionRepository).save(pendingV1);
}
+ @Test
+ void testPublishFromEntries_ShouldUpdateVisibilityOnExistingSkill() throws Exception {
+ // Arrange
+ String namespaceSlug = "test-ns";
+ String publisherId = "user-100";
+ String skillMdContent = "---\nname: test-skill\ndescription: Test\nversion: 2.0.0\n---\nBody";
+
+ PackageEntry skillMd = new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown");
+ List entries = List.of(skillMd);
+
+ Namespace namespace = new Namespace(namespaceSlug, "Test NS", "user-1");
+ setId(namespace, 1L);
+ NamespaceMember member = mock(NamespaceMember.class);
+ SkillMetadata metadata = new SkillMetadata("test-skill", "Test", "2.0.0", "Body", Map.of());
+
+ // Skill was created with PRIVATE visibility
+ Skill skill = new Skill(1L, "test-skill", publisherId, SkillVisibility.PRIVATE);
+ setId(skill, 1L);
+
+ when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
+ when(namespaceMemberRepository.findByNamespaceIdAndUserId(any(), eq(publisherId))).thenReturn(Optional.of(member));
+ when(skillPackageValidator.validate(entries)).thenReturn(ValidationResult.pass());
+ when(skillMetadataParser.parse(skillMdContent)).thenReturn(metadata);
+ when(prePublishValidator.validate(any())).thenReturn(ValidationResult.pass());
+ when(skillRepository.findByNamespaceIdAndSlug(any(), eq("test-skill"))).thenReturn(List.of(skill));
+ when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))).thenReturn(Optional.of(skill));
+ when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("2.0.0"))).thenReturn(Optional.empty());
+ when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> {
+ SkillVersion saved = invocation.getArgument(0);
+ if (saved.getId() == null) {
+ setId(saved, 20L);
+ }
+ return saved;
+ });
+ when(skillRepository.save(any())).thenReturn(skill);
+
+ // Act — publish with PUBLIC visibility on an existing PRIVATE skill
+ service.publishFromEntries(namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of());
+
+ // Assert — visibility should be updated to PUBLIC
+ assertEquals(SkillVisibility.PUBLIC, skill.getVisibility());
+ }
+
private void setId(Object entity, Long id) throws Exception {
Field idField = entity.getClass().getDeclaredField("id");
idField.setAccessible(true);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/AuditLogJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/AuditLogJpaRepository.java
index 8d6b4606..fa57defa 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/AuditLogJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/AuditLogJpaRepository.java
@@ -9,6 +9,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
+/**
+ * JPA-backed audit-log repository that adds specification-based filtering for admin queries.
+ */
@Repository
public interface AuditLogJpaRepository extends JpaRepository, JpaSpecificationExecutor, AuditLogRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaIdempotencyRecordRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaIdempotencyRecordRepository.java
index 8eafd1d7..300e7a56 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaIdempotencyRecordRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaIdempotencyRecordRepository.java
@@ -10,6 +10,10 @@ import org.springframework.stereotype.Repository;
import java.time.Instant;
+/**
+ * JPA repository that persists idempotency records and exposes cleanup operations used by
+ * background maintenance.
+ */
@Repository
public interface JpaIdempotencyRecordRepository extends JpaRepository, IdempotencyRecordRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRatingRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRatingRepository.java
index f60d8699..8996e226 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRatingRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRatingRepository.java
@@ -7,6 +7,9 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.Optional;
+/**
+ * JPA-backed repository for per-user skill ratings and their derived aggregates.
+ */
@Repository
public interface JpaSkillRatingRepository extends JpaRepository, SkillRatingRepository {
Optional findBySkillIdAndUserId(Long skillId, String userId);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java
index 6e9daecf..cf991282 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillRepositoryAdapter.java
@@ -12,6 +12,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * Primary JPA-backed adapter that fulfills the domain-level {@link SkillRepository} contract.
+ */
@Repository
@Primary
public class JpaSkillRepositoryAdapter implements SkillRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillStarRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillStarRepository.java
index e75201e5..295273ac 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillStarRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/JpaSkillStarRepository.java
@@ -8,6 +8,9 @@ import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
+/**
+ * JPA-backed repository for skill star relationships and user star listings.
+ */
@Repository
public interface JpaSkillStarRepository extends JpaRepository, SkillStarRepository {
Optional findBySkillIdAndUserId(Long skillId, String userId);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java
index 1b21cc5d..7e7f3db0 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceJpaRepository.java
@@ -11,6 +11,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * JPA-backed namespace repository that also fulfills the domain namespace repository contract.
+ */
@Repository
public interface NamespaceJpaRepository
extends JpaRepository, NamespaceRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceMemberJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceMemberJpaRepository.java
index 7a745cc3..13094042 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceMemberJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/NamespaceMemberJpaRepository.java
@@ -10,6 +10,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * JPA-backed repository for namespace membership records and paged member listings.
+ */
@Repository
public interface NamespaceMemberJpaRepository
extends JpaRepository, NamespaceMemberRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java
index 88cf99ea..d3ab5db9 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java
@@ -12,6 +12,9 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
+/**
+ * JPA-backed repository for promotion requests, including optimistic status updates.
+ */
@Repository
public interface PromotionRequestJpaRepository extends JpaRepository,
PromotionRequestRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java
index 5ff37a7e..1fc4f7ea 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java
@@ -12,6 +12,10 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
+/**
+ * JPA-backed repository for review tasks, including optimistic update support for moderation
+ * decisions.
+ */
@Repository
public interface ReviewTaskJpaRepository extends JpaRepository, ReviewTaskRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillFileJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillFileJpaRepository.java
index db239fb4..21555436 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillFileJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillFileJpaRepository.java
@@ -7,6 +7,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
+/**
+ * JPA-backed repository for package files attached to one skill version.
+ */
@Repository
public interface SkillFileJpaRepository extends JpaRepository, SkillFileRepository {
List findByVersionId(Long versionId);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java
index d784235a..cd98258e 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillJpaRepository.java
@@ -15,6 +15,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
+/**
+ * Base Spring Data JPA repository for persisted skill aggregates and common skill queries.
+ */
@Repository
public interface SkillJpaRepository extends JpaRepository, SkillRepository {
List findByIdIn(List ids);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillReportJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillReportJpaRepository.java
index 1e352760..a88a0f47 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillReportJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillReportJpaRepository.java
@@ -9,6 +9,9 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+/**
+ * JPA-backed repository for skill abuse reports and report queues ordered by creation time.
+ */
public interface SkillReportJpaRepository extends JpaRepository, SkillReportRepository {
boolean existsBySkillIdAndReporterIdAndStatus(Long skillId, String reporterId, SkillReportStatus status);
Page findByStatusOrderByCreatedAtDesc(SkillReportStatus status, Pageable pageable);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillTagJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillTagJpaRepository.java
index 9a8aef3a..a232ebe6 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillTagJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillTagJpaRepository.java
@@ -8,6 +8,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * JPA-backed repository for tags associated with a skill.
+ */
@Repository
public interface SkillTagJpaRepository extends JpaRepository, SkillTagRepository {
Optional findBySkillIdAndTagName(Long skillId, String tagName);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionJpaRepository.java
index e6095270..ca35771f 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionJpaRepository.java
@@ -11,6 +11,9 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
+/**
+ * JPA-backed repository for skill version history and status-oriented version queries.
+ */
@Repository
public interface SkillVersionJpaRepository extends JpaRepository, SkillVersionRepository {
List findByIdIn(List ids);
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionStatsJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionStatsJpaRepository.java
index 3a926142..8dae9601 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionStatsJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillVersionStatsJpaRepository.java
@@ -10,6 +10,9 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * JPA-backed repository for per-version statistics, including atomic download counter increments.
+ */
@Repository
public interface SkillVersionStatsJpaRepository extends JpaRepository, SkillVersionStatsRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java
index 8fbb45cc..f2d4835c 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserAccountJpaRepository.java
@@ -11,6 +11,9 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
+/**
+ * JPA-backed user-account repository that provides filtered admin search over account records.
+ */
@Repository
public interface UserAccountJpaRepository
extends JpaRepository, JpaSpecificationExecutor, UserAccountRepository {
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserNotificationJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserNotificationJpaRepository.java
index a4e461f6..5138ef62 100644
--- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserNotificationJpaRepository.java
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/UserNotificationJpaRepository.java
@@ -5,6 +5,9 @@ import com.iflytek.skillhub.domain.governance.UserNotificationRepository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
+/**
+ * JPA-backed repository for notifications shown in the governance inbox.
+ */
public interface UserNotificationJpaRepository extends JpaRepository, UserNotificationRepository {
List findByUserIdOrderByCreatedAtDesc(String userId);
}
diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/package-info.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/package-info.java
new file mode 100644
index 00000000..fb0b3f50
--- /dev/null
+++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * JPA-backed adapters that implement domain repository contracts and other
+ * persistence-side projections.
+ */
+package com.iflytek.skillhub.infra.jpa;
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/HashingSearchEmbeddingService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/HashingSearchEmbeddingService.java
index b92efe24..66fc338d 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/HashingSearchEmbeddingService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/HashingSearchEmbeddingService.java
@@ -6,6 +6,10 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
+/**
+ * Lightweight embedding service that hashes lexical tokens into a fixed-size vector for approximate
+ * semantic ranking.
+ */
@Service
public class HashingSearchEmbeddingService implements SearchEmbeddingService {
private static final Pattern TOKEN_SPLITTER = Pattern.compile("[^\\p{L}\\p{N}_]+");
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchEmbeddingService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchEmbeddingService.java
index 4eec4795..60273d88 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchEmbeddingService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchEmbeddingService.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.search;
+/**
+ * Converts text into a serialized vector form and evaluates similarity against stored vectors.
+ */
public interface SearchEmbeddingService {
String embed(String text);
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchIndexService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchIndexService.java
index 5b02ccbb..ccb33bd7 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchIndexService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchIndexService.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.search;
import java.util.List;
+/**
+ * Writes and removes documents in the search index implementation.
+ */
public interface SearchIndexService {
void index(SkillSearchDocument document);
void batchIndex(List documents);
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQuery.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQuery.java
index 595ab6af..3941bc5c 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQuery.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQuery.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.search;
+/**
+ * Immutable search request model shared between application code and search implementations.
+ */
public record SearchQuery(
String keyword,
Long namespaceId,
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQueryService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQueryService.java
index 28cfd5d8..66c74e9c 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQueryService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchQueryService.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.search;
+/**
+ * Read-side contract for executing skill searches against the configured search backend.
+ */
public interface SearchQueryService {
SearchResult search(SearchQuery query);
}
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchRebuildService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchRebuildService.java
index 02ade3b4..ccd61e8e 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchRebuildService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchRebuildService.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.search;
+/**
+ * Rebuilds search index state from authoritative domain data.
+ */
public interface SearchRebuildService {
void rebuildAll();
void rebuildByNamespace(Long namespaceId);
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchResult.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchResult.java
index 04ab826f..a0b6347d 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchResult.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchResult.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.search;
import java.util.List;
+/**
+ * Compact search response containing matching skill identifiers and pagination metadata.
+ */
public record SearchResult(
List skillIds,
long total,
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchVisibilityScope.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchVisibilityScope.java
index cb35d2c6..4f435e35 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchVisibilityScope.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SearchVisibilityScope.java
@@ -2,6 +2,9 @@ package com.iflytek.skillhub.search;
import java.util.Set;
+/**
+ * Caller visibility context used by search implementations to filter results consistently.
+ */
public record SearchVisibilityScope(
String userId,
Set memberNamespaceIds,
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SkillSearchDocument.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SkillSearchDocument.java
index d40fb09f..bf5e413a 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SkillSearchDocument.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/SkillSearchDocument.java
@@ -1,5 +1,8 @@
package com.iflytek.skillhub.search;
+/**
+ * Denormalized search document model written to and read from the search subsystem.
+ */
public record SkillSearchDocument(
Long skillId,
Long namespaceId,
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java
index 74cc7eca..b0fcee58 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/SearchIndexEventListener.java
@@ -10,6 +10,9 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
+/**
+ * Reacts to committed skill lifecycle events and keeps the search index synchronized.
+ */
@Component
public class SearchIndexEventListener {
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/package-info.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/package-info.java
new file mode 100644
index 00000000..cf703287
--- /dev/null
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/event/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Search indexing listeners and event adapters that keep search documents in
+ * sync with domain changes.
+ */
+package com.iflytek.skillhub.search.event;
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/package-info.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/package-info.java
new file mode 100644
index 00000000..5ed7e6f5
--- /dev/null
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Search-facing ports and DTOs that provide skill discovery capabilities.
+ */
+package com.iflytek.skillhub.search;
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java
index b856b82a..6025c7a2 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java
@@ -11,6 +11,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
+/**
+ * PostgreSQL-backed search index writer that stores searchable documents and semantic vectors.
+ */
@Service
public class PostgresFullTextIndexService implements SearchIndexService {
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java
index 158bcb25..d5eb7e43 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java
@@ -19,6 +19,13 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
+/**
+ * PostgreSQL-backed implementation of {@link SearchQueryService}.
+ *
+ *
The query pipeline combines structured visibility filters, full-text
+ * ranking, and an optional semantic re-ranking pass over a bounded candidate
+ * set.
+ */
@Service
public class PostgresFullTextQueryService implements SearchQueryService {
private static final Pattern QUERY_TERM_SPLITTER = Pattern.compile("[^\\p{L}\\p{N}_]+");
@@ -56,6 +63,10 @@ public class PostgresFullTextQueryService implements SearchQueryService {
this.maxCandidates = maxCandidates;
}
+ /**
+ * Executes a search query against the denormalized search document table
+ * and optionally re-ranks candidates using embeddings.
+ */
@Override
public SearchResult search(SearchQuery query) {
String normalizedKeyword = normalizeKeyword(query.keyword());
diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java
index 59bd51ef..c5d13f82 100644
--- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java
+++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java
@@ -1,32 +1,54 @@
package com.iflytek.skillhub.search.postgres;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.iflytek.skillhub.domain.namespace.Namespace;
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
import com.iflytek.skillhub.domain.skill.Skill;
import com.iflytek.skillhub.domain.skill.SkillRepository;
import com.iflytek.skillhub.domain.skill.SkillStatus;
+import com.iflytek.skillhub.domain.skill.SkillVersion;
+import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
import com.iflytek.skillhub.search.SearchIndexService;
import com.iflytek.skillhub.search.SearchRebuildService;
import com.iflytek.skillhub.search.SkillSearchDocument;
import org.springframework.stereotype.Service;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+/**
+ * Reconstructs PostgreSQL search documents from canonical skill and namespace records.
+ */
@Service
public class PostgresSearchRebuildService implements SearchRebuildService {
+ private static final Set RESERVED_FRONTMATTER_FIELDS = Set.of("name", "description", "version");
+ private static final Set KEYWORD_FIELD_NAMES = Set.of("keywords", "keyword", "tags", "tag");
+ private static final TypeReference