#
tokens: 49796/50000 22/349 files (page 4/7)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 7. Use http://codebase.md/higress-group/himarket?lines=false&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-server/src/main/java/com/alibaba/apiopenplatform/core/utils/TokenUtil.java:
--------------------------------------------------------------------------------

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.core.utils;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.alibaba.apiopenplatform.core.constant.CommonConstants;
import com.alibaba.apiopenplatform.support.common.User;
import com.alibaba.apiopenplatform.support.enums.UserType;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class TokenUtil {

    private static String JWT_SECRET;

    private static long JWT_EXPIRE_MILLIS;

    private static final Map<String, Long> INVALID_TOKENS = new ConcurrentHashMap<>();

    private static String getJwtSecret() {
        if (JWT_SECRET == null) {
            JWT_SECRET = SpringUtil.getProperty("jwt.secret");
        }

        if (StrUtil.isBlank(JWT_SECRET)) {
            throw new RuntimeException("JWT secret cannot be empty");
        }
        return JWT_SECRET;
    }

    private static long getJwtExpireMillis() {
        if (JWT_EXPIRE_MILLIS == 0) {
            String expiration = SpringUtil.getProperty("jwt.expiration");
            if (StrUtil.isBlank(expiration)) {
                throw new RuntimeException("JWT expiration is empty");
            }

            if (expiration.matches("\\d+[smhd]")) {
                JWT_EXPIRE_MILLIS = Duration.parse("PT" + expiration.toUpperCase()).toMillis();
            } else {
                JWT_EXPIRE_MILLIS = Long.parseLong(expiration);
            }
        }
        return JWT_EXPIRE_MILLIS;
    }

    public static String generateAdminToken(String userId) {
        return generateToken(UserType.ADMIN, userId);
    }

    public static String generateDeveloperToken(String userId) {
        return generateToken(UserType.DEVELOPER, userId);
    }

    /**
     * 生成令牌
     *
     * @param userType
     * @param userId
     * @return
     */
    private static String generateToken(UserType userType, String userId) {
        long now = System.currentTimeMillis();

        Map<String, String> claims = MapUtil.<String, String>builder()
                .put(CommonConstants.USER_TYPE, userType.name())
                .put(CommonConstants.USER_ID, userId)
                .build();

        return JWT.create()
                .addPayloads(claims)
                .setIssuedAt(new Date(now))
                .setExpiresAt(new Date(now + getJwtExpireMillis()))
                .setSigner(JWTSignerUtil.hs256(getJwtSecret().getBytes(StandardCharsets.UTF_8)))
                .sign();
    }

    /**
     * 解析Token
     *
     * @param token
     * @return
     */
    public static User parseUser(String token) {
        JWT jwt = JWTUtil.parseToken(token);

        // 验证签名
        boolean isValid = jwt.setSigner(JWTSignerUtil.hs256(getJwtSecret().getBytes(StandardCharsets.UTF_8))).verify();
        if (!isValid) {
            throw new IllegalArgumentException("Invalid token signature");
        }

        // 验证过期时间
        Object expObj = jwt.getPayloads().get(JWT.EXPIRES_AT);
        if (ObjectUtil.isNotNull(expObj)) {
            long expireAt = Long.parseLong(expObj.toString());
            if (expireAt * 1000 <= System.currentTimeMillis()) {
                throw new IllegalArgumentException("Token has expired");
            }
        }

        return jwt.getPayloads().toBean(User.class);
    }

    public static String getTokenFromRequest(HttpServletRequest request) {
        // 从Header中获取token
        String authHeader = request.getHeader(CommonConstants.AUTHORIZATION_HEADER);

        String token = null;
        if (authHeader != null && authHeader.startsWith(CommonConstants.BEARER_PREFIX)) {
            token = authHeader.substring(CommonConstants.BEARER_PREFIX.length());
        }

        // 从Cookie中获取token
        if (StrUtil.isBlank(token)) {
            token = Optional.ofNullable(request.getCookies())
                    .flatMap(cookies -> Arrays.stream(cookies)
                            .filter(cookie -> CommonConstants.AUTH_TOKEN_COOKIE.equals(cookie.getName()))
                            .map(Cookie::getValue)
                            .findFirst())
                    .orElse(null);
        }
        if (StrUtil.isBlank(token) || isTokenRevoked(token)) {
            return null;
        }

        return token;
    }

    public static void revokeToken(String token) {
        if (StrUtil.isBlank(token)) {
            return;
        }
        long expireAt = getTokenExpireTime(token);
        INVALID_TOKENS.put(token, expireAt);
        cleanExpiredTokens();
    }

    private static long getTokenExpireTime(String token) {
        JWT jwt = JWTUtil.parseToken(token);
        Object expObj = jwt.getPayloads().get(JWT.EXPIRES_AT);
        if (ObjectUtil.isNotNull(expObj)) {
            return Long.parseLong(expObj.toString()) * 1000; // JWT过期时间是秒,转换为毫秒
        }
        return System.currentTimeMillis() + getJwtExpireMillis(); // 默认过期时间
    }

    public static void revokeToken(HttpServletRequest request) {
        String token = getTokenFromRequest(request);
        if (StrUtil.isNotBlank(token)) {
            revokeToken(token);
        }
    }

    public static boolean isTokenRevoked(String token) {
        if (StrUtil.isBlank(token)) {
            return false;
        }
        Long expireAt = INVALID_TOKENS.get(token);
        if (expireAt == null) {
            return false;
        }
        if (expireAt <= System.currentTimeMillis()) {
            INVALID_TOKENS.remove(token);
            return false;
        }
        return true;
    }

    private static void cleanExpiredTokens() {
        long now = System.currentTimeMillis();
        INVALID_TOKENS.entrySet().removeIf(entry -> entry.getValue() <= now);
    }

    public static long getTokenExpiresIn() {
        return getJwtExpireMillis() / 1000;
    }
}

```

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

```typescript
import { Card, Table, Button, Space, Typography, Input, Avatar } from "antd";
import { SearchOutlined, DeleteOutlined, EyeOutlined } from "@ant-design/icons";
import { Layout } from "../components/Layout";
import { useEffect, useState, useCallback } from "react";
import { getConsumers, deleteConsumer, createConsumer } from "../lib/api";
import { message, Modal } from "antd";
import { Link, useSearchParams } from "react-router-dom";
import { formatDateTime } from "../lib/utils";
import type { Consumer } from "../types/consumer";

const { Title, Paragraph } = Typography;
const { Search } = Input;

function ConsumersPage() {
  const [searchParams] = useSearchParams();
  const productId = searchParams.get('productId');
  
  const [consumers, setConsumers] = useState<Consumer[]>([]);
  const [loading, setLoading] = useState(false);
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(10);
  const [total, setTotal] = useState(0);
  const [searchInput, setSearchInput] = useState(""); // 输入框的值
  const [searchName, setSearchName] = useState(""); // 实际搜索的值
  const [addModalOpen, setAddModalOpen] = useState(false);
  const [addLoading, setAddLoading] = useState(false);
  const [addForm, setAddForm] = useState({ name: '', description: '' });

  const fetchConsumers = useCallback(async (searchKeyword?: string, targetPage?: number) => {
    setLoading(true);
    try {
      const res = await getConsumers(
        { name: searchKeyword || '' },
        { page: targetPage || page, size: pageSize }
      );
      setConsumers(res.data?.content || []);
      setTotal(res.data?.totalElements || 0);
    } catch {
      // message.error("获取消费者列表失败");
    } finally {
      setLoading(false);
    }
  }, [page, pageSize]); // 不依赖 searchName

  // 初始加载和分页变化时调用
  useEffect(() => {
    fetchConsumers(searchName);
  }, [page, pageSize, fetchConsumers]); // 包含fetchConsumers以确保初始加载

  // 处理搜索
  const handleSearch = useCallback(async (searchValue?: string) => {
    const actualSearchValue = searchValue !== undefined ? searchValue : searchInput;
    setSearchName(actualSearchValue);
    setPage(1);
    // 直接调用API,不依赖状态变化
    await fetchConsumers(actualSearchValue, 1);
  }, [searchInput, fetchConsumers]);

  const handleDelete = (record: Consumer) => {
    Modal.confirm({
      title: `确定要删除消费者「${record.name}」吗?`,
      onOk: async () => {
        try {
          await deleteConsumer(record.consumerId);
          message.success("删除成功");
          await fetchConsumers(searchName); // 使用当前搜索条件重新加载
        } catch {
          // message.error("删除失败");
        }
      },
    });
  };

  const handleAdd = async () => {
    if (!addForm.name.trim()) {
      message.warning('请输入消费者名称');
      return;
    }
    setAddLoading(true);
    try {
      await createConsumer({ name: addForm.name, description: addForm.description });
      message.success('新增成功');
      setAddModalOpen(false);
      setAddForm({ name: '', description: '' });
      await fetchConsumers(searchName); // 使用当前搜索条件重新加载
    } catch {
      // message.error('新增失败');
    } finally {
      setAddLoading(false);
    }
  };

  const columns = [
    {
      title: '消费者',
      dataIndex: 'name',
      key: 'name',
      render: (name: string, record: Consumer) => (
        <div className="flex items-center space-x-3">
          <Avatar className="bg-blue-500">
            {name?.charAt(0).toUpperCase()}
          </Avatar>
          <div>
            <div className="font-medium">{name}</div>
            <div className="text-xs text-gray-400">{record.description}</div>
          </div>
        </div>
      ),
    },
    {
      title: '创建时间',
      dataIndex: 'createAt',
      key: 'createAt',
      render: (date: string) => date ? formatDateTime(date) : '-',
    },
    {
      title: '操作',
      key: 'action',
      render: (_: unknown, record: Consumer) => (
        <Space>
          <Link to={`/consumers/${record.consumerId}`}>
            <Button 
              type="link" 
              icon={<EyeOutlined />}
            >
              查看详情
            </Button>
          </Link>
          <Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
            删除
          </Button>
        </Space>
      ),
    },
  ];

  return (
    <Layout>
      <div className="mb-8">
        <Title level={1} className="mb-2">
          {productId ? '产品订阅管理' : '消费者管理'}
        </Title>
        <Paragraph className="text-gray-600">
          {productId ? '管理此产品的消费者订阅情况' : '管理API的消费者用户和订阅信息'}
        </Paragraph>
      </div>

      <Card>
        <div className="mb-4 flex gap-4">
          {!productId && (
            <Button type="primary" onClick={() => setAddModalOpen(true)}>
              新增消费者
            </Button>
          )}
          <Search
            placeholder={"搜索消费者..."}
            prefix={<SearchOutlined />}
            style={{ width: 300 }}
            value={searchInput}
            onChange={e => setSearchInput(e.target.value)}
            onSearch={handleSearch}
            allowClear
          />
        </div>
        <Table
          columns={columns}
          dataSource={consumers}
          rowKey="consumerId"
          loading={loading}
          pagination={{
            total,
            current: page,
            pageSize,
            showSizeChanger: true,
            showQuickJumper: true,
            showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
            onChange: (p, ps) => {
              setPage(p);
              setPageSize(ps);
            },
          }}
        />
        <Modal
          title="新增消费者"
          open={addModalOpen}
          onCancel={() => { setAddModalOpen(false); setAddForm({ name: '', description: '' }); }}
          onOk={handleAdd}
          confirmLoading={addLoading}
          okText="提交"
          cancelText="取消"
        >
          <div style={{ marginBottom: 16 }}>
            <Input
              placeholder="消费者名称"
              value={addForm.name}
              maxLength={50}
              onChange={e => setAddForm(f => ({ ...f, name: e.target.value }))}
              disabled={addLoading}
            />
          </div>
          <div>
            <Input.TextArea
              placeholder="描述(可选),长度限制64"
              value={addForm.description}
              maxLength={64}
              onChange={e => setAddForm(f => ({ ...f, description: e.target.value }))}
              disabled={addLoading}
              rows={3}
            />
          </div>
        </Modal>
      </Card>

      <Card title="消费者统计" className="mt-8">
        <div className="flex justify-center">
          <div className="text-center">
            <div className="text-2xl font-bold text-blue-600">{total}</div>
            <div className="text-sm text-gray-500">总消费者</div>
          </div>
          {/* 其他统计项可根据接口返回字段补充 */}
        </div>
      </Card>
    </Layout>
  );
}

export default ConsumersPage; 
```

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

```typescript
import { useEffect, useState } from "react";
import { Card, Tag, Typography, Input, Avatar, Skeleton } from "antd";
import { Link } from "react-router-dom";
import { Layout } from "../components/Layout";
import api from "../lib/api";
import { ProductStatus } from "../types";
import type { Product, ApiResponse, PaginatedResponse, ProductIcon } from "../types";
// import { getCategoryText, getCategoryColor } from "../lib/statusUtils";

const { Title, Paragraph } = Typography;
const { Search } = Input;

interface McpServer {
  key: string;
  name: string;
  description: string;
  status: string;
  version: string;
  endpoints: number;
  category: string;
  creator: string;
  icon?: ProductIcon;
  mcpConfig?: any;
  updatedAt: string;
}

function McpPage() {
  const [loading, setLoading] = useState(false);
  const [mcpServers, setMcpServers] = useState<McpServer[]>([]);
  const [searchText, setSearchText] = useState('');

  useEffect(() => {
    fetchMcpServers();
  }, []);
  // 处理产品图标的函数
  const getIconUrl = (icon?: ProductIcon | null): string => {
    const fallback = "/MCP.svg";
    
    if (!icon) {
      return fallback;
    }
    
    switch (icon.type) {
      case "URL":
        return icon.value || fallback;
      case "BASE64":
        // 如果value已经包含data URL前缀,直接使用;否则添加前缀
        return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
      default:
        return fallback;
    }
  };
  const fetchMcpServers = async () => {
    setLoading(true);
    try {
      const response: ApiResponse<PaginatedResponse<Product>> = await api.get("/products?type=MCP_SERVER&page=0&size=100");
      if (response.code === "SUCCESS" && response.data) {
        // 移除重复过滤,简化数据映射
        const mapped = response.data.content.map((item: Product) => ({
          key: item.productId,
          name: item.name,
          description: item.description,
          status: item.status === ProductStatus.ENABLE ? 'active' : 'inactive',
          version: 'v1.0.0',
          endpoints: 0,
          category: item.category,
          creator: 'Unknown',
          icon: item.icon || undefined,
          mcpConfig: item.mcpConfig,
          updatedAt: item.updatedAt?.slice(0, 10) || ''
        }));
        setMcpServers(mapped);
      }
    } catch (error) {
      console.error('获取MCP服务器列表失败:', error);
    } finally {
      setLoading(false);
    }
  };



  const filteredMcpServers = mcpServers.filter(server => {
    return server.name.toLowerCase().includes(searchText.toLowerCase()) ||
           server.description.toLowerCase().includes(searchText.toLowerCase()) ||
           server.creator.toLowerCase().includes(searchText.toLowerCase());
  });

  return (
    <Layout>
      {/* Header Section */}
      <div className="text-center mb-8">
        <Title level={1} className="mb-4">
          MCP 市场
        </Title>
        <Paragraph className="text-gray-600 text-lg max-w-4xl mx-auto text-flow text-flow-grey slow">
          支持私有化部署,共建和兼容MCP市场官方协议,具备更多管理能力,支持自动注册、智能路由的MCP市场
        </Paragraph>
      </div>

      {/* Search Section */}
      <div className="flex justify-center mb-8">
        <div className="relative w-full max-w-2xl">
          <Search
            placeholder="请输入内容"
            size="large"
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
            className="rounded-lg shadow-lg"
          />
        </div>
      </div>

      {/* Servers Section */}
      <div className="mb-6">
        <Title level={3} className="mb-4">
          热门/推荐 MCP Servers: {filteredMcpServers.length}
        </Title>
      </div>

      {/* Servers Grid */}
      {loading ? (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
          {Array.from({ length: 6 }).map((_, index) => (
            <Card key={index} className="h-full rounded-lg shadow-lg">
              <Skeleton loading active>
                <div className="flex items-start space-x-4 mb-2">
                  <Skeleton.Avatar size={48} active />
                  <div className="flex-1 min-w-0">
                    <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
                    <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
                    <Skeleton.Input active size="small" style={{ width: '60%' }} />
                  </div>
                </div>
              </Skeleton>
            </Card>
          ))}
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
          {filteredMcpServers.map((server) => (
          <Link key={server.key} to={`/mcp/${server.key}`} className="block">
            <Card
              hoverable
              className="h-full transition-all duration-200 hover:shadow-lg cursor-pointer rounded-lg shadow-lg"
            >
              <div className="flex items-start space-x-4 mb-2">
                {/* Server Icon */}
                {server.icon ? (
                  <Avatar
                    size={48}
                    src={getIconUrl(server.icon)}
                  />
                ) : (
                  <Avatar
                    size={48}
                    className="bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg"
                    style={{ fontSize: "18px", fontWeight: "600" }}
                  >
                    {server.name[0]}
                  </Avatar>
                )}

                {/* Server Info */}
                <div className="flex-1 min-w-0">
                  <div className="flex items-center justify-between mb-2">
                    <Title level={5} className="mb-0 truncate">
                      {server.name}
                    </Title>
                    <Tag className="text-xs text-green-700 border-0 bg-transparent px-0">
                      {server.mcpConfig?.mcpServerConfig?.transportMode || 'remote'}
                    </Tag>
                  </div>
                  </div>
                </div>
                  <Paragraph className="text-sm text-gray-600 mb-3 line-clamp-2">
                    {server.description}
                  </Paragraph>

                  <div className="flex items-center justify-between">
                    {/* <Tag color={getCategoryColor(server.category || 'OFFICIAL')} className="">
                      {getCategoryText(server.category || 'OFFICIAL')}
                    </Tag> */}
                    <div className="text-xs text-gray-400">
                      更新 {server.updatedAt}
                    </div>
                  </div>
            </Card>
          </Link>
        ))}
      </div>
      )}

      {/* Empty State */}
      {filteredMcpServers.length === 0 && (
        <div className="text-center py-8">
          <div className="text-gray-500">暂无MCP服务器</div>
        </div>
      )}
    </Layout>
  );
}

export default McpPage; 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/lib/api.ts:
--------------------------------------------------------------------------------

