#
tokens: 48768/50000 25/349 files (page 4/9)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 9. Use http://codebase.md/higress-group/himarket?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       ├── api-style.mdc
│       └── project-architecture.mdc
├── .gitignore
├── build.sh
├── deploy
│   ├── docker
│   │   ├── docker-compose.yml
│   │   └── Docker部署说明.md
│   └── helm
│       ├── Chart.yaml
│       ├── Helm部署说明.md
│       ├── templates
│       │   ├── _helpers.tpl
│       │   ├── himarket-admin-cm.yaml
│       │   ├── himarket-admin-deployment.yaml
│       │   ├── himarket-admin-service.yaml
│       │   ├── himarket-frontend-cm.yaml
│       │   ├── himarket-frontend-deployment.yaml
│       │   ├── himarket-frontend-service.yaml
│       │   ├── himarket-server-cm.yaml
│       │   ├── himarket-server-deployment.yaml
│       │   ├── himarket-server-service.yaml
│       │   ├── mysql.yaml
│       │   └── serviceaccount.yaml
│       └── values.yaml
├── LICENSE
├── NOTICE
├── pom.xml
├── portal-bootstrap
│   ├── Dockerfile
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── alibaba
│       │   │           └── apiopenplatform
│       │   │               ├── config
│       │   │               │   ├── AsyncConfig.java
│       │   │               │   ├── FilterConfig.java
│       │   │               │   ├── PageConfig.java
│       │   │               │   ├── RestTemplateConfig.java
│       │   │               │   ├── SecurityConfig.java
│       │   │               │   └── SwaggerConfig.java
│       │   │               ├── filter
│       │   │               │   └── PortalResolvingFilter.java
│       │   │               └── PortalApplication.java
│       │   └── resources
│       │       └── application.yaml
│       └── test
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           └── integration
│                               └── AdministratorAuthIntegrationTest.java
├── portal-dal
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── converter
│                           │   ├── AdpAIGatewayConfigConverter.java
│                           │   ├── APIGConfigConverter.java
│                           │   ├── APIGRefConfigConverter.java
│                           │   ├── ApiKeyConfigConverter.java
│                           │   ├── ConsumerAuthConfigConverter.java
│                           │   ├── GatewayConfigConverter.java
│                           │   ├── HigressConfigConverter.java
│                           │   ├── HigressRefConfigConverter.java
│                           │   ├── HmacConfigConverter.java
│                           │   ├── JsonConverter.java
│                           │   ├── JwtConfigConverter.java
│                           │   ├── NacosRefConfigConverter.java
│                           │   ├── PortalSettingConfigConverter.java
│                           │   ├── PortalUiConfigConverter.java
│                           │   └── ProductIconConverter.java
│                           ├── entity
│                           │   ├── Administrator.java
│                           │   ├── BaseEntity.java
│                           │   ├── Consumer.java
│                           │   ├── ConsumerCredential.java
│                           │   ├── ConsumerRef.java
│                           │   ├── Developer.java
│                           │   ├── DeveloperExternalIdentity.java
│                           │   ├── Gateway.java
│                           │   ├── NacosInstance.java
│                           │   ├── Portal.java
│                           │   ├── PortalDomain.java
│                           │   ├── Product.java
│                           │   ├── ProductPublication.java
│                           │   ├── ProductRef.java
│                           │   └── ProductSubscription.java
│                           ├── repository
│                           │   ├── AdministratorRepository.java
│                           │   ├── BaseRepository.java
│                           │   ├── ConsumerCredentialRepository.java
│                           │   ├── ConsumerRefRepository.java
│                           │   ├── ConsumerRepository.java
│                           │   ├── DeveloperExternalIdentityRepository.java
│                           │   ├── DeveloperRepository.java
│                           │   ├── GatewayRepository.java
│                           │   ├── NacosInstanceRepository.java
│                           │   ├── PortalDomainRepository.java
│                           │   ├── PortalRepository.java
│                           │   ├── ProductPublicationRepository.java
│                           │   ├── ProductRefRepository.java
│                           │   ├── ProductRepository.java
│                           │   └── SubscriptionRepository.java
│                           └── support
│                               ├── common
│                               │   ├── Encrypted.java
│                               │   ├── Encryptor.java
│                               │   └── User.java
│                               ├── consumer
│                               │   ├── AdpAIAuthConfig.java
│                               │   ├── APIGAuthConfig.java
│                               │   ├── ApiKeyConfig.java
│                               │   ├── ConsumerAuthConfig.java
│                               │   ├── HigressAuthConfig.java
│                               │   ├── HmacConfig.java
│                               │   └── JwtConfig.java
│                               ├── enums
│                               │   ├── APIGAPIType.java
│                               │   ├── ConsumerAuthType.java
│                               │   ├── ConsumerStatus.java
│                               │   ├── CredentialMode.java
│                               │   ├── DeveloperAuthType.java
│                               │   ├── DeveloperStatus.java
│                               │   ├── DomainType.java
│                               │   ├── GatewayType.java
│                               │   ├── GrantType.java
│                               │   ├── HigressAPIType.java
│                               │   ├── JwtAlgorithm.java
│                               │   ├── ProductIconType.java
│                               │   ├── ProductStatus.java
│                               │   ├── ProductType.java
│                               │   ├── ProtocolType.java
│                               │   ├── PublicKeyFormat.java
│                               │   ├── SourceType.java
│                               │   ├── SubscriptionStatus.java
│                               │   └── UserType.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayConfig.java
│                               │   ├── APIGConfig.java
│                               │   ├── GatewayConfig.java
│                               │   └── HigressConfig.java
│                               ├── portal
│                               │   ├── AuthCodeConfig.java
│                               │   ├── IdentityMapping.java
│                               │   ├── JwtBearerConfig.java
│                               │   ├── OAuth2Config.java
│                               │   ├── OidcConfig.java
│                               │   ├── PortalSettingConfig.java
│                               │   ├── PortalUiConfig.java
│                               │   └── PublicKeyConfig.java
│                               └── product
│                                   ├── APIGRefConfig.java
│                                   ├── HigressRefConfig.java
│                                   ├── NacosRefConfig.java
│                                   └── ProductIcon.java
├── portal-server
│   ├── pom.xml
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── alibaba
│                       └── apiopenplatform
│                           ├── controller
│                           │   ├── AdministratorController.java
│                           │   ├── ConsumerController.java
│                           │   ├── DeveloperController.java
│                           │   ├── GatewayController.java
│                           │   ├── NacosController.java
│                           │   ├── OAuth2Controller.java
│                           │   ├── OidcController.java
│                           │   ├── PortalController.java
│                           │   └── ProductController.java
│                           ├── core
│                           │   ├── advice
│                           │   │   ├── ExceptionAdvice.java
│                           │   │   └── ResponseAdvice.java
│                           │   ├── annotation
│                           │   │   ├── AdminAuth.java
│                           │   │   ├── AdminOrDeveloperAuth.java
│                           │   │   └── DeveloperAuth.java
│                           │   ├── constant
│                           │   │   ├── CommonConstants.java
│                           │   │   ├── IdpConstants.java
│                           │   │   ├── JwtConstants.java
│                           │   │   └── Resources.java
│                           │   ├── event
│                           │   │   ├── DeveloperDeletingEvent.java
│                           │   │   ├── PortalDeletingEvent.java
│                           │   │   └── ProductDeletingEvent.java
│                           │   ├── exception
│                           │   │   ├── BusinessException.java
│                           │   │   └── ErrorCode.java
│                           │   ├── response
│                           │   │   └── Response.java
│                           │   ├── security
│                           │   │   ├── ContextHolder.java
│                           │   │   ├── DeveloperAuthenticationProvider.java
│                           │   │   └── JwtAuthenticationFilter.java
│                           │   └── utils
│                           │       ├── IdGenerator.java
│                           │       ├── PasswordHasher.java
│                           │       └── TokenUtil.java
│                           ├── dto
│                           │   ├── converter
│                           │   │   ├── InputConverter.java
│                           │   │   ├── NacosToGatewayToolsConverter.java
│                           │   │   └── OutputConverter.java
│                           │   ├── params
│                           │   │   ├── admin
│                           │   │   │   ├── AdminCreateParam.java
│                           │   │   │   ├── AdminLoginParam.java
│                           │   │   │   └── ResetPasswordParam.java
│                           │   │   ├── consumer
│                           │   │   │   ├── CreateConsumerParam.java
│                           │   │   │   ├── CreateCredentialParam.java
│                           │   │   │   ├── CreateSubscriptionParam.java
│                           │   │   │   ├── QueryConsumerParam.java
│                           │   │   │   ├── QuerySubscriptionParam.java
│                           │   │   │   └── UpdateCredentialParam.java
│                           │   │   ├── developer
│                           │   │   │   ├── CreateDeveloperParam.java
│                           │   │   │   ├── CreateExternalDeveloperParam.java
│                           │   │   │   ├── DeveloperLoginParam.java
│                           │   │   │   ├── QueryDeveloperParam.java
│                           │   │   │   ├── UnbindExternalIdentityParam.java
│                           │   │   │   ├── UpdateDeveloperParam.java
│                           │   │   │   └── UpdateDeveloperStatusParam.java
│                           │   │   ├── gateway
│                           │   │   │   ├── ImportGatewayParam.java
│                           │   │   │   ├── QueryAdpAIGatewayParam.java
│                           │   │   │   ├── QueryAPIGParam.java
│                           │   │   │   └── QueryGatewayParam.java
│                           │   │   ├── nacos
│                           │   │   │   ├── CreateNacosParam.java
│                           │   │   │   ├── QueryNacosNamespaceParam.java
│                           │   │   │   ├── QueryNacosParam.java
│                           │   │   │   └── UpdateNacosParam.java
│                           │   │   ├── portal
│                           │   │   │   ├── BindDomainParam.java
│                           │   │   │   ├── CreatePortalParam.java
│                           │   │   │   └── UpdatePortalParam.java
│                           │   │   └── product
│                           │   │       ├── CreateProductParam.java
│                           │   │       ├── CreateProductRefParam.java
│                           │   │       ├── PublishProductParam.java
│                           │   │       ├── QueryProductParam.java
│                           │   │       ├── QueryProductSubscriptionParam.java
│                           │   │       ├── UnPublishProductParam.java
│                           │   │       └── UpdateProductParam.java
│                           │   └── result
│                           │       ├── AdminResult.java
│                           │       ├── AdpGatewayInstanceResult.java
│                           │       ├── AdpMcpServerListResult.java
│                           │       ├── AdpMCPServerResult.java
│                           │       ├── APIConfigResult.java
│                           │       ├── APIGMCPServerResult.java
│                           │       ├── APIResult.java
│                           │       ├── AuthResult.java
│                           │       ├── ConsumerCredentialResult.java
│                           │       ├── ConsumerResult.java
│                           │       ├── DeveloperResult.java
│                           │       ├── GatewayMCPServerResult.java
│                           │       ├── GatewayResult.java
│                           │       ├── HigressMCPServerResult.java
│                           │       ├── IdpResult.java
│                           │       ├── IdpState.java
│                           │       ├── IdpTokenResult.java
│                           │       ├── MCPConfigResult.java
│                           │       ├── MCPServerResult.java
│                           │       ├── MseNacosResult.java
│                           │       ├── NacosMCPServerResult.java
│                           │       ├── NacosNamespaceResult.java
│                           │       ├── NacosResult.java
│                           │       ├── PageResult.java
│                           │       ├── PortalResult.java
│                           │       ├── ProductPublicationResult.java
│                           │       ├── ProductRefResult.java
│                           │       ├── ProductResult.java
│                           │       └── SubscriptionResult.java
│                           └── service
│                               ├── AdministratorService.java
│                               ├── AdpAIGatewayService.java
│                               ├── ConsumerService.java
│                               ├── DeveloperService.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayOperator.java
│                               │   ├── AIGatewayOperator.java
│                               │   ├── APIGOperator.java
│                               │   ├── client
│                               │   │   ├── AdpAIGatewayClient.java
│                               │   │   ├── APIGClient.java
│                               │   │   ├── GatewayClient.java
│                               │   │   ├── HigressClient.java
│                               │   │   ├── PopGatewayClient.java
│                               │   │   └── SLSClient.java
│                               │   ├── factory
│                               │   │   └── HTTPClientFactory.java
│                               │   ├── GatewayOperator.java
│                               │   └── HigressOperator.java
│                               ├── GatewayService.java
│                               ├── IdpService.java
│                               ├── impl
│                               │   ├── AdministratorServiceImpl.java
│                               │   ├── ConsumerServiceImpl.java
│                               │   ├── DeveloperServiceImpl.java
│                               │   ├── GatewayServiceImpl.java
│                               │   ├── IdpServiceImpl.java
│                               │   ├── NacosServiceImpl.java
│                               │   ├── OAuth2ServiceImpl.java
│                               │   ├── OidcServiceImpl.java
│                               │   ├── PortalServiceImpl.java
│                               │   └── ProductServiceImpl.java
│                               ├── NacosService.java
│                               ├── OAuth2Service.java
│                               ├── OidcService.java
│                               ├── PortalService.java
│                               └── ProductService.java
├── portal-web
│   ├── api-portal-admin
│   │   ├── .env
│   │   ├── .gitignore
│   │   ├── bin
│   │   │   ├── replace_var.py
│   │   │   └── start.sh
│   │   ├── Dockerfile
│   │   ├── eslint.config.js
│   │   ├── index.html
│   │   ├── nginx.conf
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   ├── proxy.conf
│   │   ├── public
│   │   │   ├── logo.png
│   │   │   └── vite.svg
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── aliyunThemeToken.ts
│   │   │   ├── App.css
│   │   │   ├── App.tsx
│   │   │   ├── assets
│   │   │   │   └── react.svg
│   │   │   ├── components
│   │   │   │   ├── api-product
│   │   │   │   │   ├── ApiProductApiDocs.tsx
│   │   │   │   │   ├── ApiProductDashboard.tsx
│   │   │   │   │   ├── ApiProductFormModal.tsx
│   │   │   │   │   ├── ApiProductLinkApi.tsx
│   │   │   │   │   ├── ApiProductOverview.tsx
│   │   │   │   │   ├── ApiProductPolicy.tsx
│   │   │   │   │   ├── ApiProductPortal.tsx
│   │   │   │   │   ├── ApiProductUsageGuide.tsx
│   │   │   │   │   ├── SwaggerUIWrapper.css
│   │   │   │   │   └── SwaggerUIWrapper.tsx
│   │   │   │   ├── common
│   │   │   │   │   ├── AdvancedSearch.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── console
│   │   │   │   │   ├── GatewayTypeSelector.tsx
│   │   │   │   │   ├── ImportGatewayModal.tsx
│   │   │   │   │   ├── ImportHigressModal.tsx
│   │   │   │   │   ├── ImportMseNacosModal.tsx
│   │   │   │   │   └── NacosTypeSelector.tsx
│   │   │   │   ├── icons
│   │   │   │   │   └── McpServerIcon.tsx
│   │   │   │   ├── Layout.tsx
│   │   │   │   ├── LayoutWrapper.tsx
│   │   │   │   ├── portal
│   │   │   │   │   ├── PortalConsumers.tsx
│   │   │   │   │   ├── PortalDashboard.tsx
│   │   │   │   │   ├── PortalDevelopers.tsx
│   │   │   │   │   ├── PortalDomain.tsx
│   │   │   │   │   ├── PortalFormModal.tsx
│   │   │   │   │   ├── PortalOverview.tsx
│   │   │   │   │   ├── PortalPublishedApis.tsx
│   │   │   │   │   ├── PortalSecurity.tsx
│   │   │   │   │   ├── PortalSettings.tsx
│   │   │   │   │   ├── PublicKeyManager.tsx
│   │   │   │   │   └── ThirdPartyAuthManager.tsx
│   │   │   │   └── subscription
│   │   │   │       └── SubscriptionListModal.tsx
│   │   │   ├── contexts
│   │   │   │   └── LoadingContext.tsx
│   │   │   ├── index.css
│   │   │   ├── lib
│   │   │   │   ├── api.ts
│   │   │   │   ├── constant.ts
│   │   │   │   └── utils.ts
│   │   │   ├── main.tsx
│   │   │   ├── pages
│   │   │   │   ├── ApiProductDetail.tsx
│   │   │   │   ├── ApiProducts.tsx
│   │   │   │   ├── Dashboard.tsx
│   │   │   │   ├── GatewayConsoles.tsx
│   │   │   │   ├── Login.tsx
│   │   │   │   ├── NacosConsoles.tsx
│   │   │   │   ├── PortalDetail.tsx
│   │   │   │   ├── Portals.tsx
│   │   │   │   └── Register.tsx
│   │   │   ├── routes
│   │   │   │   └── index.tsx
│   │   │   ├── types
│   │   │   │   ├── api-product.ts
│   │   │   │   ├── consumer.ts
│   │   │   │   ├── gateway.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── portal.ts
│   │   │   │   ├── shims-js-yaml.d.ts
│   │   │   │   └── subscription.ts
│   │   │   └── vite-env.d.ts
│   │   ├── tailwind.config.js
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── api-portal-frontend
│       ├── .env
│       ├── .gitignore
│       ├── .husky
│       │   └── pre-commit
│       ├── bin
│       │   ├── replace_var.py
│       │   └── start.sh
│       ├── Dockerfile
│       ├── eslint.config.js
│       ├── index.html
│       ├── nginx.conf
│       ├── package.json
│       ├── postcss.config.js
│       ├── proxy.conf
│       ├── public
│       │   ├── favicon.ico
│       │   ├── logo.png
│       │   ├── logo.svg
│       │   ├── MCP.png
│       │   ├── MCP.svg
│       │   └── vite.svg
│       ├── README.md
│       ├── src
│       │   ├── aliyunThemeToken.ts
│       │   ├── App.css
│       │   ├── App.tsx
│       │   ├── assets
│       │   │   ├── aliyun.png
│       │   │   ├── github.png
│       │   │   ├── google.png
│       │   │   └── react.svg
│       │   ├── components
│       │   │   ├── consumer
│       │   │   │   ├── ConsumerBasicInfo.tsx
│       │   │   │   ├── CredentialManager.tsx
│       │   │   │   ├── index.ts
│       │   │   │   └── SubscriptionManager.tsx
│       │   │   ├── Layout.tsx
│       │   │   ├── Navigation.tsx
│       │   │   ├── ProductHeader.tsx
│       │   │   ├── SwaggerUIWrapper.css
│       │   │   ├── SwaggerUIWrapper.tsx
│       │   │   └── UserInfo.tsx
│       │   ├── index.css
│       │   ├── lib
│       │   │   ├── api.ts
│       │   │   ├── statusUtils.ts
│       │   │   └── utils.ts
│       │   ├── main.tsx
│       │   ├── pages
│       │   │   ├── ApiDetail.tsx
│       │   │   ├── Apis.tsx
│       │   │   ├── Callback.tsx
│       │   │   ├── ConsumerDetail.tsx
│       │   │   ├── Consumers.tsx
│       │   │   ├── GettingStarted.tsx
│       │   │   ├── Home.tsx
│       │   │   ├── Login.tsx
│       │   │   ├── Mcp.tsx
│       │   │   ├── McpDetail.tsx
│       │   │   ├── OidcCallback.tsx
│       │   │   ├── Profile.tsx
│       │   │   ├── Register.tsx
│       │   │   └── Test.css
│       │   ├── router.tsx
│       │   ├── types
│       │   │   ├── consumer.ts
│       │   │   └── index.ts
│       │   └── vite-env.d.ts
│       ├── tailwind.config.js
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       └── vite.config.ts
└── README.md
```

# Files

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/assets/react.svg:
--------------------------------------------------------------------------------

```
1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/assets/react.svg:
--------------------------------------------------------------------------------