```typescript
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { getToken, removeToken } from './utils'
import {  message } from 'antd'



const api: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true, // 确保跨域请求时携带 cookie
})

// 请求拦截器
api.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = getToken()
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  (response: AxiosResponse) => {
    return response.data
  },
  (error) => {
    message.error(error.response?.data?.message || '请求发生错误');
    if (error.response?.status === 403 || error.response?.status === 401) {
      removeToken()
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)


export default api

// 用户相关API
export const authApi = {
  getNeedInit: () => {
    return api.get('/admins/need-init')
  }
}
// Portal相关API
export const portalApi = {
  // 获取portal列表
  getPortals: (params?: { page?: number; size?: number }) => {
    return api.get(`/portals`, { params })
  },
  // 获取Portal Dashboard URL
  getPortalDashboard: (portalId: string, type: string = 'Portal') => {
    return api.get(`/portals/${portalId}/dashboard`, { params: { type } })
  },
  deletePortal: (portalId: string) => {
    return api.delete(`/portals/${portalId}`)
  },
  createPortal: (data: any) => {
    return api.post(`/portals`, data)
  },
  // 获取portal详情
  getPortalDetail: (portalId: string) => {
    return api.get(`/portals/${portalId}`)
  },
  // 绑定域名
  bindDomain: (portalId: string, domainData: { domain: string; protocol: string; type: string }) => {
    return api.post(`/portals/${portalId}/domains`, domainData)
  },
  // 解绑域名
  unbindDomain: (portalId: string, domain: string) => {
    const encodedDomain = encodeURIComponent(domain)
    return api.delete(`/portals/${portalId}/domains/${encodedDomain}`)
  },
  // 更新Portal
  updatePortal: (portalId: string, data: any) => {
    return api.put(`/portals/${portalId}`, data)
  },
  // 更新Portal设置
  updatePortalSettings: (portalId: string, settings: any) => {
    return api.put(`/portals/${portalId}/setting`, settings)
  },
  // 获取Portal的开发者列表
  getDeveloperList: (portalId: string, pagination?: { page: number; size: number }) => {
    return api.get(`/developers`, {
      params: {
        portalId,
        ...pagination
      }
    })
  },
  // 更新开发者状态
  updateDeveloperStatus: (portalId: string, developerId: string, status: string) => {
    return api.patch(`/developers/${developerId}/status`, {
      portalId,
      status
    })
  },
  deleteDeveloper: (developerId: string) => {
    return api.delete(`/developers/${developerId}`)
  },
  getConsumerList: (portalId: string, developerId: string, pagination?: { page: number; size: number }) => {
    return api.get(`/consumers`, {
      params: {
        portalId,
        developerId,
        ...pagination
      }
    })
  },
  // 审批consumer
  approveConsumer: (consumerId: string) => {
    return api.patch(`/consumers/${consumerId}/status`)
  },
  // 获取Consumer的订阅列表
  getConsumerSubscriptions: (consumerId: string, params?: { page?: number; size?: number; status?: string }) => {
    return api.get(`/consumers/${consumerId}/subscriptions`, { params })
  },
  // 审批订阅申请
  approveSubscription: (consumerId: string, productId: string) => {
    return api.patch(`/consumers/${consumerId}/subscriptions/${productId}`)
  },
  // 删除订阅
  deleteSubscription: (consumerId: string, productId: string) => {
    return api.delete(`/consumers/${consumerId}/subscriptions/${productId}`)
  }
}

// API Product相关API
export const apiProductApi = {
  // 获取API产品列表
  getApiProducts: (params?: any) => {
    return api.get(`/products`, { params })
  },
  // 获取API产品详情
  getApiProductDetail: (productId: string) => {
    return api.get(`/products/${productId}`)
  },
  // 创建API产品
  createApiProduct: (data: any) => {
    return api.post(`/products`, data)
  },
  // 删除API产品
  deleteApiProduct: (productId: string) => {
    return api.delete(`/products/${productId}`)
  },
  // 更新API产品
  updateApiProduct: (productId: string, data: any) => {
    return api.put(`/products/${productId}`, data)
  },
  // 获取API产品关联的服务
  getApiProductRef: (productId: string) => {
    return api.get(`/products/${productId}/ref`)
  },
  // 创建API产品关联
  createApiProductRef: (productId: string, data: any) => {
    return api.post(`/products/${productId}/ref`, data)
  },
  // 删除API产品关联
  deleteApiProductRef: (productId: string) => {
    return api.delete(`/products/${productId}/ref`)
  },
  // 获取API产品已发布的门户列表
  getApiProductPublications: (productId: string, params?: any) => {
    return api.get(`/products/${productId}/publications`, { params })
  },
  // 发布API产品到门户
  publishToPortal: (productId: string, portalId: string) => {
    return api.post(`/products/${productId}/publications/${portalId}`)
  },
  // 取消发布API产品到门户
  cancelPublishToPortal: (productId: string, portalId: string) => {
    return api.delete(`/products/${productId}/publications/${portalId}`)
  },
  // 获取API产品的Dashboard监控面板URL
  getProductDashboard: (productId: string) => {
    return api.get(`/products/${productId}/dashboard`)
  }
}

// Gateway相关API
export const gatewayApi = {
  // 获取网关列表
  getGateways: (params?: any) => {
    return api.get(`/gateways`, { params })
  },
  // 获取APIG网关
  getApigGateway: (data: any) => {
    return api.get(`/gateways/apig`, { params: {
      ...data,
    } })
  },
  // 获取ADP网关
  getAdpGateways: (data: any) => {
    return api.post(`/gateways/adp`, data)
  },
  // 删除网关
  deleteGateway: (gatewayId: string) => {
    return api.delete(`/gateways/${gatewayId}`)
  },
  // 导入网关
  importGateway: (data: any) => {
    return api.post(`/gateways`, { ...data })
  },
  // 获取网关的REST API列表
  getGatewayRestApis: (gatewayId: string, data: any) => {
    return api.get(`/gateways/${gatewayId}/rest-apis`, {
      params: data
    })
  },
  // 获取网关的MCP Server列表
  getGatewayMcpServers: (gatewayId: string, data: any) => {
    return api.get(`/gateways/${gatewayId}/mcp-servers`, {
      params: data
    })
  },
  // 获取网关的Dashboard URL
  getDashboard: (gatewayId: string) => {
    return api.get(`/gateways/${gatewayId}/dashboard`)
  }
} 

export const nacosApi = {
  getNacos: (params?: any) => {
    return api.get(`/nacos`, { params })
  },
  // 从阿里云 MSE 获取 Nacos 集群列表
  getMseNacos: (params: { regionId: string; accessKey: string; secretKey: string; page?: number; size?: number }) => {
    return api.get(`/nacos/mse`, { params })
  },
  createNacos: (data: any) => {
    return api.post(`/nacos`, data)
  },
  deleteNacos: (nacosId: string) => {
    return api.delete(`/nacos/${nacosId}`)
  },
  updateNacos: (nacosId: string, data: any) => {
    return api.put(`/nacos/${nacosId}`, data)
  },
  getNacosMcpServers: (nacosId: string, data: any) => {
    return api.get(`/nacos/${nacosId}/mcp-servers`, {
      params: data
    })
  },
  // 获取指定 Nacos 实例的命名空间列表
  getNamespaces: (nacosId: string, params?: { page?: number; size?: number }) => {
    return api.get(`/nacos/${nacosId}/namespaces`, { params })
  }
}
```

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

```typescript
import { useState, useEffect } from 'react'
import { Card, Table, Modal, Form, Button, Space, Select, message, Checkbox } from 'antd'
import { EyeOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { Portal, ApiProduct } from '@/types'
import { apiProductApi } from '@/lib/api'
import { useNavigate } from 'react-router-dom'
import { ProductTypeMap } from '@/lib/utils'

interface PortalApiProductsProps {
  portal: Portal
}

export function PortalPublishedApis({ portal }: PortalApiProductsProps) {
  const navigate = useNavigate()
  const [apiProducts, setApiProducts] = useState<ApiProduct[]>([])
  const [apiProductsOptions, setApiProductsOptions] = useState<ApiProduct[]>([])
  const [isModalVisible, setIsModalVisible] = useState(false)
  const [selectedApiIds, setSelectedApiIds] = useState<string[]>([])
  const [loading, setLoading] = useState(false)
  const [modalLoading, setModalLoading] = useState(false)
  
  // 分页状态
  const [currentPage, setCurrentPage] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const [total, setTotal] = useState(0)

  const [form] = Form.useForm()
  
  useEffect(() => {
    if (portal.portalId) {
      fetchApiProducts()
    }
  }, [portal.portalId, currentPage, pageSize])

  const fetchApiProducts = () => {
    setLoading(true)
    apiProductApi.getApiProducts({
      portalId: portal.portalId,
      page: currentPage,
      size: pageSize
    }).then((res) => {
      setApiProducts(res.data.content)
      setTotal(res.data.totalElements || 0)
    }).finally(() => {
      setLoading(false)
    })
  }

  useEffect(() => {
    if (isModalVisible) {
      setModalLoading(true)
      apiProductApi.getApiProducts({
        page: 1,
        size: 500, // 获取所有可用的API
        status: 'READY'
      }).then((res) => {
        // 过滤掉已发布在该门户里的api
        setApiProductsOptions(res.data.content.filter((api: ApiProduct) => 
          !apiProducts.some((a: ApiProduct) => a.productId === api.productId)
        ))
      }).finally(() => {
        setModalLoading(false)
      })
    }
  }, [isModalVisible]) // 移除apiProducts依赖,避免重复请求

  const handlePageChange = (page: number, size?: number) => {
    setCurrentPage(page)
    if (size) {
      setPageSize(size)
    }
  }

  const columns = [
    {
      title: '名称/ID',
      key: 'nameAndId',
      width: 280,
      render: (_: any, record: ApiProduct) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">{record.name}</div>
          <div className="text-xs text-gray-500 truncate">{record.productId}</div>
        </div>
      ),
    },
    {
      title: '类型',
      dataIndex: 'type',
      key: 'type',
      width: 120,
      render: (text: string) => ProductTypeMap[text] || text
    },
    {
      title: '描述',
      dataIndex: 'description',
      key: 'description',
      width: 400,
    },
    // {
    //   title: '分类',
    //   dataIndex: 'category',
    //   key: 'category',
    // },
    {
      title: '操作',
      key: 'action',
      width: 180,
      render: (_: any, record: ApiProduct) => (
        <Space size="middle">
          <Button
            onClick={() => {
              navigate(`/api-products/detail?productId=${record.productId}`)
            }}
            type="link" icon={<EyeOutlined />}>
            查看
          </Button>
          
          <Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.productId, record.name)}>
            移除
          </Button>
        </Space>
      ),
    },
  ]

  const modalColumns = [
    {
      title: '选择',
      dataIndex: 'select',
      key: 'select',
      width: 60,
      render: (_: any, record: ApiProduct) => (
        <Checkbox
          checked={selectedApiIds.includes(record.productId)}
          onChange={(e) => {
            if (e.target.checked) {
              setSelectedApiIds([...selectedApiIds, record.productId])
            } else {
              setSelectedApiIds(selectedApiIds.filter(id => id !== record.productId))
            }
          }}
        />
      ),
    },
    {
      title: '名称',
      dataIndex: 'name',
      key: 'name',
      width: 320,
      render: (_: any, record: ApiProduct) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">
            {record.name}
          </div>
          <div className="text-xs text-gray-500 truncate">
            {record.productId}
          </div>
        </div>
      ),
    },
    {
      title: '类型',
      dataIndex: 'type',
      key: 'type',
      width: 100,
      render: (type: string) => ProductTypeMap[type] || type,
    },
    {
      title: '描述',
      dataIndex: 'description',
      key: 'description',
      width: 300,
    },
  ]

  const handleDelete = (productId: string, productName: string) => {
    Modal.confirm({
      title: '确认移除',
      icon: <ExclamationCircleOutlined />,
      content: `确定要从门户中移除API产品 "${productName}" 吗?此操作不可恢复。`,
      okText: '确认移除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        apiProductApi.cancelPublishToPortal(productId, portal.portalId).then((res) => {
          message.success('移除成功')
          fetchApiProducts()
          setIsModalVisible(false)
        }).catch((error) => {
          // message.error('移除失败')
        })
      },
    })
  }

  const handlePublish = async () => {
    if (selectedApiIds.length === 0) {
      message.warning('请至少选择一个API')
      return
    }

    setModalLoading(true)
    try {
      // 批量发布选中的API
      for (const productId of selectedApiIds) {
        await apiProductApi.publishToPortal(productId, portal.portalId)
      }
      message.success(`成功发布 ${selectedApiIds.length} 个API`)
      setSelectedApiIds([])
      fetchApiProducts()
      setIsModalVisible(false)
    } catch (error) {
      // message.error('发布失败')
    } finally {
      setModalLoading(false)
    }
  }

  const handleModalCancel = () => {
    setIsModalVisible(false)
    setSelectedApiIds([])
  }

  return (
    <div className="p-6 space-y-6">
      <div className="flex justify-between items-center">
        <div>
          <h1 className="text-2xl font-bold mb-2">API Product</h1>
          <p className="text-gray-600">管理在此Portal中发布的API产品</p>
        </div>
        <Button type="primary" onClick={() => setIsModalVisible(true)}>
          发布新API
        </Button>
      </div>

      <Card>
        <Table 
          columns={columns} 
          dataSource={apiProducts}
          rowKey="productId"
          loading={loading}
          pagination={{
            current: currentPage,
            pageSize: pageSize,
            total: total,
            showSizeChanger: true,
            showQuickJumper: true,
            showTotal: (total) => `共 ${total} 条`,
            onChange: handlePageChange,
            onShowSizeChange: handlePageChange,
          }}
        />
      </Card>

      <Modal
        title="发布API产品"
        open={isModalVisible}
        onOk={handlePublish}
        onCancel={handleModalCancel}
        okText="发布"
        cancelText="取消"
        width={800}
        confirmLoading={modalLoading}
      >
        <Table
          columns={modalColumns}
          dataSource={apiProductsOptions}
          rowKey="productId"
          loading={modalLoading}
          pagination={false}
          scroll={{ y: 400 }}
        />
      </Modal>
    </div>
  )
} 
```

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

```typescript
import { useEffect, useState } from "react";
import { Card, Tag, Typography, Input, Avatar, Skeleton } from "antd";
import { Link } from "react-router-dom";
import { Layout } from "../components/Layout";
import api from "../lib/api";
import { ProductStatus } from "../types";
import type { Product, ApiResponse, PaginatedResponse, ProductIcon } from "../types";
// import { getCategoryText, getCategoryColor } from "../lib/statusUtils";
import './Test.css';

const { Title, Paragraph } = Typography;
const { Search } = Input;



interface ApiProductListItem {
  key: string;
  name: string;
  description: string;
  status: string;
  version: string;
  endpoints: number;
  category: string;
  creator: string;
  icon?: ProductIcon;
  updatedAt: string;
}

function APIsPage() {
  const [loading, setLoading] = useState(false);
  const [apiProducts, setApiProducts] = useState<ApiProductListItem[]>([]);
  const [searchText, setSearchText] = useState('');

  useEffect(() => {
    fetchApiProducts();
  }, []);

  // 处理产品图标的函数
  const getIconUrl = (icon?: ProductIcon | null): string => {
    const fallback = "/logo.svg";
    
    if (!icon) {
      return fallback;
    }
    
    switch (icon.type) {
      case "URL":
        return icon.value || fallback;
      case "BASE64":
        // 如果value已经包含data URL前缀,直接使用;否则添加前缀
        return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
      default:
        return fallback;
    }
  };
  const fetchApiProducts = async () => {
    setLoading(true);
    try {
      const response: ApiResponse<PaginatedResponse<Product>> = await api.get("/products?type=REST_API&page=0&size=100");
      if (response.code === "SUCCESS" && response.data) {
        // 移除重复过滤,简化数据映射
        const mapped = response.data.content.map((item: Product) => ({
          key: item.productId,
          name: item.name,
          description: item.description,
          status: item.status === ProductStatus.ENABLE ? 'active' : 'inactive',
          version: 'v1.0.0',
          endpoints: 0,
          category: item.category,
          creator: 'Unknown',
          icon: item.icon || undefined,
          updatedAt: item.updatedAt?.slice(0, 10) || ''
        }));
        setApiProducts(mapped);
      }
    } catch (error) {
      console.error('获取API产品列表失败:', error);
    } finally {
      setLoading(false);
    }
  };



  const filteredApiProducts = apiProducts.filter(product => {
    return product.name.toLowerCase().includes(searchText.toLowerCase()) ||
           product.description.toLowerCase().includes(searchText.toLowerCase()) ||
           product.creator.toLowerCase().includes(searchText.toLowerCase());
  });

  const getApiIcon = (name: string) => {
    // Generate initials for API icon
    const words = name.split(' ');
    if (words.length >= 2) {
      return words[0][0] + words[1][0];
    }
    return name.substring(0, 2).toUpperCase();
  };

  const getApiIconColor = (name: string) => {
    const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
    const index = name.charCodeAt(0) % colors.length;
    return colors[index];
  };

  return (
    <Layout>
      {/* Header Section */}
      <div className="text-center mb-8">
        <Title level={1} className="mb-4">
          API 市场
        </Title>
        <Paragraph className="text-gray-600 text-lg max-w-4xl mx-auto text-flow text-flow-grey slow">
          支持私有化部署,具备更多管理能力,支持自动注册、智能路由的API市场
        </Paragraph>
      </div>

      {/* Search Section */}
      <div className="flex justify-center mb-8">
        <div className="relative w-full max-w-2xl">
          <Search
            placeholder="请输入内容"
            size="large"
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
            className="rounded-lg shadow-lg"
          />
        </div>
      </div>

      {/* APIs Section */}
      <div className="mb-6">
        <Title level={3} className="mb-4">
          热门/推荐 APIs: {filteredApiProducts.length}
        </Title>
      </div>

      {/* APIs Grid */}
      {loading ? (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
          {Array.from({ length: 6 }).map((_, index) => (
            <Card key={index} className="h-full rounded-lg shadow-lg">
              <Skeleton loading active>
                <div className="flex items-start space-x-4">
                  <Skeleton.Avatar size={48} active />
                  <div className="flex-1 min-w-0">
                    <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
                    <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
                    <Skeleton.Input active size="small" style={{ width: '60%' }} />
                  </div>
                </div>
              </Skeleton>
            </Card>
          ))}
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
          {filteredApiProducts.map((product) => (
          <Link key={product.key} to={`/apis/${product.key}`} className="block">
            <Card
              hoverable
              className="h-full transition-all duration-200 hover:shadow-lg cursor-pointer rounded-lg shadow-lg"
            >
              <div className="flex items-start space-x-4">
                {/* API Icon */}
                <Avatar
                  size={48}
                  src={product.icon ? getIconUrl(product.icon) : undefined}
                  style={{ 
                    backgroundColor: getApiIconColor(product.name),
                    fontSize: '18px',
                    fontWeight: 'bold'
                  }}
                >
                  {!product.icon && getApiIcon(product.name)}
                </Avatar>

                {/* API Info */}
                <div className="flex-1 min-w-0">
                  <div className="flex items-center justify-between mb-2">
                    <Title level={5} className="mb-0 truncate">
                      {product.name}
                    </Title>
                    <Tag className="text-xs text-green-700 border-0 bg-transparent px-0">
                      REST
                    </Tag>
                  </div>

                  {/* <div className="text-sm text-gray-500 mb-2">
                    创建者: {product.creator}
                  </div> */}

                  <Paragraph className="text-sm text-gray-600 mb-3 line-clamp-2">
                    {product.description}
                  </Paragraph>

                  <div className="flex items-center justify-between">
                    {/* <Tag color={getCategoryColor(product.category)} className="">
                      {getCategoryText(product.category)}
                    </Tag> */}
                    <div className="text-xs text-gray-400">
                      更新 {product.updatedAt}
                    </div>
                  </div>
                </div>
              </div>
            </Card>
          </Link>
        ))}
      </div>
      )}

      {/* Empty State */}
      {filteredApiProducts.length === 0 && (
        <div className="text-center py-8">
          <div className="text-gray-500">暂无API产品</div>
        </div>
      )}
    </Layout>
  );
}

export default APIsPage; 
```

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