```
1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/AdministratorServiceImpl.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.impl;
 21 | 
 22 | import cn.hutool.core.util.StrUtil;
 23 | import com.alibaba.apiopenplatform.core.constant.Resources;
 24 | import com.alibaba.apiopenplatform.core.security.ContextHolder;
 25 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 26 | import com.alibaba.apiopenplatform.dto.result.AdminResult;
 27 | import com.alibaba.apiopenplatform.dto.result.AuthResult;
 28 | import com.alibaba.apiopenplatform.entity.Administrator;
 29 | import com.alibaba.apiopenplatform.repository.AdministratorRepository;
 30 | import com.alibaba.apiopenplatform.service.AdministratorService;
 31 | import com.alibaba.apiopenplatform.core.utils.PasswordHasher;
 32 | import com.alibaba.apiopenplatform.core.utils.IdGenerator;
 33 | import lombok.RequiredArgsConstructor;
 34 | import org.springframework.stereotype.Service;
 35 | import org.springframework.transaction.annotation.Transactional;
 36 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 37 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 38 | 
 39 | @Service
 40 | @RequiredArgsConstructor
 41 | @Transactional
 42 | public class AdministratorServiceImpl implements AdministratorService {
 43 | 
 44 |     private final AdministratorRepository administratorRepository;
 45 | 
 46 |     private final ContextHolder contextHolder;
 47 | 
 48 |     @Override
 49 |     public AuthResult login(String username, String password) {
 50 |         Administrator admin = administratorRepository.findByUsername(username)
 51 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.ADMINISTRATOR, username));
 52 | 
 53 |         if (!PasswordHasher.verify(password, admin.getPasswordHash())) {
 54 |             throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
 55 |         }
 56 | 
 57 |         String token = TokenUtil.generateAdminToken(admin.getAdminId());
 58 |         return AuthResult.of(token, TokenUtil.getTokenExpiresIn());
 59 |     }
 60 | 
 61 |     @Override
 62 |     public boolean needInit() {
 63 |         return administratorRepository.count() == 0;
 64 |     }
 65 | 
 66 |     @Override
 67 |     public AdminResult initAdmin(String username, String password) {
 68 |         Administrator admin = Administrator.builder()
 69 |                 .adminId(generateAdminId())
 70 |                 .username(username)
 71 |                 .passwordHash(PasswordHasher.hash(password))
 72 |                 .build();
 73 |         administratorRepository.save(admin);
 74 |         return new AdminResult().convertFrom(admin);
 75 |     }
 76 | 
 77 |     @Override
 78 |     public AdminResult getAdministrator() {
 79 |         Administrator administrator = findAdministrator(contextHolder.getUser());
 80 |         return new AdminResult().convertFrom(administrator);
 81 |     }
 82 | 
 83 |     @Override
 84 |     @Transactional
 85 |     public void resetPassword(String oldPassword, String newPassword) {
 86 |         Administrator admin = findAdministrator(contextHolder.getUser());
 87 | 
 88 |         if (!PasswordHasher.verify(oldPassword, admin.getPasswordHash())) {
 89 |             throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
 90 |         }
 91 | 
 92 |         admin.setPasswordHash(PasswordHasher.hash(newPassword));
 93 |         administratorRepository.save(admin);
 94 |     }
 95 | 
 96 |     private String generateAdminId() {
 97 |         return IdGenerator.genAdministratorId();
 98 |     }
 99 | 
100 |     private Administrator findAdministrator(String adminId) {
101 |         return administratorRepository.findByAdminId(adminId)
102 |                 .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.ADMINISTRATOR, adminId));
103 |     }
104 | } 
```

--------------------------------------------------------------------------------
/deploy/helm/templates/mysql.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | {{- if .Values.mysql.enabled }}
  2 | {{- $existingSecret := (lookup "v1" "Secret" .Release.Namespace "mysql-secret") }}
  3 | {{- $rootPassword := "" }}
  4 | {{- $userPassword := "" }}
  5 | {{- if $existingSecret }}
  6 |   {{- $rootPassword = (index $existingSecret.data "MYSQL_ROOT_PASSWORD" | b64dec) }}
  7 |   {{- $userPassword = (index $existingSecret.data "MYSQL_PASSWORD" | b64dec) }}
  8 | {{- else }}
  9 |   {{- if .Values.mysql.auth.rootPassword }}
 10 |     {{- $rootPassword = .Values.mysql.auth.rootPassword }}
 11 |   {{- else }}
 12 |     {{- $rootPassword = randAlphaNum 16 }}
 13 |   {{- end }}
 14 |   {{- if .Values.mysql.auth.password }}
 15 |     {{- $userPassword = .Values.mysql.auth.password }}
 16 |   {{- else }}
 17 |     {{- $userPassword = randAlphaNum 16 }}
 18 |   {{- end }}
 19 | {{- end }}
 20 | ---
 21 | # MySQL Secret: 存储敏感的数据库凭据(自动生成随机密码)
 22 | apiVersion: v1
 23 | kind: Secret
 24 | metadata:
 25 |   name: mysql-secret
 26 | type: Opaque
 27 | stringData:
 28 |   MYSQL_ROOT_PASSWORD: {{ $rootPassword | quote }}
 29 |   MYSQL_DATABASE: {{ .Values.mysql.auth.database | quote }}
 30 |   MYSQL_USER: {{ .Values.mysql.auth.username | quote }}
 31 |   MYSQL_PASSWORD: {{ $userPassword | quote }}
 32 | 
 33 | ---
 34 | # HiMarket Server Secret: 应用专用敏感配置(使用相同的密码)
 35 | apiVersion: v1
 36 | kind: Secret
 37 | metadata:
 38 |   name: himarket-server-secret
 39 |   labels:
 40 |     app: himarket-server
 41 | type: Opaque
 42 | stringData:
 43 |   # 使用相同的 MySQL 密码变量,确保一致性
 44 |   DB_HOST: "mysql-headless-svc"
 45 |   DB_PORT: "3306"
 46 |   DB_NAME: {{ .Values.mysql.auth.database | quote }}
 47 |   DB_USERNAME: {{ .Values.mysql.auth.username | quote }}
 48 |   DB_PASSWORD: {{ $userPassword | quote }}
 49 | 
 50 | ---
 51 | # MySQL Headless Service: 为 StatefulSet 提供稳定的网络域
 52 | apiVersion: v1
 53 | kind: Service
 54 | metadata:
 55 |   name: mysql-headless-svc
 56 | spec:
 57 |   ports:
 58 |     - port: 3306
 59 |       name: mysql
 60 |   clusterIP: None
 61 |   selector:
 62 |     app: mysql
 63 | 
 64 | ---
 65 | # MySQL External Service: 暴露数据库给外部访问(可选)
 66 | {{- if .Values.mysql.service.external.enabled }}
 67 | apiVersion: v1
 68 | kind: Service
 69 | metadata:
 70 |   name: mysql-external-svc
 71 | spec:
 72 |   type: {{ .Values.mysql.service.external.type }}
 73 |   ports:
 74 |     - port: 3306
 75 |       targetPort: 3306
 76 |       protocol: TCP
 77 |   selector:
 78 |     app: mysql
 79 | {{- end }}
 80 | 
 81 | ---
 82 | # MySQL StatefulSet: 部署 MySQL 应用
 83 | apiVersion: apps/v1
 84 | kind: StatefulSet
 85 | metadata:
 86 |   name: mysql
 87 |   labels:
 88 |     app: mysql
 89 | spec:
 90 |   replicas: {{ .Values.mysql.replicaCount }}
 91 |   selector:
 92 |     matchLabels:
 93 |       app: mysql
 94 |   serviceName: "mysql-headless-svc"
 95 |   template:
 96 |     metadata:
 97 |       labels:
 98 |         app: mysql
 99 |     spec:
100 |       serviceAccountName: {{ include "himarket.serviceAccountName" . }}
101 |       containers:
102 |         - name: mysql
103 |           image: "{{ .Values.hub }}/{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}"
104 |           imagePullPolicy: {{ .Values.mysql.image.pullPolicy }}
105 |           ports:
106 |             - containerPort: 3306
107 |               name: mysql
108 |           envFrom:
109 |             - secretRef:
110 |                 name: mysql-secret
111 |           volumeMounts:
112 |             - name: mysql-data
113 |               mountPath: /var/lib/mysql
114 |           {{- with .Values.mysql.resources }}
115 |           resources:
116 |             {{- toYaml . | nindent 12 }}
117 |           {{- end }}
118 |           # 健康检查
119 |           livenessProbe:
120 |             exec:
121 |               command:
122 |                 - mysqladmin
123 |                 - ping
124 |                 - -h
125 |                 - localhost
126 |                 - -u{{ .Values.mysql.auth.username }}
127 |                 - -p{{ $userPassword }}
128 |             initialDelaySeconds: 60
129 |             periodSeconds: 10
130 |             timeoutSeconds: 5
131 |           readinessProbe:
132 |             exec:
133 |               command:
134 |                 - mysql
135 |                 - -h
136 |                 - localhost
137 |                 - -u{{ .Values.mysql.auth.username }}
138 |                 - -p{{ $userPassword }}
139 |                 - -e
140 |                 - "SELECT 1"
141 |             initialDelaySeconds: 1
142 |             periodSeconds: 1
143 |             timeoutSeconds: 5
144 | 
145 |   volumeClaimTemplates:
146 |     - metadata:
147 |         name: mysql-data
148 |       spec:
149 |         accessModes: 
150 |           - {{ .Values.mysql.persistence.accessMode }}
151 |         resources:
152 |           requests:
153 |             storage: {{ .Values.mysql.persistence.size }}
154 |         {{- if .Values.mysql.persistence.storageClass }}
155 |         {{- if (eq "-" .Values.mysql.persistence.storageClass) }}
156 |         storageClassName: ""
157 |         {{- else }}
158 |         storageClassName: {{ .Values.mysql.persistence.storageClass | quote }}
159 |         {{- end }}
160 |         {{- end }}
161 | {{- end }}
162 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/controller/NacosController.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.controller;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.annotation.AdminAuth;
 23 | import com.alibaba.apiopenplatform.dto.params.nacos.CreateNacosParam;
 24 | import com.alibaba.apiopenplatform.dto.params.nacos.QueryNacosParam;
 25 | import com.alibaba.apiopenplatform.dto.params.nacos.UpdateNacosParam;
 26 | import com.alibaba.apiopenplatform.dto.result.MseNacosResult;
 27 | import com.alibaba.apiopenplatform.dto.result.NacosMCPServerResult;
 28 | import com.alibaba.apiopenplatform.dto.result.NacosNamespaceResult;
 29 | import com.alibaba.apiopenplatform.dto.result.NacosResult;
 30 | import com.alibaba.apiopenplatform.dto.result.PageResult;
 31 | import com.alibaba.apiopenplatform.service.NacosService;
 32 | import io.swagger.v3.oas.annotations.Operation;
 33 | import io.swagger.v3.oas.annotations.tags.Tag;
 34 | import lombok.RequiredArgsConstructor;
 35 | import org.springframework.data.domain.Pageable;
 36 | import org.springframework.web.bind.annotation.*;
 37 | 
 38 | import javax.validation.Valid;
 39 | 
 40 | @Tag(name = "Nacos资源管理", description = "Nacos实例管理与能力市场统一控制器")
 41 | @RestController
 42 | @RequestMapping("/nacos")
 43 | @RequiredArgsConstructor
 44 | @AdminAuth
 45 | public class NacosController {
 46 | 
 47 |     private final NacosService nacosService;
 48 | 
 49 |     @Operation(summary = "获取Nacos实例列表", description = "分页获取Nacos实例列表")
 50 |     @GetMapping
 51 |     public PageResult<NacosResult> listNacosInstances(Pageable pageable) {
 52 |         return nacosService.listNacosInstances(pageable);
 53 |     }
 54 | 
 55 |     @Operation(summary = "从阿里云MSE获取Nacos集群列表")
 56 |     @GetMapping("/mse")
 57 |     public PageResult<MseNacosResult> fetchNacos(@Valid QueryNacosParam param,
 58 |                                               Pageable pageable) {
 59 |         return nacosService.fetchNacos(param, pageable);
 60 |     }
 61 | 
 62 |     @Operation(summary = "获取Nacos实例详情", description = "根据ID获取Nacos实例详细信息")
 63 |     @GetMapping("/{nacosId}")
 64 |     public NacosResult getNacosInstance(@PathVariable String nacosId) {
 65 |         return nacosService.getNacosInstance(nacosId);
 66 |     }
 67 | 
 68 |     @Operation(summary = "创建Nacos实例", description = "创建新的Nacos实例")
 69 |     @PostMapping
 70 |     public void createNacosInstance(@RequestBody @Valid CreateNacosParam param) {
 71 |         nacosService.createNacosInstance(param);
 72 |     }
 73 | 
 74 |     @Operation(summary = "更新Nacos实例", description = "更新指定Nacos实例信息")
 75 |     @PutMapping("/{nacosId}")
 76 |     public void updateNacosInstance(@PathVariable String nacosId, @RequestBody @Valid UpdateNacosParam param) {
 77 |         nacosService.updateNacosInstance(nacosId, param);
 78 |     }
 79 | 
 80 |     @Operation(summary = "删除Nacos实例", description = "删除指定的Nacos实例")
 81 |     @DeleteMapping("/{nacosId}")
 82 |     public void deleteNacosInstance(@PathVariable String nacosId) {
 83 |         nacosService.deleteNacosInstance(nacosId);
 84 |     }
 85 | 
 86 |     @Operation(summary = "获取Nacos中的MCP Server列表", description = "获取指定Nacos实例中的MCP Server列表,可按命名空间过滤")
 87 |     @GetMapping("/{nacosId}/mcp-servers")
 88 |     public PageResult<NacosMCPServerResult> fetchMcpServers(@PathVariable String nacosId,
 89 |                                                             @RequestParam(value = "namespaceId", required = false) String namespaceId,
 90 |                                                             Pageable pageable) throws Exception {
 91 |         return nacosService.fetchMcpServers(nacosId, namespaceId, pageable);
 92 |     }
 93 | 
 94 |     @Operation(summary = "获取指定Nacos实例的命名空间列表")
 95 |     @GetMapping("/{nacosId}/namespaces")
 96 |     public PageResult<NacosNamespaceResult> fetchNamespaces(@PathVariable String nacosId,
 97 |                                                             Pageable pageable) throws Exception {
 98 |         return nacosService.fetchNamespaces(nacosId, pageable);
 99 |     }
100 | 
101 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/pages/ConsumerDetail.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useEffect, useState } from "react";
  2 | import { useParams, useNavigate } from "react-router-dom";
  3 | import { Layout } from "../components/Layout";
  4 | import { Alert, Tabs } from "antd";
  5 | import { ArrowLeftOutlined } from "@ant-design/icons";
  6 | import api from "../lib/api";
  7 | import { ConsumerBasicInfo, CredentialManager, SubscriptionManager } from "../components/consumer";
  8 | import type { Consumer, Subscription } from "../types/consumer";
  9 | import type { ApiResponse } from "../types";
 10 | 
 11 | function ConsumerDetailPage() {
 12 |   const { consumerId } = useParams();
 13 |   const navigate = useNavigate();
 14 |   const [subscriptionsLoading, setSubscriptionsLoading] = useState(false);
 15 |   const [error, setError] = useState('');
 16 |   const [consumer, setConsumer] = useState<Consumer>();
 17 |   const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
 18 |   const [activeTab, setActiveTab] = useState('basic');
 19 | 
 20 |   useEffect(() => {
 21 |     if (!consumerId) return;
 22 |     
 23 |     const fetchConsumerDetail = async () => {
 24 |       try {
 25 |         const response: ApiResponse<Consumer> = await api.get(`/consumers/${consumerId}`);
 26 |         if (response?.code === "SUCCESS" && response?.data) {
 27 |           setConsumer(response.data);
 28 |         }
 29 |       } catch (error) {
 30 |         console.error('获取消费者详情失败:', error);
 31 |         setError('加载失败,请稍后重试');
 32 |       }
 33 |     };
 34 | 
 35 |     const fetchSubscriptions = async () => {
 36 |       setSubscriptionsLoading(true);
 37 |       try {
 38 |         const response: ApiResponse<{content: Subscription[], totalElements: number}> = await api.get(`/consumers/${consumerId}/subscriptions`);
 39 |         if (response?.code === "SUCCESS" && response?.data) {
 40 |           // 从分页数据中提取实际的订阅数组
 41 |           const subscriptionsData = response.data.content || [];
 42 |           setSubscriptions(subscriptionsData);
 43 |         }
 44 |       } catch (error) {
 45 |         console.error('获取订阅列表失败:', error);
 46 |       } finally {
 47 |         setSubscriptionsLoading(false);
 48 |       }
 49 |     };
 50 |     
 51 |     const loadData = async () => {
 52 |       try {
 53 |         await Promise.all([
 54 |           fetchConsumerDetail(),
 55 |           fetchSubscriptions()
 56 |         ]);
 57 |       } finally {
 58 |         // 不设置loading状态,避免闪烁
 59 |       }
 60 |     };
 61 |     
 62 |     loadData();
 63 |   }, [consumerId]);
 64 | 
 65 |   if (error) {
 66 |     return (
 67 |       <Layout>
 68 |         <Alert
 69 |           message="加载失败"
 70 |           description={error}
 71 |           type="error"
 72 |           showIcon
 73 |           className="my-8" />
 74 |       </Layout>
 75 |     );
 76 |   }
 77 | 
 78 |   return (
 79 |     <Layout>
 80 |       {consumer ? (
 81 |         <>
 82 |           {/* 消费者头部 - 返回按钮 + 消费者名称 */}
 83 |           <div className="mb-2">
 84 |             <div className="flex items-center gap-2">
 85 |               <ArrowLeftOutlined 
 86 |                 className="text-gray-500 hover:text-gray-700 cursor-pointer"
 87 |                 style={{ fontSize: '20px', fontWeight: 'normal' }}
 88 |                 onClick={() => navigate('/consumers')}
 89 |               />
 90 |               <span className="text-2xl font-normal text-gray-500">
 91 |                 {consumer.name}
 92 |               </span>
 93 |             </div>
 94 |           </div>
 95 |           
 96 |           <Tabs activeKey={activeTab} onChange={setActiveTab}>
 97 |             <Tabs.TabPane tab="基本信息" key="basic">
 98 |               <ConsumerBasicInfo consumer={consumer} />
 99 |               <div className="mt-6">
100 |                 <CredentialManager 
101 |                   consumerId={consumerId!}
102 |                 />
103 |               </div>
104 |             </Tabs.TabPane>
105 | 
106 |             <Tabs.TabPane tab="订阅列表" key="authorization">
107 |               <SubscriptionManager 
108 |                 consumerId={consumerId!}
109 |                 subscriptions={subscriptions}
110 |                 onSubscriptionsChange={async () => {
111 |                   // 重新获取订阅列表
112 |                   if (consumerId) {
113 |                     setSubscriptionsLoading(true);
114 |                     try {
115 |                       const response: ApiResponse<{content: Subscription[], totalElements: number}> = await api.get(`/consumers/${consumerId}/subscriptions`);
116 |                       if (response?.code === "SUCCESS" && response?.data) {
117 |                         // 从分页数据中提取实际的订阅数组
118 |                         const subscriptionsData = response.data.content || [];
119 |                         setSubscriptions(subscriptionsData);
120 |                       }
121 |                     } catch (error) {
122 |                       console.error('获取订阅列表失败:', error);
123 |                     } finally {
124 |                       setSubscriptionsLoading(false);
125 |                     }
126 |                   }
127 |                 }}
128 |                 loading={subscriptionsLoading}
129 |               />
130 |             </Tabs.TabPane>
131 |           </Tabs>
132 |         </>
133 |       ) : (
134 |         <div className="flex items-center justify-center h-64">
135 |           <div className="text-gray-500">加载中...</div>
136 |         </div>
137 |       )}
138 |     </Layout>
139 |   );
140 | }
141 | 
142 | export default ConsumerDetailPage;
143 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/core/security/JwtAuthenticationFilter.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.core.security;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.constant.CommonConstants;
 23 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 24 | import com.alibaba.apiopenplatform.support.common.User;
 25 | import lombok.extern.slf4j.Slf4j;
 26 | import org.jetbrains.annotations.NotNull;
 27 | import org.springframework.security.core.Authentication;
 28 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
 29 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 30 | import org.springframework.security.core.context.SecurityContextHolder;
 31 | import org.springframework.web.filter.OncePerRequestFilter;
 32 | 
 33 | import javax.servlet.FilterChain;
 34 | import javax.servlet.ServletException;
 35 | import javax.servlet.http.HttpServletRequest;
 36 | import javax.servlet.http.HttpServletResponse;
 37 | import java.io.IOException;
 38 | import java.util.Collections;
 39 | 
 40 | @Slf4j
 41 | public class JwtAuthenticationFilter extends OncePerRequestFilter {
 42 | 
 43 |     // 白名单路径
 44 |     private static final String[] WHITELIST_PATHS = {
 45 |             "/admins/init",
 46 |             "/admins/need-init", 
 47 |             "/admins/login",
 48 |             "/developers",
 49 |             "/developers/login",
 50 |             "/developers/authorize",
 51 |             "/developers/callback",
 52 |             "/developers/providers",
 53 |             "/developers/oidc/authorize",
 54 |             "/developers/oidc/callback",
 55 |             "/developers/oidc/providers",
 56 |             "/developers/oauth2/token",
 57 |             "/portal/swagger-ui.html",
 58 |             "/portal/swagger-ui/**",
 59 |             "/portal/v3/api-docs/**",
 60 |             "/favicon.ico",
 61 |             "/error"
 62 |     };
 63 | 
 64 |     @Override
 65 |     protected void doFilterInternal(@NotNull HttpServletRequest request,
 66 |                                     @NotNull HttpServletResponse response,
 67 |                                     @NotNull FilterChain chain)
 68 |             throws IOException, ServletException {
 69 | 
 70 |         // 检查是否是白名单路径
 71 |         if (isWhitelistPath(request.getRequestURI())) {
 72 |             chain.doFilter(request, response);
 73 |             return;
 74 |         }
 75 | 
 76 |         try {
 77 |             String token = TokenUtil.getTokenFromRequest(request);
 78 |             if (token != null) {
 79 |                 // 检查token是否被撤销
 80 |                 if (TokenUtil.isTokenRevoked(token)) {
 81 |                     log.debug("Token已被撤销: {}", token);
 82 |                     SecurityContextHolder.clearContext();
 83 |                 } else {
 84 |                     try {
 85 |                         authenticateRequest(token);
 86 |                     } catch (Exception e) {
 87 |                         log.debug("Token认证失败: {}", e.getMessage());
 88 |                         SecurityContextHolder.clearContext();
 89 |                     }
 90 |                 }
 91 |             }
 92 |         } catch (Exception e) {
 93 |             log.debug("Token处理异常: {}", e.getMessage());
 94 |             SecurityContextHolder.clearContext();
 95 |         }
 96 |         chain.doFilter(request, response);
 97 |     }
 98 | 
 99 |     private boolean isWhitelistPath(String requestURI) {
100 |         for (String whitelistPath : WHITELIST_PATHS) {
101 |             if (whitelistPath.endsWith("/**")) {
102 |                 // 处理通配符路径
103 |                 String basePath = whitelistPath.substring(0, whitelistPath.length() - 2);
104 |                 if (requestURI.startsWith(basePath)) {
105 |                     return true;
106 |                 }
107 |             } else if (requestURI.equals(whitelistPath)) {
108 |                 return true;
109 |             }
110 |         }
111 |         return false;
112 |     }
113 | 
114 |     private void authenticateRequest(String token) {
115 |         User user = TokenUtil.parseUser(token);
116 |         // 设置认证信息
117 |         String role = CommonConstants.ROLE_PREFIX + user.getUserType().name();
118 |         Authentication authentication = new UsernamePasswordAuthenticationToken(
119 |                 user.getUserId(),
120 |                 null,
121 |                 Collections.singletonList(new SimpleGrantedAuthority(role))
122 |         );
123 |         SecurityContextHolder.getContext().setAuthentication(authentication);
124 |     }
125 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/pages/Login.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useEffect, useState } from "react";
  2 | import { useNavigate } from "react-router-dom";
  3 | import api from "../lib/api";
  4 | import { authApi } from '@/lib/api'
  5 | import { Form, Input, Button, Alert } from "antd";
  6 | 
  7 | const Login: React.FC = () => {
  8 |   const [loading, setLoading] = useState(false);
  9 |   const [error, setError] = useState("");
 10 |   const [isRegister, setIsRegister] = useState<boolean | null>(null); // null 表示正在加载
 11 |   const navigate = useNavigate();
 12 | 
 13 |   // 页面加载时检查权限
 14 |   useEffect(() => {
 15 |     const checkAuth = async () => {
 16 |       try {
 17 |         const response = await authApi.getNeedInit(); // 替换为你的权限接口
 18 |         setIsRegister(response.data === true); // 根据接口返回值决定是否显示注册表单
 19 |       } catch (err) {
 20 |         setIsRegister(false); // 默认显示登录表单
 21 |       }
 22 |     };
 23 | 
 24 |     checkAuth();
 25 |   }, []);
 26 | 
 27 |   // 登录表单提交
 28 |   const handleLogin = async (values: { username: string; password: string }) => {
 29 |     setLoading(true);
 30 |     setError("");
 31 |     try {
 32 |       const response = await api.post("/admins/login", {
 33 |         username: values.username,
 34 |         password: values.password,
 35 |       });
 36 |       const accessToken = response.data.access_token;
 37 |       localStorage.setItem('access_token', accessToken);
 38 |       localStorage.setItem('userInfo', JSON.stringify(response.data));
 39 |       navigate('/portals');
 40 |     } catch {
 41 |       setError("账号或密码错误");
 42 |     } finally {
 43 |       setLoading(false);
 44 |     }
 45 |   };
 46 | 
 47 |   // 注册表单提交
 48 |   const handleRegister = async (values: { username: string; password: string; confirmPassword: string }) => {
 49 |     setLoading(true);
 50 |     setError("");
 51 |     if (values.password !== values.confirmPassword) {
 52 |       setError("两次输入的密码不一致");
 53 |       setLoading(false);
 54 |       return;
 55 |     }
 56 |     try {
 57 |       const response = await api.post("/admins/init", {
 58 |         username: values.username,
 59 |         password: values.password,
 60 |       });
 61 |       if (response.data.adminId) {
 62 |         setIsRegister(false); // 初始化成功后切换到登录状态
 63 |       }
 64 |     } catch {
 65 |       setError("初始化失败,请重试");
 66 |     } finally {
 67 |       setLoading(false);
 68 |     }
 69 |   };
 70 | 
 71 |   return (
 72 |     <div className="flex items-center justify-center min-h-screen bg-white">
 73 |       <div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md flex flex-col items-center border border-gray-100">
 74 |         {/* Logo */}
 75 |         <div className="mb-4">
 76 |           <img src="/logo.png" alt="Logo" className="w-16 h-16 mx-auto mb-4" />
 77 |         </div>
 78 |         <h2 className="text-2xl font-bold mb-6 text-gray-900 text-center">
 79 |           {isRegister ? "注册Admin账号" : "登录HiMarket-后台"}
 80 |         </h2>
 81 | 
 82 |         {/* 登录表单 */}
 83 |         {!isRegister && (
 84 |           <Form
 85 |             className="w-full flex flex-col gap-4"
 86 |             layout="vertical"
 87 |             onFinish={handleLogin}
 88 |           >
 89 |             <Form.Item
 90 |               name="username"
 91 |               rules={[{ required: true, message: "请输入账号" }]}
 92 |             >
 93 |               <Input placeholder="账号" size="large" />
 94 |             </Form.Item>
 95 |             <Form.Item
 96 |               name="password"
 97 |               rules={[{ required: true, message: "请输入密码" }]}
 98 |             >
 99 |               <Input.Password placeholder="密码" size="large" />
100 |             </Form.Item>
101 |             {error && <Alert message={error} type="error" showIcon className="mb-2" />}
102 |             <Form.Item>
103 |               <Button
104 |                 type="primary"
105 |                 htmlType="submit"
106 |                 className="w-full"
107 |                 loading={loading}
108 |                 size="large"
109 |               >
110 |                 登录
111 |               </Button>
112 |             </Form.Item>
113 |           </Form>
114 |         )}
115 | 
116 |         {/* 注册表单 */}
117 |         {isRegister && (
118 |           <Form
119 |             className="w-full flex flex-col gap-4"
120 |             layout="vertical"
121 |             onFinish={handleRegister}
122 |           >
123 |             <Form.Item
124 |               name="username"
125 |               rules={[{ required: true, message: "请输入账号" }]}
126 |             >
127 |               <Input placeholder="账号" size="large" />
128 |             </Form.Item>
129 |             <Form.Item
130 |               name="password"
131 |               rules={[{ required: true, message: "请输入密码" }]}
132 |             >
133 |               <Input.Password placeholder="密码" size="large" />
134 |             </Form.Item>
135 |             <Form.Item
136 |               name="confirmPassword"
137 |               rules={[{ required: true, message: "请确认密码" }]}
138 |             >
139 |               <Input.Password placeholder="确认密码" size="large" />
140 |             </Form.Item>
141 |             {error && <Alert message={error} type="error" showIcon className="mb-2" />}
142 |             <Form.Item>
143 |               <Button
144 |                 type="primary"
145 |                 htmlType="submit"
146 |                 className="w-full"
147 |                 loading={loading}
148 |                 size="large"
149 |               >
150 |                 初始化
151 |               </Button>
152 |             </Form.Item>
153 |           </Form>
154 |         )}
155 |       </div>
156 |     </div>
157 |   );
158 | };
159 | 
160 | export default Login;
161 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/console/ImportMseNacosModal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState } from 'react'
  2 | import { Button, Table, Modal, Form, Input, message } from 'antd'
  3 | import { nacosApi } from '@/lib/api'
  4 | 
  5 | interface MseNacosItem {
  6 |   instanceId: string
  7 |   name: string
  8 |   serverIntranetEndpoint?: string
  9 |   serverInternetEndpoint?: string
 10 |   version?: string
 11 | }
 12 | 
 13 | interface ImportMseNacosModalProps {
 14 |   visible: boolean
 15 |   onCancel: () => void
 16 |   // 将选中的 MSE Nacos 信息带入创建表单
 17 |   onPrefill: (values: {
 18 |     nacosName?: string
 19 |     serverUrl?: string
 20 |   serverInternetEndpoint?: string
 21 |   serverIntranetEndpoint?: string
 22 |     username?: string
 23 |     password?: string
 24 |     accessKey?: string
 25 |     secretKey?: string
 26 |     description?: string
 27 |   nacosId?: string
 28 |   }) => void
 29 | }
 30 | 
 31 | export default function ImportMseNacosModal({ visible, onCancel, onPrefill }: ImportMseNacosModalProps) {
 32 |   const [importForm] = Form.useForm()
 33 | 
 34 |   const [loading, setLoading] = useState(false)
 35 |   const [list, setList] = useState<MseNacosItem[]>([])
 36 |   const [selected, setSelected] = useState<MseNacosItem | null>(null)
 37 |   const [auth, setAuth] = useState({
 38 |     regionId: '',
 39 |     accessKey: '',
 40 |     secretKey: ''
 41 |   })
 42 |   const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 })
 43 | 
 44 |   const fetchMseNacos = async (values: any, page = 0, size = 10) => {
 45 |     setLoading(true)
 46 |     try {
 47 |       const res = await nacosApi.getMseNacos({ ...values, page, size })
 48 |       setList(res.data?.content || [])
 49 |       setPagination({ current: page + 1, pageSize: size, total: res.data?.totalElements || 0 })
 50 |     } catch (e: any) {
 51 |       // message.error(e?.response?.data?.message || '获取 MSE Nacos 列表失败')
 52 |     } finally {
 53 |       setLoading(false)
 54 |     }
 55 |   }
 56 | 
 57 |   const handleImport = async () => {
 58 |     if (!selected) {
 59 |       message.warning('请选择一个 Nacos 实例')
 60 |       return
 61 |     }
 62 |     // 将关键信息带出到创建表单,供用户补充
 63 |     onPrefill({
 64 |       nacosName: selected.name,
 65 |       serverUrl: selected.serverInternetEndpoint || selected.serverIntranetEndpoint,
 66 |   serverInternetEndpoint: selected.serverInternetEndpoint,
 67 |   serverIntranetEndpoint: selected.serverIntranetEndpoint,
 68 |   accessKey: auth.accessKey,
 69 |   secretKey: auth.secretKey,
 70 |   nacosId: selected.instanceId,
 71 |     })
 72 |     handleCancel()
 73 |   }
 74 | 
 75 |   const handleCancel = () => {
 76 |     setSelected(null)
 77 |     setList([])
 78 |     setPagination({ current: 1, pageSize: 10, total: 0 })
 79 |     importForm.resetFields()
 80 |     onCancel()
 81 |   }
 82 | 
 83 |   return (
 84 |     <Modal title="导入 MSE Nacos 实例" open={visible} onCancel={handleCancel} footer={null} width={800}>
 85 |       <Form form={importForm} layout="vertical" preserve={false}>
 86 |         {list.length === 0 && (
 87 |           <div className="mb-4">
 88 |             <h3 className="text-lg font-medium mb-3">认证信息</h3>
 89 |             <Form.Item label="Region" name="regionId" rules={[{ required: true, message: '请输入region' }]}>
 90 |               <Input />
 91 |             </Form.Item>
 92 |             <Form.Item label="Access Key" name="accessKey" rules={[{ required: true, message: '请输入accessKey' }]}>
 93 |               <Input />
 94 |             </Form.Item>
 95 |             <Form.Item label="Secret Key" name="secretKey" rules={[{ required: true, message: '请输入secretKey' }]}>
 96 |               <Input.Password />
 97 |             </Form.Item>
 98 |             <Button 
 99 |               type="primary" 
100 |               onClick={() => {
101 |                 importForm.validateFields().then((values) => {
102 |                   setAuth(values)
103 |                   fetchMseNacos(values)
104 |                 })
105 |               }}
106 |               loading={loading}
107 |             >
108 |               获取实例列表
109 |             </Button>
110 |           </div>
111 |         )}
112 | 
113 |         {list.length > 0 && (
114 |           <div className="mb-4">
115 |             <h3 className="text-lg font-medium mb-3">选择 Nacos 实例</h3>
116 |             <Table
117 |               rowKey="instanceId"
118 |               columns={[
119 |                 { title: '实例ID', dataIndex: 'instanceId' },
120 |                 { title: '名称', dataIndex: 'name' },
121 |                 { title: '版本', dataIndex: 'version' },
122 |               ]}
123 |               dataSource={list}
124 |               rowSelection={{
125 |                 type: 'radio',
126 |                 selectedRowKeys: selected ? [selected.instanceId] : [],
127 |                 onChange: (_selectedRowKeys, selectedRows) => setSelected(selectedRows[0]),
128 |               }}
129 |               pagination={{
130 |                 current: pagination.current,
131 |                 pageSize: pagination.pageSize,
132 |                 total: pagination.total,
133 |                 onChange: (page, pageSize) => fetchMseNacos(auth, page - 1, pageSize),
134 |                 showSizeChanger: true,
135 |                 showQuickJumper: true,
136 |                 showTotal: (total) => `共 ${total} 条`,
137 |               }}
138 |               size="small"
139 |             />
140 |           </div>
141 |         )}
142 | 
143 |         {selected && (
144 |           <div className="text-right">
145 |             <Button type="primary" onClick={handleImport}>
146 |               导入
147 |             </Button>
148 |           </div>
149 |         )}
150 |       </Form>
151 |     </Modal>
152 |   )
153 | }
154 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/controller/GatewayController.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.controller;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.annotation.AdminAuth;
 23 | import com.alibaba.apiopenplatform.dto.params.gateway.ImportGatewayParam;
 24 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam;
 25 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryAdpAIGatewayParam;
 26 | import com.alibaba.apiopenplatform.dto.params.gateway.QueryGatewayParam;
 27 | import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult;
 28 | import com.alibaba.apiopenplatform.dto.result.*;
 29 | import com.alibaba.apiopenplatform.service.GatewayService;
 30 | import com.alibaba.apiopenplatform.service.AdpAIGatewayService;
 31 | import io.swagger.v3.oas.annotations.Operation;
 32 | import io.swagger.v3.oas.annotations.tags.Tag;
 33 | import lombok.RequiredArgsConstructor;
 34 | import org.springframework.data.domain.Pageable;
 35 | import org.springframework.web.bind.annotation.*;
 36 | 
 37 | import javax.validation.Valid;
 38 | 
 39 | @Tag(name = "网关资源管理")
 40 | @RestController
 41 | @RequestMapping("/gateways")
 42 | @RequiredArgsConstructor
 43 | @AdminAuth
 44 | public class GatewayController {
 45 | 
 46 |     private final GatewayService gatewayService;
 47 |     private final AdpAIGatewayService adpAIGatewayService;
 48 | 
 49 |     @Operation(summary = "获取APIG Gateway列表")
 50 |     @GetMapping("/apig")
 51 |     public PageResult<GatewayResult> fetchGateways(@Valid QueryAPIGParam param,
 52 |                                                    @RequestParam(defaultValue = "1") int page,
 53 |                                                    @RequestParam(defaultValue = "500") int size) {
 54 |         return gatewayService.fetchGateways(param, page, size);
 55 |     }
 56 | 
 57 |     @Operation(summary = "获取ADP AI Gateway列表")
 58 |     @PostMapping("/adp")
 59 |     public PageResult<GatewayResult> fetchAdpGateways(@RequestBody @Valid QueryAdpAIGatewayParam param,
 60 |                                                       @RequestParam(defaultValue = "1") int page,
 61 |                                                       @RequestParam(defaultValue = "500") int size) {
 62 |         return adpAIGatewayService.fetchGateways(param, page, size);
 63 |     }
 64 | 
 65 |     @Operation(summary = "获取导入的Gateway列表")
 66 |     @GetMapping
 67 |     public PageResult<GatewayResult> listGateways(QueryGatewayParam param, Pageable pageable) {
 68 |         return gatewayService.listGateways(param, pageable);
 69 |     }
 70 | 
 71 |     @Operation(summary = "导入Gateway")
 72 |     @PostMapping
 73 |     public void importGateway(@RequestBody @Valid ImportGatewayParam param) {
 74 |         gatewayService.importGateway(param);
 75 |     }
 76 | 
 77 |     @Operation(summary = "删除Gateway")
 78 |     @DeleteMapping("/{gatewayId}")
 79 |     public void deleteGateway(@PathVariable String gatewayId) {
 80 |         gatewayService.deleteGateway(gatewayId);
 81 |     }
 82 | 
 83 |     @Operation(summary = "获取REST API列表")
 84 |     @GetMapping("/{gatewayId}/rest-apis")
 85 |     public PageResult<APIResult> fetchRESTAPIs(@PathVariable String gatewayId,
 86 |                                                @RequestParam(defaultValue = "1") int page,
 87 |                                                @RequestParam(defaultValue = "500") int size) {
 88 |         return gatewayService.fetchRESTAPIs(gatewayId, page, size);
 89 |     }
 90 | 
 91 | //    @Operation(summary = "获取API列表")
 92 | //    @GetMapping("/{gatewayId}/apis")
 93 | //    public PageResult<APIResult> fetchAPIs(@PathVariable String gatewayId,
 94 | //                                           @RequestParam String apiType,
 95 | //                                           Pageable pageable) {
 96 | //        return gatewayService.fetchAPIs(gatewayId, apiType, pageable);
 97 | //    }
 98 | 
 99 |     @Operation(summary = "获取MCP Server列表")
100 |     @GetMapping("/{gatewayId}/mcp-servers")
101 |     public PageResult<GatewayMCPServerResult> fetchMcpServers(@PathVariable String gatewayId,
102 |                                                               @RequestParam(defaultValue = "1") int page,
103 |                                                               @RequestParam(defaultValue = "500") int size) {
104 |         return gatewayService.fetchMcpServers(gatewayId, page, size);
105 |     }
106 | 
107 |     @Operation(summary = "获取仪表板URL")
108 |     @GetMapping("/{gatewayId}/dashboard")
109 |     public String getDashboard(@PathVariable String gatewayId,
110 |                                @RequestParam(required = false, defaultValue = "API") String type) {
111 |         return gatewayService.getDashboard(gatewayId, type);
112 |     }
113 | }
114 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/GatewayOperator.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.service.gateway;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.exception.BusinessException;
 23 | import com.alibaba.apiopenplatform.core.exception.ErrorCode;
 24 | import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult;
 25 | import com.alibaba.apiopenplatform.dto.result.*;
 26 | import com.alibaba.apiopenplatform.entity.*;
 27 | import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
 28 | import com.alibaba.apiopenplatform.service.gateway.client.GatewayClient;
 29 | import com.alibaba.apiopenplatform.service.gateway.client.HigressClient;
 30 | import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
 31 | import com.alibaba.apiopenplatform.support.enums.GatewayType;
 32 | import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
 33 | import lombok.extern.slf4j.Slf4j;
 34 | 
 35 | import java.util.Map;
 36 | import java.util.concurrent.ConcurrentHashMap;
 37 | 
 38 | @Slf4j
 39 | public abstract class GatewayOperator<T> {
 40 | 
 41 |     private final Map<String, GatewayClient> clientCache = new ConcurrentHashMap<>();
 42 | 
 43 |     abstract public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size);
 44 | 
 45 |     abstract public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size);
 46 | 
 47 |     abstract public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size);
 48 | 
 49 |     abstract public String fetchAPIConfig(Gateway gateway, Object config);
 50 | 
 51 |     abstract public String fetchMcpConfig(Gateway gateway, Object conf);
 52 | 
 53 |     abstract public PageResult<GatewayResult> fetchGateways(Object param, int page, int size);
 54 | 
 55 |     abstract public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config);
 56 | 
 57 |     abstract public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config);
 58 | 
 59 |     abstract public void deleteConsumer(String consumerId, GatewayConfig config);
 60 | 
 61 |     /**
 62 |      * 检查消费者是否存在于网关中
 63 |      * @param consumerId 消费者ID
 64 |      * @param config 网关配置
 65 |      * @return 是否存在
 66 |      */
 67 |     abstract public boolean isConsumerExists(String consumerId, GatewayConfig config);
 68 | 
 69 |     abstract public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig);
 70 | 
 71 |     abstract public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig);
 72 | 
 73 |     abstract public APIResult fetchAPI(Gateway gateway, String apiId);
 74 | 
 75 |     abstract public GatewayType getGatewayType();
 76 | 
 77 |     /**
 78 |      * 获取网关控制台仪表盘链接
 79 |      * @param gateway 网关实体
 80 |      * @return 仪表盘访问链接
 81 |      */
 82 |     abstract public String getDashboard(Gateway gateway,String type);
 83 | 
 84 |     @SuppressWarnings("unchecked")
 85 |     protected T getClient(Gateway gateway) {
 86 |         String clientKey = gateway.getGatewayType().isAPIG() ?
 87 |                 gateway.getApigConfig().buildUniqueKey() : gateway.getHigressConfig().buildUniqueKey();
 88 |         return (T) clientCache.computeIfAbsent(
 89 |                 clientKey,
 90 |                 key -> createClient(gateway)
 91 |         );
 92 |     }
 93 | 
 94 | //    @SuppressWarnings("unchecked")
 95 | //    protected T getClient(Gateway gateway) {
 96 | //        String clientKey = gateway.getGatewayType().isAPIG() ?
 97 | //                gateway.getApigConfig().buildUniqueKey() : gateway.getHigressConfig().buildUniqueKey();
 98 | //        return (T) clientCache.computeIfAbsent(
 99 | //                clientKey,