```typescript
import { Card, Button, Table, Tag, Space, Modal, Form, Input, Select, Switch, message } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined, SettingOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { useState } from 'react'
import type { ApiProduct } from '@/types/api-product';
import { formatDateTime } from '@/lib/utils'

interface ApiProductPolicyProps {
  apiProduct: ApiProduct
}

interface Policy {
  id: string
  name: string
  type: string
  status: string
  description: string
  createdAt: string
  config: any
}

const mockPolicies: Policy[] = [
  {
    id: "1",
    name: "Rate Limiting",
    type: "rate-limiting",
    status: "enabled",
    description: "限制API调用频率",
    createdAt: "2025-01-01T10:00:00Z",
    config: {
      minute: 100,
      hour: 1000
    }
  },
  {
    id: "2",
    name: "Authentication",
    type: "key-auth",
    status: "enabled",
    description: "API密钥认证",
    createdAt: "2025-01-02T11:00:00Z",
    config: {
      key_names: ["apikey"],
      hide_credentials: true
    }
  },
  {
    id: "3",
    name: "CORS",
    type: "cors",
    status: "enabled",
    description: "跨域资源共享",
    createdAt: "2025-01-03T12:00:00Z",
    config: {
      origins: ["*"],
      methods: ["GET", "POST", "PUT", "DELETE"]
    }
  }
]

export function ApiProductPolicy({ apiProduct }: ApiProductPolicyProps) {
  const [policies, setPolicies] = useState<Policy[]>(mockPolicies)
  const [isModalVisible, setIsModalVisible] = useState(false)
  const [editingPolicy, setEditingPolicy] = useState<Policy | null>(null)
  const [form] = Form.useForm()

  const columns = [
    {
      title: '策略名称',
      dataIndex: 'name',
      key: 'name',
    },
    {
      title: '类型',
      dataIndex: 'type',
      key: 'type',
      render: (type: string) => {
        const typeMap: { [key: string]: string } = {
          'rate-limiting': '限流',
          'key-auth': '认证',
          'cors': 'CORS',
          'acl': '访问控制'
        }
        return <Tag color="blue">{typeMap[type] || type}</Tag>
      }
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status: string) => (
        <Tag color={status === 'enabled' ? 'green' : 'red'}>
          {status === 'enabled' ? '启用' : '禁用'}
        </Tag>
      )
    },
    {
      title: '描述',
      dataIndex: 'description',
      key: 'description',
      ellipsis: true,
    },
    {
      title: '创建时间',
      dataIndex: 'createdAt',
      key: 'createdAt',
      render: (date: string) => formatDateTime(date)
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: Policy) => (
        <Space size="middle">
          <Button type="link" icon={<SettingOutlined />}>
            配置
          </Button>
          <Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
            编辑
          </Button>
          <Button 
            type="link" 
            danger 
            icon={<DeleteOutlined />}
            onClick={() => handleDelete(record.id, record.name)}
          >
            删除
          </Button>
        </Space>
      ),
    },
  ]

  const handleAdd = () => {
    setEditingPolicy(null)
    setIsModalVisible(true)
  }

  const handleEdit = (policy: Policy) => {
    setEditingPolicy(policy)
    form.setFieldsValue({
      name: policy.name,
      type: policy.type,
      description: policy.description,
      status: policy.status
    })
    setIsModalVisible(true)
  }

  const handleDelete = (id: string, policyName: string) => {
    Modal.confirm({
      title: '确认删除',
      icon: <ExclamationCircleOutlined />,
      content: `确定要删除策略 "${policyName}" 吗?此操作不可恢复。`,
      okText: '确认删除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        setPolicies(policies.filter(policy => policy.id !== id))
        message.success('策略删除成功')
      },
    })
  }

  const handleModalOk = () => {
    form.validateFields().then((values) => {
      if (editingPolicy) {
        // 编辑现有策略
        setPolicies(policies.map(policy => 
          policy.id === editingPolicy.id 
            ? { ...policy, ...values }
            : policy
        ))
      } else {
        // 添加新策略
        const newPolicy: Policy = {
          id: Date.now().toString(),
          name: values.name,
          type: values.type,
          status: values.status,
          description: values.description,
          createdAt: new Date().toISOString(),
          config: {}
        }
        setPolicies([...policies, newPolicy])
      }
      setIsModalVisible(false)
      form.resetFields()
      setEditingPolicy(null)
    })
  }

  const handleModalCancel = () => {
    setIsModalVisible(false)
    form.resetFields()
    setEditingPolicy(null)
  }

  return (
    <div className="p-6 space-y-6">
      <div className="flex justify-between items-center">
        <div>
          <h1 className="text-2xl font-bold mb-2">策略管理</h1>
          <p className="text-gray-600">管理API产品的策略配置</p>
        </div>
        <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
          添加策略
        </Button>
      </div>

      <Card>
        <Table 
          columns={columns} 
          dataSource={policies}
          rowKey="id"
          pagination={false}
        />
      </Card>

      <Card title="策略设置">
        <div className="space-y-4">
          <div className="flex justify-between items-center">
            <span>策略继承</span>
            <Switch defaultChecked />
          </div>
          <div className="flex justify-between items-center">
            <span>策略优先级</span>
            <Switch defaultChecked />
          </div>
          <div className="flex justify-between items-center">
            <span>策略日志</span>
            <Switch defaultChecked />
          </div>
        </div>
      </Card>

      <Modal
        title={editingPolicy ? "编辑策略" : "添加策略"}
        open={isModalVisible}
        onOk={handleModalOk}
        onCancel={handleModalCancel}
        okText={editingPolicy ? "更新" : "添加"}
        cancelText="取消"
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="name"
            label="策略名称"
            rules={[{ required: true, message: '请输入策略名称' }]}
          >
            <Input placeholder="请输入策略名称" />
          </Form.Item>
          <Form.Item
            name="type"
            label="策略类型"
            rules={[{ required: true, message: '请选择策略类型' }]}
          >
            <Select placeholder="请选择策略类型">
              <Select.Option value="rate-limiting">限流</Select.Option>
              <Select.Option value="key-auth">认证</Select.Option>
              <Select.Option value="cors">CORS</Select.Option>
              <Select.Option value="acl">访问控制</Select.Option>
            </Select>
          </Form.Item>
          <Form.Item
            name="description"
            label="描述"
            rules={[{ required: true, message: '请输入策略描述' }]}
          >
            <Input.TextArea placeholder="请输入策略描述" rows={3} />
          </Form.Item>
          <Form.Item
            name="status"
            label="状态"
            rules={[{ required: true, message: '请选择状态' }]}
          >
            <Select placeholder="请选择状态">
              <Select.Option value="enabled">启用</Select.Option>
              <Select.Option value="disabled">禁用</Select.Option>
            </Select>
          </Form.Item>
        </Form>
      </Modal>
    </div>
  )
} 
```

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

```typescript
import { useState, useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Button, Dropdown, MenuProps, Modal, message } from 'antd'
import {
  MoreOutlined,
  LeftOutlined,
  EyeOutlined,
  LinkOutlined,
  BookOutlined,
  GlobalOutlined,
  DashboardOutlined
} from '@ant-design/icons'
import { ApiProductOverview } from '@/components/api-product/ApiProductOverview'
import { ApiProductLinkApi } from '@/components/api-product/ApiProductLinkApi'
import { ApiProductUsageGuide } from '@/components/api-product/ApiProductUsageGuide'
import { ApiProductPortal } from '@/components/api-product/ApiProductPortal'
import { ApiProductDashboard } from '@/components/api-product/ApiProductDashboard'
import { apiProductApi } from '@/lib/api';
import type { ApiProduct, LinkedService } from '@/types/api-product';

import ApiProductFormModal from '@/components/api-product/ApiProductFormModal';

const menuItems = [
  {
    key: "overview",
    label: "Overview",
    description: "产品概览",
    icon: EyeOutlined
  },
  {
    key: "link-api",
    label: "Link API",
    description: "API关联",
    icon: LinkOutlined
  },
  {
    key: "usage-guide",
    label: "Usage Guide",
    description: "使用指南",
    icon: BookOutlined
  },
  {
    key: "portal",
    label: "Portal",
    description: "发布的门户",
    icon: GlobalOutlined
  },
  {
    key: "dashboard",
    label: "Dashboard",
    description: "实时监控和统计",
    icon: DashboardOutlined
  }
]

export default function ApiProductDetail() {
  const navigate = useNavigate()
  const [searchParams, setSearchParams] = useSearchParams()
  const [apiProduct, setApiProduct] = useState<ApiProduct | null>(null)
  const [linkedService, setLinkedService] = useState<LinkedService | null>(null)
  const [loading, setLoading] = useState(true) // 添加 loading 状态
  
  // 从URL query参数获取当前tab,默认为overview
  const currentTab = searchParams.get('tab') || 'overview'
  // 验证tab值是否有效,如果无效则使用默认值
  const validTab = menuItems.some(item => item.key === currentTab) ? currentTab : 'overview'
  const [activeTab, setActiveTab] = useState(validTab)

  const [editModalVisible, setEditModalVisible] = useState(false)

  
  const fetchApiProduct = async () => {
    const productId = searchParams.get('productId')
    if (productId) {
      setLoading(true)
      try {
        // 并行获取Product详情和关联信息
        const [productRes, refRes] = await Promise.all([
          apiProductApi.getApiProductDetail(productId),
          apiProductApi.getApiProductRef(productId).catch(() => ({ data: null })) // 关联信息获取失败不影响页面显示
        ])
        
        setApiProduct(productRes.data)
        setLinkedService(refRes.data || null)
      } catch (error) {
        console.error('获取Product详情失败:', error)
      } finally {
        setLoading(false)
      }
    }
  }

  // 更新关联信息的回调函数
  const handleLinkedServiceUpdate = (newLinkedService: LinkedService | null) => {
    setLinkedService(newLinkedService)
  }

  useEffect(() => {
    fetchApiProduct()
  }, [searchParams.get('productId')])

  // 同步URL参数和activeTab状态
  useEffect(() => {
    setActiveTab(validTab)
  }, [validTab, searchParams])

  const handleBackToApiProducts = () => {
    navigate('/api-products')
  }

  const handleTabChange = (tabKey: string) => {
    setActiveTab(tabKey)
    // 更新URL query参数
    const newSearchParams = new URLSearchParams(searchParams)
    newSearchParams.set('tab', tabKey)
    setSearchParams(newSearchParams)
  }

  const renderContent = () => {
    if (!apiProduct) {
      return <div className="p-6">Loading...</div>
    }
    
    switch (activeTab) {
      case "overview":
        return <ApiProductOverview apiProduct={apiProduct} linkedService={linkedService} onEdit={handleEdit} />
      case "link-api":
        return <ApiProductLinkApi 
          apiProduct={apiProduct} 
          linkedService={linkedService}
          onLinkedServiceUpdate={handleLinkedServiceUpdate}
          handleRefresh={fetchApiProduct}
        />
      case "usage-guide":
        return <ApiProductUsageGuide apiProduct={apiProduct} handleRefresh={fetchApiProduct} />
      case "portal":
        return <ApiProductPortal apiProduct={apiProduct} />
      case "dashboard":
        return <ApiProductDashboard apiProduct={apiProduct} />
      default:
        return <ApiProductOverview apiProduct={apiProduct} linkedService={linkedService} onEdit={handleEdit} />
    }
  }

  const dropdownItems: MenuProps['items'] = [
    {
      key: 'delete',
      label: '删除',
      onClick: () => {
        Modal.confirm({
          title: '确认删除',
          content: '确定要删除该产品吗?',
          onOk: () => {
            handleDeleteApiProduct()
          },
        })
      },
      danger: true,
    },
  ]

  const handleDeleteApiProduct = () => {
    if (!apiProduct) return;
    
    apiProductApi.deleteApiProduct(apiProduct.productId).then(() => {
      message.success('删除成功')
      navigate('/api-products')
    }).catch((error) => {
      // message.error(error.response?.data?.message || '删除失败')
    })
  }

  const handleEdit = () => {
    setEditModalVisible(true)
  }

  const handleEditSuccess = () => {
    setEditModalVisible(false)
    fetchApiProduct()
  }

  const handleEditCancel = () => {
    setEditModalVisible(false)
  }

  return (
    <div className="flex h-full w-full overflow-hidden">
      {/* API Product 详情侧边栏 */}
      <div className="w-64 border-r bg-white flex flex-col flex-shrink-0">
        {/* 返回按钮 */}
        <div className="pb-4 border-b">
          <Button
            type="text"
            // className="w-full justify-start"
            onClick={handleBackToApiProducts}
            icon={<LeftOutlined />}
          >
            返回
          </Button>
        </div>

        {/* API Product 信息 */}
        <div className="p-4 border-b">
          <div className="flex items-center justify-between mb-2">
            <h2 className="text-lg font-semibold">{apiProduct?.name || 'Loading...'}</h2>
            <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
              <Button type="text" icon={<MoreOutlined />} />
            </Dropdown>
          </div>
        </div>

        {/* 导航菜单 */}
        <nav className="flex-1 p-4 space-y-1">
          {menuItems.map((item) => {
            const Icon = item.icon;
            return (
              <button
                key={item.key}
                onClick={() => handleTabChange(item.key)}
                className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors ${
                  activeTab === item.key
                    ? "bg-blue-500 text-white"
                    : "hover:bg-gray-100"
                }`}
              >
                <Icon className="h-4 w-4 flex-shrink-0" />
                <div>
                  <div className="font-medium">{item.label}</div>
                  <div className="text-xs opacity-70">{item.description}</div>
                </div>
              </button>
            );
          })}
        </nav>
      </div>

      {/* 主内容区域 */}
      <div className="flex-1 overflow-auto min-w-0">
        <div className="w-full max-w-full">
          {renderContent()}
        </div>
      </div>

      {apiProduct && (
        <ApiProductFormModal
          visible={editModalVisible}
          onCancel={handleEditCancel}
          onSuccess={handleEditSuccess}
          productId={apiProduct.productId}
          initialData={apiProduct}
        />
      )}
    </div>
  )
}

```

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

```typescript
import { useState, useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Button, Dropdown, MenuProps, Typography, Spin, Modal, message } from 'antd'
import {
  MoreOutlined,
  LeftOutlined,
  EyeOutlined,
  ApiOutlined,
  TeamOutlined,
  SafetyOutlined,
  CloudOutlined,
  DashboardOutlined
} from '@ant-design/icons'
import { PortalOverview } from '@/components/portal/PortalOverview'
import { PortalPublishedApis } from '@/components/portal/PortalPublishedApis'
import { PortalDevelopers } from '@/components/portal/PortalDevelopers'
import { PortalConsumers } from '@/components/portal/PortalConsumers'
import { PortalDashboard } from '@/components/portal/PortalDashboard'
import { PortalSecurity } from '@/components/portal/PortalSecurity'
import { PortalDomain } from '@/components/portal/PortalDomain'
import PortalFormModal from '@/components/portal/PortalFormModal'
import { portalApi } from '@/lib/api'
import { Portal } from '@/types'

const { Title } = Typography

// 移除mockPortal,使用真实API数据

const menuItems = [
  {
    key: "overview",
    label: "Overview",
    icon: EyeOutlined,
    description: "Portal概览"
  },
  {
    key: "published-apis",
    label: "Products",
    icon: ApiOutlined,
    description: "已发布的API产品"
  },
  {
    key: "developers",
    label: "Developers",
    icon: TeamOutlined,
    description: "开发者管理"
  },
  {
    key: "security",
    label: "Security",
    icon: SafetyOutlined,
    description: "安全设置"
  },
  {
    key: "domain",
    label: "Domain",
    icon: CloudOutlined,
    description: "域名管理"
  },
  // {
  //   key: "consumers",
  //   label: "Consumers",
  //   icon: UserOutlined,
  //   description: "消费者管理"
  // },
  {
    key: "dashboard",
    label: "Dashboard",
    icon: DashboardOutlined,
    description: "监控面板"
  }
]

export default function PortalDetail() {
  const navigate = useNavigate()
  const [searchParams, setSearchParams] = useSearchParams()
  const [portal, setPortal] = useState<Portal | null>(null)
  const [loading, setLoading] = useState(true) // 初始状态为 loading
  const [error, setError] = useState<string | null>(null)
  const [editModalVisible, setEditModalVisible] = useState(false)

  // 从URL查询参数获取当前tab,默认为overview
  const currentTab = searchParams.get('tab') || 'overview'
  const [activeTab, setActiveTab] = useState(currentTab)

  const fetchPortalData = async () => {
    try {
      setLoading(true)
      const portalId = searchParams.get('id') || 'portal-6882e06f4fd0c963020e3485'
      const response = await portalApi.getPortalDetail(portalId) as any
      if (response && response.code === 'SUCCESS') {
        setPortal(response.data)
      } else {
        setError(response?.message || '获取Portal信息失败')
      }
    } catch (err) {
      console.error('获取Portal信息失败:', err)
      setError('获取Portal信息失败')
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchPortalData()
  }, [])

  // 当URL中的tab参数变化时,更新activeTab
  useEffect(() => {
    setActiveTab(currentTab)
  }, [currentTab])

  const handleBackToPortals = () => {
    navigate('/portals')
  }

  // 处理tab切换,同时更新URL查询参数
  const handleTabChange = (tabKey: string) => {
    setActiveTab(tabKey)
    const newSearchParams = new URLSearchParams(searchParams)
    newSearchParams.set('tab', tabKey)
    setSearchParams(newSearchParams)
  }

  const handleEdit = () => {
    setEditModalVisible(true)
  }

  const handleEditSuccess = () => {
    setEditModalVisible(false)
    fetchPortalData()
  }

  const handleEditCancel = () => {
    setEditModalVisible(false)
  }

  const renderContent = () => {
    if (!portal) return null
    
    switch (activeTab) {
      case "overview":
        return <PortalOverview portal={portal} onEdit={handleEdit} />
      case "published-apis":
        return <PortalPublishedApis portal={portal} />
      case "developers":
        return <PortalDevelopers portal={portal} />
      case "security":
        return <PortalSecurity portal={portal} onRefresh={fetchPortalData} />
      case "domain":
        return <PortalDomain portal={portal} onRefresh={fetchPortalData} />
      case "consumers":
        return <PortalConsumers portal={portal} />
      case "dashboard":
        return <PortalDashboard portal={portal} />
      default:
        return <PortalOverview portal={portal} onEdit={handleEdit} />
    }
  }

  const dropdownItems: MenuProps['items'] = [
    {
      key: "delete",
      label: "删除",
      danger: true,
      onClick: () => {
        Modal.confirm({
          title: "删除Portal",
          content: "确定要删除该Portal吗?",
          onOk: () => {
            return handleDeletePortal();
          },
        });
      },
    },
  ]
  const handleDeletePortal = () => {
    return portalApi.deletePortal(searchParams.get('id') || '').then(() => {
      message.success('删除成功')
      navigate('/portals')
    }).catch((error) => {
      message.error(error?.response?.data?.message || '删除失败,请稍后重试')
      throw error; // 重新抛出错误,让Modal保持loading状态
    })
  }

  if (error || !portal) {
    return (
      <div className="flex h-full items-center justify-center">
        <div className="text-center">
          {error && <><p className=" mb-4">{error || 'Portal信息不存在'}</p>
          <Button onClick={() => navigate('/portals')}>返回Portal列表</Button></>}
          {!error && <Spin fullscreen spinning={loading} />}
        </div>
      </div>
    )
  }

  return (
    <div className="flex h-full">
      <Spin fullscreen spinning={loading} />
      {/* Portal详情侧边栏 */}
      <div className="w-64 border-r bg-white flex flex-col">
        {/* 返回按钮 */}
        <div className="pb-4 border-b">
          <Button 
            type="text"
            // className="w-full justify-start text-gray-600 hover:text-gray-900"
            onClick={handleBackToPortals}
            icon={<LeftOutlined />}
          >
            返回
          </Button>
        </div>

        {/* Portal 信息 */}
        <div className="p-4 border-b">
          <div className="flex items-center justify-between mb-2">
            <Title level={5} className="mb-0">{portal.name}</Title>
            <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
              <Button type="text" icon={<MoreOutlined />} size="small" />
            </Dropdown>
          </div>
        </div>

        {/* 导航菜单 */}
        <nav className="flex-1 p-4 space-y-2">
          {menuItems.map((item) => {
            const Icon = item.icon
            return (
              <button
                key={item.key}
                onClick={() => handleTabChange(item.key)}
                className={`w-full flex items-center gap-3 px-3 py-3 rounded-lg text-left transition-colors ${
                  activeTab === item.key
                    ? "bg-blue-50 text-blue-600 border border-blue-200"
                    : "hover:bg-gray-50 text-gray-700"
                }`}
              >
                <Icon className="h-4 w-4 flex-shrink-0" />
                <div className="flex-1">
                  <div className="font-medium">{item.label}</div>
                  <div className="text-xs text-gray-500 mt-1">{item.description}</div>
                </div>
              </button>
            )
          })}
        </nav>
      </div>

      {/* 主内容区域 */}
      <div className="flex-1 overflow-auto">
      {renderContent()}
      </div>

      {portal && (
        <PortalFormModal
          visible={editModalVisible}
          onCancel={handleEditCancel}
          onSuccess={handleEditSuccess}
          portal={portal}
        />
      )}
    </div>
  )
} 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/dto/converter/NacosToGatewayToolsConverter.java:
--------------------------------------------------------------------------------

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.dto.converter;

import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import lombok.Data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Data
public class NacosToGatewayToolsConverter {

    private Server server = new Server();
    private List<Tool> tools = new ArrayList<>();
    private List<String> allowTools = new ArrayList<>();

    public void convertFromNacos(McpServerDetailInfo nacosDetail) {
        server.setName(nacosDetail.getName());
        server.getConfig().put("apiKey", "your-api-key-here");
        allowTools.add(nacosDetail.getName());
        
        if (nacosDetail.getToolSpec() != null) {
            convertTools(nacosDetail.getToolSpec());
        }
    }

    public String toYaml() {
        try {
            ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
            return yamlMapper.writeValueAsString(this);
        } catch (Exception e) {
            throw new RuntimeException("Failed to convert to YAML", e);
        }
    }

    private void convertTools(Object toolSpec) {
        try {
            ObjectMapper jsonMapper = new ObjectMapper();
            String toolSpecJson = jsonMapper.writeValueAsString(toolSpec);
            JsonNode toolSpecNode = jsonMapper.readTree(toolSpecJson);
            
            if (toolSpecNode.isArray()) {
                for (JsonNode toolNode : toolSpecNode) {
                    Tool tool = convertToolNode(toolNode);
                    if (tool != null) {
                        tools.add(tool);
                    }
                }
            } else if (toolSpecNode.has("tools") && toolSpecNode.get("tools").isArray()) {
                JsonNode toolsNode = toolSpecNode.get("tools");
                for (JsonNode toolNode : toolsNode) {
                    Tool tool = convertToolNode(toolNode);
                    if (tool != null) {
                        tools.add(tool);
                    }
                }
            }
        } catch (Exception e) {
            // 转换失败时,tools保持空列表
        }
    }
    
    private Tool convertToolNode(JsonNode toolNode) {
        Tool result = new Tool();
        result.setName(getStringValue(toolNode, "name"));
        result.setDescription(getStringValue(toolNode, "description"));
        
        if (result.getName() == null) {
            return null;
        }
        
        List<Arg> args = convertArgs(toolNode);
        result.setArgs(args);
        result.setRequestTemplate(buildDefaultRequestTemplate(result.getName()));
        result.setResponseTemplate(buildDefaultResponseTemplate());
        
        return result;
    }
    
    private List<Arg> convertArgs(JsonNode toolNode) {
        List<Arg> args = new ArrayList<>();
        
        try {
            if (toolNode.has("inputSchema") && toolNode.get("inputSchema").has("properties")) {
                JsonNode properties = toolNode.get("inputSchema").get("properties");
                properties.fields().forEachRemaining(entry -> {
                    String argName = entry.getKey();
                    JsonNode argNode = entry.getValue();
                    
                    Arg arg = new Arg();
                    arg.setName(argName);
                    arg.setDescription(getStringValue(argNode, "description"));
                    arg.setType(getStringValue(argNode, "type"));
                    arg.setRequired(getBooleanValue(argNode, "required", false));
                    arg.setPosition("query");
                    
                    args.add(arg);
                });
            } else if (toolNode.has("args") && toolNode.get("args").isArray()) {
                JsonNode argsNode = toolNode.get("args");
                for (JsonNode argNode : argsNode) {
                    Arg arg = new Arg();
                    arg.setName(getStringValue(argNode, "name"));
                    arg.setDescription(getStringValue(argNode, "description"));
                    arg.setType(getStringValue(argNode, "type"));
                    arg.setRequired(getBooleanValue(argNode, "required", false));
                    arg.setPosition(getStringValue(argNode, "position"));
                    arg.setDefaultValue(getStringValue(argNode, "default"));
                    
                    args.add(arg);
                }
            }
        } catch (Exception e) {
            // 转换失败时,args保持空列表
        }
        
        return args;
    }
    
    private RequestTemplate buildDefaultRequestTemplate(String toolName) {
        RequestTemplate template = new RequestTemplate();
        template.setUrl("https://api.example.com/v1/" + toolName);
        template.setMethod("GET");
        
        Header header = new Header();
        header.setKey("Content-Type");
        header.setValue("application/json");
        template.getHeaders().add(header);
        
        return template;
    }
    
    private ResponseTemplate buildDefaultResponseTemplate() {
        ResponseTemplate template = new ResponseTemplate();
        template.setBody("");
        return template;
    }
    
    private String getStringValue(JsonNode node, String fieldName) {
        return node.has(fieldName) && !node.get(fieldName).isNull() ? 
            node.get(fieldName).asText() : null;
    }
    
    private boolean getBooleanValue(JsonNode node, String fieldName, boolean defaultValue) {
        return node.has(fieldName) && !node.get(fieldName).isNull() ? 
            node.get(fieldName).asBoolean() : defaultValue;
    }

    @Data
    public static class Server {
        private String name;
        private Map<String, Object> config = new HashMap<>();
    }
    
    @Data
    public static class Tool {
        private String name;
        private String description;
        private List<Arg> args = new ArrayList<>();
        private RequestTemplate requestTemplate;
        private ResponseTemplate responseTemplate;
    }
    
    @Data
    public static class Arg {
        private String name;
        private String description;
        private String type;
        private boolean required;
        private String position;
        private String defaultValue;
        private List<String> enumValues;
    }
    
    @Data
    public static class RequestTemplate {
        private String url;
        private String method;
        private List<Header> headers = new ArrayList<>();
    }
    
    @Data
    public static class ResponseTemplate {
        private String body;
    }
    
    @Data
    public static class Header {
        private String key;
        private String value;
    }
}