100 | //                key -> createClient(gateway)
101 | //        );
102 | //    }
103 | 
104 |     /**
105 |      * 创建网关客户端
106 |      */
107 |     private GatewayClient createClient(Gateway gateway) {
108 |         switch (gateway.getGatewayType()) {
109 |             case APIG_API:
110 |             case APIG_AI:
111 |                 return new APIGClient(gateway.getApigConfig());
112 |             case HIGRESS:
113 |                 return new HigressClient(gateway.getHigressConfig());
114 |             default:
115 |                 throw new BusinessException(ErrorCode.INTERNAL_ERROR,
116 |                         "No factory found for gateway type: " + gateway.getGatewayType());
117 |         }
118 |     }
119 | 
120 |     /**
121 |      * 移除网关客户端
122 |      */
123 |     public void removeClient(String instanceId) {
124 |         GatewayClient client = clientCache.remove(instanceId);
125 |         try {
126 |             client.close();
127 |         } catch (Exception e) {
128 |             log.error("Error closing client for instance: {}", instanceId, e);
129 |         }
130 |     }
131 | }
132 | 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/controller/ProductController.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.controller;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.annotation.AdminAuth;
 23 | import com.alibaba.apiopenplatform.core.annotation.AdminOrDeveloperAuth;
 24 | import com.alibaba.apiopenplatform.dto.params.product.*;
 25 | import com.alibaba.apiopenplatform.dto.params.product.CreateProductRefParam;
 26 | import com.alibaba.apiopenplatform.dto.result.*;
 27 | import com.alibaba.apiopenplatform.service.ProductService;
 28 | import io.swagger.v3.oas.annotations.Operation;
 29 | import io.swagger.v3.oas.annotations.tags.Tag;
 30 | import lombok.RequiredArgsConstructor;
 31 | import lombok.extern.slf4j.Slf4j;
 32 | import org.springframework.data.domain.Pageable;
 33 | import org.springframework.web.bind.annotation.*;
 34 | 
 35 | import javax.validation.Valid;
 36 | 
 37 | @Tag(name = "API产品管理", description = "提供API产品的创建、更新、删除、查询、订阅等管理功能")
 38 | @RestController
 39 | @RequestMapping("/products")
 40 | @Slf4j
 41 | @RequiredArgsConstructor
 42 | public class ProductController {
 43 | 
 44 |     private final ProductService productService;
 45 | 
 46 |     @Operation(summary = "创建API产品")
 47 |     @PostMapping
 48 |     @AdminAuth
 49 |     public ProductResult createProduct(@RequestBody @Valid CreateProductParam param) {
 50 |         return productService.createProduct(param);
 51 |     }
 52 | 
 53 |     @Operation(summary = "获取API产品列表")
 54 |     @GetMapping
 55 |     public PageResult<ProductResult> listProducts(QueryProductParam param,
 56 |                                                   Pageable pageable) {
 57 |         return productService.listProducts(param, pageable);
 58 |     }
 59 | 
 60 |     @Operation(summary = "获取API产品详情")
 61 |     @GetMapping("/{productId}")
 62 |     public ProductResult getProduct(@PathVariable String productId) {
 63 |         return productService.getProduct(productId);
 64 |     }
 65 | 
 66 |     @Operation(summary = "更新API产品")
 67 |     @PutMapping("/{productId}")
 68 |     @AdminAuth
 69 |     public ProductResult updateProduct(@PathVariable String productId, @RequestBody @Valid UpdateProductParam param) {
 70 |         return productService.updateProduct(productId, param);
 71 |     }
 72 | 
 73 |     @Operation(summary = "发布API产品")
 74 |     @PostMapping("/{productId}/publications/{portalId}")
 75 |     @AdminAuth
 76 |     public void publishProduct(@PathVariable String productId, @PathVariable String portalId) {
 77 |         productService.publishProduct(productId, portalId);
 78 |     }
 79 | 
 80 |     @Operation(summary = "获取API产品的发布信息")
 81 |     @GetMapping("/{productId}/publications")
 82 |     @AdminAuth
 83 |     public PageResult<ProductPublicationResult> getPublications(@PathVariable String productId, Pageable pageable) {
 84 |         return productService.getPublications(productId, pageable);
 85 |     }
 86 | 
 87 |     @Operation(summary = "下线API产品")
 88 |     @DeleteMapping("/{productId}/publications/{portalId}")
 89 |     @AdminAuth
 90 |     public void unpublishProduct(@PathVariable String productId, @PathVariable String portalId) {
 91 |         productService.unpublishProduct(productId, portalId);
 92 |     }
 93 | 
 94 |     @Operation(summary = "删除API产品")
 95 |     @DeleteMapping("/{productId}")
 96 |     @AdminAuth
 97 |     public void deleteProduct(@PathVariable String productId) {
 98 |         productService.deleteProduct(productId);
 99 |     }
100 | 
101 |     @Operation(summary = "API产品关联API或MCP Server")
102 |     @PostMapping("/{productId}/ref")
103 |     @AdminAuth
104 |     public void addProductRef(@PathVariable String productId, @RequestBody @Valid CreateProductRefParam param) throws Exception {
105 |         productService.addProductRef(productId, param);
106 |     }
107 | 
108 |     @Operation(summary = "获取API产品关联的API或MCP Server")
109 |     @GetMapping("/{productId}/ref")
110 |     public ProductRefResult getProductRef(@PathVariable String productId) {
111 |         return productService.getProductRef(productId);
112 |     }
113 | 
114 |     @Operation(summary = "删除API产品关联的API或MCP Server")
115 |     @DeleteMapping("/{productId}/ref")
116 |     @AdminAuth
117 |     public void deleteProductRef(@PathVariable String productId) {
118 |         productService.deleteProductRef(productId);
119 |     }
120 | 
121 |     @Operation(summary = "获取API产品的Dashboard监控面板URL")
122 |     @GetMapping("/{productId}/dashboard")
123 |     public String getProductDashboard(@PathVariable String productId) {
124 |         return productService.getProductDashboard(productId);
125 |     }
126 | 
127 |     @Operation(summary = "获取产品的订阅列表")
128 |     @GetMapping("/{productId}/subscriptions")
129 |     @AdminOrDeveloperAuth
130 |     public PageResult<SubscriptionResult> listProductSubscriptions(
131 |             @PathVariable String productId,
132 |             QueryProductSubscriptionParam param,
133 |             Pageable pageable) {
134 |         return productService.listProductSubscriptions(productId, param, pageable);
135 |     }
136 | }
137 | 
```

--------------------------------------------------------------------------------
/portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/SecurityConfig.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.config;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.security.JwtAuthenticationFilter;
 23 | import com.alibaba.apiopenplatform.core.utils.TokenUtil;
 24 | import lombok.RequiredArgsConstructor;
 25 | import lombok.extern.slf4j.Slf4j;
 26 | import org.springframework.context.annotation.Bean;
 27 | import org.springframework.context.annotation.Configuration;
 28 | import org.springframework.security.authentication.AuthenticationManager;
 29 | import org.springframework.security.config.Customizer;
 30 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
 31 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 32 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 33 | import org.springframework.security.config.http.SessionCreationPolicy;
 34 | import org.springframework.security.web.SecurityFilterChain;
 35 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 36 |  
 37 | import org.springframework.web.cors.CorsConfiguration;
 38 | import org.springframework.web.cors.CorsConfigurationSource;
 39 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 40 | 
 41 | import java.util.*;
 42 | 
 43 | import com.alibaba.apiopenplatform.core.security.DeveloperAuthenticationProvider;
 44 | import org.springframework.http.HttpMethod;
 45 | 
 46 | /**
 47 |  * Spring Security安全配置,集成JWT认证与接口权限控制,支持管理员和开发者多用户体系
 48 |  *
 49 |  */
 50 | @Configuration
 51 | @RequiredArgsConstructor
 52 | @Slf4j
 53 | @EnableGlobalMethodSecurity(prePostEnabled = true)
 54 | public class SecurityConfig {
 55 | 
 56 |     private final DeveloperAuthenticationProvider developerAuthenticationProvider;
 57 | 
 58 |     // Auth相关
 59 |     private static final String[] AUTH_WHITELIST = {
 60 |             "/admins/init",
 61 |             "/admins/need-init",
 62 |             "/admins/login",
 63 |             "/developers",
 64 |             "/developers/login",
 65 |             "/developers/authorize",
 66 |             "/developers/callback",
 67 |             "/developers/providers",
 68 |             "/developers/oidc/authorize",
 69 |             "/developers/oidc/callback",
 70 |             "/developers/oidc/providers",
 71 |             "/developers/oauth2/token"
 72 |     };
 73 | 
 74 |     // Swagger API文档相关
 75 |     private static final String[] SWAGGER_WHITELIST = {
 76 |             "/portal/swagger-ui.html",
 77 |             "/portal/swagger-ui/**",
 78 |             "/portal/v3/api-docs/**"
 79 |     };
 80 | 
 81 |     // 系统路径白名单
 82 |     private static final String[] SYSTEM_WHITELIST = {
 83 |             "/favicon.ico",
 84 |             "/error"
 85 |     };
 86 | 
 87 |     @Bean
 88 |     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 89 |         http
 90 |                 .cors(Customizer.withDefaults())
 91 |                 .csrf().disable()
 92 |                 .sessionManagement()
 93 |                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
 94 | //                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
 95 |                 .and()
 96 |                 .authorizeRequests()
 97 |                 // OPTIONS请求放行
 98 |                 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
 99 |                 // 认证相关接口放行
100 |                 .antMatchers(AUTH_WHITELIST).permitAll()
101 |                 // Swagger相关接口放行
102 |                 .antMatchers(SWAGGER_WHITELIST).permitAll()
103 |                 // 系统路径放行
104 |                 .antMatchers(SYSTEM_WHITELIST).permitAll()
105 |                 .anyRequest().authenticated()
106 |                 .and()
107 |                 .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
108 |                 .authenticationProvider(developerAuthenticationProvider);
109 |         return http.build();
110 |     }
111 | 
112 |     @Bean
113 |     public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
114 |         return authenticationConfiguration.getAuthenticationManager();
115 |     }
116 | 
117 |     @Bean
118 |     public CorsConfigurationSource corsConfigurationSource() {
119 |         CorsConfiguration corsConfig = new CorsConfiguration();
120 |         corsConfig.setAllowedOriginPatterns(Collections.singletonList("*"));
121 |         corsConfig.setAllowedMethods(Collections.singletonList("*"));
122 |         corsConfig.setAllowedHeaders(Collections.singletonList("*"));
123 |         corsConfig.setAllowCredentials(true);
124 |         corsConfig.setMaxAge(3600L);
125 | 
126 |         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
127 |         source.registerCorsConfiguration("/**", corsConfig);
128 |         return source;
129 |     }
130 | }
```

--------------------------------------------------------------------------------
/portal-server/pom.xml:
--------------------------------------------------------------------------------