```

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

```css
/* Swagger UI 自定义样式 */
.swagger-ui-wrapper {
  /* 隐藏顶部的信息栏,因为我们已经在上层显示了 */
  .swagger-ui .info {
    display: none;
  }

  /* 完全隐藏服务器选择器的容器样式 */
  .swagger-ui .scheme-container {
    padding: 0;
    background: transparent;
    border: none;
    margin-bottom: 16px;
    position: relative;
    box-shadow: none;
  }

  /* 隐藏服务器区域的所有边框和背景 */
  .swagger-ui .scheme-container > div {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
  }

  /* 服务器URL样式优化 */
  .swagger-ui .servers-title {
    font-weight: 600;
    margin-bottom: 8px;
    color: #262626;
  }

  .swagger-ui .servers select {
    font-family: Monaco, Consolas, monospace;
    background: white;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 8px 40px 8px 12px;
    font-size: 14px;
    color: #1890ff;
    cursor: pointer;
    min-width: 300px;
    position: relative;
  }

  .swagger-ui .servers select:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 服务器选择器容器 */
  .swagger-ui .servers {
    position: relative;
  }

  /* 调整操作项的样式 */
  .swagger-ui .opblock {
    border-radius: 6px;
    border: 1px solid #e5e7eb;
    margin-bottom: 8px;
    box-shadow: none;
    width: 100%;
    margin-left: 0;
    margin-right: 0;
  }

  .swagger-ui .opblock.opblock-get {
    border-color: #61affe;
    background: rgba(97, 175, 254, 0.03);
  }

  .swagger-ui .opblock.opblock-post {
    border-color: #49cc90;
    background: rgba(73, 204, 144, 0.03);
  }

  .swagger-ui .opblock.opblock-put {
    border-color: #fca130;
    background: rgba(252, 161, 48, 0.03);
  }

  .swagger-ui .opblock.opblock-delete {
    border-color: #f93e3e;
    background: rgba(249, 62, 62, 0.03);
  }

  .swagger-ui .opblock.opblock-patch {
    border-color: #50e3c2;
    background: rgba(80, 227, 194, 0.03);
  }

  /* 调整展开的操作项样式 */
  .swagger-ui .opblock.is-open {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

  /* 调整参数表格样式 */
  .swagger-ui .parameters-container {
    background: transparent;
  }

  .swagger-ui .parameter__name {
    font-family: Monaco, Consolas, monospace;
    font-weight: 600;
  }

  /* 调整响应区域样式 */
  .swagger-ui .responses-wrapper {
    background: transparent;
  }

  /* 调整Try it out按钮 */
  .swagger-ui .btn.try-out__btn {
    background: #1890ff;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 6px 16px;
    font-size: 14px;
  }

  .swagger-ui .btn.try-out__btn:hover {
    background: #40a9ff;
  }

  /* 调整Execute按钮 */
  .swagger-ui .btn.execute {
    background: #52c41a;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 8px 20px;
    font-size: 14px;
    font-weight: 500;
  }

  .swagger-ui .btn.execute:hover {
    background: #73d13d;
  }

  /* 调整Clear按钮 */
  .swagger-ui .btn.btn-clear {
    background: #ff4d4f;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 6px 16px;
    font-size: 14px;
  }

  .swagger-ui .btn.btn-clear:hover {
    background: #ff7875;
  }

  /* 调整模型区域 */
  .swagger-ui .model-container {
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 4px;
  }

  /* 调整代码高亮 */
  .swagger-ui .highlight-code {
    background: #2d3748;
    border-radius: 4px;
  }

  /* 调整输入框样式 */
  .swagger-ui input[type=text],
  .swagger-ui input[type=password],
  .swagger-ui input[type=search],
  .swagger-ui input[type=email],
  .swagger-ui input[type=url],
  .swagger-ui input[type=number] {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui input[type=text]:focus,
  .swagger-ui input[type=password]:focus,
  .swagger-ui input[type=search]:focus,
  .swagger-ui input[type=email]:focus,
  .swagger-ui input[type=url]:focus,
  .swagger-ui input[type=number]:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 调整文本域样式 */
  .swagger-ui textarea {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui textarea:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 调整下拉选择样式 */
  .swagger-ui select {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui select:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 隐藏授权部分(如果不需要) */
  .swagger-ui .auth-wrapper {
    display: none;
  }

  /* 调整整体字体 */
  .swagger-ui {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  }


  /* 调整标题样式 */
  .swagger-ui .opblock-summary-description {
    font-size: 14px;
    color: #666;
  }

  /* 调整HTTP方法标签 */
  .swagger-ui .opblock-summary-method {
    font-weight: bold;
    text-transform: uppercase;
    border-radius: 3px;
    padding: 6px 12px;
    font-size: 12px;
    min-width: 60px;
    text-align: center;
  }

  /* 调整路径显示 */
  .swagger-ui .opblock-summary-path {
    font-family: Monaco, Consolas, monospace;
    font-size: 16px;
    font-weight: 500;
  }

  /* 移除不必要的边距 */
  .swagger-ui .swagger-container {
    max-width: none !important;
    width: 100% !important;
    padding: 0;
    margin: 0;
  }

  /* 调整顶层wrapper */
  .swagger-ui .wrapper {
    padding: 0;
    margin: 0;
    width: 100% !important;
    max-width: none !important;
  }

  /* 移除左侧空白 */
  .swagger-ui .information-container {
    margin: 0;
    padding: 0;
  }

  /* 移除整体左边距 */
  .swagger-ui {
    margin-left: 0 !important;
    padding-left: 0 !important;
  }

  /* 移除操作块的左边距 */
  .swagger-ui .opblock-tag-section {
    margin-left: 0 !important;
    padding-left: 0 !important;
    margin-right: 0 !important;
    padding-right: 0 !important;
    width: 100% !important;
  }

  /* 确保接口标签区域占满宽度 */
  .swagger-ui .opblock-tag {
    width: 100%;
    margin: 0;
  }

  /* 强制所有Swagger UI容器占满宽度 */
  .swagger-ui-wrapper {
    width: 100% !important;
  }

  .swagger-ui {
    width: 100% !important;
    max-width: none !important;
  }

  .swagger-ui .info {
    width: 100% !important;
  }

  .swagger-ui .scheme-container {
    width: 100% !important;
    max-width: none !important;
  }

  /* 强制内容区域占满宽度 */
  .swagger-ui .swagger-container .wrapper {
    width: 100% !important;
    max-width: none !important;
  }

  /* 强制操作列表容器占满宽度 */
  .swagger-ui .swagger-container .wrapper .col-12 {
    width: 100% !important;
    max-width: none !important;
    flex: 0 0 100% !important;
  }

  /* Servers标题样式 */
  .swagger-ui .servers-title {
    font-size: 14px !important;
    font-weight: 500 !important;
    margin-bottom: 8px !important;
    color: #262626 !important;
    text-align: left !important;
  }

  /* 接口列表标题样式 */
  .swagger-ui .opblock-tag {
    font-size: 16px !important;
    font-weight: 500 !important;
    text-align: left !important;
    margin-left: 0 !important;
  }

  /* 去掉复制按钮的边框 */
  .copy-server-btn {
    border: none !important;
    background: transparent !important;
    padding: 6px 8px !important;
    color: #666 !important;
    transition: all 0.2s !important;
  }

  .copy-server-btn:hover {
    background: #f5f5f5 !important;
    color: #1890ff !important;
  }

  /* 调整接口列表与上方分割线的距离 */
  .swagger-ui .opblock-tag-section {
    margin-top: 20px !important;
  }

  /* 调整分割线样式,确保与接口边框分开 */
  .swagger-ui .opblock-tag h3 {
    margin-bottom: 20px !important;
    padding-bottom: 12px !important;
    border-bottom: 1px solid #e8e8e8 !important;
  }

  /* 确保第一个接口容器与分割线有足够间距 */
  .swagger-ui .opblock-tag-section .opblock:first-child {
    margin-top: 16px !important;
  }
}

```

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

```css
/* Swagger UI 自定义样式 */
.swagger-ui-wrapper {
  /* 隐藏顶部的信息栏,因为我们已经在上层显示了 */
  .swagger-ui .info {
    display: none;
  }

  /* 完全隐藏服务器选择器的容器样式 */
  .swagger-ui .scheme-container {
    padding: 0;
    background: transparent;
    border: none;
    margin-bottom: 16px;
    position: relative;
    box-shadow: none;
  }

  /* 隐藏服务器区域的所有边框和背景 */
  .swagger-ui .scheme-container > div {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
  }

  /* 服务器URL样式优化 */
  .swagger-ui .servers-title {
    font-weight: 600;
    margin-bottom: 8px;
    color: #262626;
  }

  .swagger-ui .servers select {
    font-family: Monaco, Consolas, monospace;
    background: white;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 8px 40px 8px 12px;
    font-size: 14px;
    color: #1890ff;
    cursor: pointer;
    min-width: 300px;
    position: relative;
  }

  .swagger-ui .servers select:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 服务器选择器容器 */
  .swagger-ui .servers {
    position: relative;
  }

  /* 调整操作项的样式 */
  .swagger-ui .opblock {
    border-radius: 6px;
    border: 1px solid #e5e7eb;
    margin-bottom: 8px;
    box-shadow: none;
    width: 100%;
    margin-left: 0;
    margin-right: 0;
  }

  .swagger-ui .opblock.opblock-get {
    border-color: #61affe;
    background: rgba(97, 175, 254, 0.03);
  }

  .swagger-ui .opblock.opblock-post {
    border-color: #49cc90;
    background: rgba(73, 204, 144, 0.03);
  }

  .swagger-ui .opblock.opblock-put {
    border-color: #fca130;
    background: rgba(252, 161, 48, 0.03);
  }

  .swagger-ui .opblock.opblock-delete {
    border-color: #f93e3e;
    background: rgba(249, 62, 62, 0.03);
  }

  .swagger-ui .opblock.opblock-patch {
    border-color: #50e3c2;
    background: rgba(80, 227, 194, 0.03);
  }

  /* 调整展开的操作项样式 */
  .swagger-ui .opblock.is-open {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

  /* 调整参数表格样式 */
  .swagger-ui .parameters-container {
    background: transparent;
  }

  .swagger-ui .parameter__name {
    font-family: Monaco, Consolas, monospace;
    font-weight: 600;
  }

  /* 调整响应区域样式 */
  .swagger-ui .responses-wrapper {
    background: transparent;
  }

  /* 调整Try it out按钮 */
  .swagger-ui .btn.try-out__btn {
    background: #1890ff;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 6px 16px;
    font-size: 14px;
  }

  .swagger-ui .btn.try-out__btn:hover {
    background: #40a9ff;
  }

  /* 调整Execute按钮 */
  .swagger-ui .btn.execute {
    background: #52c41a;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 8px 20px;
    font-size: 14px;
    font-weight: 500;
  }

  .swagger-ui .btn.execute:hover {
    background: #73d13d;
  }

  /* 调整Clear按钮 */
  .swagger-ui .btn.btn-clear {
    background: #ff4d4f;
    color: white;
    border: none;
    border-radius: 4px;
    padding: 6px 16px;
    font-size: 14px;
  }

  .swagger-ui .btn.btn-clear:hover {
    background: #ff7875;
  }

  /* 调整模型区域 */
  .swagger-ui .model-container {
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 4px;
  }

  /* 调整代码高亮 */
  .swagger-ui .highlight-code {
    background: #2d3748;
    border-radius: 4px;
  }

  /* 调整输入框样式 */
  .swagger-ui input[type=text],
  .swagger-ui input[type=password],
  .swagger-ui input[type=search],
  .swagger-ui input[type=email],
  .swagger-ui input[type=url],
  .swagger-ui input[type=number] {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui input[type=text]:focus,
  .swagger-ui input[type=password]:focus,
  .swagger-ui input[type=search]:focus,
  .swagger-ui input[type=email]:focus,
  .swagger-ui input[type=url]:focus,
  .swagger-ui input[type=number]:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 调整文本域样式 */
  .swagger-ui textarea {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui textarea:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 调整下拉选择样式 */
  .swagger-ui select {
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 6px 11px;
    font-size: 14px;
    line-height: 1.5;
  }

  .swagger-ui select:focus {
    border-color: #40a9ff;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
    outline: none;
  }

  /* 隐藏授权部分(如果不需要) */
  .swagger-ui .auth-wrapper {
    display: none;
  }

  /* 调整整体字体 */
  .swagger-ui {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  }


  /* 调整标题样式 */
  .swagger-ui .opblock-summary-description {
    font-size: 14px;
    color: #666;
  }

  /* 调整HTTP方法标签 */
  .swagger-ui .opblock-summary-method {
    font-weight: bold;
    text-transform: uppercase;
    border-radius: 3px;
    padding: 6px 12px;
    font-size: 12px;
    min-width: 60px;
    text-align: center;
  }

  /* 调整路径显示 */
  .swagger-ui .opblock-summary-path {
    font-family: Monaco, Consolas, monospace;
    font-size: 16px;
    font-weight: 500;
  }

  /* 移除不必要的边距 */
  .swagger-ui .swagger-container {
    max-width: none !important;
    width: 100% !important;
    padding: 0;
    margin: 0;
  }

  /* 调整顶层wrapper */
  .swagger-ui .wrapper {
    padding: 0;
    margin: 0;
    width: 100% !important;
    max-width: none !important;
  }

  /* 移除左侧空白 */
  .swagger-ui .information-container {
    margin: 0;
    padding: 0;
  }

  /* 移除整体左边距 */
  .swagger-ui {
    margin-left: 0 !important;
    padding-left: 0 !important;
  }

  /* 移除操作块的左边距 */
  .swagger-ui .opblock-tag-section {
    margin-left: 0 !important;
    padding-left: 0 !important;
    margin-right: 0 !important;
    padding-right: 0 !important;
    width: 100% !important;
  }

  /* 确保接口标签区域占满宽度 */
  .swagger-ui .opblock-tag {
    width: 100%;
    margin: 0;
  }

  /* 强制所有Swagger UI容器占满宽度 */
  .swagger-ui-wrapper {
    width: 100% !important;
  }

  .swagger-ui {
    width: 100% !important;
    max-width: none !important;
  }

  .swagger-ui .info {
    width: 100% !important;
  }

  .swagger-ui .scheme-container {
    width: 100% !important;
    max-width: none !important;
  }

  /* 强制内容区域占满宽度 */
  .swagger-ui .swagger-container .wrapper {
    width: 100% !important;
    max-width: none !important;
  }

  /* 强制操作列表容器占满宽度 */
  .swagger-ui .swagger-container .wrapper .col-12 {
    width: 100% !important;
    max-width: none !important;
    flex: 0 0 100% !important;
  }

  /* Servers标题样式 */
  .swagger-ui .servers-title {
    font-size: 14px !important;
    font-weight: 500 !important;
    margin-bottom: 8px !important;
    color: #262626 !important;
    text-align: left !important;
  }

  /* 接口列表标题样式 */
  .swagger-ui .opblock-tag {
    font-size: 16px !important;
    font-weight: 500 !important;
    text-align: left !important;
    margin-left: 0 !important;
  }

  /* 去掉复制按钮的边框 */
  .copy-server-btn {
    border: none !important;
    background: transparent !important;
    padding: 6px 8px !important;
    color: #666 !important;
    transition: all 0.2s !important;
  }

  .copy-server-btn:hover {
    background: #f5f5f5 !important;
    color: #1890ff !important;
  }

  /* 调整接口列表与上方分割线的距离 */
  .swagger-ui .opblock-tag-section {
    margin-top: 20px !important;
  }

  /* 调整分割线样式,确保与接口边框分开 */
  .swagger-ui .opblock-tag h3 {
    margin-bottom: 20px !important;
    padding-bottom: 12px !important;
    border-bottom: 1px solid #e8e8e8 !important;
  }

  /* 确保第一个接口容器与分割线有足够间距 */
  .swagger-ui .opblock-tag-section .opblock:first-child {
    margin-top: 16px !important;
  }
}

```

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

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.service.gateway.client;

import cn.hutool.core.map.MapBuilder;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.service.gateway.HigressOperator;
import com.alibaba.apiopenplatform.service.gateway.factory.HTTPClientFactory;
import com.alibaba.apiopenplatform.support.gateway.HigressConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Map;

@Slf4j
public class HigressClient extends GatewayClient {

    private static final String HIGRESS_COOKIE_NAME = "_hi_sess";

    private final RestTemplate restTemplate;
    private final HigressConfig config;
    private String higressToken;
    private final ThreadLocal<Boolean> isRetrying = new ThreadLocal<>();

    public HigressClient(HigressConfig higressConfig) {
        this.config = higressConfig;
        this.restTemplate = HTTPClientFactory.createRestTemplate();
    }

    public <T, R> T execute(String path,
                            HttpMethod method,
                            Map<String, String> queryParams,
                            R body,
                            ParameterizedTypeReference<T> responseType) {
        return execute(path, method, null, queryParams, body, responseType);
    }

    public <T, R> T execute(String path,
                            HttpMethod method,
                            Map<String, String> queryParams,
                            R body,
                            Class<T> responseType) {
        return execute(path, method, queryParams, body,
                ParameterizedTypeReference.forType(responseType));
    }

    public <T, R> T execute(String path,
                            HttpMethod method,
                            HttpHeaders headers,
                            Map<String, String> queryParams,
                            R body,
                            ParameterizedTypeReference<T> responseType) {
        try {
            return doExecute(path, method, headers, queryParams, body, responseType);
        } finally {
            isRetrying.remove();
        }
    }

    private <T, R> T doExecute(String path,
                               HttpMethod method,
                               HttpHeaders headers,
                               Map<String, String> queryParams,
                               R body,
                               ParameterizedTypeReference<T> responseType) {
        try {
            ensureConsoleToken();

            // 构建URL
            String url = buildUrlWithParams(path, queryParams);

            // Headers
            HttpHeaders mergedHeaders = new HttpHeaders();
            if (headers != null) {
                mergedHeaders.putAll(headers);
            }
            mergedHeaders.add("Cookie", HIGRESS_COOKIE_NAME + "=" + higressToken);

            ResponseEntity<T> response = restTemplate.exchange(
                    url,
                    method,
                    new HttpEntity<>(body, mergedHeaders),
                    responseType
            );

            log.info("Higress response: status={}, body={}",
                    response.getStatusCode(), JSONUtil.toJsonStr(response.getBody()));

            return response.getBody();
        } catch (HttpClientErrorException e) {
            // 401重新登录,且只重试一次
            if (e.getStatusCode() == HttpStatus.UNAUTHORIZED
                    && !Boolean.TRUE.equals(isRetrying.get())) {
                log.warn("Token expired, trying to relogin");
                higressToken = null;
                isRetrying.set(true);
                return doExecute(path, method, headers, queryParams, body, responseType);
            }
            log.error("HTTP error executing Higress request: status={}, body={}",
                    e.getStatusCode(), e.getResponseBodyAsString());
            throw e;
        } catch (Exception e) {
            log.error("Error executing Higress request: {}", e.getMessage());
            throw new RuntimeException("Failed to execute Higress request", e);
        }
    }

    private String buildUrlWithParams(String path, Map<String, String> queryParams) {
        StringBuilder url = new StringBuilder(buildUrl(path));

        if (queryParams != null && !queryParams.isEmpty()) {
            url.append('?');
            queryParams.forEach((key, value) -> {
                if (url.charAt(url.length() - 1) != '?') {
                    url.append('&');
                }
                url.append(key).append('=').append(value);
            });
        }

        return url.toString();
    }

    private String buildUrl(String path) {
        String baseUrl = config.getAddress();

        baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
        path = path.startsWith("/") ? path : "/" + path;
        return baseUrl + path;
    }

    private void ensureConsoleToken() {
        if (higressToken == null) {
            login();
        }
    }

    private void login() {
        Map<Object, Object> loginParam = MapBuilder.create()
                .put("username", config.getUsername())
                .put("password", config.getPassword())
                .build();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> response = restTemplate.exchange(
                buildUrl("/session/login"),
                HttpMethod.POST,
                new HttpEntity<>(loginParam, headers),
                String.class
        );

        List<String> cookies = response.getHeaders().get("Set-Cookie");
        if (cookies == null || cookies.isEmpty()) {
            throw new RuntimeException("No cookies received from server");
        }

        this.higressToken = cookies.stream()
                .filter(cookie -> cookie.startsWith(HIGRESS_COOKIE_NAME + "="))
                .findFirst()
                .map(cookie -> {
                    int endIndex = cookie.indexOf(';');
                    return endIndex == -1
                            ? cookie.substring(HIGRESS_COOKIE_NAME.length() + 1)
                            : cookie.substring(HIGRESS_COOKIE_NAME.length() + 1, endIndex);
                })
                .orElseThrow(() -> new RuntimeException("Failed to get Higress session token"));
    }

    @Override
    public void close() {
        HTTPClientFactory.closeClient(restTemplate);
    }

    public static void main(String[] args) {
        HigressConfig higressConfig = new HigressConfig();
        higressConfig.setAddress("http://demo.higress.io");
        higressConfig.setUsername("admin");
        higressConfig.setPassword("admin");

        HigressClient higressClient = new HigressClient(higressConfig);
//        Object  mcpServerInfo = higressClient.execute("/v1/mcpServer", HttpMethod.GET, null, null, new ParameterizedTypeReference<Object>() {
//        });

        HigressOperator.HigressPageResponse<HigressOperator.HigressMCPConfig> response = higressClient.execute("/v1/mcpServer", HttpMethod.GET, null, null, new ParameterizedTypeReference<HigressOperator.HigressPageResponse<HigressOperator.HigressMCPConfig>>() {
        });
        System.out.println(JSONUtil.toJsonStr(response));
    }

}
```

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

```typescript
import {Card, Row, Col, Statistic, Button, message} from 'antd'
import {
    UserOutlined,
    ApiOutlined,
    LinkOutlined,
    CheckCircleFilled,
    MinusCircleFilled,
    EditOutlined,
    CopyOutlined
} from '@ant-design/icons'
import {Portal} from '@/types'
import {useState, useEffect} from 'react'
import {portalApi, apiProductApi} from '@/lib/api'
import {copyToClipboard} from '@/lib/utils'
import {useNavigate} from 'react-router-dom'

interface PortalOverviewProps {
    portal: Portal
    onEdit?: () => void
}

export function PortalOverview({portal, onEdit}: PortalOverviewProps) {
    const navigate = useNavigate()
    const [apiCount, setApiCount] = useState(0)
    const [developerCount, setDeveloperCount] = useState(0)

    useEffect(() => {
        if (!portal.portalId) return;
        
        portalApi.getDeveloperList(portal.portalId, {
            page: 1,
            size: 10
        }).then((res: any) => {
            setDeveloperCount(res.data.totalElements || 0)
        })
        apiProductApi.getApiProducts({
            portalId: portal.portalId,
            page: 1,
            size: 10
        }).then((res: any) => {
            setApiCount(res.data.totalElements || 0)
        })

    }, [portal.portalId]) // 只依赖portalId,而不是整个portal对象

    return (
        <div className="p-6 space-y-6">
            <div>
                <h1 className="text-2xl font-bold mb-2">概览</h1>
                <p className="text-gray-600">Portal概览</p>
            </div>

            {/* 基本信息 */}
            <Card 
                title="基本信息"
                extra={
                    onEdit && (
                        <Button 
                            type="primary" 
                            icon={<EditOutlined />} 
                            onClick={onEdit}
                        >
                            编辑
                        </Button>
                    )
                }
            >
                <div>
                    <div className="grid grid-cols-6 gap-8 items-center pt-0 pb-2">
                        <span className="text-xs text-gray-600">Portal名称:</span>
                        <span className="col-span-2 text-xs text-gray-900">{portal.name}</span>
                        <span className="text-xs text-gray-600">Portal ID:</span>
                        <div className="col-span-2 flex items-center gap-2">
                            <span className="text-xs text-gray-700">{portal.portalId}</span>
                            <Button 
                                type="text" 
                                size="small"
                                icon={<CopyOutlined />}
                                onClick={async () => {
                                    try {
                                        await copyToClipboard(portal.portalId);
                                        message.success('Portal ID已复制');
                                    } catch {
                                        message.error('复制失败,请手动复制');
                                    }
                                }}
                                className="h-auto p-1 min-w-0"
                            />
                        </div>
                    </div>
                    
                    <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
                        <span className="text-xs text-gray-600">域名:</span>
                        <div className="col-span-2 flex items-center gap-2">
                            <LinkOutlined className="text-blue-500" />
                            <a 
                                href={`http://${portal.portalDomainConfig?.[0]?.domain}`} 
                                target="_blank"
                                rel="noopener noreferrer" 
                                className="text-xs text-blue-600 hover:underline"
                            >
                                {portal.portalDomainConfig?.[0]?.domain}
                            </a>
                        </div>
                        <span className="text-xs text-gray-600">账号密码登录:</span>
                        <div className="col-span-2 flex items-center">
                            {portal.portalSettingConfig?.builtinAuthEnabled ? (
                                <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
                            ) : (
                                <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
                            )}
                            <span className="text-xs text-gray-900">
                                {portal.portalSettingConfig?.builtinAuthEnabled ? '已启用' : '已停用'}
                            </span>
                        </div>
                    </div>
                    
                    <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
                        <span className="text-xs text-gray-600">开发者自动审批:</span>
                        <div className="col-span-2 flex items-center">
                            {portal.portalSettingConfig?.autoApproveDevelopers ? (
                                <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
                            ) : (
                                <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
                            )}
                            <span className="text-xs text-gray-900">
                                {portal.portalSettingConfig?.autoApproveDevelopers ? '已启用' : '已停用'}
                            </span>
                        </div>
                        <span className="text-xs text-gray-600">订阅自动审批:</span>
                        <div className="col-span-2 flex items-center">
                            {portal.portalSettingConfig?.autoApproveSubscriptions ? (
                                <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
                            ) : (
                                <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} />
                            )}
                            <span className="text-xs text-gray-900">
                                {portal.portalSettingConfig?.autoApproveSubscriptions ? '已启用' : '已停用'}
                            </span>
                        </div>
                    </div>

                    <div className="grid grid-cols-6 gap-8 items-start pt-2 pb-2">
                        <span className="text-xs text-gray-600">描述:</span>
                        <span className="col-span-5 text-xs text-gray-900 leading-relaxed">
                            {portal.description || '-'}
                        </span>
                    </div>
                </div>
            </Card>

            {/* 统计数据 */}
            <Row gutter={[16, 16]}>
                <Col xs={24} sm={12} lg={12}>
                    <Card 
                        className="cursor-pointer hover:shadow-md transition-shadow"
                        onClick={() => {
                            navigate(`/portals/detail?id=${portal.portalId}&tab=developers`)
                        }}
                    >
                        <Statistic
                            title="注册开发者"
                            value={developerCount}
                            prefix={<UserOutlined className="text-blue-500" />}
                            valueStyle={{ color: '#1677ff', fontSize: '24px' }}
                        />
                    </Card>
                </Col>
                <Col xs={24} sm={12} lg={12}>
                    <Card 
                        className="cursor-pointer hover:shadow-md transition-shadow"
                        onClick={() => {
                            navigate(`/portals/detail?id=${portal.portalId}&tab=published-apis`)
                        }}
                    >
                        <Statistic
                            title="已发布的API"
                            value={apiCount}
                            prefix={<ApiOutlined className="text-blue-500" />}
                            valueStyle={{ color: '#1677ff', fontSize: '24px' }}
                        />
                    </Card>
                </Col>
            </Row>
        </div>
    )
} 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/consumer/SubscriptionManager.tsx:
--------------------------------------------------------------------------------

```typescript
import { useState } from "react";
import {
  Card,
  Button,
  message,
  Input,
  Modal,
  Table,
  Badge,
  Popconfirm,
  Select,
} from "antd";
import {
  PlusOutlined,
} from "@ant-design/icons";
import api from "../../lib/api";
import type { Subscription } from "../../types/consumer";
import type { ApiResponse, Product } from "../../types";
import { getSubscriptionStatusText, getSubscriptionStatusColor } from "../../lib/statusUtils";
import { formatDateTime } from "../../lib/utils";

interface SubscriptionManagerProps {
  consumerId: string;
  subscriptions: Subscription[];
  onSubscriptionsChange: (searchParams?: { productName: string; status: string }) => void;
  loading?: boolean;
}

export function SubscriptionManager({ consumerId, subscriptions, onSubscriptionsChange, loading = false }: SubscriptionManagerProps) {
  const [productModalVisible, setProductModalVisible] = useState(false);
  const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
  const [productLoading, setProductLoading] = useState(false);
  const [subscribeLoading, setSubscribeLoading] = useState(false);
  const [selectedProduct, setSelectedProduct] = useState<string>('');
  const [subscriptionSearch, setSubscriptionSearch] = useState({ productName: '', status: '' as 'PENDING' | 'APPROVED' | '' });

  // 过滤产品:移除已订阅的产品
  const filterProducts = (allProducts: Product[]) => {
    // 获取已订阅的产品ID列表
    const subscribedProductIds = subscriptions.map(sub => sub.productId);
    
    // 过滤掉已订阅的产品
    return allProducts.filter(product => 
      !subscribedProductIds.includes(product.productId)
    );
  };

  const openProductModal = async () => {
    setProductModalVisible(true);
    setProductLoading(true);
    try {
      const response: ApiResponse<{ content: Product[] }> = await api.get("/products?page=0&size=100");
      if (response?.code === "SUCCESS" && response?.data) {
        const allProducts = response.data.content || [];
        // 初始化时过滤掉已订阅的产品
        const filtered = filterProducts(allProducts);
        setFilteredProducts(filtered);
      }
    } catch (error) {
      console.error('获取产品列表失败:', error);
      // message.error('获取产品列表失败');
    } finally {
      setProductLoading(false);
    }
  };


  const handleSubscribeProducts = async () => {
    if (!selectedProduct) {
      message.warning('请选择要订阅的产品');
      return;
    }

    setSubscribeLoading(true);
    try {
      await api.post(`/consumers/${consumerId}/subscriptions`, { productId: selectedProduct });
      message.success('订阅成功');
      setProductModalVisible(false);
      setSelectedProduct('');
      onSubscriptionsChange();
    } catch (error) {
      console.error('订阅失败:', error);
      // message.error('订阅失败');
    } finally {
      setSubscribeLoading(false);
    }
  };

  const handleUnsubscribe = async (productId: string) => {
    try {
      await api.delete(`/consumers/${consumerId}/subscriptions/${productId}`);
      message.success('取消订阅成功');
      onSubscriptionsChange();
    } catch (error) {
      console.error('取消订阅失败:', error);
      // message.error('取消订阅失败');
    }
  };



  const subscriptionColumns = [
    {
      title: '产品名称',
      dataIndex: 'productName',
      key: 'productName',
      render: (productName: Product['productName']) => productName || '-',
    },
    {
      title: '产品类型',
      dataIndex: 'productType',
      key: 'productType',
      render: (productType: Product['productType']) => {
        const typeMap = {
          'REST_API': 'REST API',
          'HTTP_API': 'HTTP API',
          'MCP_SERVER': 'MCP Server'
        };
        return typeMap[productType as keyof typeof typeMap] || productType || '-';
      }
    },
    {
      title: '订阅状态',
      dataIndex: 'status',
      key: 'status',
      render: (status: string) => (
        <Badge status={getSubscriptionStatusColor(status) as 'success' | 'processing' | 'error' | 'default' | 'warning'} text={getSubscriptionStatusText(status)} />
      ),
    },
    {
      title: '订阅时间',
      dataIndex: 'createAt',
      key: 'createAt',
      render: (date: string) => date ? formatDateTime(date) : '-',
    },
    {
      title: '操作',
      key: 'action',
      render: (record: Subscription) => (
        <Popconfirm
          title="确定要取消订阅吗?"
          onConfirm={() => handleUnsubscribe(record.productId)}
        >
          <Button type="link" danger size="small">
            取消订阅
          </Button>
        </Popconfirm>
      ),
    },
  ];

  // 确保 subscriptions 始终是数组
  const safeSubscriptions = Array.isArray(subscriptions) ? subscriptions : [];

  return (
    <>
      <Card>
        <div className="mb-4 flex justify-between items-center">
          <div className="flex space-x-4">
            <Button
              type="primary"
              icon={<PlusOutlined />}
              onClick={openProductModal}
            >
              订阅
            </Button>
            <Input.Search
              placeholder="请输入API名称进行搜索"
              style={{ width: 300 }}
              onSearch={(value) => {
                const newSearch = { ...subscriptionSearch, productName: value };
                setSubscriptionSearch(newSearch);
                onSubscriptionsChange(newSearch);
              }}
            />
            <Select
              placeholder="订阅状态"
              style={{ width: 120 }}
              allowClear
              value={subscriptionSearch.status || undefined}
              onChange={(value) => {
                const newSearch = { ...subscriptionSearch, status: value as 'PENDING' | 'APPROVED' | '' };
                setSubscriptionSearch(newSearch);
                onSubscriptionsChange(newSearch);
              }}
            >
              <Select.Option value="PENDING">待审批</Select.Option>
              <Select.Option value="APPROVED">已通过</Select.Option>
            </Select>
          </div>
        </div>
        <Table
          columns={subscriptionColumns}
          dataSource={safeSubscriptions}
          rowKey={(record) => record.productId}
          pagination={false}
          size="small"
          loading={loading}
          locale={{ emptyText: '暂无订阅记录,请点击上方按钮进行订阅' }}
        />
      </Card>

      {/* 产品选择弹窗 */}
      <Modal
        title="订阅产品"
        open={productModalVisible}
        onCancel={() => {
          if (!subscribeLoading) {
            setProductModalVisible(false);
            setSelectedProduct('');
          }
        }}
        footer={
          <div className="flex justify-end space-x-2">
            <Button
              onClick={() => {
                if (!subscribeLoading) {
                  setProductModalVisible(false);
                  setSelectedProduct('');
                }
              }}
              disabled={subscribeLoading}
            >
              取消
            </Button>
            <Button
              type="primary"
              onClick={handleSubscribeProducts}
              disabled={!selectedProduct}
              loading={subscribeLoading}
            >
              确定订阅
            </Button>
          </div>
        }
        width={500}
        styles={{
          content: {
            borderRadius: '8px',
            padding: 0
          },
          header: {
            borderRadius: '8px 8px 0 0',
            marginBottom: 0,
            paddingBottom: '8px'
          },
          body: {
            padding: '24px'
          }
        }}
      >
        <div>
          <div className="text-sm text-gray-700 mb-3 font-medium">选择要订阅的产品:</div>
          <Select
            placeholder="请输入产品名称进行搜索或直接选择"
            style={{ width: '100%' }}
            value={selectedProduct}
            onChange={setSelectedProduct}
            loading={productLoading}
            showSearch={true}
            filterOption={(input, option) => {
              const product = filteredProducts.find(p => p.productId === option?.value);
              if (!product) return false;
              
              const searchText = input.toLowerCase();
              return (
                product.name?.toLowerCase().includes(searchText) ||
                product.description?.toLowerCase().includes(searchText)
              );
            }}
            notFoundContent={productLoading ? '加载中...' : '暂无可订阅的产品'}
          >
            {filteredProducts.map(product => (
              <Select.Option key={product.productId} value={product.productId}>
                {product.name}
              </Select.Option>
            ))}
          </Select>
        </div>
      </Modal>
    </>
  );
} 
```

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

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.alibaba.apiopenplatform.core.constant.JwtConstants;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.utils.TokenUtil;
import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
import com.alibaba.apiopenplatform.dto.result.AuthResult;
import com.alibaba.apiopenplatform.dto.result.DeveloperResult;
import com.alibaba.apiopenplatform.dto.result.PortalResult;
import com.alibaba.apiopenplatform.service.DeveloperService;
import com.alibaba.apiopenplatform.service.IdpService;
import com.alibaba.apiopenplatform.service.OAuth2Service;
import com.alibaba.apiopenplatform.service.PortalService;
import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType;
import com.alibaba.apiopenplatform.support.enums.GrantType;
import com.alibaba.apiopenplatform.support.enums.JwtAlgorithm;
import com.alibaba.apiopenplatform.support.portal.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.security.PublicKey;
import java.util.*;

/**
 * @author zh
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class OAuth2ServiceImpl implements OAuth2Service {

    private final PortalService portalService;

    private final DeveloperService developerService;

    private final IdpService idpService;

    @Override
    public AuthResult authenticate(String grantType, String jwtToken) {
        if (!GrantType.JWT_BEARER.getType().equals(grantType)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "不支持的授权模式");
        }

        // 解析JWT
        JWT jwt = JWTUtil.parseToken(jwtToken);
        String kid = (String) jwt.getHeader(JwtConstants.HEADER_KID);
        if (StrUtil.isBlank(kid)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT header缺少字段kid");
        }
        String provider = (String) jwt.getPayload(JwtConstants.PAYLOAD_PROVIDER);
        if (StrUtil.isBlank(provider)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload缺少字段provider");
        }

        String portalId = (String) jwt.getPayload(JwtConstants.PAYLOAD_PORTAL);
        if (StrUtil.isBlank(portalId)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload缺少字段portal");
        }

        // 根据provider确定OAuth2配置
        PortalResult portal = portalService.getPortal(portalId);
        List<OAuth2Config> oauth2Configs = Optional.ofNullable(portal.getPortalSettingConfig())
                .map(PortalSettingConfig::getOauth2Configs)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OAUTH2_CONFIG, portalId));

        OAuth2Config oAuth2Config = oauth2Configs.stream()
                // JWT Bearer模式
                .filter(config -> config.getGrantType() == GrantType.JWT_BEARER)
                .filter(config -> config.getJwtBearerConfig() != null
                        && CollUtil.isNotEmpty(config.getJwtBearerConfig().getPublicKeys()))
                // provider标识
                .filter(config -> config.getProvider().equals(provider))
                .findFirst()
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OAUTH2_CONFIG, provider));

        // 根据kid找到对应公钥
        JwtBearerConfig jwtConfig = oAuth2Config.getJwtBearerConfig();
        PublicKeyConfig publicKeyConfig = jwtConfig.getPublicKeys().stream()
                .filter(key -> kid.equals(key.getKid()))
                .findFirst()
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PUBLIC_KEY, kid));

        // 验签
        if (!verifySignature(jwt, publicKeyConfig)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT签名验证失败");
        }

        // 验证Claims
        validateJwtClaims(jwt);

        // Developer
        String developerId = createOrGetDeveloper(jwt, oAuth2Config);

        // 生成Access Token
        String accessToken = TokenUtil.generateDeveloperToken(developerId);
        log.info("JWT Bearer认证成功,provider: {}, developer: {}", oAuth2Config.getProvider(), developerId);
        return AuthResult.of(accessToken, TokenUtil.getTokenExpiresIn());
    }

    private boolean verifySignature(JWT jwt, PublicKeyConfig keyConfig) {
        // 加载公钥
        PublicKey publicKey = idpService.loadPublicKey(keyConfig.getFormat(), keyConfig.getValue());

        // 验证JWT
        JWTSigner signer = createJWTSigner(keyConfig.getAlgorithm(), publicKey);
        return jwt.setSigner(signer).verify();
    }

    private JWTSigner createJWTSigner(String algorithm, PublicKey publicKey) {
        JwtAlgorithm alg = EnumUtil.fromString(JwtAlgorithm.class, algorithm.toUpperCase());

        switch (alg) {
            case RS256:
                return JWTSignerUtil.rs256(publicKey);
            case RS384:
                return JWTSignerUtil.rs384(publicKey);
            case RS512:
                return JWTSignerUtil.rs512(publicKey);
            case ES256:
                return JWTSignerUtil.es256(publicKey);
            case ES384:
                return JWTSignerUtil.es384(publicKey);
            case ES512:
                return JWTSignerUtil.es512(publicKey);
            default:
                throw new BusinessException(ErrorCode.INVALID_PARAMETER, "不支持的JWT签名算法");
        }
    }

    private void validateJwtClaims(JWT jwt) {
        // 过期时间
        Object expObj = jwt.getPayload(JwtConstants.PAYLOAD_EXP);
        Long exp = Convert.toLong(expObj);
        // 签发时间
        Object iatObj = jwt.getPayload(JwtConstants.PAYLOAD_IAT);
        Long iat = Convert.toLong(iatObj);

        if (iat == null || exp == null || iat > exp) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload中exp或iat不合法");
        }

        long currentTime = System.currentTimeMillis() / 1000;
        if (exp <= currentTime) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT已过期");
        }
    }

    private String createOrGetDeveloper(JWT jwt, OAuth2Config config) {
        IdentityMapping identityMapping = config.getIdentityMapping();
        // userId & userName
        String userIdField = StrUtil.isBlank(identityMapping.getUserIdField()) ?
                JwtConstants.PAYLOAD_USER_ID : identityMapping.getUserIdField();
        String userNameField = StrUtil.isBlank(identityMapping.getUserNameField()) ?
                JwtConstants.PAYLOAD_USER_NAME : identityMapping.getUserNameField();
        Object userIdObj = jwt.getPayload(userIdField);
        Object userNameObj = jwt.getPayload(userNameField);

        String userId = Convert.toStr(userIdObj);
        String userName = Convert.toStr(userNameObj);
        if (StrUtil.isBlank(userId) || StrUtil.isBlank(userName)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWT payload中缺少用户ID字段或用户名称");
        }

        // 复用已有的Developer,否则创建
        return Optional.ofNullable(developerService.getExternalDeveloper(config.getProvider(), userId))
                .map(DeveloperResult::getDeveloperId)
                .orElseGet(() -> {
                    CreateExternalDeveloperParam param = CreateExternalDeveloperParam.builder()
                            .provider(config.getProvider())
                            .subject(userId)
                            .displayName(userName)
                            .authType(DeveloperAuthType.OAUTH2)
                            .build();

                    return developerService.createExternalDeveloper(param).getDeveloperId();
                });
    }

}

```

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

```java
package com.alibaba.apiopenplatform.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.constant.IdpConstants;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.service.IdpService;
import com.alibaba.apiopenplatform.support.enums.GrantType;
import com.alibaba.apiopenplatform.support.enums.PublicKeyFormat;
import com.alibaba.apiopenplatform.support.portal.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zh
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class IdpServiceImpl implements IdpService {

    private final RestTemplate restTemplate;

    @Override
    public void validateOidcConfigs(List<OidcConfig> oidcConfigs) {
        if (CollUtil.isEmpty(oidcConfigs)) {
            return;
        }

        // provider唯一
        Set<String> providers = oidcConfigs.stream()
                .map(OidcConfig::getProvider)
                .filter(StrUtil::isNotBlank)
                .collect(Collectors.toSet());
        if (providers.size() != oidcConfigs.size()) {
            throw new BusinessException(ErrorCode.CONFLICT, "OIDC配置中存在空或重复的provider");
        }

        oidcConfigs.forEach(config -> {
            AuthCodeConfig authConfig = Optional.ofNullable(config.getAuthCodeConfig())
                    .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PARAMETER,
                            StrUtil.format("OIDC配置{}缺少授权码配置", config.getProvider())));
            // 基础参数
            if (StrUtil.isBlank(authConfig.getClientId()) ||
                    StrUtil.isBlank(authConfig.getClientSecret()) ||
                    StrUtil.isBlank(authConfig.getScopes())) {
                throw new BusinessException(ErrorCode.INVALID_PARAMETER,
                        StrUtil.format("OIDC配置{}缺少必要参数: Client ID, Client Secret 或 Scopes", config.getProvider()));
            }

            // 端点配置
            if (StrUtil.isNotBlank(authConfig.getIssuer())) {
                discoverAndSetEndpoints(config.getProvider(), authConfig);
            } else {
                if (StrUtil.isBlank(authConfig.getAuthorizationEndpoint()) ||
                        StrUtil.isBlank(authConfig.getTokenEndpoint()) ||
                        StrUtil.isBlank(authConfig.getUserInfoEndpoint())) {
                    throw new BusinessException(ErrorCode.INVALID_PARAMETER,
                            StrUtil.format("OIDC配置{}缺少必要端点配置", config.getProvider()));
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    private void discoverAndSetEndpoints(String provider, AuthCodeConfig config) {
        String discoveryUrl = config.getIssuer().replaceAll("/$", "") + "/.well-known/openid-configuration";
        try {
            Map<String, Object> discovery = restTemplate.exchange(
                            discoveryUrl,
                            HttpMethod.GET,
                            null,
                            Map.class)
                    .getBody();

            // 验证并设置端点
            String authEndpoint = getRequiredEndpoint(discovery, IdpConstants.AUTHORIZATION_ENDPOINT);
            String tokenEndpoint = getRequiredEndpoint(discovery, IdpConstants.TOKEN_ENDPOINT);
            String userInfoEndpoint = getRequiredEndpoint(discovery, IdpConstants.USERINFO_ENDPOINT);

            config.setAuthorizationEndpoint(authEndpoint);
            config.setTokenEndpoint(tokenEndpoint);
            config.setUserInfoEndpoint(userInfoEndpoint);
        } catch (Exception e) {
            log.error("Failed to discover OIDC endpoints from discovery URL: {}", discoveryUrl, e);
            throw new BusinessException(ErrorCode.INVALID_PARAMETER, StrUtil.format("OIDC配置{}的Issuer无效或无法访问", provider));
        }
    }

    private String getRequiredEndpoint(Map<String, Object> discovery, String name) {
        return Optional.ofNullable(discovery.get(name))
                .map(Object::toString)
                .filter(StrUtil::isNotBlank)
                .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PARAMETER,
                        "OIDC Discovery配置中缺少端点: " + name));
    }

    @Override
    public void validateOAuth2Configs(List<OAuth2Config> oauth2Configs) {
        if (CollUtil.isEmpty(oauth2Configs)) {
            return;
        }

        // provider唯一
        Set<String> providers = oauth2Configs.stream()
                .map(OAuth2Config::getProvider)
                .filter(StrUtil::isNotBlank)
                .collect(Collectors.toSet());
        if (providers.size() != oauth2Configs.size()) {
            throw new BusinessException(ErrorCode.CONFLICT, "OAuth2配置中存在空或重复的provider");
        }

        oauth2Configs.forEach(config -> {
            if (GrantType.JWT_BEARER.equals(config.getGrantType())) {
                validateJwtBearerConfig(config);
            }
        });
    }

    private void validateJwtBearerConfig(OAuth2Config config) {
        JwtBearerConfig jwtBearerConfig = config.getJwtBearerConfig();
        if (jwtBearerConfig == null) {
            throw new BusinessException(ErrorCode.INVALID_PARAMETER,
                    StrUtil.format("OAuth2配置{}使用JWT断言模式但缺少JWT断言配置", config.getProvider()));
        }

        List<PublicKeyConfig> publicKeys = jwtBearerConfig.getPublicKeys();
        if (CollUtil.isEmpty(publicKeys)) {
            throw new BusinessException(ErrorCode.INVALID_PARAMETER,
                    StrUtil.format("OAuth2配置{}缺少公钥配置", config.getProvider()));
        }

        if (publicKeys.stream()
                .map(key -> {
                    // 加载公钥验证有效性
                    loadPublicKey(key.getFormat(), key.getValue());
                    return key.getKid();
                })
                .collect(Collectors.toSet()).size() != publicKeys.size()) {
            throw new BusinessException(ErrorCode.CONFLICT,
                    StrUtil.format("OAuth2配置{}的公钥ID存在重复", config.getProvider()));
        }
    }


    @Override
    public PublicKey loadPublicKey(PublicKeyFormat format, String publicKey) {
        switch (format) {
            case PEM:
                return loadPublicKeyFromPem(publicKey);
            case JWK:
                return loadPublicKeyFromJwk(publicKey);
            default:
                throw new BusinessException(ErrorCode.INVALID_PARAMETER, "公钥格式不支持");
        }
    }

    private PublicKey loadPublicKeyFromPem(String pemContent) {
        // 清理PEM格式标记和空白字符
        String publicKeyPEM = pemContent
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replace("-----BEGIN RSA PUBLIC KEY-----", "")
                .replace("-----END RSA PUBLIC KEY-----", "")
                .replaceAll("\\s", "");

        if (StrUtil.isBlank(publicKeyPEM)) {
            throw new IllegalArgumentException("PEM内容为空");
        }

        try {
            // Base64解码
            byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);

            // 公钥对象
            X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(spec);
        } catch (Exception e) {
            log.error("PEM公钥解析失败", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "PEM公钥解析失败: " + e.getMessage());
        }
    }

    private PublicKey loadPublicKeyFromJwk(String jwkContent) {
        JSONObject jwk = JSONUtil.parseObj(jwkContent);

        // 验证必需字段
        String kty = getRequiredField(jwk, "kty");
        if (!"RSA".equals(kty)) {
            throw new IllegalArgumentException("当前仅支持RSA类型的JWK");
        }

        return loadRSAPublicKeyFromJwk(jwk);
    }

    private PublicKey loadRSAPublicKeyFromJwk(JSONObject jwk) {
        // 获取必需的RSA参数
        String nStr = getRequiredField(jwk, "n");
        String eStr = getRequiredField(jwk, "e");

        try {
            // Base64解码参数
            byte[] nBytes = Base64.getUrlDecoder().decode(nStr);
            byte[] eBytes = Base64.getUrlDecoder().decode(eStr);

            // 构建RSA公钥
            BigInteger modulus = new BigInteger(1, nBytes);
            BigInteger exponent = new BigInteger(1, eBytes);

            RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(spec);
        } catch (Exception e) {
            log.error("JWK RSA参数解析失败", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "JWK RSA参数解析失败: " + e.getMessage());
        }
    }

    private String getRequiredField(JSONObject jwk, String fieldName) {
        String value = jwk.getStr(fieldName);
        if (StrUtil.isBlank(value)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "JWK中缺少字段: " + fieldName);
        }
        return value;
    }
}

```

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

```typescript
import {useState} from 'react'
import {Button, Form, Input, Select, Table, Modal, Space, Tag, message, Card, Row, Col} from 'antd'
import {PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
import {PublicKeyConfig, PublicKeyFormat} from '@/types'

interface PublicKeyManagerProps {
  provider?: string | null
  publicKeys: PublicKeyConfig[]
  onSave: (publicKeys: PublicKeyConfig[]) => void
}

interface PublicKeyFormData {
  kid: string
  format: PublicKeyFormat
  algorithm: string
  value: string
}

export function PublicKeyManager({provider, publicKeys, onSave}: PublicKeyManagerProps) {
  const [form] = Form.useForm<PublicKeyFormData>()
  const [modalVisible, setModalVisible] = useState(false)
  const [editingIndex, setEditingIndex] = useState<number | null>(null)
  const [localPublicKeys, setLocalPublicKeys] = useState<PublicKeyConfig[]>(publicKeys)
  const [selectedFormat, setSelectedFormat] = useState<PublicKeyFormat>(PublicKeyFormat.PEM)

  const handleAdd = () => {
    setEditingIndex(null)
    setModalVisible(true)
    form.resetFields()
    setSelectedFormat(PublicKeyFormat.PEM)
    form.setFieldsValue({
      format: PublicKeyFormat.PEM,
      algorithm: 'RS256'
    })
  }

  const handleEdit = (index: number) => {
    setEditingIndex(index)
    setModalVisible(true)
    const publicKey = localPublicKeys[index]
    setSelectedFormat(publicKey.format)
    form.setFieldsValue(publicKey)
  }

  const handleDelete = (index: number) => {
    Modal.confirm({
      title: '确认删除',
      icon: <ExclamationCircleOutlined/>,
      content: '确定要删除这个公钥配置吗?',
      okText: '确认删除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        const updatedKeys = localPublicKeys.filter((_, i) => i !== index)
        setLocalPublicKeys(updatedKeys)
        onSave(updatedKeys)
        message.success('公钥删除成功')
      },
    })
  }

  const handleModalOk = async () => {
    try {
      const values = await form.validateFields()
      
      // 验证Kid的唯一性
      const isKidExists = localPublicKeys.some((key, index) => 
        key.kid === values.kid && index !== editingIndex
      )
      
      if (isKidExists) {
        message.error('公钥ID已存在,请使用不同的ID')
        return
      }

      let updatedKeys
      if (editingIndex !== null) {
        // 编辑模式
        updatedKeys = localPublicKeys.map((key, index) => 
          index === editingIndex ? values as PublicKeyConfig : key
        )
      } else {
        // 新增模式
        updatedKeys = [...localPublicKeys, values as PublicKeyConfig]
      }

      setLocalPublicKeys(updatedKeys)
      onSave(updatedKeys)
      setModalVisible(false)
      message.success(editingIndex !== null ? '公钥更新成功' : '公钥添加成功')
    } catch (error) {
      message.error('保存公钥失败')
    }
  }

  const handleModalCancel = () => {
    setModalVisible(false)
    setEditingIndex(null)
    setSelectedFormat(PublicKeyFormat.PEM)
    form.resetFields()
  }

  // 验证公钥内容格式
  const validatePublicKey = (_: any, value: string) => {
    if (!value) {
      return Promise.reject(new Error('请输入公钥内容'))
    }
    
    if (selectedFormat === PublicKeyFormat.PEM) {
      // 简单的PEM格式验证
      if (!value.includes('-----BEGIN') || !value.includes('-----END')) {
        return Promise.reject(new Error('PEM格式公钥应包含BEGIN和END标记'))
      }
    } else if (selectedFormat === PublicKeyFormat.JWK) {
      // 简单的JWK格式验证
      try {
        const jwk = JSON.parse(value)
        if (!jwk.kty || !jwk.n || !jwk.e) {
          return Promise.reject(new Error('JWK格式应包含kty、n、e字段'))
        }
      } catch {
        return Promise.reject(new Error('JWK格式应为有效的JSON'))
      }
    }
    
    return Promise.resolve()
  }

  const columns = [
    {
      title: '公钥ID (kid)',
      dataIndex: 'kid',
      key: 'kid',
      render: (kid: string) => (
        <Tag color="blue">{kid}</Tag>
      )
    },
    {
      title: '格式',
      dataIndex: 'format',
      key: 'format',
      render: (format: PublicKeyFormat) => (
        <Tag color={format === PublicKeyFormat.PEM ? 'green' : 'orange'}>
          {format}
        </Tag>
      )
    },
    {
      title: '算法',
      dataIndex: 'algorithm',
      key: 'algorithm',
      render: (algorithm: string) => (
        <Tag color="purple">{algorithm}</Tag>
      )
    },
    {
      title: '公钥内容',
      key: 'value',
      render: (record: PublicKeyConfig) => (
        <span className="font-mono text-xs text-gray-600">
          {record.format === PublicKeyFormat.PEM 
            ? record.value.substring(0, 50) + '...'
            : JSON.stringify(JSON.parse(record.value || '{}')).substring(0, 50) + '...'
          }
        </span>
      )
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, _record: PublicKeyConfig, index: number) => (
        <Space>
          <Button
            type="link"
            size="small"
            icon={<EditOutlined/>}
            onClick={() => handleEdit(index)}
          >
            编辑
          </Button>
          <Button
            type="link"
            danger
            size="small"
            icon={<DeleteOutlined/>}
            onClick={() => handleDelete(index)}
          >
            删除
          </Button>
        </Space>
      )
    }
  ]

  return (
    <div>
      <div className="flex justify-between items-center mb-4">
        <div>
          <h4 className="text-lg font-medium">
            {provider && `${provider} - `}JWT签名公钥管理
          </h4>
          <p className="text-sm text-gray-500">
            管理用于验证JWT签名的公钥,支持PEM和JWK格式
          </p>
        </div>
        <Button
          type="primary"
          icon={<PlusOutlined/>}
          onClick={handleAdd}
        >
          添加公钥
        </Button>
      </div>

      <Table
        columns={columns}
        dataSource={localPublicKeys}
        rowKey="kid"
        pagination={false}
        size="small"
        locale={{
          emptyText: '暂无公钥配置'
        }}
      />

      {/* 公钥配置说明 */}
      <Card size="small" className="mt-4 bg-blue-50">
        <Row gutter={16}>
          <Col span={12}>
            <div className="text-sm">
              <h5 className="font-medium mb-2 text-blue-800">PEM格式示例:</h5>
              <div className="bg-white p-2 rounded font-mono text-xs border">
                -----BEGIN PUBLIC KEY-----<br/>
                MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...<br/>
                -----END PUBLIC KEY-----
              </div>
            </div>
          </Col>
          <Col span={12}>
            <div className="text-sm">
              <h5 className="font-medium mb-2 text-blue-800">JWK格式示例:</h5>
              <div className="bg-white p-2 rounded font-mono text-xs border">
                {`{
  "kty": "RSA",
  "kid": "key1",
  "n": "...",
  "e": "AQAB"
}`}
              </div>
            </div>
          </Col>
        </Row>
      </Card>

      {/* 公钥配置模态框 */}
      <Modal
        title={editingIndex !== null ? '编辑公钥' : '添加公钥'}
        open={modalVisible}
        onOk={handleModalOk}
        onCancel={handleModalCancel}
        width={700}
        okText={editingIndex !== null ? '更新' : '添加'}
        cancelText="取消"
      >
        <Form
          form={form}
          layout="vertical"
        >
          <div className="grid grid-cols-2 gap-4">
            <Form.Item
              name="kid"
              label="公钥ID (kid)"
              rules={[
                {required: true, message: '请输入公钥ID'},
                {pattern: /^[a-zA-Z0-9_-]+$/, message: '公钥ID只能包含字母、数字、下划线和连字符'}
              ]}
            >
              <Input placeholder="如: key1, auth-key-2024"/>
            </Form.Item>
            <Form.Item
              name="algorithm"
              label="签名算法"
              rules={[{required: true, message: '请选择签名算法'}]}
            >
              <Select placeholder="选择签名算法">
                <Select.Option value="RS256">RS256</Select.Option>
                <Select.Option value="RS384">RS384</Select.Option>
                <Select.Option value="RS512">RS512</Select.Option>
                <Select.Option value="ES256">ES256</Select.Option>
                <Select.Option value="ES384">ES384</Select.Option>
                <Select.Option value="ES512">ES512</Select.Option>
              </Select>
            </Form.Item>
          </div>

          <Form.Item
            name="format"
            label="公钥格式"
            rules={[{required: true, message: '请选择公钥格式'}]}
          >
            <Select 
              placeholder="选择公钥格式"
              onChange={(value) => setSelectedFormat(value as PublicKeyFormat)}
            >
              <Select.Option value={PublicKeyFormat.PEM}>PEM格式</Select.Option>
              <Select.Option value={PublicKeyFormat.JWK}>JWK格式</Select.Option>
            </Select>
          </Form.Item>

          <Form.Item
            name="value"
            label="公钥内容"
            rules={[
              {required: true, message: '请输入公钥内容'},
              {validator: validatePublicKey}
            ]}
          >
            <Input.TextArea
              rows={8}
              placeholder={
                selectedFormat === PublicKeyFormat.JWK
                  ? '请输入JWK格式的公钥,例如:\n{\n  "kty": "RSA",\n  "kid": "key1",\n  "n": "...",\n  "e": "AQAB"\n}'
                  : '请输入PEM格式的公钥,例如:\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----'
              }
              style={{fontFamily: 'monospace'}}
            />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  )
}

```

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

```typescript
import { useNavigate } from 'react-router-dom'
import { Card, Button, Table, Tag, Space, Switch, Modal, Form, Input, Select, message } from 'antd'
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, GlobalOutlined, CheckCircleFilled, MinusCircleFilled } from '@ant-design/icons'
import { useState, useEffect } from 'react'
import type { ApiProduct } from '@/types/api-product';
import { apiProductApi, portalApi } from '@/lib/api';