```
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <project xmlns="http://maven.apache.org/POM/4.0.0"
  3 |          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 |          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 |     <modelVersion>4.0.0</modelVersion>
  6 |     <parent>
  7 |         <groupId>com.alibaba.himarket</groupId>
  8 |         <artifactId>himarket</artifactId>
  9 |         <version>1.0-SNAPSHOT</version>
 10 |     </parent>
 11 | 
 12 |     <artifactId>portal-server</artifactId>
 13 | 
 14 |     <properties>
 15 |         <maven.compiler.source>8</maven.compiler.source>
 16 |         <maven.compiler.target>8</maven.compiler.target>
 17 |         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 18 |     </properties>
 19 | 
 20 |     <dependencies>
 21 |         <dependency>
 22 |             <groupId>com.alibaba.himarket</groupId>
 23 |             <artifactId>portal-dal</artifactId>
 24 |             <version>1.0-SNAPSHOT</version>
 25 |             <exclusions>
 26 |                 <exclusion>
 27 |                     <artifactId>checker-qual</artifactId>
 28 |                     <groupId>org.checkerframework</groupId>
 29 |                 </exclusion>
 30 |             </exclusions>
 31 |         </dependency>
 32 | 
 33 |         <dependency>
 34 |             <groupId>org.springframework.boot</groupId>
 35 |             <artifactId>spring-boot-starter-web</artifactId>
 36 |         </dependency>
 37 | 
 38 |         <dependency>
 39 |             <groupId>org.springframework.boot</groupId>
 40 |             <artifactId>spring-boot-starter-validation</artifactId>
 41 |         </dependency>
 42 | 
 43 |         <dependency>
 44 |             <groupId>org.springframework.boot</groupId>
 45 |             <artifactId>spring-boot-starter-oauth2-client</artifactId>
 46 |             <version>${spring-boot.version}</version>
 47 |         </dependency>
 48 | 
 49 |         <dependency>
 50 |             <groupId>org.springdoc</groupId>
 51 |             <artifactId>springdoc-openapi-ui</artifactId>
 52 |         </dependency>
 53 | 
 54 |         <dependency>
 55 |             <groupId>org.springframework.boot</groupId>
 56 |             <artifactId>spring-boot-starter-test</artifactId>
 57 |         </dependency>
 58 | 
 59 |         <dependency>
 60 |             <groupId>org.springframework.boot</groupId>
 61 |             <artifactId>spring-boot-starter-security</artifactId>
 62 |         </dependency>
 63 | 
 64 | <!--        <dependency>-->
 65 | <!--            <groupId>org.springframework.boot</groupId>-->
 66 | <!--            <artifactId>spring-boot-starter-mail</artifactId>-->
 67 | <!--        </dependency>-->
 68 | 
 69 |         <dependency>
 70 |             <groupId>org.springframework.boot</groupId>
 71 |             <artifactId>spring-boot-starter-thymeleaf</artifactId>
 72 |         </dependency>
 73 | 
 74 |         <dependency>
 75 |             <groupId>com.fasterxml.jackson.datatype</groupId>
 76 |             <artifactId>jackson-datatype-jsr310</artifactId>
 77 |         </dependency>
 78 | 
 79 |         <dependency>
 80 |             <groupId>com.aliyun</groupId>
 81 |             <artifactId>aliyun-java-sdk-core</artifactId>
 82 |             <exclusions>
 83 |                 <exclusion>
 84 |                     <artifactId>org.jacoco.agent</artifactId>
 85 |                     <groupId>org.jacoco</groupId>
 86 |                 </exclusion>
 87 |             </exclusions>
 88 |         </dependency>
 89 | 
 90 |         <dependency>
 91 |             <groupId>com.aliyun</groupId>
 92 |             <artifactId>alibabacloud-apig20240327</artifactId>
 93 |             <exclusions>
 94 |                 <exclusion>
 95 |                     <artifactId>annotations</artifactId>
 96 |                     <groupId>org.jetbrains</groupId>
 97 |                 </exclusion>
 98 |             </exclusions>
 99 |         </dependency>
100 | 
101 |         <dependency>
102 |             <groupId>com.squareup.okhttp3</groupId>
103 |             <artifactId>okhttp</artifactId>
104 |         </dependency>
105 | 
106 |         <dependency>
107 |             <groupId>com.alibaba.nacos</groupId>
108 |             <artifactId>nacos-maintainer-client</artifactId>
109 |             <version>3.0.2</version>
110 |         </dependency>
111 | 
112 |         <dependency>
113 |             <groupId>com.aliyun</groupId>
114 |             <artifactId>mse20190531</artifactId>
115 |             <exclusions>
116 |                 <exclusion>
117 |                     <artifactId>dom4j</artifactId>
118 |                     <groupId>org.dom4j</groupId>
119 |                 </exclusion>
120 |                 <exclusion>
121 |                     <artifactId>credentials-java</artifactId>
122 |                     <groupId>com.aliyun</groupId>
123 |                 </exclusion>
124 |             </exclusions>
125 |         </dependency>
126 | 
127 |         <dependency>
128 |           <groupId>com.aliyun</groupId>
129 |           <artifactId>alibabacloud-sls20201230</artifactId>
130 |           <version>4.0.11</version>
131 |         </dependency>
132 |         
133 |         <dependency>
134 |             <groupId>org.bouncycastle</groupId>
135 |             <artifactId>bcprov-jdk15to18</artifactId>
136 |         </dependency>
137 |         <dependency>
138 |             <groupId>cn.hutool</groupId>
139 |             <artifactId>hutool-all</artifactId>
140 |             <exclusions>
141 |                 <exclusion>
142 |                     <groupId>org.bouncycastle</groupId>
143 |                     <artifactId>bcprov-jdk16</artifactId>
144 |                 </exclusion>
145 |             </exclusions>
146 |         </dependency>
147 |         
148 |         <!-- SnakeYAML for JSON to YAML conversion -->
149 |         <dependency>
150 |             <groupId>org.yaml</groupId>
151 |             <artifactId>snakeyaml</artifactId>
152 |             <version>2.0</version>
153 |         </dependency>
154 |     </dependencies>
155 | 
156 | </project>
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/Layout.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect } from 'react'
  2 | import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom'
  3 | import { GlobalOutlined, AppstoreOutlined, DesktopOutlined, UserOutlined, MenuOutlined, SettingOutlined } from '@ant-design/icons'
  4 | import { Button } from 'antd'
  5 | import { isAuthenticated, removeToken } from '../lib/utils'
  6 | 
  7 | interface NavigationItem {
  8 |   name: string
  9 |   cn: string
 10 |   href: string
 11 |   icon: React.ComponentType<any>
 12 |   children?: NavigationItem[]
 13 | }
 14 | 
 15 | const Layout: React.FC = () => {
 16 |   const location = useLocation()
 17 |   const navigate = useNavigate()
 18 |   const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false)
 19 |   const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false)
 20 | 
 21 |   useEffect(() => {
 22 |     // 检查 cookie 中的 token 来判断登录状态
 23 |     const checkAuthStatus = () => {
 24 |       const hasToken = isAuthenticated()
 25 |       setIsLoggedIn(hasToken)
 26 |     }
 27 |     
 28 |     checkAuthStatus()
 29 |     // 监听 storage 变化(当其他标签页登录/登出时)
 30 |     window.addEventListener('storage', checkAuthStatus)
 31 |     
 32 |     return () => {
 33 |       window.removeEventListener('storage', checkAuthStatus)
 34 |     }
 35 |   }, [])
 36 | 
 37 |   useEffect(() => {
 38 |     // 进入详情页自动折叠侧边栏
 39 |     if (location.pathname.startsWith('/portals/detail') || location.pathname.startsWith('/api-products/detail')) {
 40 |       setSidebarCollapsed(true)
 41 |     } else {
 42 |       setSidebarCollapsed(false)
 43 |     }
 44 |   }, [location.pathname])
 45 | 
 46 |   const navigation: NavigationItem[] = [
 47 |     { name: 'Portal', cn: '门户', href: '/portals', icon: GlobalOutlined },
 48 |     { name: 'API Products', cn: 'API产品', href: '/api-products', icon: AppstoreOutlined },
 49 |     { 
 50 |       name: '实例管理', 
 51 |       cn: '实例管理', 
 52 |       href: '/consoles', 
 53 |       icon: SettingOutlined,
 54 |       children: [
 55 |         { name: 'Nacos实例', cn: 'Nacos实例', href: '/consoles/nacos', icon: DesktopOutlined },
 56 |         { name: '网关实例', cn: '网关实例', href: '/consoles/gateway', icon: DesktopOutlined },
 57 |       ]
 58 |     },
 59 |   ]
 60 | 
 61 |   const toggleSidebar = () => {
 62 |     setSidebarCollapsed(!sidebarCollapsed)
 63 |   }
 64 | 
 65 |   const handleLogout = () => {
 66 |     removeToken()
 67 |     setIsLoggedIn(false)
 68 |     navigate('/login')
 69 |   }
 70 | 
 71 |   const isMenuActive = (item: NavigationItem): boolean => {
 72 |     if (location.pathname === item.href) return true
 73 |     if (item.children) {
 74 |       return item.children.some(child => location.pathname === child.href)
 75 |     }
 76 |     return false
 77 |   }
 78 | 
 79 |   const renderMenuItem = (item: NavigationItem, level: number = 0) => {
 80 |     const Icon = item.icon
 81 |     const isActive = isMenuActive(item)
 82 |     const hasChildren = item.children && item.children.length > 0
 83 | 
 84 |     return (
 85 |       <div key={item.name}>
 86 |           <Link
 87 |             to={item.href}
 88 |             className={`flex items-center mt-2 px-3 py-3 rounded-lg transition-colors duration-150 ${
 89 |               level > 0 ? 'ml-4' : ''
 90 |             } ${
 91 |               isActive && !hasChildren
 92 |                 ? 'bg-gray-100 text-black font-semibold'
 93 |                 : 'text-gray-500 hover:text-black hover:bg-gray-50'
 94 |             }`}
 95 |             title={sidebarCollapsed ? item.name : ''}
 96 |           >
 97 |             <Icon className="mr-3 h-5 w-5 flex-shrink-0" />
 98 |             {!sidebarCollapsed && (
 99 |               <div className="flex flex-col flex-1">
100 |                 <span className="text-base leading-none">{item.name}</span>
101 |               </div>
102 |             )}
103 |           </Link>
104 |         {!sidebarCollapsed && hasChildren && (
105 |           <div className="ml-2">
106 |             {item.children!.map(child => renderMenuItem(child, level + 1))}
107 |           </div>
108 |         )}
109 |       </div>
110 |     )
111 |   }
112 | 
113 |   return (
114 |     <div className="min-h-screen bg-background">
115 |       {/* 顶部导航栏 */}
116 |       <header className="w-full h-16 flex items-center justify-between px-8 bg-white border-b shadow-sm">
117 |         <div className="flex items-center space-x-2">
118 |         <div className="bg-white">
119 |               <Button
120 |                 type="text"
121 |                 icon={<MenuOutlined />}
122 |                 onClick={toggleSidebar}
123 |                 className="hover:bg-gray-100"
124 |               />
125 |             </div>
126 |           <span className="text-2xl font-bold">HiMarket</span>
127 |         </div>
128 |         {/* 顶部右侧用户信息或登录按钮 */}
129 |         {isLoggedIn ? (
130 |           <div className="flex items-center space-x-2">
131 |             <UserOutlined className="mr-2 text-lg" />
132 |             <span>admin</span>
133 |             <button
134 |               onClick={handleLogout}
135 |               className="ml-2 px-2 py-1 rounded bg-gray-200 hover:bg-gray-300"
136 |             >
137 |               退出
138 |             </button>
139 |           </div>
140 |         ) : (
141 |           <button onClick={() => navigate('/login')} className="flex items-center px-4 py-2 rounded bg-black text-white hover:bg-gray-800">
142 |             <UserOutlined className="mr-2" /> 登录
143 |           </button>
144 |         )}
145 |       </header>
146 |       <div className="flex">
147 |         {/* 侧边栏 */}
148 |         <aside className={`bg-white border-r min-h-screen pt-8 transition-all duration-300 ${
149 |           sidebarCollapsed ? 'w-16' : 'w-64'
150 |         }`}>
151 |           <nav className="flex flex-col space-y-2 px-4">
152 |             {navigation.map(item => renderMenuItem(item))}
153 |           </nav>
154 |         </aside>
155 | 
156 |         {/* 主内容区域 */}
157 |         <div className="flex-1 min-h-screen overflow-hidden">
158 |           <main className="p-8 w-full max-w-full overflow-x-hidden">
159 |             <Outlet />
160 |           </main>
161 |         </div>
162 |       </div>
163 |     </div>
164 |   )
165 | }
166 | 
167 | export default Layout
168 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/SwaggerUIWrapper.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React from 'react';
  2 | import SwaggerUI from 'swagger-ui-react';
  3 | import 'swagger-ui-react/swagger-ui.css';
  4 | import './SwaggerUIWrapper.css';
  5 | import * as yaml from 'js-yaml';
  6 | import { message } from 'antd';
  7 | 
  8 | interface SwaggerUIWrapperProps {
  9 |   apiSpec: string;
 10 | }
 11 | 
 12 | export const SwaggerUIWrapper: React.FC<SwaggerUIWrapperProps> = ({ apiSpec }) => {
 13 |   // 直接解析原始规范,不进行重新构建
 14 |   let swaggerSpec: any;
 15 |   
 16 |   try {
 17 |     // 尝试解析YAML格式
 18 |     try {
 19 |       swaggerSpec = yaml.load(apiSpec);
 20 |     } catch {
 21 |       // 如果YAML解析失败,尝试JSON格式
 22 |       swaggerSpec = JSON.parse(apiSpec);
 23 |     }
 24 | 
 25 |     if (!swaggerSpec || !swaggerSpec.paths) {
 26 |       throw new Error('Invalid OpenAPI specification');
 27 |     }
 28 | 
 29 |     // 为没有tags的操作添加默认标签,避免显示"default"
 30 |     Object.keys(swaggerSpec.paths).forEach(path => {
 31 |       const pathItem = swaggerSpec.paths[path];
 32 |       Object.keys(pathItem).forEach(method => {
 33 |         const operation = pathItem[method];
 34 |         if (operation && typeof operation === 'object' && !operation.tags) {
 35 |           operation.tags = ['接口列表'];
 36 |         }
 37 |       });
 38 |     });
 39 |   } catch (error) {
 40 |     console.error('OpenAPI规范解析失败:', error);
 41 |     return (
 42 |       <div className="text-center text-gray-500 py-8 bg-gray-50 rounded-lg">
 43 |         <p>无法解析OpenAPI规范</p>
 44 |         <div className="text-sm text-gray-400 mt-2">
 45 |           请检查API配置格式是否正确
 46 |         </div>
 47 |         <div className="text-xs text-gray-400 mt-1">
 48 |           错误详情: {error instanceof Error ? error.message : String(error)}
 49 |         </div>
 50 |       </div>
 51 |     );
 52 |   }
 53 | 
 54 |   return (
 55 |     <div className="swagger-ui-wrapper">
 56 |       <SwaggerUI
 57 |         spec={swaggerSpec}
 58 |         docExpansion="list"
 59 |         displayRequestDuration={true}
 60 |         tryItOutEnabled={true}
 61 |         filter={false}
 62 |         defaultModelsExpandDepth={0}
 63 |         defaultModelExpandDepth={0}
 64 |         displayOperationId={true}
 65 |         supportedSubmitMethods={['get', 'post', 'put', 'delete', 'patch', 'head', 'options']}
 66 |         deepLinking={false}
 67 |         requestInterceptor={(request: any) => {
 68 |           console.log('Request:', request);
 69 |           return request;
 70 |         }}
 71 |         responseInterceptor={(response: any) => {
 72 |           console.log('Response:', response);
 73 |           return response;
 74 |         }}
 75 |         onComplete={() => {
 76 |           console.log('Swagger UI loaded');
 77 |           // 添加服务器复制功能 - 使用requestAnimationFrame优化性能
 78 |           const addCopyButton = () => {
 79 |             const serversContainer = document.querySelector('.swagger-ui .servers');
 80 |             if (serversContainer && !serversContainer.querySelector('.copy-server-btn')) {
 81 |               const copyBtn = document.createElement('button');
 82 |               copyBtn.className = 'copy-server-btn';
 83 |               copyBtn.innerHTML = `
 84 |                 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
 85 |                   <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
 86 |                 </svg>
 87 |               `;
 88 |               copyBtn.title = '复制服务器地址';
 89 |               copyBtn.style.cssText = `
 90 |                 position: absolute;
 91 |                 right: 12px;
 92 |                 top: 50%;
 93 |                 transform: translateY(-50%);
 94 |                 background: transparent;
 95 |                 border: none;
 96 |                 border-radius: 4px;
 97 |                 padding: 6px 8px;
 98 |                 cursor: pointer;
 99 |                 color: #666;
100 |                 transition: all 0.2s;
101 |                 z-index: 10;
102 |                 display: flex;
103 |                 align-items: center;
104 |                 justify-content: center;
105 |               `;
106 |               
107 |               // 添加hover效果
108 |               copyBtn.addEventListener('mouseenter', () => {
109 |                 copyBtn.style.background = '#f5f5f5';
110 |                 copyBtn.style.color = '#1890ff';
111 |               });
112 |               
113 |               copyBtn.addEventListener('mouseleave', () => {
114 |                 copyBtn.style.background = 'transparent';
115 |                 copyBtn.style.color = '#666';
116 |               });
117 |               
118 |               copyBtn.addEventListener('click', () => {
119 |                 const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement;
120 |                 if (serverSelect && serverSelect.value) {
121 |                   navigator.clipboard.writeText(serverSelect.value)
122 |                     .then(() => {
123 |                       message.success('服务器地址已复制到剪贴板', 1);
124 |                     })
125 |                     .catch(() => {
126 |                       // 降级到传统复制方法
127 |                       const textArea = document.createElement('textarea');
128 |                       textArea.value = serverSelect.value;
129 |                       document.body.appendChild(textArea);
130 |                       textArea.select();
131 |                       document.execCommand('copy');
132 |                       document.body.removeChild(textArea);
133 |                       message.success('服务器地址已复制到剪贴板', 1);
134 |                     });
135 |                 }
136 |               });
137 | 
138 | 
139 |               serversContainer.appendChild(copyBtn);
140 |               
141 |               // 调整服务器选择框的padding
142 |               const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement;
143 |               if (serverSelect) {
144 |                 serverSelect.style.paddingRight = '50px';
145 |               }
146 |             }
147 |           };
148 |           
149 |           // 立即尝试添加按钮,如果失败则在下一帧重试
150 |           addCopyButton();
151 |           if (!document.querySelector('.swagger-ui .servers .copy-server-btn')) {
152 |             requestAnimationFrame(addCopyButton);
153 |           }
154 |         }}
155 |       />
156 |     </div>
157 |   );
158 | };
159 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/SwaggerUIWrapper.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React from 'react';
  2 | import SwaggerUI from 'swagger-ui-react';
  3 | import 'swagger-ui-react/swagger-ui.css';
  4 | import './SwaggerUIWrapper.css';
  5 | import * as yaml from 'js-yaml';
  6 | import { message } from 'antd';
  7 | import { copyToClipboard } from '@/lib/utils';
  8 | 
  9 | interface SwaggerUIWrapperProps {
 10 |   apiSpec: string;
 11 | }
 12 | 
 13 | export const SwaggerUIWrapper: React.FC<SwaggerUIWrapperProps> = ({ apiSpec }) => {
 14 |   // 直接解析原始规范,不进行重新构建
 15 |   let swaggerSpec: any;
 16 |   
 17 |   try {
 18 |     // 尝试解析YAML格式
 19 |     try {
 20 |       swaggerSpec = yaml.load(apiSpec);
 21 |     } catch {
 22 |       // 如果YAML解析失败,尝试JSON格式
 23 |       swaggerSpec = JSON.parse(apiSpec);
 24 |     }
 25 | 
 26 |     if (!swaggerSpec || !swaggerSpec.paths) {
 27 |       throw new Error('Invalid OpenAPI specification');
 28 |     }
 29 | 
 30 |     // 为没有tags的操作添加默认标签,避免显示"default"
 31 |     Object.keys(swaggerSpec.paths).forEach(path => {
 32 |       const pathItem = swaggerSpec.paths[path];
 33 |       Object.keys(pathItem).forEach(method => {
 34 |         const operation = pathItem[method];
 35 |         if (operation && typeof operation === 'object' && !operation.tags) {
 36 |           operation.tags = ['接口列表'];
 37 |         }
 38 |       });
 39 |     });
 40 |   } catch (error) {
 41 |     console.error('OpenAPI规范解析失败:', error);
 42 |     return (
 43 |       <div className="text-center text-gray-500 py-8 bg-gray-50 rounded-lg">
 44 |         <p>无法解析OpenAPI规范</p>
 45 |         <div className="text-sm text-gray-400 mt-2">
 46 |           请检查API配置格式是否正确
 47 |         </div>
 48 |         <div className="text-xs text-gray-400 mt-1">
 49 |           错误详情: {error instanceof Error ? error.message : String(error)}
 50 |         </div>
 51 |       </div>
 52 |     );
 53 |   }
 54 | 
 55 |   return (
 56 |     <div className="swagger-ui-wrapper">
 57 |       <SwaggerUI
 58 |         spec={swaggerSpec}
 59 |         docExpansion="list"
 60 |         displayRequestDuration={true}
 61 |         tryItOutEnabled={true}
 62 |         filter={false}
 63 |         showRequestHeaders={true}
 64 |         showCommonExtensions={true}
 65 |         defaultModelsExpandDepth={0}
 66 |         defaultModelExpandDepth={0}
 67 |         displayOperationId={true}
 68 |         enableCORS={true}
 69 |         supportedSubmitMethods={['get', 'post', 'put', 'delete', 'patch', 'head', 'options']}
 70 |         deepLinking={false}
 71 |         showMutatedRequest={true}
 72 |         requestInterceptor={(request: any) => {
 73 |           console.log('Request:', request);
 74 |           return request;
 75 |         }}
 76 |         responseInterceptor={(response: any) => {
 77 |           console.log('Response:', response);
 78 |           return response;
 79 |         }}
 80 |         onComplete={() => {
 81 |           console.log('Swagger UI loaded');
 82 |           // 添加服务器复制功能
 83 |           setTimeout(() => {
 84 |             const serversContainer = document.querySelector('.swagger-ui .servers');
 85 |             if (serversContainer && !serversContainer.querySelector('.copy-server-btn')) {
 86 |               const copyBtn = document.createElement('button');
 87 |               copyBtn.className = 'copy-server-btn';
 88 |               copyBtn.innerHTML = `
 89 |                 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
 90 |                   <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
 91 |                 </svg>
 92 |               `;
 93 |               copyBtn.title = '复制服务器地址';
 94 |               copyBtn.style.cssText = `
 95 |                 position: absolute;
 96 |                 right: 12px;
 97 |                 top: 50%;
 98 |                 transform: translateY(-50%);
 99 |                 background: transparent;