interface ApiProductPortalProps {
  apiProduct: ApiProduct
}

interface Portal {
  portalId: string
  portalName: string
  autoApproveSubscription: boolean
  createdAt: string
}

export function ApiProductPortal({ apiProduct }: ApiProductPortalProps) {
  const [publishedPortals, setPublishedPortals] = useState<Portal[]>([])
  const [allPortals, setAllPortals] = useState<Portal[]>([])
  const [isModalVisible, setIsModalVisible] = useState(false)
  const [selectedPortalIds, setSelectedPortalIds] = useState<string[]>([])
  const [form] = Form.useForm()
  const [loading, setLoading] = useState(false)
  const [portalLoading, setPortalLoading] = useState(false)
  const [modalLoading, setModalLoading] = useState(false)

  // 分页状态
  const [currentPage, setCurrentPage] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const [total, setTotal] = useState(0)

  const navigate = useNavigate()

  // 获取已发布的门户列表
  useEffect(() => {
    if (apiProduct.productId) {
      fetchPublishedPortals()
    }
  }, [apiProduct.productId, currentPage, pageSize])

  // 获取所有门户列表
  useEffect(() => {
    fetchAllPortals()
  }, [])

  const fetchPublishedPortals = async () => {
    setLoading(true)
    try {
      const res = await apiProductApi.getApiProductPublications(apiProduct.productId, {
        page: currentPage,
        size: pageSize
      })
      setPublishedPortals(res.data.content?.map((item: any) => ({
        ...item,
        autoApproveSubscription: item.autoApproveSubscriptions || false,
      })) || [])
      setTotal(res.data.totalElements || 0)
    } catch (error) {
      console.error('获取已发布门户失败:', error)
      // message.error('获取已发布门户失败')
    } finally {
      setLoading(false)
    }
  }

  const fetchAllPortals = async () => {
    setPortalLoading(true)
    try {
      const res = await portalApi.getPortals({
        page: 1,
        size: 500 // 获取所有门户
      })
      setAllPortals(res.data.content?.map((item: any) => ({
        ...item,
        portalName: item.name,
        autoApproveSubscription: item.portalSettingConfig?.autoApproveSubscriptions || false,
      })) || [])
    } catch (error) {
      console.error('获取门户列表失败:', error)
      // message.error('获取门户列表失败')
    } finally {
      setPortalLoading(false)
    }
  }

  const handlePageChange = (page: number, size?: number) => {
    setCurrentPage(page)
    if (size) {
      setPageSize(size)
    }
  }

  const columns = [
    {
      title: '门户信息',
      key: 'portalInfo',
      width: 400,
      render: (_: any, record: Portal) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">
            {record.portalName}
          </div>
          <div className="text-xs text-gray-500 truncate">
            {record.portalId}
          </div>
        </div>
      ),
    },
    {
      title: '订阅自动审批',
      key: 'autoApprove',
      width: 160,
      render: (_: any, record: Portal) => (
        <div className="flex items-center">
          {record.autoApproveSubscription ? (
            <>
              <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
              <span className="text-xs text-gray-900">已开启</span>
            </>
          ) : (
            <>
              <MinusCircleFilled className="text-gray-400 mr-1" style={{fontSize: '10px'}} />
              <span className="text-xs text-gray-900">已关闭</span>
            </>
          )}
        </div>
      ),
    },
    {
      title: '操作',
      key: 'action',
      width: 180,
      render: (_: any, record: Portal) => (
        <Space size="middle">
          <Button onClick={() => {
            navigate(`/portals/detail?id=${record.portalId}`)
          }} type="link" icon={<EyeOutlined />}>
            查看
          </Button>
         
          <Button 
            type="link" 
            danger 
            icon={<DeleteOutlined />}
            onClick={() => handleDelete(record.portalId, record.portalName)}
          >
            移除
          </Button>
        </Space>
      ),
    },
  ]

  const modalColumns = [
    {
      title: '门户信息',
      key: 'portalInfo',
      render: (_: any, record: Portal) => (
        <div>
          <div className="text-xs font-normal text-gray-900 truncate">
            {record.portalName}
          </div>
          <div className="text-xs text-gray-500">
            {record.portalId}
          </div>
        </div>
      ),
    },
    {
      title: '订阅自动审批',
      key: 'autoApprove',
      width: 140,
      render: (_: any, record: Portal) => (
        <div className="flex items-center">
          {record.autoApproveSubscription ? (
            <>
              <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
              <span className="text-xs text-gray-900">已开启</span>
            </>
          ) : (
            <>
              <MinusCircleFilled className="text-gray-400 mr-1" style={{fontSize: '10px'}} />
              <span className="text-xs text-gray-900">已关闭</span>
            </>
          )}
        </div>
      ),
    },
  ]

  const handleAdd = () => {
    setIsModalVisible(true)
  }

  const handleDelete = (portalId: string, portalName: string) => {
    Modal.confirm({
      title: '确认移除',
      icon: <ExclamationCircleOutlined />,
      content: `确定要从API产品中移除门户 "${portalName}" 吗?此操作不可恢复。`,
      okText: '确认移除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        apiProductApi.cancelPublishToPortal(apiProduct.productId, portalId).then((res) => {
          message.success('移除成功')
          fetchPublishedPortals()
        }).catch((error) => {
          console.error('移除失败:', error)
          // message.error('移除失败')
        })
      },
    })
  }

  const handleModalOk = async () => {
    if (selectedPortalIds.length === 0) {
      message.warning('请至少选择一个门户')
      return
    }

    setModalLoading(true)
    try {
      // 批量发布到选中的门户
      for (const portalId of selectedPortalIds) {
        await apiProductApi.publishToPortal(apiProduct.productId, portalId)
      }
      message.success(`成功发布到 ${selectedPortalIds.length} 个门户`)
      setSelectedPortalIds([])
      setIsModalVisible(false)
      // 重新获取已发布的门户列表
      fetchPublishedPortals()
    } catch (error) {
      console.error('发布失败:', error)
      // message.error('发布失败')
    } finally {
      setModalLoading(false)
    }
  }

  const handleModalCancel = () => {
    setIsModalVisible(false)
    setSelectedPortalIds([])
  }

  return (
    <div className="p-6 space-y-6">
      <div className="flex justify-between items-center">
        <div>
          <h1 className="text-2xl font-bold mb-2">发布门户</h1>
          <p className="text-gray-600">管理API产品发布的门户</p>
        </div>
        <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
          发布到门户
        </Button>
      </div>

      <Card>
        {publishedPortals.length === 0 && !loading ? (
          <div className="text-center py-8 text-gray-500">
            <p>暂未发布到任何门户</p>
          </div>
        ) : (
          <Table 
            columns={columns} 
            dataSource={publishedPortals}
            rowKey="portalId"
            loading={loading}
            pagination={{
              current: currentPage,
              pageSize: pageSize,
              total: total,
              showSizeChanger: true,
              showQuickJumper: true,
              showTotal: (total) => `共 ${total} 条`,
              onChange: handlePageChange,
              onShowSizeChange: handlePageChange,
            }}
          />
        )}
      </Card>

      <Modal
        title="发布到门户"
        open={isModalVisible}
        onOk={handleModalOk}
        onCancel={handleModalCancel}
        okText="发布"
        cancelText="取消"
        width={700}
        confirmLoading={modalLoading}
        destroyOnClose
      >
        <div className="border border-gray-200 rounded-lg overflow-hidden">
          <Table
            columns={modalColumns}
            dataSource={allPortals.filter(portal => 
              !publishedPortals.some(published => published.portalId === portal.portalId)
            )}
            rowKey="portalId"
            loading={portalLoading}
            pagination={false}
            scroll={{ y: 350 }}
            size="middle"
            className="portal-selection-table"
            rowSelection={{
              type: 'checkbox',
              selectedRowKeys: selectedPortalIds,
              onChange: (selectedRowKeys) => {
                setSelectedPortalIds(selectedRowKeys as string[])
              },
              columnWidth: 50,
            }}
            rowClassName={(record) => 
              selectedPortalIds.includes(record.portalId) 
                ? 'bg-blue-50 hover:bg-blue-100' 
                : 'hover:bg-gray-50'
            }
            locale={{
              emptyText: (
                <div className="py-8">
                  <div className="text-gray-400 mb-2">
                    <GlobalOutlined style={{ fontSize: '24px' }} />
                  </div>
                  <div className="text-gray-500 text-sm">暂无可发布的门户</div>
                </div>
              )
            }}
          />
        </div>
      </Modal>
    </div>
  )
} 
```

--------------------------------------------------------------------------------
/portal-bootstrap/src/test/java/com/alibaba/apiopenplatform/integration/AdministratorAuthIntegrationTest.java:
--------------------------------------------------------------------------------

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.integration;

import com.alibaba.apiopenplatform.dto.params.admin.AdminCreateParam;
import com.alibaba.apiopenplatform.dto.params.admin.AdminLoginParam;
import com.alibaba.apiopenplatform.core.response.Response;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

/**
 * 管理员初始化、登录、token认证、权限保护、黑名单集成测试
 *
 */
@SpringBootTest(classes = com.alibaba.apiopenplatform.PortalApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AdministratorAuthIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void testAdminRegister() {
        AdminCreateParam createDto = new AdminCreateParam();
        createDto.setUsername("admintest001");
        createDto.setPassword("admin123456");
        ResponseEntity<Response> registerResp = restTemplate.postForEntity(
                "/api/admin/init", createDto, Response.class);
        System.out.println("管理员初始化响应:" + registerResp);
        assertThat(registerResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(registerResp.getBody().getCode()).isEqualTo("Success");
    }

    @Test
    void testAdminLogin() {
        AdminLoginParam loginDto = new AdminLoginParam();
        loginDto.setUsername("admintest001");
        loginDto.setPassword("admin123456");
        ResponseEntity<Response> loginResp = restTemplate.postForEntity(
                "/api/admin/login", loginDto, Response.class);
        System.out.println("管理员登录响应:" + loginResp);
        assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
    }

    @Test
    void testAdminProtectedApiWithValidToken() {
        // 登录获取token
        AdminLoginParam loginDto = new AdminLoginParam();
        loginDto.setUsername("admintest001");
        loginDto.setPassword("admin123456");
        ResponseEntity<Response> loginResp = restTemplate.postForEntity(
                "/api/admin/login", loginDto, Response.class);
        assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
        String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();

        // 用token访问受保护接口
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + token);
        HttpEntity<Void> entity = new HttpEntity<>(headers);
        // 你需要在管理员Controller实现 /api/admin/profile 受保护接口
        ResponseEntity<String> protectedResp = restTemplate.exchange(
                "/api/admin/profile", HttpMethod.GET, entity, String.class);
        System.out.println("管理员带token访问受保护接口响应:" + protectedResp);
        assertThat(protectedResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(protectedResp.getBody()).contains("管理员受保护信息");
    }

    @Test
    void testAdminProtectedApiWithoutToken() {
        // 不带token访问受保护接口
        ResponseEntity<String> protectedResp = restTemplate.getForEntity(
                "/api/admin/profile", String.class);
        System.out.println("管理员不带token访问受保护接口响应:" + protectedResp);
        assertThat(protectedResp.getStatusCode().value()).isIn(401, 403);
    }

    @Test
    void testAdminTokenBlacklist() {
        // 登录获取token
        AdminLoginParam loginDto = new AdminLoginParam();
        loginDto.setUsername("admintest001");
        loginDto.setPassword("admin123456");
        ResponseEntity<Response> loginResp = restTemplate.postForEntity(
                "/api/admin/login", loginDto, Response.class);
        assertThat(loginResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(loginResp.getBody().getCode()).isEqualTo("Success");
        String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();

        // 调用登出接口,将token加入黑名单
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + token);
        HttpEntity<Void> entity = new HttpEntity<>(headers);
        // 修正:带上portalId参数
        ResponseEntity<Response> logoutResp = restTemplate.postForEntity(
                "/api/admin/logout?portalId=default", entity, Response.class);
        System.out.println("管理员登出响应:" + logoutResp);
        assertThat(logoutResp.getStatusCode()).isEqualTo(HttpStatus.OK);

        // 再次用该token访问受保护接口,预期401或403
        ResponseEntity<String> protectedResp = restTemplate.exchange(
                "/api/admin/profile", HttpMethod.GET, entity, String.class);
        System.out.println("管理员黑名单token访问受保护接口响应:" + protectedResp);
        assertThat(protectedResp.getStatusCode().value()).isIn(401, 403);
    }

    @Test
    void testNeedInitBeforeAndAfterInit() {
        // 初始化前,need-init 应为 true
        ResponseEntity<Response> respBefore = restTemplate.getForEntity(
                "/api/admin/need-init?portalId=default", Response.class);
        assertThat(respBefore.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(respBefore.getBody()).isNotNull();
        assertThat(respBefore.getBody().getCode()).isEqualTo("SUCCESS");
        assertThat(respBefore.getBody().getData()).isEqualTo(true);
        assertThat(respBefore.getBody().getMessage()).isNotNull();

        // 初始化
        AdminCreateParam createDto = new AdminCreateParam();
        createDto.setUsername("admintest002");
        createDto.setPassword("admin123456");
        ResponseEntity<Response> initResp = restTemplate.postForEntity(
                "/api/admin/init", createDto, Response.class);
        assertThat(initResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(initResp.getBody()).isNotNull();
        assertThat(initResp.getBody().getCode()).isEqualTo("SUCCESS");
        assertThat(initResp.getBody().getMessage()).isNotNull();

        // 初始化后,need-init 应为 false
        ResponseEntity<Response<Boolean>> respAfter = restTemplate.exchange(
                "/api/admin/need-init?portalId=default",
                HttpMethod.GET,
                null,
                new org.springframework.core.ParameterizedTypeReference<Response<Boolean>>() {}
        );
        assertThat(respAfter.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(respAfter.getBody()).isNotNull();
        assertThat(respAfter.getBody().getCode()).isEqualTo("SUCCESS");
        assertThat(respAfter.getBody().getData()).isEqualTo(false);
        assertThat(respAfter.getBody().getMessage()).isNotNull();

    }

    @Test
    void testChangePasswordSuccessAndFail() {
        // 初始化并登录
        AdminCreateParam createDto = new AdminCreateParam();
        createDto.setUsername("admintest004");
        createDto.setPassword("admin123456");
        restTemplate.postForEntity("/api/admin/init", createDto, Response.class);
        AdminLoginParam loginDto = new AdminLoginParam();
        loginDto.setUsername("admintest004");
        loginDto.setPassword("admin123456");
        ResponseEntity<Response> loginResp = restTemplate.postForEntity(
                "/api/admin/login", loginDto, Response.class);
        String token = ((Map<String, Object>)loginResp.getBody().getData()).get("token").toString();
        String adminId = ((Map<String, Object>)loginResp.getBody().getData()).get("userId").toString();

        // 正确修改密码
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + token);
        headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> emptyBody = new LinkedMultiValueMap<>();
        String changeUrl = String.format("/api/admin/change-password?portalId=%s&adminId=%s&oldPassword=%s&newPassword=%s",
                "default", adminId, "admin123456", "admin654321");
        HttpEntity<MultiValueMap<String, String>> changeEntity = new HttpEntity<>(emptyBody, headers);
        ResponseEntity<Response> changeResp = restTemplate.postForEntity(
                changeUrl, changeEntity, Response.class);
        assertThat(changeResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(changeResp.getBody()).isNotNull();
        assertThat(changeResp.getBody().getCode()).isEqualTo("SUCCESS");
        assertThat(changeResp.getBody().getMessage()).isNotNull();

        // 原密码错误
        String wrongUrl = String.format("/api/admin/change-password?portalId=%s&adminId=%s&oldPassword=%s&newPassword=%s",
                "default", adminId, "wrongpass", "admin654321");
        HttpEntity<MultiValueMap<String, String>> wrongEntity = new HttpEntity<>(emptyBody, headers);
        ResponseEntity<Response> failResp = restTemplate.postForEntity(
                wrongUrl, wrongEntity, Response.class);
        assertThat(failResp.getStatusCode().value()).isIn(401, 400, 409, 403);
        assertThat(failResp.getBody()).isNotNull();
        assertThat(failResp.getBody().getCode()).isIn("ADMIN_PASSWORD_INCORRECT", "INVALID_PARAMETER");
        assertThat(failResp.getBody().getMessage()).isNotNull();
    }
} 
```

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