100 |                 border: none;
101 |                 border-radius: 4px;
102 |                 padding: 6px 8px;
103 |                 cursor: pointer;
104 |                 color: #666;
105 |                 transition: all 0.2s;
106 |                 z-index: 10;
107 |                 display: flex;
108 |                 align-items: center;
109 |                 justify-content: center;
110 |               `;
111 |               
112 |               copyBtn.addEventListener('click', async () => {
113 |                 const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement;
114 |                 if (serverSelect && serverSelect.value) {
115 |                   try {
116 |                     await copyToClipboard(serverSelect.value);
117 |                     message.success('服务器地址已复制到剪贴板', 1);
118 |                   } catch {
119 |                     message.error('复制失败,请手动复制');
120 |                   }
121 |                 }
122 |               });
123 | 
124 |               // 添加hover效果
125 |               copyBtn.addEventListener('mouseenter', () => {
126 |                 copyBtn.style.background = '#f5f5f5';
127 |                 copyBtn.style.color = '#1890ff';
128 |               });
129 |               
130 |               copyBtn.addEventListener('mouseleave', () => {
131 |                 copyBtn.style.background = 'transparent';
132 |                 copyBtn.style.color = '#666';
133 |               });
134 | 
135 |               serversContainer.appendChild(copyBtn);
136 |               
137 |               // 调整服务器选择框的padding
138 |               const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement;
139 |               if (serverSelect) {
140 |                 serverSelect.style.paddingRight = '50px';
141 |               }
142 |             }
143 |           }, 1000);
144 |         }}
145 |         syntaxHighlight={{
146 |           activated: true,
147 |           theme: 'agate'
148 |         }}
149 |         requestSnippetsEnabled={true}
150 |         requestSnippets={{
151 |           generators: {
152 |             'curl_bash': {
153 |               title: 'cURL (bash)',
154 |               syntax: 'bash'
155 |             },
156 |             'curl_powershell': {
157 |               title: 'cURL (PowerShell)',
158 |               syntax: 'powershell'
159 |             },
160 |             'curl_cmd': {
161 |               title: 'cURL (CMD)',
162 |               syntax: 'bash'
163 |             }
164 |           }
165 |         }}
166 |       />
167 |     </div>
168 |   );
169 | };
170 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/pages/Dashboard.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Card, Row, Col, Statistic, Progress, Table } from 'antd'
  2 | import { 
  3 |   EyeOutlined, 
  4 |   UserOutlined, 
  5 |   ApiOutlined,
  6 |   GlobalOutlined,
  7 |   ArrowUpOutlined,
  8 |   ArrowDownOutlined
  9 | } from '@ant-design/icons'
 10 | 
 11 | const mockRecentActivity = [
 12 |   {
 13 |     key: '1',
 14 |     action: 'Portal访问',
 15 |     description: 'Company Portal被访问了1250次',
 16 |     time: '2小时前',
 17 |     user: '[email protected]'
 18 |   },
 19 |   {
 20 |     key: '2',
 21 |     action: 'API调用',
 22 |     description: 'Payment API被调用了8765次',
 23 |     time: '4小时前',
 24 |     user: '[email protected]'
 25 |   },
 26 |   {
 27 |     key: '3',
 28 |     action: '新用户注册',
 29 |     description: '新开发者注册了账户',
 30 |     time: '6小时前',
 31 |     user: '[email protected]'
 32 |   }
 33 | ]
 34 | 
 35 | export default function Dashboard() {
 36 |   const activityColumns = [
 37 |     {
 38 |       title: '操作',
 39 |       dataIndex: 'action',
 40 |       key: 'action',
 41 |     },
 42 |     {
 43 |       title: '描述',
 44 |       dataIndex: 'description',
 45 |       key: 'description',
 46 |     },
 47 |     {
 48 |       title: '用户',
 49 |       dataIndex: 'user',
 50 |       key: 'user',
 51 |     },
 52 |     {
 53 |       title: '时间',
 54 |       dataIndex: 'time',
 55 |       key: 'time',
 56 |     },
 57 |   ]
 58 | 
 59 |   return (
 60 |     <div className="space-y-6">
 61 |       <div>
 62 |         <h1 className="text-3xl font-bold tracking-tight">仪表板</h1>
 63 |         <p className="text-gray-500 mt-2">
 64 |           欢迎使用HiMarket管理系统
 65 |         </p>
 66 |       </div>
 67 | 
 68 |       {/* 统计卡片 */}
 69 |       <Row gutter={[16, 16]}>
 70 |         <Col xs={24} sm={12} lg={6}>
 71 |           <Card>
 72 |             <Statistic
 73 |               title="Portal访问量"
 74 |               value={1250}
 75 |               prefix={<EyeOutlined />}
 76 |               valueStyle={{ color: '#3f8600' }}
 77 |               suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />}
 78 |             />
 79 |           </Card>
 80 |         </Col>
 81 |         <Col xs={24} sm={12} lg={6}>
 82 |           <Card>
 83 |             <Statistic
 84 |               title="注册用户"
 85 |               value={45}
 86 |               prefix={<UserOutlined />}
 87 |               valueStyle={{ color: '#1890ff' }}
 88 |               suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />}
 89 |             />
 90 |           </Card>
 91 |         </Col>
 92 |         <Col xs={24} sm={12} lg={6}>
 93 |           <Card>
 94 |             <Statistic
 95 |               title="API调用"
 96 |               value={8765}
 97 |               prefix={<ApiOutlined />}
 98 |               valueStyle={{ color: '#722ed1' }}
 99 |               suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />}
100 |             />
101 |           </Card>
102 |         </Col>
103 |         <Col xs={24} sm={12} lg={6}>
104 |           <Card>
105 |             <Statistic
106 |               title="活跃Portal"
107 |               value={3}
108 |               prefix={<GlobalOutlined />}
109 |               valueStyle={{ color: '#fa8c16' }}
110 |               suffix={<ArrowDownOutlined style={{ fontSize: '14px' }} />}
111 |             />
112 |           </Card>
113 |         </Col>
114 |       </Row>
115 | 
116 |       {/* 详细信息 */}
117 |       <Row gutter={[16, 16]}>
118 |         <Col xs={24} lg={12}>
119 |           <Card title="系统状态" className="h-full">
120 |             <div className="space-y-4">
121 |               <div>
122 |                 <div className="flex justify-between mb-2">
123 |                   <span>系统负载</span>
124 |                   <span className="text-blue-600">75%</span>
125 |                 </div>
126 |                 <Progress percent={75} strokeColor="#1890ff" />
127 |               </div>
128 |               <div>
129 |                 <div className="flex justify-between mb-2">
130 |                   <span>API响应时间</span>
131 |                   <span className="text-green-600">245ms</span>
132 |                 </div>
133 |                 <Progress percent={85} strokeColor="#52c41a" />
134 |               </div>
135 |               <div>
136 |                 <div className="flex justify-between mb-2">
137 |                   <span>错误率</span>
138 |                   <span className="text-red-600">0.12%</span>
139 |                 </div>
140 |                 <Progress percent={1.2} strokeColor="#ff4d4f" />
141 |               </div>
142 |             </div>
143 |           </Card>
144 |         </Col>
145 |         <Col xs={24} lg={12}>
146 |           <Card title="快速操作" className="h-full">
147 |             <div className="space-y-4">
148 |               <div className="grid grid-cols-2 gap-4">
149 |                 <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer">
150 |                   <GlobalOutlined className="text-2xl text-blue-500 mb-2" />
151 |                   <div className="font-medium">创建Portal</div>
152 |                   <div className="text-sm text-gray-500">新建开发者门户</div>
153 |                 </div>
154 |                 <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer">
155 |                   <ApiOutlined className="text-2xl text-green-500 mb-2" />
156 |                   <div className="font-medium">发布API</div>
157 |                   <div className="text-sm text-gray-500">发布新的API产品</div>
158 |                 </div>
159 |                 <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer">
160 |                   <UserOutlined className="text-2xl text-purple-500 mb-2" />
161 |                   <div className="font-medium">管理用户</div>
162 |                   <div className="text-sm text-gray-500">管理开发者账户</div>
163 |                 </div>
164 |                 <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer">
165 |                   <EyeOutlined className="text-2xl text-orange-500 mb-2" />
166 |                   <div className="font-medium">查看统计</div>
167 |                   <div className="text-sm text-gray-500">查看使用统计</div>
168 |                 </div>
169 |               </div>
170 |             </div>
171 |           </Card>
172 |         </Col>
173 |       </Row>
174 | 
175 |       {/* 最近活动 */}
176 |       <Card title="最近活动">
177 |         <Table 
178 |           columns={activityColumns} 
179 |           dataSource={mockRecentActivity}
180 |           rowKey="key"
181 |           pagination={false}
182 |           size="small"
183 |         />
184 |       </Card>
185 |     </div>
186 |   )
187 | } 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/portal/PortalDomain.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import {Card, Button, Table, Modal, Form, Input, Select, message, Space} from 'antd'
  2 | import {PlusOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
  3 | import {useState} from 'react'
  4 | import {Portal} from '@/types'
  5 | import {portalApi} from '@/lib/api'
  6 | 
  7 | interface PortalDomainProps {
  8 |     portal: Portal
  9 |     onRefresh?: () => void
 10 | }
 11 | 
 12 | export function PortalDomain({portal, onRefresh}: PortalDomainProps) {
 13 |     const [domainModalVisible, setDomainModalVisible] = useState(false)
 14 |     const [domainForm] = Form.useForm()
 15 |     const [domainLoading, setDomainLoading] = useState(false)
 16 | 
 17 |     const handleAddDomain = () => {
 18 |         setDomainModalVisible(true)
 19 |     }
 20 | 
 21 |     const handleDomainModalOk = async () => {
 22 |         try {
 23 |             setDomainLoading(true)
 24 |             const values = await domainForm.validateFields()
 25 |             
 26 |             await portalApi.bindDomain(portal.portalId, {
 27 |                 domain: values.domain,
 28 |                 type: 'CUSTOM',
 29 |                 protocol: values.protocol
 30 |             })
 31 |             
 32 |             message.success('域名绑定成功')
 33 |             setDomainModalVisible(false)
 34 |             domainForm.resetFields()
 35 |             onRefresh?.()
 36 |         } catch (error) {
 37 |             message.error('绑定域名失败')
 38 |         } finally {
 39 |             setDomainLoading(false)
 40 |         }
 41 |     }
 42 | 
 43 |     const handleDomainModalCancel = () => {
 44 |         setDomainModalVisible(false)
 45 |         domainForm.resetFields()
 46 |     }
 47 | 
 48 |     const handleDeleteDomain = async (domain: string) => {
 49 |         Modal.confirm({
 50 |             title: '确认解绑',
 51 |             icon: <ExclamationCircleOutlined/>,
 52 |             content: `确定要解绑域名 "${domain}" 吗?此操作不可恢复。`,
 53 |             okText: '确认解绑',
 54 |             okType: 'danger',
 55 |             cancelText: '取消',
 56 |             async onOk() {
 57 |                 try {
 58 |                     await portalApi.unbindDomain(portal.portalId, domain)
 59 |                     message.success('域名解绑成功')
 60 |                     onRefresh?.()
 61 |                 } catch (error) {
 62 |                     message.error('解绑域名失败')
 63 |                 }
 64 |             },
 65 |         })
 66 |     }
 67 | 
 68 |     const domainColumns = [
 69 |         {
 70 |             title: '域名',
 71 |             dataIndex: 'domain',
 72 |             key: 'domain',
 73 |         },
 74 |         {
 75 |             title: '协议',
 76 |             dataIndex: 'protocol',
 77 |             key: 'protocol',
 78 |             render: (protocol: string) => protocol?.toUpperCase() || 'HTTP'
 79 |         },
 80 |         {
 81 |             title: '类型',
 82 |             dataIndex: 'type',
 83 |             key: 'type',
 84 |             render: (type: string) => type === 'CUSTOM' ? '自定义域名' : '系统域名'
 85 |         },
 86 |         {
 87 |             title: '操作',
 88 |             key: 'action',
 89 |             render: (_: any, record: any) => (
 90 |                 <Space>
 91 |                     {record.type === 'CUSTOM' ? (
 92 |                         <Button 
 93 |                             type="link" 
 94 |                             danger 
 95 |                             size="small"
 96 |                             onClick={() => handleDeleteDomain(record.domain)}
 97 |                         >
 98 |                             解绑
 99 |                         </Button>
100 |                     ) : (
101 |                         <span className="text-gray-400 text-sm">-</span>
102 |                     )}
103 |                 </Space>
104 |             ),
105 |         },
106 |     ]
107 | 
108 |     return (
109 |         <div className="p-6 space-y-6">
110 |             <div className="flex justify-between items-center">
111 |                 <div>
112 |                     <h1 className="text-2xl font-bold mb-2">域名列表</h1>
113 |                     <p className="text-gray-600">管理Portal的域名配置</p>
114 |                 </div>
115 |                 <Space>
116 |                     <Button type="primary" icon={<PlusOutlined/>} onClick={handleAddDomain}>
117 |                         绑定域名
118 |                     </Button>
119 |                 </Space>
120 |             </div>
121 | 
122 |             <Card>
123 |                 <div className="space-y-6">
124 |                     {/* 域名列表内容 */}
125 |                     <div>
126 |                         <Table
127 |                             columns={domainColumns}
128 |                             dataSource={portal.portalDomainConfig || []}
129 |                             rowKey="domain"
130 |                             pagination={false}
131 |                             size="small"
132 |                             locale={{
133 |                                 emptyText: '暂无绑定域名'
134 |                             }}
135 |                         />
136 |                     </div>
137 |                 </div>
138 |             </Card>
139 | 
140 |             {/* 域名绑定模态框 */}
141 |             <Modal
142 |                 title="绑定域名"
143 |                 open={domainModalVisible}
144 |                 onOk={handleDomainModalOk}
145 |                 onCancel={handleDomainModalCancel}
146 |                 confirmLoading={domainLoading}
147 |                 destroyOnClose
148 |             >
149 |                 <Form form={domainForm} layout="vertical" initialValues={{ protocol: 'HTTP' }}>
150 |                     <Form.Item
151 |                         name="domain"
152 |                         label="域名"
153 |                         rules={[{ required: true, message: '请输入要绑定的域名' }]}
154 |                     >
155 |                         <Input placeholder="例如:example.com" />
156 |                     </Form.Item>
157 |                     
158 |                     <Form.Item
159 |                         name="protocol"
160 |                         label="协议"
161 |                         rules={[{ required: true, message: '请选择协议' }]}
162 |                     >
163 |                         <Select placeholder="请选择协议">
164 |                             <Select.Option value="HTTPS">HTTPS</Select.Option>
165 |                             <Select.Option value="HTTP">HTTP</Select.Option>
166 |                         </Select>
167 |                     </Form.Item>
168 |                 </Form>
169 |             </Modal>
170 |         </div>
171 |     )
172 | }
173 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/pages/Login.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useEffect, useState } from "react";
  2 | import { Link, useNavigate } from "react-router-dom";
  3 | import { Form, Input, Button, Card, Divider, message } from "antd";
  4 | import { UserOutlined, LockOutlined } from "@ant-design/icons";
  5 | import api, { getOidcProviders, type IdpResult } from "../lib/api";
  6 | import aliyunIcon from "../assets/aliyun.png";
  7 | import githubIcon from "../assets/github.png";
  8 | import googleIcon from "../assets/google.png";
  9 | import { AxiosError } from "axios";
 10 | 
 11 | 
 12 | const oidcIcons: Record<string, React.ReactNode> = {
 13 |   google: <img src={googleIcon} alt="Google" className="w-5 h-5 mr-2" />,
 14 |   github: <img src={githubIcon} alt="GitHub" className="w-6 h-6 mr-2" />,
 15 |   aliyun: <img src={aliyunIcon} alt="Aliyun" className="w-6 h-6 mr-2" />,
 16 | };
 17 | 
 18 | const Login: React.FC = () => {
 19 |   const [providers, setProviders] = useState<IdpResult[]>([]);
 20 |   const [loading, setLoading] = useState(false);
 21 |   const navigate = useNavigate();
 22 | 
 23 |   useEffect(() => {
 24 |     // 使用OidcController的接口获取OIDC提供商
 25 |     getOidcProviders()
 26 |       .then((response: any) => {
 27 |         console.log('OIDC providers response:', response);
 28 |         
 29 |         // 处理不同的响应格式
 30 |         let providersData: IdpResult[];
 31 |         if (Array.isArray(response)) {
 32 |           providersData = response;
 33 |         } else if (response && Array.isArray(response.data)) {
 34 |           providersData = response.data;
 35 |         } else if (response && response.data) {
 36 |           console.warn('Unexpected response format:', response);
 37 |           providersData = [];
 38 |         } else {
 39 |           providersData = [];
 40 |         }
 41 |         
 42 |         console.log('Processed providers data:', providersData);
 43 |         setProviders(providersData);
 44 |       })
 45 |       .catch((error) => {
 46 |         console.error('Failed to fetch OIDC providers:', error);
 47 |         setProviders([]);
 48 |       });
 49 |   }, []);
 50 | 
 51 |   // 账号密码登录
 52 |   const handlePasswordLogin = async (values: { username: string; password: string }) => {
 53 |     setLoading(true);
 54 |     try {
 55 |       const res = await api.post("/developers/login", {
 56 |         username: values.username,
 57 |         password: values.password,
 58 |       });
 59 |       // 登录成功后跳转到首页并携带access_token
 60 |       if (res && res.data && res.data.access_token) {
 61 |         message.success('登录成功!', 1);
 62 |         localStorage.setItem('access_token', res.data.access_token)
 63 |         navigate('/')
 64 |       } else {
 65 |         message.error("登录失败,未获取到access_token");
 66 |       }
 67 |     } catch (error) {
 68 |       if (error instanceof AxiosError) {
 69 |         message.error(error.response?.data.message || "登录失败,请检查账号密码是否正确");
 70 |       } else {
 71 |         message.error("登录失败");
 72 |       }
 73 |     } finally {
 74 |       setLoading(false);
 75 |     }
 76 |   };
 77 | 
 78 |   // 跳转到 OIDC 授权 - 对接OidcController
 79 |   const handleOidcLogin = (provider: string) => {
 80 |     // 获取API前缀配置
 81 |     const apiPrefix = api.defaults.baseURL || '/api/v1';
 82 |     
 83 |     // 构建授权URL - 对接 /developers/oidc/authorize
 84 |     const authUrl = new URL(`${window.location.origin}${apiPrefix}/developers/oidc/authorize`);
 85 |     authUrl.searchParams.set('provider', provider);
 86 |     
 87 |     console.log('Redirecting to OIDC authorization:', authUrl.toString());
 88 |     
 89 |     // 跳转到OIDC授权服务器
 90 |     window.location.href = authUrl.toString();
 91 |   };
 92 | 
 93 |   return (
 94 |     <div className="flex items-center justify-center min-h-screen bg-gray-50">
 95 |       <Card className="w-full max-w-md shadow-lg">
 96 |         {/* Logo */}
 97 |         <div className="text-center mb-6">
 98 |           <img src="/logo.png" alt="Logo" className="w-16 h-16 mx-auto mb-4" />
 99 |           <h2 className="text-2xl font-bold text-gray-900">登录HiMarket-前台</h2>
100 |         </div>
101 | 
102 |         {/* 账号密码登录表单 */}
103 |         <Form
104 |           name="login"
105 |           onFinish={handlePasswordLogin}
106 |           autoComplete="off"
107 |           layout="vertical"
108 |           size="large"
109 |         >
110 |           <Form.Item
111 |             name="username"
112 |             rules={[
113 |               { required: true, message: '请输入账号' }
114 |             ]}
115 |           >
116 |             <Input
117 |               prefix={<UserOutlined />}
118 |               placeholder="账号"
119 |               autoComplete="username"
120 |             />
121 |           </Form.Item>
122 | 
123 |           <Form.Item
124 |             name="password"
125 |             rules={[
126 |               { required: true, message: '请输入密码' }
127 |             ]}
128 |           >
129 |             <Input.Password
130 |               prefix={<LockOutlined />}
131 |               placeholder="密码"
132 |               autoComplete="current-password"
133 |             />
134 |           </Form.Item>
135 | 
136 |           <Form.Item>
137 |             <Button
138 |               type="primary"
139 |               htmlType="submit"
140 |               loading={loading}
141 |               className="w-full"
142 |               size="large"
143 |             >
144 |               {loading ? "登录中..." : "登录"}
145 |             </Button>
146 |           </Form.Item>
147 |         </Form>
148 | 
149 |         {/* 分隔线 */}
150 |         <Divider plain>或</Divider>
151 | 
152 |         {/* OIDC 登录按钮 */}
153 |         <div className="flex flex-col gap-3">
154 |           {!Array.isArray(providers) || providers.length === 0 ? (
155 |             <div className="text-gray-400 text-center">暂无可用第三方登录</div>
156 |           ) : (
157 |             providers.map((provider) => (
158 |               <Button
159 |                 key={provider.provider}
160 |                 onClick={() => handleOidcLogin(provider.provider)}
161 |                 className="w-full flex items-center justify-center"
162 |                 size="large"
163 |                 icon={oidcIcons[provider.provider.toLowerCase()] || <span>🆔</span>}
164 |               >
165 |                 使用{provider.name || provider.provider}登录
166 |               </Button>
167 |             ))
168 |           )}
169 |         </div>
170 | 
171 |         {/* 底部提示 */}
172 |         <div className="mt-6 text-center text-gray-500">
173 |           没有账号?
174 |           <Link to="/register" className="text-blue-500 hover:underline ml-1">
175 |             注册
176 |           </Link>
177 |         </div>
178 |       </Card>
179 |     </div>
180 |   );
181 | };
182 | 
183 | export default Login;
184 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/portal/PortalConsumers.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Card, Table, Badge, Button, Space, Avatar, Tag, Input } from 'antd'
  2 | import { SearchOutlined, UserAddOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
  3 | import { useState } from 'react'
  4 | import { Portal, DeveloperStats } from '@/types'
  5 | import { formatDateTime } from '@/lib/utils'
  6 | 
  7 | interface PortalConsumersProps {
  8 |   portal: Portal
  9 | }
 10 | 
 11 | const mockConsumers: DeveloperStats[] = [
 12 |   {
 13 |     id: "1",
 14 |     name: "企业A",
 15 |     email: "[email protected]",
 16 |     status: "active",
 17 |     plan: "premium",
 18 |     joinedAt: "2025-01-01T10:00:00Z",
 19 |     lastActive: "2025-01-08T15:30:00Z",
 20 |     apiCalls: 15420,
 21 |     subscriptions: 3
 22 |   },
 23 |   {
 24 |     id: "2",
 25 |     name: "企业B",
 26 |     email: "[email protected]",
 27 |     status: "active",
 28 |     plan: "standard",
 29 |     joinedAt: "2025-01-02T11:00:00Z",
 30 |     lastActive: "2025-01-08T14:20:00Z",
 31 |     apiCalls: 8765,
 32 |     subscriptions: 2
 33 |   },
 34 |   {
 35 |     id: "3",
 36 |     name: "企业C",
 37 |     email: "[email protected]",
 38 |     status: "inactive",
 39 |     plan: "basic",
 40 |     joinedAt: "2025-01-03T12:00:00Z",
 41 |     lastActive: "2025-01-05T09:15:00Z",
 42 |     apiCalls: 1200,
 43 |     subscriptions: 1
 44 |   }
 45 | ]
 46 | 
 47 | export function PortalConsumers({ portal }: PortalConsumersProps) {
 48 |   const [consumers, setConsumers] = useState<DeveloperStats[]>(mockConsumers)
 49 |   const [searchText, setSearchText] = useState('')
 50 | 
 51 |   const filteredConsumers = consumers.filter(consumer =>
 52 |     consumer.name.toLowerCase().includes(searchText.toLowerCase()) ||
 53 |     consumer.email.toLowerCase().includes(searchText.toLowerCase())
 54 |   )
 55 | 
 56 |   const getPlanColor = (plan: string) => {
 57 |     switch (plan) {
 58 |       case 'premium':
 59 |         return 'gold'
 60 |       case 'standard':
 61 |         return 'blue'
 62 |       case 'basic':
 63 |         return 'green'
 64 |       default:
 65 |         return 'default'
 66 |     }
 67 |   }
 68 | 
 69 |   const getPlanText = (plan: string) => {
 70 |     switch (plan) {
 71 |       case 'premium':
 72 |         return '高级版'
 73 |       case 'standard':
 74 |         return '标准版'
 75 |       case 'basic':
 76 |         return '基础版'
 77 |       default:
 78 |         return plan
 79 |     }
 80 |   }
 81 | 
 82 |   const columns = [
 83 |     {
 84 |       title: '消费者',
 85 |       dataIndex: 'name',
 86 |       key: 'name',
 87 |       render: (name: string, record: DeveloperStats) => (
 88 |         <div className="flex items-center space-x-3">
 89 |           <Avatar className="bg-green-500">
 90 |             {name.charAt(0).toUpperCase()}
 91 |           </Avatar>
 92 |           <div>
 93 |             <div className="font-medium">{name}</div>
 94 |             <div className="text-sm text-gray-500">{record.email}</div>
 95 |           </div>
 96 |         </div>
 97 |       ),
 98 |     },
 99 |     {
100 |       title: '状态',
101 |       dataIndex: 'status',
102 |       key: 'status',
103 |       render: (status: string) => (
104 |         <Badge status={status === 'active' ? 'success' : 'default'} text={status === 'active' ? '活跃' : '非活跃'} />
105 |       )
106 |     },
107 |     {
108 |       title: '套餐',
109 |       dataIndex: 'plan',
110 |       key: 'plan',
111 |       render: (plan: string) => (
112 |         <Tag color={getPlanColor(plan)}>
113 |           {getPlanText(plan)}
114 |         </Tag>
115 |       )
116 |     },
117 |     {
118 |       title: 'API调用',
119 |       dataIndex: 'apiCalls',
120 |       key: 'apiCalls',
121 |       render: (calls: number) => calls.toLocaleString()
122 |     },
123 |     {
124 |       title: '订阅数',
125 |       dataIndex: 'subscriptions',
126 |       key: 'subscriptions',
127 |       render: (subscriptions: number) => subscriptions.toLocaleString()
128 |     },
129 |     {
130 |       title: '加入时间',
131 |       dataIndex: 'joinedAt',
132 |       key: 'joinedAt',
133 |       render: (date: string) => formatDateTime(date)
134 |     },
135 |     {
136 |       title: '最后活跃',
137 |       dataIndex: 'lastActive',
138 |       key: 'lastActive',
139 |       render: (date: string) => formatDateTime(date)
140 |     },
141 |     {
142 |       title: '操作',
143 |       key: 'action',
144 |       render: (_: any, record: DeveloperStats) => (
145 |         <Space size="middle">
146 |           <Button type="link" icon={<EditOutlined />}>
147 |             编辑
148 |           </Button>
149 |           <Button type="link" danger icon={<DeleteOutlined />}>
150 |             删除
151 |           </Button>
152 |         </Space>
153 |       ),
154 |     },
155 |   ]
156 | 
157 |   return (
158 |     <div className="p-6 space-y-6">
159 |       <div className="flex justify-between items-center">
160 |         <div>
161 |           <h1 className="text-2xl font-bold mb-2">消费者</h1>
162 |           <p className="text-gray-600">管理Portal的消费者用户</p>
163 |         </div>
164 |         <Button type="primary" icon={<UserAddOutlined />}>
165 |           添加消费者
166 |         </Button>
167 |       </div>
168 | 
169 |       <Card>
170 |         <div className="mb-4">
171 |           <Input
172 |             placeholder="搜索消费者..."
173 |             prefix={<SearchOutlined />}
174 |             value={searchText}
175 |             onChange={(e) => setSearchText(e.target.value)}
176 |             style={{ width: 300 }}
177 |           />
178 |         </div>
179 |         <Table 
180 |           columns={columns} 
181 |           dataSource={filteredConsumers}
182 |           rowKey="id"
183 |           pagination={false}
184 |         />
185 |       </Card>
186 | 
187 |       {/* <Card title="消费者统计">
188 |         <div className="grid grid-cols-4 gap-4">
189 |           <div className="text-center">
190 |             <div className="text-2xl font-bold text-blue-600">{consumers.length}</div>
191 |             <div className="text-sm text-gray-500">总消费者</div>
192 |           </div>
193 |           <div className="text-center">
194 |             <div className="text-2xl font-bold text-green-600">
195 |               {consumers.filter(c => c.status === 'active').length}
196 |             </div>
197 |             <div className="text-sm text-gray-500">活跃消费者</div>
198 |           </div>
199 |           <div className="text-center">
200 |             <div className="text-2xl font-bold text-purple-600">
201 |               {consumers.reduce((sum, c) => sum + c.apiCalls, 0).toLocaleString()}
202 |             </div>
203 |             <div className="text-sm text-gray-500">总API调用</div>
204 |           </div>
205 |           <div className="text-center">
206 |             <div className="text-2xl font-bold text-orange-600">
207 |               {consumers.reduce((sum, c) => sum + c.subscriptions, 0)}
208 |             </div>
209 |             <div className="text-sm text-gray-500">总订阅数</div>
210 |           </div>
211 |         </div>
212 |       </Card> */}
213 |     </div>
214 |   )
215 | } 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/controller/ConsumerController.java:
--------------------------------------------------------------------------------

```java
  1 | /*
  2 |  * Licensed to the Apache Software Foundation (ASF) under one
  3 |  * or more contributor license agreements.  See the NOTICE file
  4 |  * distributed with this work for additional information
  5 |  * regarding copyright ownership.  The ASF licenses this file
  6 |  * to you under the Apache License, Version 2.0 (the
  7 |  * "License"); you may not use this file except in compliance
  8 |  * with the License.  You may obtain a copy of the License at
  9 |  *
 10 |  *   http://www.apache.org/licenses/LICENSE-2.0
 11 |  *
 12 |  * Unless required by applicable law or agreed to in writing,
 13 |  * software distributed under the License is distributed on an
 14 |  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 15 |  * KIND, either express or implied.  See the License for the
 16 |  * specific language governing permissions and limitations
 17 |  * under the License.
 18 |  */
 19 | 
 20 | package com.alibaba.apiopenplatform.controller;
 21 | 
 22 | import com.alibaba.apiopenplatform.core.annotation.AdminAuth;
 23 | import com.alibaba.apiopenplatform.core.annotation.DeveloperAuth;
 24 | import com.alibaba.apiopenplatform.core.annotation.AdminOrDeveloperAuth;
 25 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateCredentialParam;
 26 | import com.alibaba.apiopenplatform.dto.params.consumer.QueryConsumerParam;
 27 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateConsumerParam;
 28 | import com.alibaba.apiopenplatform.dto.params.consumer.UpdateCredentialParam;
 29 | import com.alibaba.apiopenplatform.dto.params.consumer.CreateSubscriptionParam;
 30 | import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam;
 31 | import com.alibaba.apiopenplatform.dto.result.ConsumerCredentialResult;
 32 | import com.alibaba.apiopenplatform.dto.result.ConsumerResult;
 33 | import com.alibaba.apiopenplatform.dto.result.PageResult;
 34 | import com.alibaba.apiopenplatform.dto.result.SubscriptionResult;
 35 | import com.alibaba.apiopenplatform.service.ConsumerService;
 36 | import io.swagger.v3.oas.annotations.Operation;
 37 | import io.swagger.v3.oas.annotations.tags.Tag;
 38 | import lombok.RequiredArgsConstructor;
 39 | import org.springframework.data.domain.Pageable;
 40 | import org.springframework.validation.annotation.Validated;
 41 | import org.springframework.web.bind.annotation.*;
 42 | 
 43 | import javax.validation.Valid;
 44 | 
 45 | @Tag(name = "Consumer管理", description = "提供Consumer注册、审批、产品订阅等管理功能")
 46 | @RestController
 47 | @RequestMapping("/consumers")
 48 | @RequiredArgsConstructor
 49 | @Validated
 50 | public class ConsumerController {
 51 | 
 52 |     private final ConsumerService consumerService;
 53 | 
 54 |     @Operation(summary = "获取Consumer列表")
 55 |     @GetMapping
 56 |     public PageResult<ConsumerResult> listConsumers(QueryConsumerParam param,
 57 |                                                     Pageable pageable) {
 58 |         return consumerService.listConsumers(param, pageable);
 59 |     }
 60 | 
 61 |     @Operation(summary = "获取Consumer")
 62 |     @GetMapping("/{consumerId}")
 63 |     public ConsumerResult getConsumer(@PathVariable String consumerId) {
 64 |         return consumerService.getConsumer(consumerId);
 65 |     }
 66 | 
 67 |     @Operation(summary = "注册Consumer")
 68 |     @PostMapping
 69 |     @DeveloperAuth
 70 |     public ConsumerResult createConsumer(@RequestBody @Valid CreateConsumerParam param) {
 71 |         return consumerService.createConsumer(param);
 72 |     }
 73 | 
 74 |     @Operation(summary = "删除Consumer")
 75 |     @DeleteMapping("/{consumerId}")
 76 |     public void deleteDevConsumer(@PathVariable String consumerId) {
 77 |         consumerService.deleteConsumer(consumerId);
 78 |     }
 79 | 
 80 |     @Operation(summary = "生成Consumer凭证")
 81 |     @PostMapping("/{consumerId}/credentials")
 82 |     @DeveloperAuth
 83 |     public void createCredential(@PathVariable String consumerId,
 84 |                                  @RequestBody @Valid CreateCredentialParam param) {
 85 |         consumerService.createCredential(consumerId, param);
 86 |     }
 87 | 
 88 |     @Operation(summary = "获取Consumer凭证信息")
 89 |     @GetMapping("/{consumerId}/credentials")
 90 |     @DeveloperAuth
 91 |     public ConsumerCredentialResult getCredential(@PathVariable String consumerId) {
 92 |         return consumerService.getCredential(consumerId);
 93 |     }
 94 | 
 95 |     @Operation(summary = "更新Consumer凭证")
 96 |     @PutMapping("/{consumerId}/credentials")
 97 |     @DeveloperAuth
 98 |     public void updateCredential(@PathVariable String consumerId,
 99 |                                  @RequestBody @Valid UpdateCredentialParam param) {
100 |         consumerService.updateCredential(consumerId, param);
101 |     }
102 | 
103 |     @Operation(summary = "删除Consumer凭证")
104 |     @DeleteMapping("/{consumerId}/credentials")
105 |     @DeveloperAuth
106 |     public void deleteCredential(@PathVariable String consumerId) {
107 |         consumerService.deleteCredential(consumerId);
108 |     }
109 | 
110 |     @Operation(summary = "订阅API产品")
111 |     @PostMapping("/{consumerId}/subscriptions")
112 |     @DeveloperAuth
113 |     public SubscriptionResult subscribeProduct(@PathVariable String consumerId,
114 |                                                @RequestBody @Valid CreateSubscriptionParam param) {
115 |         return consumerService.subscribeProduct(consumerId, param);
116 |     }
117 | 
118 |     @Operation(summary = "获取Consumer的订阅列表")
119 |     @GetMapping("/{consumerId}/subscriptions")
120 |     @AdminOrDeveloperAuth
121 |     public PageResult<SubscriptionResult> listSubscriptions(@PathVariable String consumerId,
122 |                                                             QuerySubscriptionParam param,
123 |                                                             Pageable pageable) {
124 |         return consumerService.listSubscriptions(consumerId, param, pageable);
125 |     }
126 | 
127 |     @Operation(summary = "取消订阅")
128 |     @DeleteMapping("/{consumerId}/subscriptions/{productId}")
129 |     public void deleteSubscription(@PathVariable String consumerId, @PathVariable String productId) {
130 |         consumerService.unsubscribeProduct(consumerId, productId);
131 |     }
132 | 
133 |     @Operation(summary = "审批订阅申请")
134 |     @PatchMapping("/{consumerId}/subscriptions/{productId}")
135 |     @AdminAuth
136 |     public SubscriptionResult approveSubscription(@PathVariable String consumerId, @PathVariable String productId) {
137 |         return consumerService.approveSubscription(consumerId, productId);
138 |     }
139 | }
140 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/common/AdvancedSearch.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect } from 'react';
  2 | import { Select, Input, Button, Tag, Space } from 'antd';
  3 | import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
  4 | // import './AdvancedSearch.css';
  5 | 
  6 | const { Option } = Select;
  7 | 
  8 | export interface SearchParam {
  9 |   label: string;
 10 |   name: string;
 11 |   placeholder: string;
 12 |   type?: 'input' | 'select';
 13 |   optionList?: Array<{ label: string; value: string }>;
 14 | }
 15 | 
 16 | interface AdvancedSearchProps {
 17 |   searchParamsList: SearchParam[];
 18 |   onSearch: (searchName: string, searchValue: string) => void;
 19 |   onClear?: () => void;
 20 |   className?: string;
 21 | }
 22 | 
 23 | export const AdvancedSearch: React.FC<AdvancedSearchProps> = ({
 24 |   searchParamsList,
 25 |   onSearch,
 26 |   onClear,
 27 |   className = ''
 28 | }) => {
 29 |   const [activeSearchName, setActiveSearchName] = useState<string>('');
 30 |   const [activeSearchValue, setActiveSearchValue] = useState<string>('');
 31 |   const [tagList, setTagList] = useState<Array<SearchParam & { value: string }>>([]);
 32 |   const [isInitialized, setIsInitialized] = useState<boolean>(false);
 33 | 
 34 |   useEffect(() => {
 35 |     // 防止初始化时自动触发搜索
 36 |     if (isInitialized && activeSearchName) {
 37 |       setActiveSearchValue(''); // 清空输入框
 38 |       setTagList([]); // 清空关联标签
 39 |       onSearch(activeSearchName, '');
 40 |     }
 41 |   }, [activeSearchName, isInitialized]); // 移除 onSearch 避免无限循环
 42 | 
 43 |   useEffect(() => {
 44 |     if (searchParamsList.length > 0) {
 45 |       setActiveSearchName(searchParamsList[0].name);
 46 |       setIsInitialized(true); // 标记为已初始化
 47 |     }
 48 |   }, [searchParamsList]);
 49 | 
 50 |   const handleSearch = () => {
 51 |     if (activeSearchValue.trim()) {
 52 |       // 添加到标签列表
 53 |       const currentParam = searchParamsList.find(item => item.name === activeSearchName);
 54 |       if (currentParam) {
 55 |         const newTag = {
 56 |           ...currentParam,
 57 |           value: activeSearchValue
 58 |         };
 59 |         setTagList(prev => {
 60 |           const filtered = prev.filter(tag => tag.name !== activeSearchName);
 61 |           return [...filtered, newTag];
 62 |         });
 63 |       }
 64 |       
 65 |       onSearch(activeSearchName, activeSearchValue);
 66 |       setActiveSearchValue('');
 67 |     }
 68 |   };
 69 | 
 70 |   const handleClearOne = (tagName: string) => {
 71 |     setTagList(prev => prev.filter(tag => tag.name !== tagName));
 72 |     onSearch(tagName, '');
 73 |   };
 74 | 
 75 |   const handleClearAll = () => {
 76 |     setTagList([]);
 77 |     if (onClear) {
 78 |       onClear();
 79 |     }
 80 |   };
 81 | 
 82 |   const handleSelectOne = (tagName: string) => {
 83 |     const tag = tagList.find(t => t.name === tagName);
 84 |     if (tag) {
 85 |       setActiveSearchName(tagName);
 86 |       setActiveSearchValue(tag.value);
 87 |     }
 88 |   };
 89 | 
 90 |   const getCurrentParam = () => {
 91 |     return searchParamsList.find(item => item.name === activeSearchName);
 92 |   };
 93 | 
 94 |   const currentParam = getCurrentParam();
 95 | 
 96 |   return (
 97 |     <div className={`flex flex-col gap-4 ${className}`}>
 98 |       {/* 搜索控件 */}
 99 |       <div className="flex items-center">
100 |         {/* 左侧:搜索字段选择器 */}
101 |         <Select
102 |           value={activeSearchName}
103 |           onChange={setActiveSearchName}
104 |           style={{ 
105 |             width: 120,
106 |             borderTopRightRadius: 0,
107 |             borderBottomRightRadius: 0,
108 |             borderRight: 'none'
109 |           }}
110 |           className="h-10"
111 |           size="large"
112 |         >
113 |           {searchParamsList.map(item => (
114 |             <Option key={item.name} value={item.name}>
115 |               {item.label}
116 |             </Option>
117 |           ))}
118 |         </Select>
119 | 
120 |         {/* 中间:搜索值输入框 */}
121 |         {currentParam?.type === 'select' ? (
122 |           <Select
123 |             placeholder={currentParam.placeholder}
124 |             value={activeSearchValue}
125 |             onChange={(value) => {
126 |               setActiveSearchValue(value);
127 |               // 自动触发搜索
128 |               if (value) {
129 |                 onSearch(activeSearchName, value);
130 |               }
131 |             }}
132 |             style={{ 
133 |               width: 400,
134 |               borderTopLeftRadius: 0,
135 |               borderBottomLeftRadius: 0
136 |             }}
137 |             allowClear
138 |             onClear={() => {
139 |               setActiveSearchValue('');
140 |               onClear?.();
141 |             }}
142 |             className="h-10"
143 |             size="large"
144 |           >
145 |             {currentParam.optionList?.map(item => (
146 |               <Option key={item.value} value={item.value}>
147 |                 {item.label}
148 |               </Option>
149 |             ))}
150 |           </Select>
151 |         ) : (
152 |           <Input
153 |             placeholder={currentParam?.placeholder}
154 |             value={activeSearchValue}
155 |             onChange={(e) => setActiveSearchValue(e.target.value)}
156 |             style={{ 
157 |               width: 400,
158 |               borderTopLeftRadius: 0,
159 |               borderBottomLeftRadius: 0
160 |             }}
161 |             onPressEnter={handleSearch}
162 |             allowClear
163 |             onClear={() => setActiveSearchValue('')}
164 |             size="large"
165 |             className="h-10"
166 |             suffix={
167 |               <Button
168 |                 type="text"
169 |                 icon={<SearchOutlined />}
170 |                 onClick={handleSearch}
171 |                 size="small"
172 |                 className="h-8 w-8 flex items-center justify-center"
173 |               />
174 |             }
175 |           />
176 |         )}
177 |       </div>
178 | 
179 |       {/* 搜索标签 */}
180 |       {tagList.length > 0 && (
181 |         <div className="mt-4">
182 |           <div className="flex items-center gap-2 mb-2">
183 |             <span className="text-sm text-gray-500">已选择的筛选条件:</span>
184 |             <Button
185 |               type="link"
186 |               size="small"
187 |               onClick={handleClearAll}
188 |               className="text-gray-400 hover:text-gray-600"
189 |             >
190 |               清除全部
191 |             </Button>
192 |           </div>
193 |           <Space wrap>
194 |             {tagList.map(tag => (
195 |               <Tag
196 |                 key={tag.name}
197 |                 closable
198 |                 onClose={() => handleClearOne(tag.name)}
199 |                 onClick={() => handleSelectOne(tag.name)}
200 |                 className="cursor-pointer"
201 |                 color={tag.name === activeSearchName ? 'blue' : 'default'}
202 |               >
203 |                 {tag.label}:{tag.value}
204 |               </Tag>
205 |             ))}
206 |           </Space>
207 |         </div>
208 |       )}
209 |     </div>
210 |   );
211 | };
212 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/subscription/SubscriptionListModal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { Modal, Table, Badge, message, Button, Popconfirm } from 'antd';
  2 | import { useEffect, useState } from 'react';
  3 | import { Subscription } from '@/types/subscription';
  4 | import { portalApi } from '@/lib/api';
  5 | import { formatDateTime } from '@/lib/utils';
  6 | 
  7 | interface SubscriptionListModalProps {
  8 |   visible: boolean;
  9 |   consumerId: string;
 10 |   consumerName: string;
 11 |   onCancel: () => void;
 12 | }
 13 | 
 14 | export function SubscriptionListModal({
 15 |   visible,
 16 |   consumerId,
 17 |   consumerName,
 18 |   onCancel
 19 | }: SubscriptionListModalProps) {
 20 |   const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
 21 |   const [loading, setLoading] = useState(false);
 22 |   const [actionLoading, setActionLoading] = useState<string | null>(null);
 23 |   const [pagination, setPagination] = useState({
 24 |     current: 1,
 25 |     pageSize: 10,
 26 |     total: 0,
 27 |     showSizeChanger: true,
 28 |     showQuickJumper: true,
 29 |     showTotal: (total: number) => `共 ${total} 条`
 30 |   });
 31 | 
 32 |   useEffect(() => {
 33 |     if (visible && consumerId) {
 34 |       fetchSubscriptions();
 35 |     }
 36 |   }, [visible, consumerId, pagination.current, pagination.pageSize]);
 37 | 
 38 |   const fetchSubscriptions = () => {
 39 |     setLoading(true);
 40 |     portalApi.getConsumerSubscriptions(consumerId, {
 41 |       page: pagination.current - 1, // 后端从0开始
 42 |       size: pagination.pageSize
 43 |     }).then((res) => {
 44 |       setSubscriptions(res.data.content || []);
 45 |       setPagination(prev => ({
 46 |         ...prev,
 47 |         total: res.data.totalElements || 0
 48 |       }));
 49 |     }).catch((err) => {
 50 |       message.error('获取订阅列表失败');
 51 |     }).finally(() => {
 52 |       setLoading(false);
 53 |     });
 54 |   };
 55 | 
 56 |   const handleTableChange = (paginationInfo: any) => {
 57 |     setPagination(prev => ({
 58 |       ...prev,
 59 |       current: paginationInfo.current,
 60 |       pageSize: paginationInfo.pageSize
 61 |     }));
 62 |   };
 63 | 
 64 |   const handleApproveSubscription = async (subscription: Subscription) => {
 65 |     setActionLoading(`${subscription.consumerId}-${subscription.productId}-approve`);
 66 |     try {
 67 |       await portalApi.approveSubscription(subscription.consumerId, subscription.productId);
 68 |       message.success('审批通过成功');
 69 |       fetchSubscriptions(); // 重新获取数据
 70 |     } catch (error: any) {
 71 |       const errorMessage = error.response?.data?.message || error.message || '审批失败';
 72 |       message.error(`审批失败: ${errorMessage}`);
 73 |     } finally {
 74 |       setActionLoading(null);
 75 |     }
 76 |   };
 77 | 
 78 |   const handleDeleteSubscription = async (subscription: Subscription) => {
 79 |     setActionLoading(`${subscription.consumerId}-${subscription.productId}-delete`);
 80 |     try {
 81 |       await portalApi.deleteSubscription(subscription.consumerId, subscription.productId);
 82 |       message.success('删除订阅成功');
 83 |       fetchSubscriptions(); // 重新获取数据
 84 |     } catch (error: any) {
 85 |       const errorMessage = error.response?.data?.message || error.message || '删除订阅失败';
 86 |       message.error(`删除订阅失败: ${errorMessage}`);
 87 |     } finally {
 88 |       setActionLoading(null);
 89 |     }
 90 |   };
 91 | 
 92 |   const columns = [
 93 |     {
 94 |       title: '产品名称',
 95 |       dataIndex: 'productName',
 96 |       key: 'productName',
 97 |       render: (productName: string) => (
 98 |         <div>
 99 |           <div className="font-medium">{productName || '未知产品'}</div>
100 |         </div>
101 |       )
102 |     },
103 |     {
104 |       title: '产品类型',
105 |       dataIndex: 'productType',
106 |       key: 'productType',
107 |       render: (productType: string) => (
108 |         <Badge 
109 |           color={productType === 'REST_API' ? 'blue' : 'purple'} 
110 |           text={productType === 'REST_API' ? 'REST API' : 'MCP Server'} 
111 |         />
112 |       )
113 |     },
114 |     {
115 |       title: '订阅状态',
116 |       dataIndex: 'status',
117 |       key: 'status',
118 |       render: (status: string) => (
119 |         <Badge 
120 |           status={status === 'APPROVED' ? 'success' : 'processing'} 
121 |           text={status === 'APPROVED' ? '已通过' : '待审批'} 
122 |         />
123 |       )
124 |     },
125 |     {
126 |       title: '订阅时间',
127 |       dataIndex: 'createAt',
128 |       key: 'createAt',
129 |       render: (date: string) => formatDateTime(date)
130 |     },
131 |     {
132 |       title: '更新时间',
133 |       dataIndex: 'updatedAt',
134 |       key: 'updatedAt',
135 |       render: (date: string) => formatDateTime(date)
136 |     },
137 |     {
138 |       title: '操作',
139 |       key: 'action',
140 |       width: 120,
141 |       render: (_: any, record: Subscription) => {
142 |         const loadingKey = `${record.consumerId}-${record.productId}`;
143 |         const isApproving = actionLoading === `${loadingKey}-approve`;
144 |         const isDeleting = actionLoading === `${loadingKey}-delete`;
145 |         
146 |         if (record.status === 'PENDING') {
147 |           return (
148 |             <Button
149 |               type="primary"
150 |               size="small"
151 |               loading={isApproving}
152 |               onClick={() => handleApproveSubscription(record)}
153 |             >
154 |               审批通过
155 |             </Button>
156 |           );
157 |         } else if (record.status === 'APPROVED') {
158 |           return (
159 |             <Popconfirm
160 |               title="确定要删除这个订阅吗?"
161 |               description="删除后将无法恢复"
162 |               onConfirm={() => handleDeleteSubscription(record)}
163 |               okText="确定"
164 |               cancelText="取消"
165 |             >
166 |               <Button
167 |                 type="default"
168 |                 size="small"
169 |                 danger
170 |                 loading={isDeleting}
171 |               >
172 |                 删除订阅
173 |               </Button>
174 |             </Popconfirm>
175 |           );
176 |         }
177 |         return null;
178 |       }
179 |     }
180 |   ];
181 | 
182 |   const pendingCount = subscriptions.filter(s => s.status === 'PENDING').length;
183 |   const approvedCount = subscriptions.filter(s => s.status === 'APPROVED').length;
184 | 
185 |   return (
186 |     <Modal
187 |       title={
188 |         <div>
189 |           <div className="text-lg font-semibold">订阅列表 - {consumerName}</div>
190 |           <div className="text-sm text-gray-500 mt-1">
191 |             待审批: <Badge count={pendingCount} style={{ backgroundColor: '#faad14' }} /> | 
192 |             已通过: <Badge count={approvedCount} style={{ backgroundColor: '#52c41a' }} />
193 |           </div>
194 |         </div>
195 |       }
196 |       open={visible}
197 |       onCancel={onCancel}
198 |       footer={null}
199 |       width={1000}
200 |       destroyOnClose
201 |     >
202 |       <Table
203 |         columns={columns}
204 |         dataSource={subscriptions}
205 |         rowKey="subscriptionId"
206 |         loading={loading}
207 |         pagination={pagination}
208 |         onChange={handleTableChange}
209 |         locale={{
210 |           emptyText: '暂无订阅记录'
211 |         }}
212 |       />
213 |     </Modal>
214 |   );
215 | }
216 | 
217 | 
218 | 
219 | 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductOverview.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect } from 'react'
  2 | import { useNavigate } from 'react-router-dom'
  3 | import { Card, Row, Col, Statistic, Button, message } from 'antd'
  4 | import { 
  5 |   ApiOutlined, 
  6 |   GlobalOutlined,
  7 |   TeamOutlined,
  8 |   EditOutlined,
  9 |   CheckCircleFilled,
 10 |   MinusCircleFilled,
 11 |   CopyOutlined,
 12 |   ExclamationCircleFilled,
 13 |   ClockCircleFilled
 14 | } from '@ant-design/icons'
 15 | import type { ApiProduct } from '@/types/api-product'
 16 | import { getServiceName, formatDateTime, copyToClipboard } from '@/lib/utils'
 17 | import { apiProductApi } from '@/lib/api'
 18 | 
 19 | 
 20 | interface ApiProductOverviewProps {
 21 |   apiProduct: ApiProduct
 22 |   linkedService: any | null
 23 |   onEdit: () => void
 24 | }
 25 | 
 26 | export function ApiProductOverview({ apiProduct, linkedService, onEdit }: ApiProductOverviewProps) {
 27 | 
 28 |   const [portalCount, setPortalCount] = useState(0)
 29 |   const [subscriberCount] = useState(0)
 30 | 
 31 |   const navigate = useNavigate()
 32 | 
 33 |   useEffect(() => {
 34 |     if (apiProduct.productId) {
 35 |       fetchPublishedPortals()
 36 |     }
 37 |   }, [apiProduct.productId])
 38 | 
 39 |   const fetchPublishedPortals = async () => {
 40 |     try {
 41 |       const res = await apiProductApi.getApiProductPublications(apiProduct.productId)
 42 |       setPortalCount(res.data.content?.length || 0)
 43 |     } catch (error) {
 44 |     } finally {
 45 |     }
 46 |   }
 47 | 
 48 | 
 49 |   return (
 50 |     <div className="p-6 space-y-6">
 51 |       <div>
 52 |         <h1 className="text-2xl font-bold mb-2">概览</h1>
 53 |         <p className="text-gray-600">API产品概览</p>
 54 |       </div>
 55 | 
 56 |       {/* 基本信息 */}
 57 |       <Card 
 58 |         title="基本信息"
 59 |         extra={
 60 |           <Button 
 61 |             type="primary" 
 62 |             icon={<EditOutlined />} 
 63 |             onClick={onEdit}
 64 |           >
 65 |             编辑
 66 |           </Button>
 67 |         }
 68 |       >
 69 |         <div>
 70 |             <div className="grid grid-cols-6 gap-8 items-center pt-0 pb-2">
 71 |              <span className="text-xs text-gray-600">产品名称:</span>
 72 |              <span className="col-span-2 text-xs text-gray-900">{apiProduct.name}</span>
 73 |              <span className="text-xs text-gray-600">产品ID:</span>
 74 |               <div className="col-span-2 flex items-center gap-2">
 75 |                 <span className="text-xs text-gray-700">{apiProduct.productId}</span>
 76 |                 <Button 
 77 |                   type="text" 
 78 |                   size="small"
 79 |                   icon={<CopyOutlined />}
 80 |                   onClick={async () => {
 81 |                     try {
 82 |                       await copyToClipboard(apiProduct.productId);
 83 |                       message.success('产品ID已复制');
 84 |                     } catch {
 85 |                       message.error('复制失败,请手动复制');
 86 |                     }
 87 |                   }}
 88 |                   className="h-auto p-1 min-w-0"
 89 |                 />
 90 |               </div>
 91 |             </div>
 92 |             
 93 |             <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
 94 |              <span className="text-xs text-gray-600">类型:</span>
 95 |               <span className="col-span-2 text-xs text-gray-900">
 96 |                 {apiProduct.type === 'REST_API' ? 'REST API' : 'MCP Server'}
 97 |               </span>
 98 |              <span className="text-xs text-gray-600">状态:</span>
 99 |               <div className="col-span-2 flex items-center">
100 |                 {apiProduct.status === "PENDING" ? (
101 |                   <ExclamationCircleFilled className="text-yellow-500 mr-2" style={{fontSize: '10px'}} />
102 |                 ) : apiProduct.status === "READY" ? (
103 |                   <ClockCircleFilled className="text-blue-500 mr-2" style={{fontSize: '10px'}} />
104 |                 ) : (
105 |                   <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
106 |                 )}
107 |                 <span className="text-xs text-gray-900">
108 |                   {apiProduct.status === "PENDING" ? "待配置" : apiProduct.status === "READY" ? "待发布" : "已发布"}
109 |                 </span>
110 |               </div>
111 |             </div>
112 |             
113 |             <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
114 |               <span className="text-xs text-gray-600">自动审批订阅:</span>
115 |               <div className="col-span-2 flex items-center">
116 |                 {apiProduct.autoApprove === true ? (
117 |                   <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
118 |                 ) : (
119 |                   <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
120 |                 )}
121 |                 <span className="text-xs text-gray-900">
122 |                  {apiProduct.autoApprove === true ? '已开启' : '已关闭'}
123 |                 </span>
124 |               </div>
125 |               <span className="text-xs text-gray-600">创建时间:</span>
126 |               <span className="col-span-2 text-xs text-gray-700">{formatDateTime(apiProduct.createAt)}</span>
127 |             </div>
128 | 
129 |             {apiProduct.description && (
130 |               <div className="grid grid-cols-6 gap-8 pt-2 pb-2">
131 |                <span className="text-xs text-gray-600">描述:</span>
132 |                 <span className="col-span-5 text-xs text-gray-700 leading-relaxed">
133 |                   {apiProduct.description}
134 |                 </span>
135 |               </div>
136 |             )}
137 | 
138 |         </div>
139 |       </Card>
140 | 
141 |       {/* 统计数据 */}
142 |       <Row gutter={[16, 16]}>
143 |         <Col xs={24} sm={12} lg={8}>
144 |           <Card 
145 |             className="cursor-pointer hover:shadow-md transition-shadow"
146 |             onClick={() => {
147 |               navigate(`/api-products/detail?productId=${apiProduct.productId}&tab=portal`)
148 |             }}
149 |           >
150 |             <Statistic
151 |               title="发布的门户"
152 |               value={portalCount}
153 |               prefix={<GlobalOutlined className="text-blue-500" />}
154 |               valueStyle={{ color: '#1677ff', fontSize: '24px' }}
155 |             />
156 |           </Card>
157 |         </Col>
158 |         <Col xs={24} sm={12} lg={8}>
159 |           <Card 
160 |             className="cursor-pointer hover:shadow-md transition-shadow"
161 |             onClick={() => {
162 |               navigate(`/api-products/detail?productId=${apiProduct.productId}&tab=link-api`)
163 |             }}
164 |           >
165 |             <Statistic
166 |               title="关联API"
167 |               value={getServiceName(linkedService) || '未关联'}
168 |               prefix={<ApiOutlined className="text-blue-500" />}
169 |               valueStyle={{ color: '#1677ff', fontSize: '24px' }}
170 |             />
171 |           </Card>
172 |         </Col>
173 |         <Col xs={24} sm={12} lg={8}>
174 |           <Card className="hover:shadow-md transition-shadow">
175 |             <Statistic
176 |               title="订阅用户"
177 |               value={subscriberCount}
178 |               prefix={<TeamOutlined className="text-blue-500" />}
179 |               valueStyle={{ color: '#1677ff', fontSize: '24px' }}
180 |             />
181 |           </Card>
182 |         </Col>
183 |       </Row>
184 | 
185 |     </div>
186 |   )
187 | } 
```
Page 4/9FirstPrevNextLast