```typescript
import { Card, Button, Space, message } from 'antd'
import { SaveOutlined, UploadOutlined, FileMarkdownOutlined, EditOutlined } from '@ant-design/icons'
import { useEffect, useState, useRef } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm';
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
import type { ApiProduct } from '@/types/api-product'
import { apiProductApi } from '@/lib/api'

interface ApiProductUsageGuideProps {
  apiProduct: ApiProduct
  handleRefresh: () => void
}

export function ApiProductUsageGuide({ apiProduct, handleRefresh }: ApiProductUsageGuideProps) {
  const [content, setContent] = useState(apiProduct.document || '')
  const [isEditing, setIsEditing] = useState(false)
  const [originalContent, setOriginalContent] = useState(apiProduct.document || '')
  const fileInputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    const doc = apiProduct.document || ''
    setContent(doc)
    setOriginalContent(doc)
  }, [apiProduct.document])

  const handleEdit = () => {
    setIsEditing(true)
  }

  const handleSave = () => {
    apiProductApi.updateApiProduct(apiProduct.productId, {
      document: content
    }).then(() => {
      message.success('保存成功')
      setIsEditing(false)
      setOriginalContent(content)
      handleRefresh();
    })
  }

  const handleCancel = () => {
    setContent(originalContent)
    setIsEditing(false)
  }

  const handleEditorChange = ({ text }: { text: string }) => {
    setContent(text)
  }

  const handleFileImport = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (file) {
      if (file.type !== 'text/markdown' && !file.name.endsWith('.md')) {
        message.error('请选择 Markdown 文件 (.md)')
        return
      }

      const reader = new FileReader()
      reader.onload = (e) => {
        const content = e.target?.result as string
        setContent(content)
        setIsEditing(true)
        message.success('文件导入成功')
      }
      reader.readAsText(file)
    }
    // 清空 input 值,允许重复选择同一文件
    if (event.target) {
      event.target.value = ''
    }
  }

  const triggerFileInput = () => {
    fileInputRef.current?.click()
  }

  return (
    <div className="p-6 space-y-6">
      <div className="flex justify-between items-center">
        <div>
          <h1 className="text-2xl font-bold mb-2">使用指南</h1>
          <p className="text-gray-600">编辑和发布使用指南</p>
        </div>
        <Space>
          {isEditing ? (
            <>
              <Button icon={<UploadOutlined />} onClick={triggerFileInput}>
                导入文件
              </Button>
              <Button onClick={handleCancel}>
                取消
              </Button>
              <Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
                保存
              </Button>
            </>
          ) : (
            <>
              <Button icon={<UploadOutlined />} onClick={triggerFileInput}>
                导入文件
              </Button>
              <Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
                编辑
              </Button>
            </>
          )}
        </Space>
      </div>

      <Card>
        {isEditing ? (
          <>
            <MdEditor
              value={content}
              onChange={handleEditorChange}
              style={{ height: '600px', width: '100%' }}
              placeholder="请输入使用指南内容..."
              renderHTML={(text) => <ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>}
              canView={{ menu: true, md: true, html: true, both: true, fullScreen: false, hideMenu: false }}
              htmlClass="custom-html-style"
              markdownClass="custom-markdown-style"
            />
            <div className="mt-4 text-sm text-gray-500">
              💡 支持Markdown格式:代码块、表格、链接、图片等语法
            </div>
          </>
        ) : (
          <div className="min-h-[400px]">
            {content ? (
              <div 
                className="prose prose-lg max-w-none"
                style={{
                  lineHeight: '1.7',
                  color: '#374151',
                  fontSize: '16px',
                  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
                }}
              >
                <style>{`
                  .prose h1 {
                    color: #111827;
                    font-weight: 700;
                    font-size: 2.25rem;
                    line-height: 1.2;
                    margin-top: 0;
                    margin-bottom: 1.5rem;
                    border-bottom: 2px solid #e5e7eb;
                    padding-bottom: 0.5rem;
                  }
                  .prose h2 {
                    color: #1f2937;
                    font-weight: 600;
                    font-size: 1.875rem;
                    line-height: 1.3;
                    margin-top: 2rem;
                    margin-bottom: 1rem;
                    border-bottom: 1px solid #e5e7eb;
                    padding-bottom: 0.25rem;
                  }
                  .prose h3 {
                    color: #374151;
                    font-weight: 600;
                    font-size: 1.5rem;
                    margin-top: 1.5rem;
                    margin-bottom: 0.75rem;
                  }
                  .prose p {
                    margin-bottom: 1.25rem;
                    color: #4b5563;
                    line-height: 1.7;
                    font-size: 16px;
                  }
                  .prose code {
                    background-color: #f3f4f6;
                    border: 1px solid #e5e7eb;
                    border-radius: 0.375rem;
                    padding: 0.125rem 0.375rem;
                    font-size: 0.875rem;
                    color: #374151;
                    font-weight: 500;
                  }
                  .prose pre {
                    background-color: #1f2937;
                    border-radius: 0.5rem;
                    padding: 1.25rem;
                    overflow-x: auto;
                    margin: 1.5rem 0;
                    border: 1px solid #374151;
                  }
                  .prose pre code {
                    background-color: transparent;
                    border: none;
                    color: #f9fafb;
                    padding: 0;
                    font-size: 0.875rem;
                    font-weight: normal;
                  }
                  .prose blockquote {
                    border-left: 4px solid #3b82f6;
                    padding-left: 1rem;
                    margin: 1.5rem 0;
                    color: #6b7280;
                    font-style: italic;
                    background-color: #f8fafc;
                    padding: 1rem;
                    border-radius: 0.375rem;
                    font-size: 16px;
                  }
                  .prose ul, .prose ol {
                    margin: 1.25rem 0;
                    padding-left: 1.5rem;
                  }
                  .prose ol {
                    list-style-type: decimal;
                    list-style-position: outside;
                  }
                  .prose ul {
                    list-style-type: disc;
                    list-style-position: outside;
                  }
                  .prose li {
                    margin: 0.5rem 0;
                    color: #4b5563;
                    display: list-item;
                    font-size: 16px;
                  }
                  .prose ol li {
                    padding-left: 0.25rem;
                  }
                  .prose ul li {
                    padding-left: 0.25rem;
                  }
                  .prose table {
                    width: 100%;
                    border-collapse: collapse;
                    margin: 1.5rem 0;
                    font-size: 16px;
                  }
                  .prose th, .prose td {
                    border: 1px solid #d1d5db;
                    padding: 0.75rem;
                    text-align: left;
                  }
                  .prose th {
                    background-color: #f9fafb;
                    font-weight: 600;
                    color: #374151;
                    font-size: 16px;
                  }
                  .prose td {
                    color: #4b5563;
                    font-size: 16px;
                  }
                  .prose a {
                    color: #3b82f6;
                    text-decoration: underline;
                    font-weight: 500;
                    transition: color 0.2s;
                    font-size: inherit;
                  }
                  .prose a:hover {
                    color: #1d4ed8;
                  }
                  .prose strong {
                    color: #111827;
                    font-weight: 600;
                    font-size: inherit;
                  }
                  .prose em {
                    color: #6b7280;
                    font-style: italic;
                    font-size: inherit;
                  }
                  .prose hr {
                    border: none;
                    height: 1px;
                    background-color: #e5e7eb;
                    margin: 2rem 0;
                  }
                `}</style>
                <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
              </div>
            ) : (
              <div className="flex items-center justify-center h-[400px] text-gray-400 border-2 border-dashed border-gray-200 rounded-lg">
                <div className="text-center">
                  <FileMarkdownOutlined className="text-4xl mb-4 text-gray-300" />
                  <p className="text-lg">暂无使用指南</p>
                  <p className="text-sm">点击编辑按钮开始撰写</p>
                </div>
              </div>
            )}
          </div>
        )}
      </Card>

      {/* 隐藏的文件输入框 */}
      <input
        ref={fileInputRef}
        type="file"
        accept=".md,text/markdown"
        onChange={handleFileImport}
        style={{ display: 'none' }}
      />
    </div>
  )
} 
```

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

```java
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.alibaba.apiopenplatform.service.gateway;

import cn.hutool.core.map.MapBuilder;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.Gateway;
import com.alibaba.apiopenplatform.entity.Consumer;
import com.alibaba.apiopenplatform.entity.ConsumerCredential;
import com.alibaba.apiopenplatform.service.gateway.client.HigressClient;
import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.HigressAuthConfig;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
import com.alibaba.apiopenplatform.support.gateway.HigressConfig;
import com.alibaba.apiopenplatform.support.product.HigressRefConfig;

import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@Slf4j
public class HigressOperator extends GatewayOperator<HigressClient> {

    @Override
    public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) {
        throw new UnsupportedOperationException("Higress gateway does not support HTTP APIs");
    }

    @Override
    public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) {
        throw new UnsupportedOperationException("Higress gateway does not support REST APIs");
    }

    @Override
    public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
        HigressClient client = getClient(gateway);

        Map<String, String> queryParams = MapBuilder.<String, String>create()
                .put("pageNum", String.valueOf(page))
                .put("pageSize", String.valueOf(size))
                .build();

        HigressPageResponse<HigressMCPConfig> response = client.execute("/v1/mcpServer",
                HttpMethod.GET,
                queryParams,
                null,
                new ParameterizedTypeReference<HigressPageResponse<HigressMCPConfig>>() {
                });

        List<HigressMCPServerResult> mcpServers = response.getData().stream()
                .map(s -> new HigressMCPServerResult().convertFrom(s))
                .collect(Collectors.toList());

        return PageResult.of(mcpServers, page, size, response.getTotal());
    }

    @Override
    public String fetchAPIConfig(Gateway gateway, Object config) {
        throw new UnsupportedOperationException("Higress gateway does not support fetching API config");
    }

    @Override
    public String fetchMcpConfig(Gateway gateway, Object conf) {
        HigressClient client = getClient(gateway);
        HigressRefConfig config = (HigressRefConfig) conf;

        HigressResponse<HigressMCPConfig> response = client.execute("/v1/mcpServer/" + config.getMcpServerName(),
                HttpMethod.GET,
                null,
                null,
                new ParameterizedTypeReference<HigressResponse<HigressMCPConfig>>() {
                });

        MCPConfigResult m = new MCPConfigResult();
        HigressMCPConfig higressMCPConfig = response.getData();
        m.setMcpServerName(higressMCPConfig.getName());

        // mcpServer config
        MCPConfigResult.MCPServerConfig c = new MCPConfigResult.MCPServerConfig();
        c.setPath("/mcp-servers/" + higressMCPConfig.getName());
        c.setDomains(higressMCPConfig.getDomains().stream().map(domain -> MCPConfigResult.Domain.builder()
                        .domain(domain)
                        // 默认HTTP
                        .protocol("http")
                        .build())
                .collect(Collectors.toList()));
        m.setMcpServerConfig(c);

        // tools
        m.setTools(higressMCPConfig.getRawConfigurations());

        // meta
        MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
        meta.setSource(GatewayType.HIGRESS.name());
        meta.setCreateFromType(higressMCPConfig.getType());
        m.setMeta(meta);

        return JSONUtil.toJsonStr(m);
    }

    @Override
    public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) {
        throw new UnsupportedOperationException("Higress gateway does not support fetching Gateways");
    }

    @Override
    public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
        HigressConfig higressConfig = config.getHigressConfig();
        HigressClient client = new HigressClient(higressConfig);

        client.execute("/v1/consumers",
                HttpMethod.POST,
                null,
                buildHigressConsumer(consumer.getConsumerId(), credential.getApiKeyConfig()),
                String.class);

        return consumer.getConsumerId();
    }

    @Override
    public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) {
        HigressConfig higressConfig = config.getHigressConfig();
        HigressClient client = new HigressClient(higressConfig);

        client.execute("/v1/consumers/" + consumerId,
                HttpMethod.PUT,
                null,
                buildHigressConsumer(consumerId, credential.getApiKeyConfig()),
                String.class);
    }

    @Override
    public void deleteConsumer(String consumerId, GatewayConfig config) {
        HigressConfig higressConfig = config.getHigressConfig();
        HigressClient client = new HigressClient(higressConfig);

        client.execute("/v1/consumers/" + consumerId,
                HttpMethod.DELETE,
                null,
                null,
                String.class);
    }

    @Override
    public boolean isConsumerExists(String consumerId, GatewayConfig config) {
        // TODO: 实现Higress网关消费者存在性检查
        return true;
    }

    @Override
    public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
        HigressRefConfig config = (HigressRefConfig) refConfig;
        HigressClient client = getClient(gateway);

        String mcpServerName = config.getMcpServerName();
        client.execute("/v1/mcpServer/consumers/",
                HttpMethod.PUT,
                null,
                buildAuthHigressConsumer(mcpServerName, consumerId),
                Void.class);

        HigressAuthConfig higressAuthConfig = HigressAuthConfig.builder()
                .resourceType("MCP_SERVER")
                .resourceName(mcpServerName)
                .build();

        return ConsumerAuthConfig.builder()
                .higressAuthConfig(higressAuthConfig)
                .build();
    }

    @Override
    public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) {
        HigressClient client = getClient(gateway);

        HigressAuthConfig higressAuthConfig = authConfig.getHigressAuthConfig();
        if (higressAuthConfig == null) {
            return;
        }

        client.execute("/v1/mcpServer/consumers/",
                HttpMethod.DELETE,
                null,
                buildAuthHigressConsumer(higressAuthConfig.getResourceName(), consumerId),
                Void.class);
    }

    @Override
    public APIResult fetchAPI(Gateway gateway, String apiId) {
        throw new UnsupportedOperationException("Higress gateway does not support fetching API");
    }

    @Override
    public GatewayType getGatewayType() {
        return GatewayType.HIGRESS;
    }

    @Override
    public String getDashboard(Gateway gateway, String type) {
        throw new UnsupportedOperationException("Higress gateway does not support getting dashboard");
    }

    @Data
    @Builder
    public static class HigressConsumerConfig {
        private String name;
        private List<HigressCredentialConfig> credentials;
    }

    @Data
    @Builder
    public static class HigressCredentialConfig {
        private String type;
        private String source;
        private String key;
        private List<String> values;
    }

    public HigressConsumerConfig buildHigressConsumer(String consumerId, ApiKeyConfig apiKeyConfig) {

        String source = mapSource(apiKeyConfig.getSource());

        List<String> apiKeys = apiKeyConfig.getCredentials().stream()
                .map(ApiKeyConfig.ApiKeyCredential::getApiKey)
                .collect(Collectors.toList());

        return HigressConsumerConfig.builder()
                .name(consumerId)
                .credentials(Collections.singletonList(
                        HigressCredentialConfig.builder()
                                .type("key-auth")
                                .source(source)
                                .key(apiKeyConfig.getKey())
                                .values(apiKeys)
                                .build())
                )
                .build();
    }

    @Data
    public static class HigressMCPConfig {
        private String name;
        private String type;
        private List<String> domains;
        private String rawConfigurations;
    }

    @Data
    public static class HigressPageResponse<T> {
        private List<T> data;
        private int total;
    }

    @Data
    public static class HigressResponse<T> {
        private T data;
    }

    public HigressAuthConsumerConfig buildAuthHigressConsumer(String gatewayName, String consumerId) {
        return HigressAuthConsumerConfig.builder()
                .mcpServerName(gatewayName)
                .consumers(Collections.singletonList(consumerId))
                .build();
    }

    @Data
    @Builder
    public static class HigressAuthConsumerConfig {
        private String mcpServerName;
        private List<String> consumers;
    }

    private String mapSource(String source) {
        if (StringUtils.isBlank(source)) return null;
        if ("Default".equalsIgnoreCase(source)) return "BEARER";
        if ("HEADER".equalsIgnoreCase(source)) return "HEADER";
        if ("QueryString".equalsIgnoreCase(source)) return "QUERY";
        return source;
    }

}

```
Page 4/7FirstPrevNextLast