#
tokens: 46561/50000 14/349 files (page 5/7)
lines: off (toggle) GitHub
raw markdown copy
This is page 5 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-web/api-portal-admin/src/pages/GatewayConsoles.tsx:
--------------------------------------------------------------------------------

```typescript
import { useState, useEffect, useCallback } from 'react'
import { Button, Table, message, Modal, Tabs } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { gatewayApi } from '@/lib/api'
import ImportGatewayModal from '@/components/console/ImportGatewayModal'
import ImportHigressModal from '@/components/console/ImportHigressModal'
import GatewayTypeSelector from '@/components/console/GatewayTypeSelector'
import { formatDateTime } from '@/lib/utils'
import { Gateway, GatewayType } from '@/types'

export default function Consoles() {
  const [gateways, setGateways] = useState<Gateway[]>([])
  const [typeSelectorVisible, setTypeSelectorVisible] = useState(false)
  const [importVisible, setImportVisible] = useState(false)
  const [higressImportVisible, setHigressImportVisible] = useState(false)
  const [selectedGatewayType, setSelectedGatewayType] = useState<GatewayType>('APIG_API')
  const [loading, setLoading] = useState(false)
  const [activeTab, setActiveTab] = useState<GatewayType>('HIGRESS')
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 10,
    total: 0,
  })

  const fetchGatewaysByType = useCallback(async (gatewayType: GatewayType, page = 1, size = 10) => {
    setLoading(true)
    try {
      const res = await gatewayApi.getGateways({ gatewayType, page, size })
      setGateways(res.data?.content || [])
      setPagination({
        current: page,
        pageSize: size,
        total: res.data?.totalElements || 0,
      })
    } catch (error) {
      // message.error('获取网关列表失败')
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    fetchGatewaysByType(activeTab, 1, 10)
  }, [fetchGatewaysByType, activeTab])

  // 处理导入成功
  const handleImportSuccess = () => {
    fetchGatewaysByType(activeTab, pagination.current, pagination.pageSize)
  }

  // 处理网关类型选择
  const handleGatewayTypeSelect = (type: GatewayType) => {
    setSelectedGatewayType(type)
    setTypeSelectorVisible(false)
    if (type === 'HIGRESS') {
      setHigressImportVisible(true)
    } else {
      setImportVisible(true)
    }
  }

  // 处理分页变化
  const handlePaginationChange = (page: number, pageSize: number) => {
    fetchGatewaysByType(activeTab, page, pageSize)
  }

  // 处理Tab切换
  const handleTabChange = (tabKey: string) => {
    const gatewayType = tabKey as GatewayType
    setActiveTab(gatewayType)
    // Tab切换时重置到第一页
    setPagination(prev => ({ ...prev, current: 1 }))
  }

  const handleDeleteGateway = async (gatewayId: string) => {
    Modal.confirm({
      title: '确认删除',
      content: '确定要删除该网关吗?',
      onOk: async () => {
        try {
          await gatewayApi.deleteGateway(gatewayId)
          message.success('删除成功')
          fetchGatewaysByType(activeTab, pagination.current, pagination.pageSize)
        } catch (error) {
          // message.error('删除失败')
        }
      },
    })
  }

  // APIG 网关的列定义
  const apigColumns = [
    {
      title: '网关名称/ID',
      key: 'nameAndId',
      width: 280,
      render: (_: any, record: Gateway) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">
            {record.gatewayName}
          </div>
          <div className="text-xs text-gray-500 truncate">
            {record.gatewayId}
          </div>
        </div>
      ),
    },
    {
      title: '区域',
      dataIndex: 'region',
      key: 'region',
      render: (_: any, record: Gateway) => {
        return record.apigConfig?.region || '-'
      }
    },
    {
      title: '创建时间',
      dataIndex: 'createAt',
      key: 'createAt',
      render: (date: string) => formatDateTime(date)
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: Gateway) => (
        <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
      ),
    },
  ]

  // 专有云 AI 网关的列定义
  const adpAiColumns = [
    {
      title: '网关名称/ID',
      key: 'nameAndId',
      width: 280,
      render: (_: any, record: Gateway) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">
            {record.gatewayName}
          </div>
          <div className="text-xs text-gray-500 truncate">
            {record.gatewayId}
          </div>
        </div>
      ),
    },
    {
      title: '创建时间',
      dataIndex: 'createAt',
      key: 'createAt',
      render: (date: string) => formatDateTime(date)
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: Gateway) => (
        <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
      ),
    }
  ]

  // Higress 网关的列定义
  const higressColumns = [
    {
      title: '网关名称/ID',
      key: 'nameAndId',
      width: 280,
      render: (_: any, record: Gateway) => (
        <div>
          <div className="text-sm font-medium text-gray-900 truncate">
            {record.gatewayName}
          </div>
          <div className="text-xs text-gray-500 truncate">
            {record.gatewayId}
          </div>
        </div>
      ),
    },
    {
      title: '服务地址',
      dataIndex: 'address',
      key: 'address',
      render: (_: any, record: Gateway) => {
        return record.higressConfig?.address || '-'
      }
    },
    {
      title: '用户名',
      dataIndex: 'username',
      key: 'username',
      render: (_: any, record: Gateway) => {
        return record.higressConfig?.username || '-'
      }
    },
    {
      title: '创建时间',
      dataIndex: 'createAt',
      key: 'createAt',
      render: (date: string) => formatDateTime(date)
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: Gateway) => (
        <Button type="link" danger onClick={() => handleDeleteGateway(record.gatewayId)}>删除</Button>
      ),
    },
  ]

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold tracking-tight">网关实例</h1>
          <p className="text-gray-500 mt-2">
            管理和配置您的网关实例
          </p>
        </div>
        <Button type="primary" icon={<PlusOutlined />} onClick={() => setTypeSelectorVisible(true)}>
          导入网关实例
        </Button>
      </div>

      <Tabs
        activeKey={activeTab}
        onChange={handleTabChange}
        items={[
          {
            key: 'HIGRESS',
            label: 'Higress 网关',
            children: (
              <div className="bg-white rounded-lg">
                <div className="py-4 pl-4 border-b border-gray-200">
                  <h3 className="text-lg font-medium text-gray-900">Higress 网关</h3>
                  <p className="text-sm text-gray-500 mt-1">Higress 云原生网关</p>
                </div>
                <Table
                  columns={higressColumns}
                  dataSource={gateways}
                  rowKey="gatewayId"
                  loading={loading}
                  pagination={{
                    current: pagination.current,
                    pageSize: pagination.pageSize,
                    total: pagination.total,
                    showSizeChanger: true,
                    showQuickJumper: true,
                    showTotal: (total) => `共 ${total} 条`,
                    onChange: handlePaginationChange,
                    onShowSizeChange: handlePaginationChange,
                  }}
                />
              </div>
            ),
          },
          {
            key: 'APIG_API',
            label: 'API 网关',
            children: (
              <div className="bg-white rounded-lg">
                <div className="py-4 pl-4 border-b border-gray-200">
                  <h3 className="text-lg font-medium text-gray-900">API 网关</h3>
                  <p className="text-sm text-gray-500 mt-1">阿里云 API 网关服务</p>
                </div>
                <Table
                  columns={apigColumns}
                  dataSource={gateways}
                  rowKey="gatewayId"
                  loading={loading}
                  pagination={{
                    current: pagination.current,
                    pageSize: pagination.pageSize,
                    total: pagination.total,
                    showSizeChanger: true,
                    showQuickJumper: true,
                    showTotal: (total) => `共 ${total} 条`,
                    onChange: handlePaginationChange,
                    onShowSizeChange: handlePaginationChange,
                  }}
                />
              </div>
            ),
          },
          {
            key: 'APIG_AI',
            label: 'AI 网关',
            children: (
              <div className="bg-white rounded-lg">
                <div className="py-4 pl-4 border-b border-gray-200">
                  <h3 className="text-lg font-medium text-gray-900">AI 网关</h3>
                  <p className="text-sm text-gray-500 mt-1">阿里云 AI 网关服务</p>
                </div>
                <Table
                  columns={apigColumns}
                  dataSource={gateways}
                  rowKey="gatewayId"
                  loading={loading}
                  pagination={{
                    current: pagination.current,
                    pageSize: pagination.pageSize,
                    total: pagination.total,
                    showSizeChanger: true,
                    showQuickJumper: true,
                    showTotal: (total) => `共 ${total} 条`,
                    onChange: handlePaginationChange,
                    onShowSizeChange: handlePaginationChange,
                  }}
                />
              </div>
            ),
          },
          {
            key: 'ADP_AI_GATEWAY',
            label: '专有云 AI 网关',
            children: (
              <div className="bg-white rounded-lg">
                <div className="py-4 pl-4 border-b border-gray-200">
                  <h3 className="text-lg font-medium text-gray-900">AI 网关</h3>
                  <p className="text-sm text-gray-500 mt-1">专有云 AI 网关服务</p>
                </div>
                <Table
                  columns={adpAiColumns}
                  dataSource={gateways}
                  rowKey="gatewayId"
                  loading={loading}
                  pagination={{
                    current: pagination.current,
                    pageSize: pagination.pageSize,
                    total: pagination.total,
                    showSizeChanger: true,
                    showQuickJumper: true,
                    showTotal: (total) => `共 ${total} 条`,
                    onChange: handlePaginationChange,
                    onShowSizeChange: handlePaginationChange,
                  }}
                />
              </div>
            ),
          },
        ]}
      />

      <ImportGatewayModal
        visible={importVisible}
        gatewayType={selectedGatewayType as 'APIG_API' | 'APIG_AI' | 'ADP_AI_GATEWAY'}
        onCancel={() => setImportVisible(false)}
        onSuccess={handleImportSuccess}
      />

      <ImportHigressModal
        visible={higressImportVisible}
        onCancel={() => setHigressImportVisible(false)}
        onSuccess={handleImportSuccess}
      />

      <GatewayTypeSelector
        visible={typeSelectorVisible}
        onCancel={() => setTypeSelectorVisible(false)}
        onSelect={handleGatewayTypeSelect}
      />
    </div>
  )
}

```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/GatewayServiceImpl.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.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
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.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.gateway.ImportGatewayParam;
import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam;
import com.alibaba.apiopenplatform.dto.params.gateway.QueryAdpAIGatewayParam;
import com.alibaba.apiopenplatform.dto.params.gateway.QueryGatewayParam;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.*;
import com.alibaba.apiopenplatform.repository.GatewayRepository;
import com.alibaba.apiopenplatform.repository.ProductRefRepository;
import com.alibaba.apiopenplatform.service.AdpAIGatewayService;
import com.alibaba.apiopenplatform.service.GatewayService;
import com.alibaba.apiopenplatform.service.gateway.GatewayOperator;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@SuppressWarnings("unchecked")
@Slf4j
public class GatewayServiceImpl implements GatewayService, ApplicationContextAware, AdpAIGatewayService {

    private final GatewayRepository gatewayRepository;
    private final ProductRefRepository productRefRepository;

    private Map<GatewayType, GatewayOperator> gatewayOperators;

    private final ContextHolder contextHolder;

    @Override
    public PageResult<GatewayResult> fetchGateways(QueryAPIGParam param, int page, int size) {
        return gatewayOperators.get(param.getGatewayType()).fetchGateways(param, page, size);
    }

    @Override
    public PageResult<GatewayResult> fetchGateways(QueryAdpAIGatewayParam param, int page, int size) {
        return gatewayOperators.get(GatewayType.ADP_AI_GATEWAY).fetchGateways(param, page, size);
    }

    @Override
    public void importGateway(ImportGatewayParam param) {
        gatewayRepository.findByGatewayId(param.getGatewayId())
                .ifPresent(gateway -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.GATEWAY, param.getGatewayId()));
                });

        Gateway gateway = param.convertTo();
        if (gateway.getGatewayType().isHigress()) {
            gateway.setGatewayId(IdGenerator.genHigressGatewayId());
        }
        gateway.setAdminId(contextHolder.getUser());
        gatewayRepository.save(gateway);
    }

    @Override
    public GatewayResult getGateway(String gatewayId) {
        Gateway gateway = findGateway(gatewayId);

        return new GatewayResult().convertFrom(gateway);
    }

    @Override
    public PageResult<GatewayResult> listGateways(QueryGatewayParam param, Pageable pageable) {
        Page<Gateway> gateways = gatewayRepository.findAll(buildGatewaySpec(param), pageable);

        return new PageResult<GatewayResult>().convertFrom(gateways, gateway -> new GatewayResult().convertFrom(gateway));
    }

    @Override
    public void deleteGateway(String gatewayId) {
        Gateway gateway = findGateway(gatewayId);
        // 已有Product引用时不允许删除
        if (productRefRepository.existsByGatewayId(gatewayId)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "网关已被Product引用");
        }

        gatewayRepository.delete(gateway);
    }

    @Override
    public PageResult<APIResult> fetchAPIs(String gatewayId, String apiType, int page, int size) {
        Gateway gateway = findGateway(gatewayId);
        GatewayType gatewayType = gateway.getGatewayType();

        if (gatewayType.isAPIG()) {
            APIGAPIType type = EnumUtil.fromString(APIGAPIType.class, apiType);
            switch (type) {
                case REST:
                    return fetchRESTAPIs(gatewayId, page, size);
                case HTTP:
                    return fetchHTTPAPIs(gatewayId, page, size);
                default:
            }
        }

        if (gatewayType.isHigress()) {
            return fetchRoutes(gatewayId, page, size);
        }

        throw new BusinessException(ErrorCode.INTERNAL_ERROR,
                String.format("Gateway type %s does not support API type %s", gatewayType, apiType));
    }

    @Override
    public PageResult<APIResult> fetchHTTPAPIs(String gatewayId, int page, int size) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).fetchHTTPAPIs(gateway, page, size);
    }

    @Override
    public PageResult<APIResult> fetchRESTAPIs(String gatewayId, int page, int size) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).fetchRESTAPIs(gateway, page, size);
    }

    @Override
    public PageResult<APIResult> fetchRoutes(String gatewayId, int page, int size) {
        return null;
    }

    @Override
    public PageResult<GatewayMCPServerResult> fetchMcpServers(String gatewayId, int page, int size) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).fetchMcpServers(gateway, page, size);
    }

    @Override
    public String fetchAPIConfig(String gatewayId, Object config) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).fetchAPIConfig(gateway, config);
    }

    @Override
    public String fetchMcpConfig(String gatewayId, Object conf) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).fetchMcpConfig(gateway, conf);
    }

    @Override
    public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
        return gatewayOperators.get(config.getGatewayType()).createConsumer(consumer, credential, config);
    }

    @Override
    public void updateConsumer(String gwConsumerId, ConsumerCredential credential, GatewayConfig config) {
        gatewayOperators.get(config.getGatewayType()).updateConsumer(gwConsumerId, credential, config);
    }

    @Override
    public void deleteConsumer(String gwConsumerId, GatewayConfig config) {
        gatewayOperators.get(config.getGatewayType()).deleteConsumer(gwConsumerId, config);
    }

    @Override
    public boolean isConsumerExists(String gwConsumerId, GatewayConfig config) {
        return gatewayOperators.get(config.getGatewayType()).isConsumerExists(gwConsumerId, config);
    }

    @Override
    public ConsumerAuthConfig authorizeConsumer(String gatewayId, String gwConsumerId, ProductRefResult productRef) {
        Gateway gateway = findGateway(gatewayId);
        Object refConfig;
        if (gateway.getGatewayType().isHigress()) {
            refConfig = productRef.getHigressRefConfig();
        } else if (gateway.getGatewayType().isAdpAIGateway()) {
            refConfig = productRef.getAdpAIGatewayRefConfig();
        } else {
            refConfig = productRef.getApigRefConfig();
        }
        return getOperator(gateway).authorizeConsumer(gateway, gwConsumerId, refConfig);
    }

    @Override
    public void revokeConsumerAuthorization(String gatewayId, String gwConsumerId, ConsumerAuthConfig config) {
        Gateway gateway = findGateway(gatewayId);

        getOperator(gateway).revokeConsumerAuthorization(gateway, gwConsumerId, config);
    }

    @Override
    public GatewayConfig getGatewayConfig(String gatewayId) {
        Gateway gateway = findGateway(gatewayId);

        return GatewayConfig.builder()
                .gatewayType(gateway.getGatewayType())
                .apigConfig(gateway.getApigConfig())
                .higressConfig(gateway.getHigressConfig())
                .adpAIGatewayConfig(gateway.getAdpAIGatewayConfig())
                .gateway(gateway)  // 添加Gateway实体引用
                .build();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, GatewayOperator> operators = applicationContext.getBeansOfType(GatewayOperator.class);

        gatewayOperators = operators.values().stream()
                .collect(Collectors.toMap(
                        operator -> operator.getGatewayType(),
                        operator -> operator,
                        (existing, replacement) -> existing));
    }

    private Gateway findGateway(String gatewayId) {
        return gatewayRepository.findByGatewayId(gatewayId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.GATEWAY, gatewayId));
    }

    private GatewayOperator getOperator(Gateway gateway) {
        GatewayOperator gatewayOperator = gatewayOperators.get(gateway.getGatewayType());
        if (gatewayOperator == null) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR,
                    "No gateway operator found for gateway type: " + gateway.getGatewayType());
        }
        return gatewayOperator;
    }

    @Override
    public String getDashboard(String gatewayId,String type) {
        Gateway gateway = findGateway(gatewayId);
        return getOperator(gateway).getDashboard(gateway,type); //type: Portal,MCP,API
    }

    private Specification<Gateway> buildGatewaySpec(QueryGatewayParam param) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (param != null && param.getGatewayType() != null) {
                predicates.add(cb.equal(root.get("gatewayType"), param.getGatewayType()));
            }

            String adminId = contextHolder.getUser();
            if (StrUtil.isNotBlank(adminId)) {
                predicates.add(cb.equal(root.get("adminId"), adminId));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
}
```

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

```typescript
import { useState } from 'react'
import { Button, Table, Modal, Form, Input, message, Select } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { gatewayApi } from '@/lib/api'
import { Gateway, ApigConfig } from '@/types'
import { getGatewayTypeLabel } from '@/lib/constant'

interface ImportGatewayModalProps {
  visible: boolean
  gatewayType: 'APIG_API' | 'APIG_AI' | 'ADP_AI_GATEWAY'
  onCancel: () => void
  onSuccess: () => void
}

export default function ImportGatewayModal({ visible, gatewayType, onCancel, onSuccess }: ImportGatewayModalProps) {
  const [importForm] = Form.useForm()

  const [gatewayLoading, setGatewayLoading] = useState(false)
  const [gatewayList, setGatewayList] = useState<Gateway[]>([])
  const [selectedGateway, setSelectedGateway] = useState<Gateway | null>(null)

  const [apigConfig, setApigConfig] = useState<ApigConfig>({
    region: '',
    accessKey: '',
    secretKey: '',
  })

  const [gatewayPagination, setGatewayPagination] = useState({
    current: 1,
    pageSize: 10,
    total: 0,
  })

  // 监听表单中的认证方式,确保切换时联动渲染
  const authType = Form.useWatch('authType', importForm)

  // 获取网关列表
  const fetchGateways = async (values: any, page = 1, size = 10) => {
    setGatewayLoading(true)
    try {
      const res = await gatewayApi.getApigGateway({
        ...values,
        page,
        size,
      })
      
      setGatewayList(res.data?.content || [])
      setGatewayPagination({
        current: page,
        pageSize: size,
        total: res.data?.totalElements || 0,
      })
    } catch {
      // message.error('获取网关列表失败')
    } finally {
      setGatewayLoading(false)
    }
  }

  const fetchAdpGateways = async (values: any, page = 1, size = 50) => {
    setGatewayLoading(true)
    try {
      const res = await gatewayApi.getAdpGateways({...values, page, size})
      setGatewayList(res.data?.content || [])
      setGatewayPagination({
        current: page,
        pageSize: size,
        total: res.data?.totalElements || 0,
      })
    } catch {
      // message.error('获取网关列表失败')
    } finally {
      setGatewayLoading(false)
    }
  }

  // 处理网关选择
  const handleGatewaySelect = (gateway: Gateway) => {
    setSelectedGateway(gateway)
  }

  // 处理网关分页变化
  const handleGatewayPaginationChange = (page: number, pageSize: number) => {
    const values = importForm.getFieldsValue()
    const data = JSON.parse(sessionStorage.getItem('importFormConfig') || '');
    if (JSON.stringify(values) === '{}') {
      fetchGateways({...data, gatewayType: gatewayType}, page, pageSize)
    } else {
      fetchGateways({...values, gatewayType: gatewayType}, page, pageSize)
    }
  }

  // 处理导入
  const handleImport = () => {
    if (!selectedGateway) {
      message.warning('请选择一个Gateway')
      return
    }
    const payload: any = {
      ...selectedGateway,
      gatewayType: gatewayType,
    }
    if (gatewayType === 'ADP_AI_GATEWAY') {
      payload.adpAIGatewayConfig = apigConfig
    } else {
      payload.apigConfig = apigConfig
    }
    gatewayApi.importGateway(payload).then(() => {
      message.success('导入成功!')
      handleCancel()
      onSuccess()
    }).catch(() => {
      // message.error(error.response?.data?.message || '导入失败!')
    })
  }

  // 重置弹窗状态
  const handleCancel = () => {
    setSelectedGateway(null)
    setGatewayList([])
    setGatewayPagination({ current: 1, pageSize: 10, total: 0 })
    importForm.resetFields()
    onCancel()
  }

  return (
    <Modal
      title="导入网关实例"
      open={visible}
      onCancel={handleCancel}
      footer={null}
      width={800}
    >
      <Form form={importForm} layout="vertical" preserve={false}>
        {gatewayList.length === 0 && ['APIG_API', 'APIG_AI'].includes(gatewayType) && (
          <div className="mb-4">
            <h3 className="text-lg font-medium mb-3">认证信息</h3>
            <Form.Item label="Region" name="region" rules={[{ required: true, message: '请输入region' }]}>
              <Input />
            </Form.Item>
            <Form.Item label="Access Key" name="accessKey" rules={[{ required: true, message: '请输入accessKey' }]}>
              <Input />
            </Form.Item>
            <Form.Item label="Secret Key" name="secretKey" rules={[{ required: true, message: '请输入secretKey' }]}>
              <Input.Password />
            </Form.Item>
            <Button 
              type="primary" 
              onClick={() => {
                importForm.validateFields().then((values) => {
                  setApigConfig(values)
                  sessionStorage.setItem('importFormConfig', JSON.stringify(values))
                  fetchGateways({...values, gatewayType: gatewayType})
                })
              }}
              loading={gatewayLoading}
            >
              获取网关列表
            </Button>
          </div>
        )}

        {['ADP_AI_GATEWAY'].includes(gatewayType) && gatewayList.length === 0 && (
          <div className="mb-4">
            <h3 className="text-lg font-medium mb-3">认证信息</h3>
            <Form.Item label="服务地址" name="baseUrl" rules={[{ required: true, message: '请输入服务地址' }, { pattern: /^https?:\/\//i, message: '必须以 http:// 或 https:// 开头' }]}> 
              <Input placeholder="如:http://apigateway.example.com 或者 http://10.236.6.144" />
            </Form.Item>
            <Form.Item 
              label="端口" 
              name="port" 
              initialValue={80} 
              rules={[
                { required: true, message: '请输入端口号' }, 
                { 
                  validator: (_, value) => {
                    if (value === undefined || value === null || value === '') return Promise.resolve()
                    const n = Number(value)
                    return n >= 1 && n <= 65535 ? Promise.resolve() : Promise.reject(new Error('端口范围需在 1-65535'))
                  }
                }
              ]}
            > 
              <Input type="text" placeholder="如:8080" />
            </Form.Item>
            <Form.Item
              label="认证方式"
              name="authType"
              initialValue="Seed"
              rules={[{ required: true, message: '请选择认证方式' }]}
            >
              <Select>
                <Select.Option value="Seed">Seed</Select.Option>
                <Select.Option value="Header">固定Header</Select.Option>
              </Select>
            </Form.Item>
            {authType === 'Seed' && (
              <Form.Item label="Seed" name="authSeed" rules={[{ required: true, message: '请输入Seed' }]}>
                <Input placeholder="通过configmap获取" />
              </Form.Item>
            )}
            {authType === 'Header' && (
              <Form.Item label="Headers">
                <Form.List name="authHeaders" initialValue={[{ key: '', value: '' }]}>
                  {(fields, { add, remove }) => (
                    <>
                      {fields.map(({ key, name, ...restField }) => (
                        <div key={key} style={{ display: 'flex', marginBottom: 8, alignItems: 'center' }}>
                          <Form.Item
                            {...restField}
                            name={[name, 'key']}
                            rules={[{ required: true, message: '请输入Header名称' }]}
                            style={{ flex: 1, marginRight: 8, marginBottom: 0 }}
                          >
                            <Input placeholder="Header名称,如:X-Auth-Token" />
                          </Form.Item>
                          <Form.Item
                            {...restField}
                            name={[name, 'value']}
                            rules={[{ required: true, message: '请输入Header值' }]}
                            style={{ flex: 1, marginRight: 8, marginBottom: 0 }}
                          >
                            <Input placeholder="Header值" />
                          </Form.Item>
                          {fields.length > 1 && (
                            <Button 
                              type="text" 
                              danger 
                              onClick={() => remove(name)}
                              style={{ marginBottom: 0 }}
                            >
                              删除
                            </Button>
                          )}
                        </div>
                      ))}
                      <Form.Item style={{ marginBottom: 0 }}>
                        <Button 
                          type="dashed" 
                          onClick={() => add({ key: '', value: '' })} 
                          block 
                          icon={<PlusOutlined />}
                        >
                          添加Header
                        </Button>
                      </Form.Item>
                    </>
                  )}
                </Form.List>
              </Form.Item>
            )}
            <Button 
              type="primary" 
              onClick={() => {
                importForm.validateFields().then((values) => {
                  // 处理认证参数
                  const processedValues = { ...values };
                  
                  // 根据认证方式设置相应的参数
                  if (values.authType === 'Seed') {
                    processedValues.authSeed = values.authSeed;
                    delete processedValues.authHeaders;
                  } else if (values.authType === 'Header') {
                    processedValues.authHeaders = values.authHeaders;
                    delete processedValues.authSeed;
                  }
                  
                  setApigConfig(processedValues)
                  sessionStorage.setItem('importFormConfig', JSON.stringify(processedValues))
                  fetchAdpGateways({...processedValues, gatewayType: gatewayType})
                })
              }}
              loading={gatewayLoading}
            >
              获取网关列表
            </Button>
          </div>
        )}

        {gatewayList.length > 0 && (
          <div className="mb-4">
            <h3 className="text-lg font-medium mb-3">选择网关实例</h3>
            <Table
              rowKey="gatewayId"
              columns={[
                { title: 'ID', dataIndex: 'gatewayId' },
                { 
                  title: '类型', 
                  dataIndex: 'gatewayType',
                  render: (gatewayType: string) => getGatewayTypeLabel(gatewayType as any)
                },
                { title: '名称', dataIndex: 'gatewayName' },
              ]}
              dataSource={gatewayList}
              rowSelection={{
                type: 'radio',
                selectedRowKeys: selectedGateway ? [selectedGateway.gatewayId] : [],
                onChange: (_selectedRowKeys, selectedRows) => {
                  handleGatewaySelect(selectedRows[0])
                },
              }}
              pagination={{
                current: gatewayPagination.current,
                pageSize: gatewayPagination.pageSize,
                total: gatewayPagination.total,
                onChange: handleGatewayPaginationChange,
                showSizeChanger: true,
                showQuickJumper: true,
                showTotal: (total) => `共 ${total} 条`,
              }}
              size="small"
            />
          </div>
        )}

        {selectedGateway && (
          <div className="text-right">
            <Button type="primary" onClick={handleImport}>
              完成导入
            </Button>
          </div>
        )}
      </Form>
    </Modal>
  )
} 
```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/PortalServiceImpl.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.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam;
import com.alibaba.apiopenplatform.dto.params.portal.*;
import com.alibaba.apiopenplatform.dto.result.PageResult;
import com.alibaba.apiopenplatform.dto.result.PortalResult;
import com.alibaba.apiopenplatform.dto.result.SubscriptionResult;
import com.alibaba.apiopenplatform.entity.Portal;
import com.alibaba.apiopenplatform.entity.PortalDomain;
import com.alibaba.apiopenplatform.entity.ProductSubscription;
import com.alibaba.apiopenplatform.repository.PortalDomainRepository;
import com.alibaba.apiopenplatform.repository.PortalRepository;
import com.alibaba.apiopenplatform.repository.SubscriptionRepository;
import com.alibaba.apiopenplatform.repository.ProductPublicationRepository;
import com.alibaba.apiopenplatform.repository.ProductRefRepository;
import com.alibaba.apiopenplatform.service.GatewayService;
import com.alibaba.apiopenplatform.entity.ProductPublication;
import com.alibaba.apiopenplatform.entity.ProductRef;
import org.springframework.data.domain.PageRequest;
import com.alibaba.apiopenplatform.service.IdpService;
import com.alibaba.apiopenplatform.service.PortalService;
import com.alibaba.apiopenplatform.support.enums.DomainType;
import com.alibaba.apiopenplatform.support.portal.OidcConfig;
import com.alibaba.apiopenplatform.support.portal.PortalSettingConfig;
import com.alibaba.apiopenplatform.support.portal.PortalUiConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.Predicate;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PortalServiceImpl implements PortalService {

    private final PortalRepository portalRepository;

    private final PortalDomainRepository portalDomainRepository;

    private final ApplicationEventPublisher eventPublisher;

    private final SubscriptionRepository subscriptionRepository;

    private final ContextHolder contextHolder;

    private final IdpService idpService;

    private final String domainFormat = "%s.api.portal.local";

    private final ProductPublicationRepository publicationRepository;

    private final ProductRefRepository productRefRepository;

    private final GatewayService gatewayService;

    public PortalResult createPortal(CreatePortalParam param) {
        portalRepository.findByName(param.getName())
                .ifPresent(portal -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName()));
                });

        String portalId = IdGenerator.genPortalId();
        Portal portal = param.convertTo();
        portal.setPortalId(portalId);
        portal.setAdminId(contextHolder.getUser());

        // Setting & Ui
        portal.setPortalSettingConfig(new PortalSettingConfig());
        portal.setPortalUiConfig(new PortalUiConfig());

        // Domain
        PortalDomain portalDomain = new PortalDomain();
        portalDomain.setDomain(String.format(domainFormat, portalId));
        portalDomain.setPortalId(portalId);

        portalDomainRepository.save(portalDomain);
        portalRepository.save(portal);

        return getPortal(portalId);
    }

    @Override
    public PortalResult getPortal(String portalId) {
        Portal portal = findPortal(portalId);
        List<PortalDomain> domains = portalDomainRepository.findAllByPortalId(portalId);
        portal.setPortalDomains(domains);

        return new PortalResult().convertFrom(portal);
    }

    @Override
    public void existsPortal(String portalId) {
        portalRepository.findByPortalId(portalId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
    }

    @Override
    public PageResult<PortalResult> listPortals(Pageable pageable) {
        Page<Portal> portals = portalRepository.findAll(pageable);

        // 填充Domain
        if (portals.hasContent()) {
            List<String> portalIds = portals.getContent().stream()
                    .map(Portal::getPortalId)
                    .collect(Collectors.toList());

            List<PortalDomain> allDomains = portalDomainRepository.findAllByPortalIdIn(portalIds);
            Map<String, List<PortalDomain>> portalDomains = allDomains.stream()
                    .collect(Collectors.groupingBy(PortalDomain::getPortalId));

            portals.getContent().forEach(portal -> {
                List<PortalDomain> domains = portalDomains.getOrDefault(portal.getPortalId(), new ArrayList<>());
                portal.setPortalDomains(domains);
            });
        }

        return new PageResult<PortalResult>().convertFrom(portals, portal -> new PortalResult().convertFrom(portal));
    }

    @Override
    public PortalResult updatePortal(String portalId, UpdatePortalParam param) {
        Portal portal = findPortal(portalId);

        Optional.ofNullable(param.getName())
                .filter(name -> !name.equals(portal.getName()))
                .flatMap(portalRepository::findByName)
                .ifPresent(p -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL, portal.getName()));
                });

        param.update(portal);
        // 验证OIDC配置
        PortalSettingConfig setting = portal.getPortalSettingConfig();
        if (CollUtil.isNotEmpty(setting.getOidcConfigs())) {
            idpService.validateOidcConfigs(setting.getOidcConfigs());
        }

        if (CollUtil.isNotEmpty(setting.getOauth2Configs())) {
            idpService.validateOAuth2Configs(setting.getOauth2Configs());
        }

        // 至少保留一种认证方式
        if (BooleanUtil.isFalse(setting.getBuiltinAuthEnabled())) {
            boolean enabledOidc = Optional.ofNullable(setting.getOidcConfigs())
                    .filter(CollUtil::isNotEmpty)
                    .map(configs -> configs.stream().anyMatch(OidcConfig::isEnabled))
                    .orElse(false);

            if (!enabledOidc) {
                throw new BusinessException(ErrorCode.INVALID_REQUEST, "至少配置一种认证方式");
            }
        }
        portalRepository.saveAndFlush(portal);

        return getPortal(portal.getPortalId());
    }

    @Override
    public void deletePortal(String portalId) {
        Portal portal = findPortal(portalId);

        // 清理Domain
        portalDomainRepository.deleteAllByPortalId(portalId);

        // 异步清理门户资源
        eventPublisher.publishEvent(new PortalDeletingEvent(portalId));
        portalRepository.delete(portal);
    }

    @Override
    public String resolvePortal(String domain) {
        return portalDomainRepository.findByDomain(domain)
                .map(PortalDomain::getPortalId)
                .orElse(null);
    }

    @Override
    public PortalResult bindDomain(String portalId, BindDomainParam param) {
        existsPortal(portalId);
        portalDomainRepository.findByPortalIdAndDomain(portalId, param.getDomain())
                .ifPresent(portalDomain -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PORTAL_DOMAIN, portalDomain.getDomain()));
                });

        PortalDomain portalDomain = param.convertTo();
        portalDomain.setPortalId(portalId);

        portalDomainRepository.save(portalDomain);
        return getPortal(portalId);
    }

    @Override
    public PortalResult unbindDomain(String portalId, String domain) {
        portalDomainRepository.findByPortalIdAndDomain(portalId, domain)
                .ifPresent(portalDomain -> {
                    // 默认域名不允许解绑
                    if (portalDomain.getType() == DomainType.DEFAULT) {
                        throw new BusinessException(ErrorCode.INVALID_REQUEST, "默认域名不允许解绑");
                    }
                    portalDomainRepository.delete(portalDomain);
                });
        return getPortal(portalId);
    }

    @Override
    public PageResult<SubscriptionResult> listSubscriptions(String portalId, QuerySubscriptionParam param, Pageable pageable) {
        // Ensure portal exists
        existsPortal(portalId);

        Specification<ProductSubscription> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            predicates.add(cb.equal(root.get("portalId"), portalId));
            if (param != null && param.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), param.getStatus()));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };

        Page<ProductSubscription> page = subscriptionRepository.findAll(spec, pageable);
        return new PageResult<SubscriptionResult>().convertFrom(page, s -> new SubscriptionResult().convertFrom(s));
    }

    @Override
    public String getDefaultPortal() {
        Portal portal = portalRepository.findFirstByOrderByIdAsc().orElse(null);
        if (portal == null) {
            return null;
        }
        return portal.getPortalId();
    }

    @Override
    public String getDashboard(String portalId) {
        existsPortal(portalId);

        // 找到该门户下任一已发布产品(取第一页第一条)
        ProductPublication pub = publicationRepository.findByPortalId(portalId, PageRequest.of(0, 1))
                .stream()
                .findFirst()
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));

        // 取产品的网关引用
        ProductRef productRef = productRefRepository.findFirstByProductId(pub.getProductId())
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, pub.getProductId()));

        if (productRef.getGatewayId() == null) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "网关", "该门户下的产品尚未关联网关服务");
        }

        // 复用网关的Dashboard能力
        return gatewayService.getDashboard(productRef.getGatewayId(),"Portal");
    }

    private Portal findPortal(String portalId) {
        return portalRepository.findByPortalId(portalId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
    }
}

```

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

```typescript
import {Card, Table, Badge, Button, Space, message, Modal} from 'antd'
import {
    EditOutlined,
    DeleteOutlined,
    ExclamationCircleOutlined,
    EyeOutlined,
    UnorderedListOutlined,
    CheckCircleFilled,
    ClockCircleOutlined
} from '@ant-design/icons'
import {useEffect, useState} from 'react'
import {Portal, Developer, Consumer} from '@/types'
import {portalApi} from '@/lib/api'
import {formatDateTime} from '@/lib/utils'
import {SubscriptionListModal} from '@/components/subscription/SubscriptionListModal'

interface PortalDevelopersProps {
    portal: Portal
}

export function PortalDevelopers({portal}: PortalDevelopersProps) {
    const [developers, setDevelopers] = useState<Developer[]>([])
    const [pagination, setPagination] = useState({
        current: 1,
        pageSize: 10,
        total: 0,
        showSizeChanger: true,
        showQuickJumper: true,
        showTotal: (total: number, range: [number, number]) =>
            `共 ${total} 条`
    })

    // Consumer相关状态
    const [consumers, setConsumers] = useState<Consumer[]>([])
    const [consumerModalVisible, setConsumerModalVisible] = useState(false)
    const [currentDeveloper, setCurrentDeveloper] = useState<Developer | null>(null)
    const [consumerPagination, setConsumerPagination] = useState({
        current: 1,
        pageSize: 10,
        total: 0,
        showSizeChanger: true,
        showQuickJumper: true,
        showTotal: (total: number, range: [number, number]) =>
            `共 ${total} 条`
    })

    // 订阅列表相关状态
    const [subscriptionModalVisible, setSubscriptionModalVisible] = useState(false)
    const [currentConsumer, setCurrentConsumer] = useState<Consumer | null>(null)

    useEffect(() => {
        fetchDevelopers()
    }, [portal.portalId, pagination.current, pagination.pageSize])

    const fetchDevelopers = () => {
        portalApi.getDeveloperList(portal.portalId, {
            page: pagination.current, // 后端从0开始
            size: pagination.pageSize
        }).then((res) => {
            setDevelopers(res.data.content)
            setPagination(prev => ({
                ...prev,
                total: res.data.totalElements || 0
            }))
        })
    }

    const handleUpdateDeveloperStatus = (developerId: string, status: string) => {
        portalApi.updateDeveloperStatus(portal.portalId, developerId, status).then(() => {
            if (status === 'PENDING') {
                message.success('取消授权成功')
            } else {
                message.success('审批成功')
            }
            fetchDevelopers()
        }).catch(() => {
            message.error('审批失败')
        })
    }

    const handleTableChange = (paginationInfo: any) => {
        setPagination(prev => ({
            ...prev,
            current: paginationInfo.current,
            pageSize: paginationInfo.pageSize
        }))
    }

    const handleDeleteDeveloper = (developerId: string, username: string) => {
        Modal.confirm({
            title: '确认删除',
            icon: <ExclamationCircleOutlined/>,
            content: `确定要删除开发者 "${username}" 吗?此操作不可恢复。`,
            okText: '确认删除',
            okType: 'danger',
            cancelText: '取消',
            onOk() {
                portalApi.deleteDeveloper(developerId).then(() => {
                    message.success('删除成功')
                    fetchDevelopers()
                }).catch(() => {
                    message.error('删除失败')
                })
            },
        })
    }

    // Consumer相关函数
    const handleViewConsumers = (developer: Developer) => {
        setCurrentDeveloper(developer)
        setConsumerModalVisible(true)
        setConsumerPagination(prev => ({...prev, current: 1}))
        fetchConsumers(developer.developerId, 1, consumerPagination.pageSize)
    }

    const fetchConsumers = (developerId: string, page: number, size: number) => {
        portalApi.getConsumerList(portal.portalId, developerId, {page: page, size}).then((res) => {
            setConsumers(res.data.content || [])
            setConsumerPagination(prev => ({
                ...prev,
                total: res.data.totalElements || 0
            }))
        }).then((res: any) => {
            setConsumers(res.data.content || [])
            setConsumerPagination(prev => ({
                ...prev,
                total: res.data.totalElements || 0
            }))
        })
    }

    const handleConsumerTableChange = (paginationInfo: any) => {
        if (currentDeveloper) {
            setConsumerPagination(prev => ({
                ...prev,
                current: paginationInfo.current,
                pageSize: paginationInfo.pageSize
            }))
            fetchConsumers(currentDeveloper.developerId, paginationInfo.current, paginationInfo.pageSize)
        }
    }

    const handleConsumerStatusUpdate = (consumerId: string) => {
        if (currentDeveloper) {
            portalApi.approveConsumer(consumerId).then((res) => {
                message.success('审批成功')
                fetchConsumers(currentDeveloper.developerId, consumerPagination.current, consumerPagination.pageSize)
            }).catch((err) => {
                // message.error('审批失败')
            })
        }
    }

    // 查看订阅列表
    const handleViewSubscriptions = (consumer: Consumer) => {
        setCurrentConsumer(consumer)
        setSubscriptionModalVisible(true)
    }

    // 关闭订阅列表模态框
    const handleSubscriptionModalCancel = () => {
        setSubscriptionModalVisible(false)
        setCurrentConsumer(null)
    }


    const columns = [
        {
            title: '开发者名称/ID',
            dataIndex: 'username',
            key: 'username',
            fixed: 'left' as const,
            width: 280,
            render: (username: string, record: Developer) => (
                <div className="ml-2">
                    <div className="font-medium">{username}</div>
                    <div className="text-sm text-gray-500">{record.developerId}</div>
                </div>
            ),
        },
        {
            title: '状态',
            dataIndex: 'status',
            key: 'status',
            width: 120,
            render: (status: string) => (
                <div className="flex items-center">
                    {status === 'APPROVED' ? (
                        <>
                            <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} />
                            <span className="text-xs text-gray-900">可用</span>
                        </>
                    ) : (
                        <>
                            <ClockCircleOutlined className="text-orange-500 mr-2" style={{fontSize: '10px'}} />
                            <span className="text-xs text-gray-900">待审核</span>
                        </>
                    )}
                </div>
            )
        },

        {
            title: '创建时间',
            dataIndex: 'createAt',
            key: 'createAt',
            width: 160,
            render: (date: string) => formatDateTime(date)
        },

        {
            title: '操作',
            key: 'action',
            fixed: 'right' as const,
            width: 250,
            render: (_: any, record: Developer) => (
                <Space size="middle">
                    <Button onClick={() => handleViewConsumers(record)} type="link" icon={<EyeOutlined/>}>
                        查看Consumer
                    </Button>
                    {
                        !portal.portalSettingConfig.autoApproveDevelopers && (
                            record.status === 'APPROVED' ? (
                                <Button onClick={() => handleUpdateDeveloperStatus(record.developerId, 'PENDING')}
                                        type="link" icon={<EditOutlined/>}>
                                    取消授权
                                </Button>
                            ) : (
                                <Button onClick={() => handleUpdateDeveloperStatus(record.developerId, 'APPROVED')}
                                        type="link" icon={<EditOutlined/>}>
                                    审批通过
                                </Button>
                            )
                        )
                    }
                    <Button onClick={() => handleDeleteDeveloper(record.developerId, record.username)} type="link"
                            danger icon={<DeleteOutlined/>}>
                        删除
                    </Button>
                </Space>
            ),
        },
    ]

    // Consumer表格列定义
    const consumerColumns = [
        {
            title: 'Consumer名称',
            dataIndex: 'name',
            key: 'name',
            width: 200,
        },
        {
            title: 'Consumer ID',
            dataIndex: 'consumerId',
            key: 'consumerId',
            width: 200,
        },
        {
            title: '描述',
            dataIndex: 'description',
            key: 'description',
            ellipsis: true,
            width: 200,
        },
        // {
        //   title: '状态',
        //   dataIndex: 'status',
        //   key: 'status',
        //   width: 120,
        //   render: (status: string) => (
        //     <Badge status={status === 'APPROVED' ? 'success' : 'default'} text={status === 'APPROVED' ? '可用' : '待审核'} />
        //   )
        // },
        {
            title: '创建时间',
            dataIndex: 'createAt',
            key: 'createAt',
            width: 150,
            render: (date: string) => formatDateTime(date)
        },
        {
            title: '操作',
            key: 'action',
            width: 120,
            render: (_: any, record: Consumer) => (
                <Button
                    onClick={() => handleViewSubscriptions(record)}
                    type="link"
                    icon={<UnorderedListOutlined/>}
                >
                    订阅列表
                </Button>
            ),
        },
    ]

    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">管理Portal的开发者用户</p>
                </div>
            </div>

            <Card>
                {/* <div className="mb-4">
          <Input
            placeholder="搜索开发者..."
            prefix={<SearchOutlined />}
            value={searchText}
            onChange={(e) => setSearchText(e.target.value)}
            style={{ width: 300 }}
          />
        </div> */}
                <Table
                    columns={columns}
                    dataSource={developers}
                    rowKey="developerId"
                    pagination={pagination}
                    onChange={handleTableChange}
                    scroll={{
                        y: 'calc(100vh - 400px)',
                        x: 'max-content'
                    }}
                />
            </Card>

            {/* Consumer弹窗 */}
            <Modal
                title={`查看Consumer - ${currentDeveloper?.username || ''}`}
                open={consumerModalVisible}
                onCancel={() => setConsumerModalVisible(false)}
                footer={null}
                width={1000}
                destroyOnClose
            >
                <Table
                    columns={consumerColumns}
                    dataSource={consumers}
                    rowKey="consumerId"
                    pagination={consumerPagination}
                    onChange={handleConsumerTableChange}
                    scroll={{y: 'calc(100vh - 400px)'}}
                />
            </Modal>

            {/* 订阅列表弹窗 */}
            {currentConsumer && (
                <SubscriptionListModal
                    visible={subscriptionModalVisible}
                    consumerId={currentConsumer.consumerId}
                    consumerName={currentConsumer.name}
                    onCancel={handleSubscriptionModalCancel}
                />
            )}

        </div>
    )
} 
```

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

```typescript
import { useState, useEffect } from "react";
import {
  Modal,
  Form,
  Input,
  Select,
  Image,
  message,
  UploadFile,
  Switch,
  Radio,
  Space,
} from "antd";
import { CameraOutlined } from "@ant-design/icons";
import { apiProductApi } from "@/lib/api";
import type { ApiProduct } from "@/types/api-product";

interface ApiProductFormModalProps {
  visible: boolean;
  onCancel: () => void;
  onSuccess: () => void;
  productId?: string;
  initialData?: Partial<ApiProduct>;
}

export default function ApiProductFormModal({
  visible,
  onCancel,
  onSuccess,
  productId,
  initialData,
}: ApiProductFormModalProps) {
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState("");
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const [iconMode, setIconMode] = useState<'BASE64' | 'URL'>('URL');
  const isEditMode = !!productId;

  // 初始化时加载已有数据
  useEffect(() => {
    if (visible && isEditMode && initialData && initialData.name) {
      setTimeout(() => {
        // 1. 先设置所有字段
        form.setFieldsValue({
          name: initialData.name,
          description: initialData.description,
          type: initialData.type,
          autoApprove: initialData.autoApprove,
        });
      }, 100);

      // 2. 处理 icon 字段
      if (initialData.icon) {
        if (typeof initialData.icon === 'object' && initialData.icon.type && initialData.icon.value) {
          // 新格式:{ type: 'BASE64' | 'URL', value: string }
          const iconType = initialData.icon.type as 'BASE64' | 'URL';
          const iconValue = initialData.icon.value;
          
          setIconMode(iconType);
          
          if (iconType === 'BASE64') {
            setFileList([
              {
                uid: "-1",
                name: "头像.png",
                status: "done",
                url: iconValue,
              },
            ]);
            form.setFieldsValue({ icon: iconValue });
          } else {
            form.setFieldsValue({ iconUrl: iconValue });
          }
        } else {
          // 兼容旧格式(字符串格式)
          const iconStr = initialData.icon as unknown as string;
          if (iconStr && typeof iconStr === 'string' && iconStr.includes("value=")) {
            const startIndex = iconStr.indexOf("value=") + 6;
            const endIndex = iconStr.length - 1;
            const base64Data = iconStr.substring(startIndex, endIndex).trim();
            
            setIconMode('BASE64');
            setFileList([
              {
                uid: "-1",
                name: "头像.png",
                status: "done",
                url: base64Data,
              },
            ]);
            form.setFieldsValue({ icon: base64Data });
          }
        }
      }
    } else if (visible && !isEditMode) {
      // 新建模式下清空表单
      form.resetFields();
      setFileList([]);
      setIconMode('URL');
    }
  }, [visible, isEditMode, initialData, form]);

  // 将文件转为 Base64
  const getBase64 = (file: File): Promise<string> =>
    new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = (error) => reject(error);
    });


  const uploadButton = (
    <div style={{ 
      display: 'flex', 
      flexDirection: 'column', 
      alignItems: 'center', 
      justifyContent: 'center',
      color: '#999'
    }}>
      <CameraOutlined style={{ fontSize: '16px', marginBottom: '6px' }} />
      <span style={{ fontSize: '12px', color: '#999' }}>上传图片</span>
    </div>
  );

  // 处理Icon模式切换
  const handleIconModeChange = (mode: 'BASE64' | 'URL') => {
    setIconMode(mode);
    // 清空相关字段
    if (mode === 'URL') {
      form.setFieldsValue({ icon: undefined });
      setFileList([]);
    } else {
      form.setFieldsValue({ iconUrl: undefined });
    }
  };

  const resetForm = () => {
    form.resetFields();
    setFileList([]);
    setPreviewImage("");
    setPreviewOpen(false);
    setIconMode('URL');
  };

  const handleCancel = () => {
    resetForm();
    onCancel();
  };

  const handleSubmit = async () => {
    try {
      const values = await form.validateFields();
      setLoading(true);

      const { icon, iconUrl, ...otherValues } = values;

      if (isEditMode) {
        let params = { ...otherValues };
        
        // 处理icon字段
        if (iconMode === 'BASE64' && icon) {
          params.icon = {
            type: "BASE64",
            value: icon,
          };
        } else if (iconMode === 'URL' && iconUrl) {
          params.icon = {
            type: "URL",
            value: iconUrl,
          };
        } else if (!icon && !iconUrl) {
          // 如果两种模式都没有提供icon,保持原有icon不变
          delete params.icon;
        }
        
        await apiProductApi.updateApiProduct(productId!, params);
        message.success("API Product 更新成功");
      } else {
        let params = { ...otherValues };
        
        // 处理icon字段
        if (iconMode === 'BASE64' && icon) {
          params.icon = {
            type: "BASE64",
            value: icon,
          };
        } else if (iconMode === 'URL' && iconUrl) {
          params.icon = {
            type: "URL",
            value: iconUrl,
          };
        }
        
        await apiProductApi.createApiProduct(params);
        message.success("API Product 创建成功");
      }

      resetForm();
      onSuccess();
    } catch (error: any) {
      if (error?.errorFields) return;
      message.error("操作失败");
    } finally {
      setLoading(false);
    }
  };

  return (
    <Modal
      title={isEditMode ? "编辑 API Product" : "创建 API Product"}
      open={visible}
      onOk={handleSubmit}
      onCancel={handleCancel}
      confirmLoading={loading}
      width={600}
    >
      <Form form={form} layout="vertical" preserve={false}>
        <Form.Item
          label="名称"
          name="name"
          rules={[{ required: true, message: "请输入API Product名称" }]}
        >
          <Input placeholder="请输入API Product名称" />
        </Form.Item>

        <Form.Item
          label="描述"
          name="description"
          rules={[{ required: true, message: "请输入描述" }]}
        >
          <Input.TextArea placeholder="请输入描述" rows={3} />
        </Form.Item>

        <Form.Item
          label="类型"
          name="type"
          rules={[{ required: true, message: "请选择类型" }]}
        >
          <Select placeholder="请选择类型">
            <Select.Option value="REST_API">REST API</Select.Option>
            <Select.Option value="MCP_SERVER">MCP Server</Select.Option>
          </Select>
        </Form.Item>

        <Form.Item
          label="自动审批订阅"
          name="autoApprove"
          tooltip={{
            title: (
              <div style={{ 
                color: '#000000', 
                backgroundColor: '#ffffff',
                fontSize: '13px',
                lineHeight: '1.4',
                padding: '4px 0'
              }}>
                启用后,该产品的订阅申请将自动审批通过,否则使用Portal的消费者订阅审批设置。
              </div>
            ),
            placement: "topLeft",
            overlayInnerStyle: {
              backgroundColor: '#ffffff',
              color: '#000000',
              border: '1px solid #d9d9d9',
              borderRadius: '6px',
              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
            },
            overlayStyle: {
              maxWidth: '300px'
            }
          }}
          valuePropName="checked"
        >
          <Switch />
        </Form.Item>

        <Form.Item label="Icon设置" style={{ marginBottom: '16px' }}>
          <Space direction="vertical" style={{ width: '100%' }}>
            <Radio.Group 
              value={iconMode} 
              onChange={(e) => handleIconModeChange(e.target.value)}
            >
              <Radio value="URL">图片链接</Radio>
              <Radio value="BASE64">本地上传</Radio>
            </Radio.Group>
            
            {iconMode === 'URL' ? (
              <Form.Item 
                name="iconUrl" 
                style={{ marginBottom: 0 }}
                rules={[
                  { 
                    type: 'url', 
                    message: '请输入有效的图片链接' 
                  }
                ]}
              >
                <Input placeholder="请输入图片链接地址" />
              </Form.Item>
            ) : (
              <Form.Item name="icon" style={{ marginBottom: 0 }}>
                <div 
                  style={{ 
                    width: '80px', 
                    height: '80px',
                    border: '1px dashed #d9d9d9',
                    borderRadius: '8px',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    cursor: 'pointer',
                    transition: 'border-color 0.3s',
                    position: 'relative'
                  }}
                  onClick={() => {
                    // 触发文件选择
                    const input = document.createElement('input');
                    input.type = 'file';
                    input.accept = 'image/*';
                    input.onchange = (e) => {
                      const file = (e.target as HTMLInputElement).files?.[0];
                      if (file) {
                        // 验证文件大小,限制为16KB
                        const maxSize = 16 * 1024; // 16KB
                        if (file.size > maxSize) {
                          message.error(`图片大小不能超过 16KB,当前图片大小为 ${Math.round(file.size / 1024)}KB`);
                          return;
                        }
                        
                        const newFileList: UploadFile[] = [{
                          uid: Date.now().toString(),
                          name: file.name,
                          status: 'done' as const,
                          url: URL.createObjectURL(file)
                        }];
                        setFileList(newFileList);
                        getBase64(file).then((base64) => {
                          form.setFieldsValue({ icon: base64 });
                        });
                      }
                    };
                    input.click();
                  }}
                  onMouseEnter={(e) => {
                    e.currentTarget.style.borderColor = '#1890ff';
                  }}
                  onMouseLeave={(e) => {
                    e.currentTarget.style.borderColor = '#d9d9d9';
                  }}
                >
                  {fileList.length >= 1 ? (
                    <img 
                      src={fileList[0].url} 
                      alt="uploaded" 
                      style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '6px' }}
                      onClick={(e) => {
                        e.stopPropagation();
                        // 预览图片
                        setPreviewImage(fileList[0].url || '');
                        setPreviewOpen(true);
                      }}
                    />
                  ) : (
                    uploadButton
                  )}
                  {fileList.length >= 1 && (
                    <div 
                      style={{ 
                        position: 'absolute', 
                        top: '4px', 
                        right: '4px', 
                        background: 'rgba(0, 0, 0, 0.5)', 
                        borderRadius: '50%', 
                        width: '16px', 
                        height: '16px', 
                        display: 'flex', 
                        alignItems: 'center', 
                        justifyContent: 'center',
                        cursor: 'pointer',
                        color: 'white',
                        fontSize: '10px'
                      }}
                      onClick={(e) => {
                        e.stopPropagation();
                        setFileList([]);
                        form.setFieldsValue({ icon: null });
                      }}
                    >
                      ×
                    </div>
                  )}
                </div>
              </Form.Item>
            )}
          </Space>
        </Form.Item>

        {/* 图片预览弹窗 */}
        {previewImage && (
          <Image
            wrapperStyle={{ display: "none" }}
            preview={{
              visible: previewOpen,
              onVisibleChange: (visible) => setPreviewOpen(visible),
              afterOpenChange: (visible) => {
                if (!visible) setPreviewImage("");
              },
            }}
            src={previewImage}
          />
        )}
      </Form>
    </Modal>
  );
}

```

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

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

import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.alibaba.apiopenplatform.core.constant.CommonConstants;
import com.alibaba.apiopenplatform.core.constant.IdpConstants;
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.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.TokenUtil;
import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.service.OidcService;
import com.alibaba.apiopenplatform.service.DeveloperService;
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.portal.AuthCodeConfig;
import com.alibaba.apiopenplatform.support.portal.IdentityMapping;
import com.alibaba.apiopenplatform.support.portal.OidcConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class OidcServiceImpl implements OidcService {

    private final PortalService portalService;

    private final DeveloperService developerService;

    private final RestTemplate restTemplate;

    private final ContextHolder contextHolder;

    @Override
    public String buildAuthorizationUrl(String provider, String apiPrefix, HttpServletRequest request) {
        OidcConfig oidcConfig = findOidcConfig(provider);
        AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();

        // state保存上下文信息
        String state = buildState(provider, apiPrefix);
        String redirectUri = buildRedirectUri(request);

        // 重定向URL
        String authUrl = UriComponentsBuilder
                .fromUriString(authCodeConfig.getAuthorizationEndpoint())
                // 授权码模式
                .queryParam(IdpConstants.RESPONSE_TYPE, IdpConstants.CODE)
                .queryParam(IdpConstants.CLIENT_ID, authCodeConfig.getClientId())
                .queryParam(IdpConstants.REDIRECT_URI, redirectUri)
                .queryParam(IdpConstants.SCOPE, authCodeConfig.getScopes())
                .queryParam(IdpConstants.STATE, state)
                .build()
                .toUriString();

        log.info("Generated OIDC authorization URL: {}", authUrl);
        return authUrl;
    }

    @Override
    public AuthResult handleCallback(String code, String state, HttpServletRequest request, HttpServletResponse response) {
        log.info("Processing OIDC callback with code: {}, state: {}", code, state);

        // 解析state获取provider信息
        IdpState idpState = parseState(state);
        String provider = idpState.getProvider();

        if (StrUtil.isBlank(provider)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "缺少OIDC provider");
        }

        OidcConfig oidcConfig = findOidcConfig(provider);

        // 使用授权码获取Token
        IdpTokenResult tokenResult = requestToken(code, oidcConfig, request);

        // 获取用户信息,优先使用ID Token,降级到UserInfo端点
        Map<String, Object> userInfo = getUserInfo(tokenResult, oidcConfig);
        log.info("Get OIDC user info: {}", userInfo);

        // 处理用户认证逻辑
        String developerId = createOrGetDeveloper(userInfo, oidcConfig);
        String accessToken = TokenUtil.generateDeveloperToken(developerId);

        return AuthResult.of(accessToken, TokenUtil.getTokenExpiresIn());
    }

    @Override
    public List<IdpResult> getAvailableProviders() {
        return Optional.ofNullable(portalService.getPortal(contextHolder.getPortal()))
                .filter(portal -> portal.getPortalSettingConfig() != null)
                .filter(portal -> portal.getPortalSettingConfig().getOidcConfigs() != null)
                .map(portal -> portal.getPortalSettingConfig().getOidcConfigs())
                // 确定当前Portal下启用的OIDC配置,返回Idp信息
                .map(configs -> configs.stream()
                        .filter(OidcConfig::isEnabled)
                        .map(config -> IdpResult.builder()
                                .provider(config.getProvider())
                                .displayName(config.getName())
                                .build())
                        .collect(Collectors.toList()))
                .orElse(Collections.emptyList());
    }

    private String buildRedirectUri(HttpServletRequest request) {
        String scheme = request.getScheme();
//        String serverName = "localhost";
//        int serverPort = 5173;
        String serverName = request.getServerName();
        int serverPort = request.getServerPort();

        String baseUrl = scheme + "://" + serverName;
        if (serverPort != CommonConstants.HTTP_PORT && serverPort != CommonConstants.HTTPS_PORT) {
            baseUrl += ":" + serverPort;
        }

        // 重定向到前端的Callback接口
        return baseUrl + "/oidc/callback";
    }

    private OidcConfig findOidcConfig(String provider) {
        return Optional.ofNullable(portalService.getPortal(contextHolder.getPortal()))
                .filter(portal -> portal.getPortalSettingConfig() != null)
                .filter(portal -> portal.getPortalSettingConfig().getOidcConfigs() != null)
                // 根据provider字段过滤
                .flatMap(portal -> portal.getPortalSettingConfig()
                        .getOidcConfigs()
                        .stream()
                        .filter(config -> provider.equals(config.getProvider()) && config.isEnabled())
                        .findFirst())
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.OIDC_CONFIG, provider));
    }

    private String buildState(String provider, String apiPrefix) {
        IdpState state = IdpState.builder()
                .provider(provider)
                .timestamp(System.currentTimeMillis())
                .nonce(IdUtil.fastSimpleUUID())
                .apiPrefix(apiPrefix)
                .build();
        return Base64.encode(JSONUtil.toJsonStr(state));
    }

    private IdpState parseState(String encodedState) {
        String stateJson = Base64.decodeStr(encodedState);
        IdpState idpState = JSONUtil.toBean(stateJson, IdpState.class);

        // 验证时间戳,10分钟有效期
        if (idpState.getTimestamp() != null) {
            long currentTime = System.currentTimeMillis();
            if (currentTime - idpState.getTimestamp() > 10 * 60 * 1000) {
                throw new BusinessException(ErrorCode.INVALID_REQUEST, "请求已过期");
            }
        }

        return idpState;
    }

    private IdpTokenResult requestToken(String code, OidcConfig oidcConfig, HttpServletRequest request) {
        AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();
        String redirectUri = buildRedirectUri(request);

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add(IdpConstants.GRANT_TYPE, GrantType.AUTHORIZATION_CODE.getType());
        params.add(IdpConstants.CODE, code);
        params.add(IdpConstants.REDIRECT_URI, redirectUri);
        params.add(IdpConstants.CLIENT_ID, authCodeConfig.getClientId());
        params.add(IdpConstants.CLIENT_SECRET, authCodeConfig.getClientSecret());

        log.info("Request tokens at: {}, params: {}", authCodeConfig.getTokenEndpoint(), params);
        return executeRequest(authCodeConfig.getTokenEndpoint(), HttpMethod.POST, null, params, IdpTokenResult.class);
    }

    private Map<String, Object> getUserInfo(IdpTokenResult tokenResult, OidcConfig oidcConfig) {
        // 优先使用ID Token
        if (StrUtil.isNotBlank(tokenResult.getIdToken())) {
            log.info("Get user info form id token: {}", tokenResult.getIdToken());
            return parseUserInfo(tokenResult.getIdToken(), oidcConfig);
        }

        // 降级策略:使用UserInfo端点
        log.warn("ID Token not available, falling back to UserInfo endpoint");
        if (StrUtil.isBlank(tokenResult.getAccessToken())) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "OIDC获取用户信息失败");
        }

        AuthCodeConfig authCodeConfig = oidcConfig.getAuthCodeConfig();
        if (StrUtil.isBlank(authCodeConfig.getUserInfoEndpoint())) {
            throw new BusinessException(ErrorCode.INVALID_PARAMETER, "OIDC配置缺少用户信息端点");
        }

        return requestUserInfo(tokenResult.getAccessToken(), authCodeConfig, oidcConfig);
    }

    private Map<String, Object> parseUserInfo(String idToken, OidcConfig oidcConfig) {
        JWT jwt = JWTUtil.parseToken(idToken);

        // 验证过期时间
        Object exp = jwt.getPayload("exp");
        if (exp != null) {
            long expTime = Convert.toLong(exp);
            long currentTime = System.currentTimeMillis() / 1000;
            if (expTime <= currentTime) {
                throw new BusinessException(ErrorCode.INVALID_REQUEST, "ID Token已过期");
            }
        }
        // TODO 验签

        Map<String, Object> userInfo = jwt.getPayload().getClaimsJson();

        log.info("Successfully extracted user info from ID Token, sub: {}", userInfo);
        return userInfo;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> requestUserInfo(String accessToken, AuthCodeConfig authCodeConfig, OidcConfig oidcConfig) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setBearerAuth(accessToken);

            log.info("Fetching user info from endpoint: {}", authCodeConfig.getUserInfoEndpoint());
            Map<String, Object> userInfo = executeRequest(authCodeConfig.getUserInfoEndpoint(), HttpMethod.GET, headers, null, Map.class);

            log.info("Successfully fetched user info from endpoint, sub: {}", userInfo);
            return userInfo;
        } catch (Exception e) {
            log.error("Failed to fetch user info from endpoint: {}", authCodeConfig.getUserInfoEndpoint(), e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "获取用户信息失败");
        }
    }

    private String createOrGetDeveloper(Map<String, Object> userInfo, OidcConfig config) {
        IdentityMapping identityMapping = config.getIdentityMapping();
        // userId & userName & email
        String userIdField = StrUtil.isBlank(identityMapping.getUserIdField()) ?
                IdpConstants.SUBJECT : identityMapping.getUserIdField();
        String userNameField = StrUtil.isBlank(identityMapping.getUserNameField()) ?
                IdpConstants.NAME : identityMapping.getUserNameField();
        String emailField = StrUtil.isBlank(identityMapping.getEmailField()) ?
                IdpConstants.EMAIL : identityMapping.getEmailField();

        Object userIdObj = userInfo.get(userIdField);
        Object userNameObj = userInfo.get(userNameField);
        Object emailObj = userInfo.get(emailField);

        String userId = Convert.toStr(userIdObj);
        String userName = Convert.toStr(userNameObj);
        String email = Convert.toStr(emailObj);
        if (StrUtil.isBlank(userId) || StrUtil.isBlank(userName)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "Id Token中缺少用户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)
                            .email(email)
                            .authType(DeveloperAuthType.OIDC)
                            .build();

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

    private <T> T executeRequest(String url, HttpMethod method, HttpHeaders headers, Object body, Class<T> responseType) {
        HttpEntity<?> requestEntity = new HttpEntity<>(body, headers);
        log.info("Executing HTTP request to: {}", url);
        ResponseEntity<String> response = restTemplate.exchange(
                url,
                method,
                requestEntity,
                String.class
        );

        log.info("Received HTTP response from: {}, status: {}, body: {}", url, response.getStatusCode(), response.getBody());

        return JSONUtil.toBean(response.getBody(), responseType);
    }
}

```

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

```typescript
import {Card, Form, Input, Select, Switch, Button, Divider, Space, Tag, Table, Modal, message, Tabs} from 'antd'
import {SaveOutlined, PlusOutlined, DeleteOutlined, ExclamationCircleOutlined} from '@ant-design/icons'
import {useState, useMemo} from 'react'
import {Portal, ThirdPartyAuthConfig, AuthenticationType, OidcConfig, OAuth2Config} from '@/types'
import {portalApi} from '@/lib/api'
import {ThirdPartyAuthManager} from './ThirdPartyAuthManager'

interface PortalSettingsProps {
    portal: Portal
    onRefresh?: () => void
}

export function PortalSettings({portal, onRefresh}: PortalSettingsProps) {
    const [form] = Form.useForm()
    const [loading, setLoading] = useState(false)
    const [domainModalVisible, setDomainModalVisible] = useState(false)
    const [domainForm] = Form.useForm()
    const [domainLoading, setDomainLoading] = useState(false)


    // 本地OIDC配置状态,避免频繁刷新
    // local的有点问题,一切tab就坏了


    const handleSave = async () => {
        try {
            setLoading(true)
            const values = await form.validateFields()
            
            await portalApi.updatePortal(portal.portalId, {
                name: portal.name, // 保持现有名称不变
                description: portal.description, // 保持现有描述不变
                portalSettingConfig: {
                    ...portal.portalSettingConfig,
                    builtinAuthEnabled: values.builtinAuthEnabled,
                    oidcAuthEnabled: values.oidcAuthEnabled,
                    autoApproveDevelopers: values.autoApproveDevelopers,
                    autoApproveSubscriptions: values.autoApproveSubscriptions,
                    frontendRedirectUrl: values.frontendRedirectUrl,
                },
                portalDomainConfig: portal.portalDomainConfig,
                portalUiConfig: portal.portalUiConfig,
            })

            message.success('Portal设置保存成功')
            onRefresh?.()
        } catch (error) {
            message.error('保存Portal设置失败')
        } finally {
            setLoading(false)
        }
    }

    const handleSettingUpdate = async (key: string, value: any) => {
        try {
            await portalApi.updatePortal(portal.portalId, {
                ...portal,
                portalSettingConfig: {
                    ...portal.portalSettingConfig,
                    [key]: value
                }
            })
            message.success('设置已更新')
            onRefresh?.()
        } catch (error) {
            message.error('设置更新失败')
        }
    }

    const handleAddDomain = () => {
        setDomainModalVisible(true)
        domainForm.resetFields()
    }

    const handleDomainModalOk = async () => {
        try {
            setDomainLoading(true)
            const values = await domainForm.validateFields()

            const newDomain = {
                domain: values.domain,
                protocol: values.protocol,
                type: 'CUSTOM'
            }

            await portalApi.bindDomain(portal.portalId, newDomain)
            message.success('域名绑定成功')
            onRefresh?.()
            setDomainModalVisible(false)

        } catch (error) {
            message.error('绑定域名失败')
        } finally {
            setDomainLoading(false)
        }
    }

    const handleDomainModalCancel = () => {
        setDomainModalVisible(false)
        domainForm.resetFields()
    }

    const handleDeleteDomain = async (domain: string) => {
        Modal.confirm({
            title: '确认解绑',
            icon: <ExclamationCircleOutlined/>,
            content: `确定要解绑域名 "${domain}" 吗?此操作不可恢复。`,
            okText: '确认解绑',
            okType: 'danger',
            cancelText: '取消',
            async onOk() {
                try {
                    await portalApi.unbindDomain(portal.portalId, domain)
                    message.success('域名解绑成功')
                    onRefresh?.()
                } catch (error) {
                    message.error('解绑域名失败')
                }
            },
        })
    }

    // 合并OIDC和OAuth2配置用于统一显示
    const thirdPartyAuthConfigs = useMemo((): ThirdPartyAuthConfig[] => {
        const configs: ThirdPartyAuthConfig[] = []
        
        // 添加OIDC配置
        if (portal.portalSettingConfig?.oidcConfigs) {
            portal.portalSettingConfig.oidcConfigs.forEach(oidcConfig => {
                configs.push({
                    ...oidcConfig,
                    type: AuthenticationType.OIDC
                })
            })
        }
        
        // 添加OAuth2配置
        if (portal.portalSettingConfig?.oauth2Configs) {
            portal.portalSettingConfig.oauth2Configs.forEach(oauth2Config => {
                configs.push({
                    ...oauth2Config,
                    type: AuthenticationType.OAUTH2
                })
            })
        }
        
        return configs
    }, [portal.portalSettingConfig?.oidcConfigs, portal.portalSettingConfig?.oauth2Configs])

    // 第三方认证配置保存函数
    const handleSaveThirdPartyAuth = async (configs: ThirdPartyAuthConfig[]) => {
        try {
            // 分离OIDC和OAuth2配置,去掉type字段
            const oidcConfigs = configs
                .filter(config => config.type === AuthenticationType.OIDC)
                .map(config => {
                    const { type, ...oidcConfig } = config as (OidcConfig & { type: AuthenticationType.OIDC })
                    return oidcConfig
                })

            const oauth2Configs = configs
                .filter(config => config.type === AuthenticationType.OAUTH2)
                .map(config => {
                    const { type, ...oauth2Config } = config as (OAuth2Config & { type: AuthenticationType.OAUTH2 })
                    return oauth2Config
                })
            
            const updateData = {
                ...portal,
                portalSettingConfig: {
                    ...portal.portalSettingConfig,
                    // 直接保存分离的配置数组
                    oidcConfigs: oidcConfigs,
                    oauth2Configs: oauth2Configs
                }
            }
            
            await portalApi.updatePortal(portal.portalId, updateData)
            
            onRefresh?.()
        } catch (error) {
            throw error
        }
    }

    // 域名表格列定义
    const domainColumns = [
        {
            title: '域名',
            dataIndex: 'domain',
            key: 'domain',
        },
        {
            title: '协议',
            dataIndex: 'protocol',
            key: 'protocol',
        },
        {
            title: '类型',
            dataIndex: 'type',
            key: 'type',
            render: (type: string) => (
                <Tag color={type === 'DEFAULT' ? 'blue' : 'green'}>
                    {type === 'DEFAULT' ? '默认域名' : '自定义域名'}
                </Tag>
            )
        },
        {
            title: '操作',
            key: 'action',
            render: (_: any, record: any) => (
                <Space>
                    {record.type === 'CUSTOM' && (
                        <Button
                            type="link"
                            danger
                            icon={<DeleteOutlined/>}
                            onClick={() => handleDeleteDomain(record.domain)}
                        >
                            解绑
                        </Button>
                    )}
                </Space>
            )
        }
    ]

    const tabItems = [
        {
            key: 'auth',
            label: '安全设置',
            children: (
                <div className="space-y-6">
                    {/* 基本安全设置 */}
                    <div className="grid grid-cols-2 gap-6">
                        <Form.Item
                            name="builtinAuthEnabled"
                            label="账号密码登录"
                            valuePropName="checked"
                        >
                            <Switch
                                onChange={(checked) => handleSettingUpdate('builtinAuthEnabled', checked)}
                            />
                        </Form.Item>
                        {/* <Form.Item
              name="oidcAuthEnabled"
              label="OIDC认证"
              valuePropName="checked"
            >
              <Switch 
                onChange={(checked) => handleSettingUpdate('oidcAuthEnabled', checked)}
              />
            </Form.Item> */}
                        <Form.Item
                            name="autoApproveDevelopers"
                            label="开发者自动审批"
                            valuePropName="checked"
                        >
                            <Switch
                                onChange={(checked) => handleSettingUpdate('autoApproveDevelopers', checked)}
                            />
                        </Form.Item>
                        <Form.Item
                            name="autoApproveSubscriptions"
                            label="订阅自动审批"
                            valuePropName="checked"
                        >
                            <Switch
                                onChange={(checked) => handleSettingUpdate('autoApproveSubscriptions', checked)}
                            />
                        </Form.Item>
                    </div>

                    {/* 第三方认证管理 */}
                    <Divider/>
                    <ThirdPartyAuthManager
                        configs={thirdPartyAuthConfigs}
                        onSave={handleSaveThirdPartyAuth}
                    />
                </div>
            )
        },
        {
            key: 'domain',
            label: '域名管理',
            children: (
                <div>
                    <div className="flex justify-between items-center mb-4">
                        <div>
                            <h3 className="text-lg font-medium">域名列表</h3>
                            <p className="text-sm text-gray-500">管理Portal的域名配置</p>
                        </div>
                        <Button
                            type="primary"
                            icon={<PlusOutlined/>}
                            onClick={handleAddDomain}
                        >
                            绑定域名
                        </Button>
                    </div>
                    <Table
                        columns={domainColumns}
                        dataSource={portal.portalDomainConfig || []}
                        rowKey="domain"
                        pagination={false}
                        size="small"
                    />
                </div>
            )
        }
    ]

    return (
        <div className="p-6 space-y-6">
            <div className="flex justify-between items-center">
                <div>
                    <h1 className="text-2xl font-bold mb-2">Portal设置</h1>
                    <p className="text-gray-600">配置Portal的基本设置和高级选项</p>
                </div>
                <Space>
                    <Button type="primary" icon={<SaveOutlined/>} loading={loading} onClick={handleSave}>
                        保存设置
                    </Button>
                </Space>
            </div>

            <Form
                form={form}
                layout="vertical"
                initialValues={{
                    portalSettingConfig: portal.portalSettingConfig,
                    builtinAuthEnabled: portal.portalSettingConfig?.builtinAuthEnabled,
                    oidcAuthEnabled: portal.portalSettingConfig?.oidcAuthEnabled,
                    autoApproveDevelopers: portal.portalSettingConfig?.autoApproveDevelopers,
                    autoApproveSubscriptions: portal.portalSettingConfig?.autoApproveSubscriptions,
                    frontendRedirectUrl: portal.portalSettingConfig?.frontendRedirectUrl,
                    portalDomainConfig: portal.portalDomainConfig,
                }}
            >
                <Card>
                    <Tabs
                        items={tabItems}
                        defaultActiveKey="auth"
                        type="card"
                    />
                </Card>
            </Form>

            {/* 域名绑定模态框 */}
            <Modal
                title="绑定域名"
                open={domainModalVisible}
                onOk={handleDomainModalOk}
                onCancel={handleDomainModalCancel}
                confirmLoading={domainLoading}
                okText="绑定"
                cancelText="取消"
            >
                <Form
                    form={domainForm}
                    layout="vertical"
                >
                    <Form.Item
                        name="domain"
                        label="域名"
                        rules={[
                            {required: true, message: '请输入域名'},
                            {
                                pattern: /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
                                message: '请输入有效的域名格式'
                            }
                        ]}
                    >
                        <Input placeholder="example.com"/>
                    </Form.Item>
                    <Form.Item
                        name="protocol"
                        label="协议"
                        rules={[{required: true, message: '请选择协议'}]}
                    >
                        <Select placeholder="请选择协议">
                            <Select.Option value="HTTP">HTTP</Select.Option>
                            <Select.Option value="HTTPS">HTTPS</Select.Option>
                        </Select>
                    </Form.Item>
                </Form>
            </Modal>
        </div>
    )
}
```

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

```typescript
import { useState, useCallback, memo, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
  Button,
  Card,
  Avatar,
  Dropdown,
  Modal,
  Form,
  Input,
  message,
  Tooltip,
  Pagination,
  Skeleton,
} from "antd";
import { PlusOutlined, MoreOutlined, LinkOutlined } from "@ant-design/icons";
import type { MenuProps } from "antd";
import { portalApi } from "../lib/api";

import { Portal } from '@/types'

// 优化的Portal卡片组件
const PortalCard = memo(
  ({
    portal,
    onNavigate,
    fetchPortals,
  }: {
    portal: Portal;
    onNavigate: (id: string) => void;
    fetchPortals: () => void;
  }) => {
    const handleCardClick = useCallback(() => {
      onNavigate(portal.portalId);
    }, [portal.portalId, onNavigate]);

    const handleLinkClick = useCallback((e: React.MouseEvent) => {
      e.stopPropagation();
    }, []);

    const dropdownItems: MenuProps["items"] = [
     
      {
        key: "delete",
        label: "删除",
        danger: true,
        onClick: (e) => {
          e?.domEvent?.stopPropagation(); // 阻止事件冒泡
          Modal.confirm({
            title: "删除Portal",
            content: "确定要删除该Portal吗?",
            onOk: () => {
              return handleDeletePortal(portal.portalId);
            },
          });
        },
      },
    ];

    const handleDeletePortal = useCallback((portalId: string) => {
      return portalApi.deletePortal(portalId).then(() => {
        message.success("Portal删除成功");
        fetchPortals();
      }).catch((error) => {
        message.error(error?.response?.data?.message || "删除失败,请稍后重试");
        throw error;
      });
    }, [fetchPortals]);

    return (
      <Card
        className="cursor-pointer hover:shadow-xl transition-all duration-300 hover:scale-[1.02] border border-gray-100 hover:border-blue-300 bg-gradient-to-br from-white to-gray-50/30"
        onClick={handleCardClick}
        bodyStyle={{ padding: "20px" }}
      >
        <div className="flex items-center justify-between mb-6">
          <div className="flex items-center space-x-4">
            <div className="relative">
              <Avatar
                size={48}
                className="bg-gradient-to-br from-blue-500 to-blue-600 shadow-lg"
                style={{ fontSize: "18px", fontWeight: "600" }}
              >
                {portal.title.charAt(0).toUpperCase()}
              </Avatar>
              <div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-400 rounded-full border-2 border-white"></div>
            </div>
            <div>
              <h3 className="text-xl font-bold text-gray-800 mb-1">
                {portal.title}
              </h3>
              <p className="text-sm text-gray-500">{portal.description}</p>
            </div>
          </div>
          <Dropdown menu={{ items: dropdownItems }} trigger={["click"]}>
            <Button
              type="text"
              icon={<MoreOutlined />}
              onClick={(e) => e.stopPropagation()}
              className="hover:bg-gray-100 rounded-full"
            />
          </Dropdown>
        </div>

        <div className="space-y-6">
          <div className="flex items-center space-x-3 p-3 bg-blue-50 rounded-lg border border-blue-100">
            <LinkOutlined className="h-4 w-4 text-blue-500" />
            <Tooltip
              title={portal.portalDomainConfig?.[0].domain}
              placement="top"
              color="#000"
            >
              <a
                href={`http://${portal.portalDomainConfig?.[0].domain}`}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-600 hover:text-blue-700 font-medium text-sm"
                onClick={handleLinkClick}
                style={{
                  display: "inline-block",
                  maxWidth: 200,
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                  whiteSpace: "nowrap",
                  verticalAlign: "bottom",
                  cursor: "pointer",
                }}
              >
                {portal.portalDomainConfig?.[0].domain}
              </a>
            </Tooltip>
          </div>

          <div className="space-y-3">
            {/* 第一行:账号密码登录 + 开发者自动审批 */}
            <div className="grid grid-cols-2 gap-4">
              <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
                <span className="text-xs font-medium text-gray-600">
                  账号密码登录
                </span>
                <span
                  className={`px-2 py-1 rounded-full text-xs font-medium ${
                    portal.portalSettingConfig?.builtinAuthEnabled
                      ? "bg-green-100 text-green-700"
                      : "bg-red-100 text-red-700"
                  }`}
                >
                  {portal.portalSettingConfig?.builtinAuthEnabled
                    ? "支持"
                    : "不支持"}
                </span>
              </div>

              <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
                <span className="text-xs font-medium text-gray-600">
                  开发者自动审批
                </span>
                <span
                  className={`px-2 py-1 rounded-full text-xs font-medium ${
                    portal.portalSettingConfig?.autoApproveDevelopers
                      ? "bg-green-100 text-green-700"
                      : "bg-yellow-100 text-yellow-700"
                  }`}
                >
                  {portal.portalSettingConfig?.autoApproveDevelopers
                    ? "是"
                    : "否"}
                </span>
              </div>
            </div>

            {/* 第二行:订阅自动审批 + 域名配置 */}
            <div className="grid grid-cols-2 gap-4">
              <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
                <span className="text-xs font-medium text-gray-600">
                  订阅自动审批
                </span>
                <span
                  className={`px-2 py-1 rounded-full text-xs font-medium ${
                    portal.portalSettingConfig?.autoApproveSubscriptions
                      ? "bg-green-100 text-green-700"
                      : "bg-yellow-100 text-yellow-700"
                  }`}
                >
                  {portal.portalSettingConfig?.autoApproveSubscriptions
                    ? "是"
                    : "否"}
                </span>
              </div>

              <div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
                <span className="text-xs font-medium text-gray-600">
                  域名配置
                </span>
                <span className="px-2 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-medium">
                  {portal.portalDomainConfig?.length || 0}个
                </span>
              </div>
            </div>
          </div>

          <div className="text-center pt-4 border-t border-gray-100">
            <div className="inline-flex items-center space-x-2 text-sm text-gray-500 hover:text-blue-600 transition-colors">
              <span onClick={handleCardClick}>点击查看详情</span>
              <svg
                className="w-4 h-4"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M9 5l7 7-7 7"
                />
              </svg>
            </div>
          </div>
        </div>
      </Card>
    );
  }
);

PortalCard.displayName = "PortalCard";

export default function Portals() {
  const navigate = useNavigate();
  const [portals, setPortals] = useState<Portal[]>([]);
  const [loading, setLoading] = useState<boolean>(true); // 初始状态为 loading
  const [error, setError] = useState<string | null>(null);
  const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
  const [form] = Form.useForm();
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 12,
    total: 0,
  });

  const fetchPortals = useCallback((page = 1, size = 12) => {
    setLoading(true);
    portalApi.getPortals({ page, size }).then((res: any) => {
      const list = res?.data?.content || [];
      const portals: Portal[] = list.map((item: any) => ({
        portalId: item.portalId,
        name: item.name,
        title: item.name,
        description: item.description,
        adminId: item.adminId,
        portalSettingConfig: item.portalSettingConfig,
        portalUiConfig: item.portalUiConfig,
        portalDomainConfig: item.portalDomainConfig || [],
      }));
      setPortals(portals);
      setPagination({
        current: page,
        pageSize: size,
        total: res?.data?.totalElements || 0,
      });
    }).catch((err: any) => {
      setError(err?.message || "加载失败");
    }).finally(() => {
      setLoading(false);
    });
  }, []);

  useEffect(() => {
    setError(null);
    fetchPortals(1, 12);
  }, [fetchPortals]);

  // 处理分页变化
  const handlePaginationChange = (page: number, pageSize: number) => {
    fetchPortals(page, pageSize);
  };

  const handleCreatePortal = useCallback(() => {
    setIsModalVisible(true);
  }, []);

  const handleModalOk = useCallback(async () => {
    try {
      const values = await form.validateFields();
      setLoading(true);

      const newPortal = {
        name: values.name,
        title: values.title,
        description: values.description,
      };

      await portalApi.createPortal(newPortal);
      message.success("Portal创建成功");
      setIsModalVisible(false);
      form.resetFields();

      fetchPortals()
    } catch (error: any) {
      // message.error(error?.message || "创建失败");
    } finally {
      setLoading(false);
    }
  }, [form]);

  const handleModalCancel = useCallback(() => {
    setIsModalVisible(false);
    form.resetFields();
  }, [form]);

  const handlePortalClick = useCallback(
    (portalId: string) => {
      navigate(`/portals/detail?id=${portalId}`);
    },
    [navigate]
  );

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold tracking-tight">Portal</h1>
          <p className="text-gray-500 mt-2">管理和配置您的开发者门户</p>
        </div>
        <Button
          type="primary"
          icon={<PlusOutlined />}
          onClick={handleCreatePortal}
        >
          创建 Portal
        </Button>
      </div>
      {error && <div className="text-red-500">{error}</div>}
      
      {loading ? (
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {Array.from({ length: pagination.pageSize || 12 }).map((_, index) => (
            <div key={index} className="h-full rounded-lg shadow-lg bg-white p-4">
              <div className="flex items-start space-x-4">
                <Skeleton.Avatar size={48} active />
                <div className="flex-1 min-w-0">
                  <div className="flex items-center justify-between mb-2">
                    <Skeleton.Input active size="small" style={{ width: 120 }} />
                    <Skeleton.Input active size="small" style={{ width: 60 }} />
                  </div>
                  <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
                  <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
                  <div className="flex items-center justify-between">
                    <Skeleton.Input active size="small" style={{ width: 60 }} />
                    <Skeleton.Input active size="small" style={{ width: 80 }} />
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
      ) : (
        <>
          <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
            {portals.map((portal) => (
              <PortalCard
                key={portal.portalId}
                portal={portal}
                onNavigate={handlePortalClick}
                fetchPortals={() => fetchPortals(pagination.current, pagination.pageSize)}
              />
            ))}
          </div>

          {pagination.total > 0 && (
            <div className="flex justify-center mt-6">
              <Pagination
                current={pagination.current}
                pageSize={pagination.pageSize}
                total={pagination.total}
                onChange={handlePaginationChange}
                showSizeChanger
                showQuickJumper
                showTotal={(total) => `共 ${total} 条`}
                pageSizeOptions={['6', '12', '24', '48']}
              />
            </div>
          )}
        </>
      )}

      <Modal
        title="创建Portal"
        open={isModalVisible}
        onOk={handleModalOk}
        onCancel={handleModalCancel}
        confirmLoading={loading}
        width={600}
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="name"
            label="名称"
            rules={[{ required: true, message: "请输入Portal名称" }]}
          >
            <Input placeholder="请输入Portal名称" />
          </Form.Item>

          {/* <Form.Item
            name="title"
            label="标题"
            rules={[{ required: true, message: "请输入Portal标题" }]}
          >
            <Input placeholder="请输入Portal标题" />
          </Form.Item> */}

          <Form.Item
            name="description"
            label="描述"
            rules={[{ message: "请输入描述" }]}
          >
            <Input.TextArea rows={3} placeholder="请输入Portal描述" />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
}

```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/DeveloperServiceImpl.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.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.event.DeveloperDeletingEvent;
import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
import com.alibaba.apiopenplatform.core.utils.TokenUtil;
import com.alibaba.apiopenplatform.dto.params.developer.CreateDeveloperParam;
import com.alibaba.apiopenplatform.dto.params.developer.CreateExternalDeveloperParam;
import com.alibaba.apiopenplatform.dto.params.developer.QueryDeveloperParam;
import com.alibaba.apiopenplatform.dto.params.developer.UpdateDeveloperParam;
import com.alibaba.apiopenplatform.dto.result.AuthResult;
import com.alibaba.apiopenplatform.dto.result.DeveloperResult;
import com.alibaba.apiopenplatform.dto.result.PageResult;
import com.alibaba.apiopenplatform.entity.Developer;
import com.alibaba.apiopenplatform.entity.Portal;
import com.alibaba.apiopenplatform.repository.DeveloperRepository;
import com.alibaba.apiopenplatform.repository.PortalRepository;
import com.alibaba.apiopenplatform.service.DeveloperService;
import com.alibaba.apiopenplatform.core.utils.PasswordHasher;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.repository.DeveloperExternalIdentityRepository;
import com.alibaba.apiopenplatform.entity.DeveloperExternalIdentity;
import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType;
import com.alibaba.apiopenplatform.support.enums.DeveloperStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;

import javax.persistence.criteria.Predicate;
import java.util.*;
import javax.servlet.http.HttpServletRequest;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class DeveloperServiceImpl implements DeveloperService {

    private final DeveloperRepository developerRepository;

    private final DeveloperExternalIdentityRepository externalRepository;

    private final PortalRepository portalRepository;

    private final ContextHolder contextHolder;

    private final ApplicationEventPublisher eventPublisher;

    @Override
    public AuthResult registerDeveloper(CreateDeveloperParam param) {
        DeveloperResult developer = createDeveloper(param);

        // 检查是否自动审批
        String portalId = contextHolder.getPortal();
        Portal portal = findPortal(portalId);
        boolean autoApprove = portal.getPortalSettingConfig() != null
                && BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveDevelopers());

        if (autoApprove) {
            String token = generateToken(developer.getDeveloperId());
            return AuthResult.of(token, TokenUtil.getTokenExpiresIn());
        }
        return null;
    }

    @Override
    public DeveloperResult createDeveloper(CreateDeveloperParam param) {
        String portalId = contextHolder.getPortal();
        developerRepository.findByPortalIdAndUsername(portalId, param.getUsername()).ifPresent(developer -> {
            throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.DEVELOPER, param.getUsername()));
        });

        Developer developer = param.convertTo();
        developer.setDeveloperId(generateDeveloperId());
        developer.setPortalId(portalId);
        developer.setPasswordHash(PasswordHasher.hash(param.getPassword()));

        Portal portal = findPortal(portalId);
        boolean autoApprove = portal.getPortalSettingConfig() != null
                && BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveDevelopers());
        developer.setStatus(autoApprove ? DeveloperStatus.APPROVED : DeveloperStatus.PENDING);
        developer.setAuthType(DeveloperAuthType.BUILTIN);

        developerRepository.save(developer);
        return new DeveloperResult().convertFrom(developer);
    }

    @Override
    public AuthResult login(String username, String password) {
        String portalId = contextHolder.getPortal();
        Developer developer = developerRepository.findByPortalIdAndUsername(portalId, username)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, username));

        if (!DeveloperStatus.APPROVED.equals(developer.getStatus())) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "账号审批中");
        }

        if (!PasswordHasher.verify(password, developer.getPasswordHash())) {
            throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
        }

        String token = generateToken(developer.getDeveloperId());
        return AuthResult.builder()
                .accessToken(token)
                .expiresIn(TokenUtil.getTokenExpiresIn())
                .build();
    }

    @Override
    public void existsDeveloper(String developerId) {
        developerRepository.findByDeveloperId(developerId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, developerId));
    }

    @Override
    public DeveloperResult createExternalDeveloper(CreateExternalDeveloperParam param) {
        Developer developer = Developer.builder()
                .developerId(IdGenerator.genDeveloperId())
                .portalId(contextHolder.getPortal())
                .username(buildExternalName(param.getProvider(), param.getDisplayName()))
                .email(param.getEmail())
                // 默认APPROVED
                .status(DeveloperStatus.APPROVED)
                .build();

        DeveloperExternalIdentity externalIdentity = DeveloperExternalIdentity.builder()
                .provider(param.getProvider())
                .subject(param.getSubject())
                .displayName(param.getDisplayName())
                .authType(param.getAuthType())
                .developer(developer)
                .build();

        developerRepository.save(developer);
        externalRepository.save(externalIdentity);
        return new DeveloperResult().convertFrom(developer);
    }

    @Override
    public DeveloperResult getExternalDeveloper(String provider, String subject) {
        return externalRepository.findByProviderAndSubject(provider, subject)
                .map(o -> new DeveloperResult().convertFrom(o.getDeveloper()))
                .orElse(null);
    }

    private String buildExternalName(String provider, String subject) {
        return StrUtil.format("{}_{}", provider, subject);
    }

    @Override
    public void deleteDeveloper(String developerId) {
        eventPublisher.publishEvent(new DeveloperDeletingEvent(developerId));
        externalRepository.deleteByDeveloper_DeveloperId(developerId);
        developerRepository.findByDeveloperId(developerId).ifPresent(developerRepository::delete);
    }

    @Override
    public DeveloperResult getDeveloper(String developerId) {
        Developer developer = findDeveloper(developerId);
        return new DeveloperResult().convertFrom(developer);
    }

    @Override
    public PageResult<DeveloperResult> listDevelopers(QueryDeveloperParam param, Pageable pageable) {
        if (contextHolder.isDeveloper()) {
            param.setPortalId(contextHolder.getPortal());
        }
        Page<Developer> developers = developerRepository.findAll(buildSpecification(param), pageable);
        return new PageResult<DeveloperResult>().convertFrom(developers, developer -> new DeveloperResult().convertFrom(developer));
    }

    @Override
    public void setDeveloperStatus(String developerId, DeveloperStatus status) {
        Developer developer = findDeveloper(developerId);
        developer.setStatus(status);
        developerRepository.save(developer);
    }

    @Override
    @Transactional
    public boolean resetPassword(String developerId, String oldPassword, String newPassword) {
        Developer developer = findDeveloper(developerId);

        if (!PasswordHasher.verify(oldPassword, developer.getPasswordHash())) {
            throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
        }

        developer.setPasswordHash(PasswordHasher.hash(newPassword));
        developerRepository.save(developer);
        return true;
    }

    @Override
    public boolean updateProfile(UpdateDeveloperParam param) {
        Developer developer = findDeveloper(contextHolder.getUser());

        String username = param.getUsername();
        if (username != null && !username.equals(developer.getUsername())) {
            if (developerRepository.findByPortalIdAndUsername(developer.getPortalId(), username).isPresent()) {
                throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.DEVELOPER, username));
            }
        }
        param.update(developer);

        developerRepository.save(developer);
        return true;
    }

    @EventListener
    @Async("taskExecutor")
    public void handlePortalDeletion(PortalDeletingEvent event) {
        String portalId = event.getPortalId();
        List<Developer> developers = developerRepository.findByPortalId(portalId);
        developers.forEach(developer -> deleteDeveloper(developer.getDeveloperId()));
    }

    private String generateToken(String developerId) {
        return TokenUtil.generateDeveloperToken(developerId);
    }

    private Developer createExternalDeveloper(String providerName, String providerSubject, String email, String displayName, String rawInfoJson) {
        String portalId = contextHolder.getPortal();
        String username = generateUniqueUsername(portalId, displayName, providerName, providerSubject);

        Developer developer = Developer.builder()
                .developerId(generateDeveloperId())
                .portalId(portalId)
                .username(username)
                .email(email)
                .status(DeveloperStatus.APPROVED)
                .authType(DeveloperAuthType.OIDC)
                .build();
        developer = developerRepository.save(developer);

        DeveloperExternalIdentity ext = DeveloperExternalIdentity.builder()
                .provider(providerName)
                .subject(providerSubject)
                .displayName(displayName)
                .rawInfoJson(rawInfoJson)
                .developer(developer)
                .build();
        externalRepository.save(ext);
        return developer;
    }

    private String generateUniqueUsername(String portalId, String displayName, String providerName, String providerSubject) {
        String username = displayName != null ? displayName : providerName + "_" + providerSubject;
        String originalUsername = username;
        int suffix = 1;
        while (developerRepository.findByPortalIdAndUsername(portalId, username).isPresent()) {
            username = originalUsername + "_" + suffix;
            suffix++;
        }
        return username;
    }

    private String generateDeveloperId() {
        return IdGenerator.genDeveloperId();
    }

    private Developer findDeveloper(String developerId) {
        return developerRepository.findByDeveloperId(developerId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.DEVELOPER, developerId));
    }

    private Portal findPortal(String portalId) {
        return portalRepository.findByPortalId(portalId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PORTAL, portalId));
    }

    private Specification<Developer> buildSpecification(QueryDeveloperParam param) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (StrUtil.isNotBlank(param.getPortalId())) {
                predicates.add(cb.equal(root.get("portalId"), param.getPortalId()));
            }
            if (StrUtil.isNotBlank(param.getUsername())) {
                String likePattern = "%" + param.getUsername() + "%";
                predicates.add(cb.like(root.get("username"), likePattern));
            }
            if (param.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), param.getStatus()));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    @Override
    public void logout(HttpServletRequest request) {
        // 使用TokenUtil处理登出逻辑
        com.alibaba.apiopenplatform.core.utils.TokenUtil.revokeToken(request);
    }

    @Override
    public DeveloperResult getCurrentDeveloperInfo() {
        String currentUserId = contextHolder.getUser();
        Developer developer = findDeveloper(currentUserId);
        return new DeveloperResult().convertFrom(developer);
    }

    @Override
    public boolean changeCurrentDeveloperPassword(String oldPassword, String newPassword) {
        String currentUserId = contextHolder.getUser();
        return resetPassword(currentUserId, oldPassword, newPassword);
    }

}
```

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

```typescript
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Card, Alert, Row, Col, Tabs } from "antd";
import { Layout } from "../components/Layout";
import { ProductHeader } from "../components/ProductHeader";
import { SwaggerUIWrapper } from "../components/SwaggerUIWrapper";
import api from "../lib/api";
import type { Product, ApiResponse } from "../types";
import ReactMarkdown from "react-markdown";
import remarkGfm from 'remark-gfm';
import 'react-markdown-editor-lite/lib/index.css';
import * as yaml from 'js-yaml';
import { Button, Typography, Space, Divider, message } from "antd";
import { CopyOutlined, RocketOutlined, DownloadOutlined } from "@ant-design/icons";

const { Title, Paragraph } = Typography;

interface UpdatedProduct extends Omit<Product, 'apiSpec'> {
  apiConfig?: {
    spec: string;
    meta: {
      source: string;
      type: string;
    };
  };
  createAt: string;
  enabled: boolean;
}

function ApiDetailPage() {
  const { id } = useParams();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [apiData, setApiData] = useState<UpdatedProduct | null>(null);
  const [baseUrl, setBaseUrl] = useState<string>('');
  const [examplePath, setExamplePath] = useState<string>('/{path}');
  const [exampleMethod, setExampleMethod] = useState<string>('GET');

  useEffect(() => {
    if (!id) return;
    fetchApiDetail();
  }, [id]);

  const fetchApiDetail = async () => {
    setLoading(true);
    setError('');
    try {
      const response: ApiResponse<UpdatedProduct> = await api.get(`/products/${id}`);
      if (response.code === "SUCCESS" && response.data) {
        setApiData(response.data);
        
        // 提取基础URL和示例路径用于curl示例
        if (response.data.apiConfig?.spec) {
          try {
            let openApiDoc: any;
            try {
              openApiDoc = yaml.load(response.data.apiConfig.spec);
            } catch {
              openApiDoc = JSON.parse(response.data.apiConfig.spec);
            }
            
            // 提取服务器URL并处理尾部斜杠
            let serverUrl = openApiDoc?.servers?.[0]?.url || '';
            if (serverUrl && serverUrl.endsWith('/')) {
              serverUrl = serverUrl.slice(0, -1); // 移除末尾的斜杠
            }
            setBaseUrl(serverUrl);
            
            // 提取第一个可用的路径和方法作为示例
            const paths = openApiDoc?.paths;
            if (paths && typeof paths === 'object') {
              const pathEntries = Object.entries(paths);
              if (pathEntries.length > 0) {
                const [firstPath, pathMethods] = pathEntries[0] as [string, any];
                if (pathMethods && typeof pathMethods === 'object') {
                  const methods = Object.keys(pathMethods);
                  if (methods.length > 0) {
                    const firstMethod = methods[0].toUpperCase();
                    setExamplePath(firstPath);
                    setExampleMethod(firstMethod);
                  }
                }
              }
            }
          } catch (error) {
            console.error('解析OpenAPI规范失败:', error);
          }
        }
      }
    } catch (error) {
      console.error('获取API详情失败:', error);
      setError('加载失败,请稍后重试');
    } finally {
      setLoading(false);
    }
  };




  if (error) {
    return (
      <Layout loading={loading}>
        <Alert message={error} type="error" showIcon className="my-8" />
      </Layout>
    );
  }

  if (!apiData) {
    return (
      <Layout loading={loading}>
        <Alert message="未找到API信息" type="warning" showIcon className="my-8" />
      </Layout>
    );
  }

  return (
    <Layout loading={loading}>
      <div className="mb-6">
        <ProductHeader
          name={apiData.name}
          description={apiData.description}
          icon={apiData.icon}
          defaultIcon="/logo.svg"
          updatedAt={apiData.updatedAt}
          productType="REST_API"
        />
        <hr className="border-gray-200 mt-4" />
      </div>

      {/* 主要内容区域 - 左右布局 */}
      <Row gutter={24}>
        {/* 左侧内容 */}
        <Col span={15}>
          <Card className="mb-6 rounded-lg border-gray-200">
            <Tabs
              defaultActiveKey="overview"
              items={[
                {
                  key: "overview",
                  label: "Overview",
                  children: apiData.document ? (
                    <div className="min-h-[400px]">
                      <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]}>{apiData.document}</ReactMarkdown>
                      </div>
                    </div>
                  ) : (
                    <div className="text-gray-500 text-center py-8">
                      暂无文档内容
                    </div>
                  ),
                },
                {
                  key: "openapi-spec",
                  label: "OpenAPI Specification",
                  children: (
                    <div>
                      {apiData.apiConfig && apiData.apiConfig.spec ? (
                        <SwaggerUIWrapper apiSpec={apiData.apiConfig.spec} />
                      ) : (
                        <div className="text-gray-500 text-center py-8">
                          暂无OpenAPI规范
                        </div>
                      )}
                    </div>
                  ),
                },
              ]}
            />
          </Card>
        </Col>

        {/* 右侧内容 */}
        <Col span={9}>
          <Card 
            className="rounded-lg border-gray-200"
            title={
              <Space>
                <RocketOutlined />
                <span>快速开始</span>
              </Space>
            }>
            <Space direction="vertical" className="w-full" size="middle">
              {/* cURL示例 */}
              <div>
                <Title level={5}>cURL调用示例</Title>
                <div className="bg-gray-50 p-3 rounded border relative">
                  <pre className="text-sm mb-0">
{`curl -X ${exampleMethod} \\
  '${baseUrl || 'https://api.example.com'}${examplePath}' \\
  -H 'Accept: application/json' \\
  -H 'Content-Type: application/json'`}
                  </pre>
                  <Button 
                    type="text" 
                    size="small"
                    icon={<CopyOutlined />}
                    className="absolute top-2 right-2"
                    onClick={() => {
                      const curlCommand = `curl -X ${exampleMethod} \\\n  '${baseUrl || 'https://api.example.com'}${examplePath}' \\\n  -H 'Accept: application/json' \\\n  -H 'Content-Type: application/json'`;
                      navigator.clipboard.writeText(curlCommand);
                      message.success('cURL命令已复制到剪贴板', 1);
                    }}
                  />
                </div>
              </div>

              <Divider />

              {/* 下载OAS文件 */}
              <div>
                <Title level={5}>OpenAPI规范文件</Title>
                <Paragraph type="secondary">
                  下载完整的OpenAPI规范文件,用于代码生成、API测试等场景
                </Paragraph>
                <Space>
                  <Button 
                    type="primary"
                    icon={<DownloadOutlined />}
                    onClick={() => {
                      if (apiData?.apiConfig?.spec) {
                        const blob = new Blob([apiData.apiConfig.spec], { type: 'text/yaml' });
                        const url = URL.createObjectURL(blob);
                        const link = document.createElement('a');
                        link.href = url;
                        link.download = `${apiData.name || 'api'}-openapi.yaml`;
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);
                        URL.revokeObjectURL(url);
                        message.success('OpenAPI规范文件下载成功', 1);
                      }
                    }}
                  >
                    下载YAML
                  </Button>
                  <Button 
                    icon={<DownloadOutlined />}
                    onClick={() => {
                      if (apiData?.apiConfig?.spec) {
                        try {
                          const yamlDoc = yaml.load(apiData.apiConfig.spec);
                          const jsonSpec = JSON.stringify(yamlDoc, null, 2);
                          const blob = new Blob([jsonSpec], { type: 'application/json' });
                          const url = URL.createObjectURL(blob);
                          const link = document.createElement('a');
                          link.href = url;
                          link.download = `${apiData.name || 'api'}-openapi.json`;
                          document.body.appendChild(link);
                          link.click();
                          document.body.removeChild(link);
                          URL.revokeObjectURL(url);
                          message.success('OpenAPI规范文件下载成功', 1);
                        } catch (error) {
                          message.error('转换JSON格式失败');
                        }
                      }
                    }}
                  >
                    下载JSON
                  </Button>
                </Space>
              </div>
            </Space>
          </Card>
        </Col>
      </Row>

    </Layout>
  );
}

export default ApiDetailPage; 
```

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

```typescript
import { memo, useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { MenuProps } from 'antd';
import { Badge, Button, Card, Dropdown, Modal, message, Pagination, Skeleton, Input, Select, Tag, Space } from 'antd';
import type { ApiProduct, ProductIcon } from '@/types/api-product';
import { ApiOutlined, MoreOutlined, PlusOutlined, ExclamationCircleOutlined, ExclamationCircleFilled, ClockCircleFilled, CheckCircleFilled, SearchOutlined } from '@ant-design/icons';
import McpServerIcon from '@/components/icons/McpServerIcon';
import { apiProductApi } from '@/lib/api';
import ApiProductFormModal from '@/components/api-product/ApiProductFormModal';

// 优化的产品卡片组件
const ProductCard = memo(({ product, onNavigate, handleRefresh, onEdit }: {
  product: ApiProduct;
  onNavigate: (productId: string) => void;
  handleRefresh: () => void;
  onEdit: (product: ApiProduct) => void;
}) => {
  // 处理产品图标的函数
  const getTypeIcon = (icon: ProductIcon | null | undefined, type: string) => {
    if (icon) {
      switch (icon.type) {
        case "URL":
          return <img src={icon.value} alt="icon" style={{ borderRadius: '8px', minHeight: '40px', width: '40px', height: '40px', objectFit: 'cover' }} />
        case "BASE64":
          // 如果value已经包含data URL前缀,直接使用;否则添加前缀
          const src = icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`;
          return <img src={src} alt="icon" style={{ borderRadius: '8px', minHeight: '40px', width: '40px', height: '40px', objectFit: 'cover' }} />
        default:
          return type === "REST_API" ? <ApiOutlined style={{ fontSize: '16px', width: '16px', height: '16px' }} /> : <McpServerIcon style={{ fontSize: '16px', width: '16px', height: '16px' }} />
      }
    } else {
      return type === "REST_API" ? <ApiOutlined style={{ fontSize: '16px', width: '16px', height: '16px' }} /> : <McpServerIcon style={{ fontSize: '16px', width: '16px', height: '16px' }} />
    }
  }

  const handleClick = useCallback(() => {
    onNavigate(product.productId)
  }, [product.productId, onNavigate]);

  const handleDelete = useCallback((productId: string, productName: string, e?: React.MouseEvent | any) => {
    if (e && e.stopPropagation) e.stopPropagation();
    Modal.confirm({
      title: '确认删除',
      icon: <ExclamationCircleOutlined />,
      content: `确定要删除API产品 "${productName}" 吗?此操作不可恢复。`,
      okText: '确认删除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        apiProductApi.deleteApiProduct(productId).then(() => {
          message.success('API Product 删除成功');
          handleRefresh();
        });
      },
    });
  }, [handleRefresh]);

  const handleEdit = useCallback((e?: React.MouseEvent | any) => {
    if (e && e?.domEvent?.stopPropagation) e.domEvent.stopPropagation();
    onEdit(product);
  }, [product, onEdit]);

  const dropdownItems: MenuProps['items'] = [
    {
      key: 'edit',
      label: '编辑',
      onClick: handleEdit,
    },
    {
      type: 'divider',
    },
    {
      key: 'delete',
      label: '删除',
      danger: true,
      onClick: (info: any) => handleDelete(product.productId, product.name, info?.domEvent),
    },
  ]

  return (
    <Card
      className="hover:shadow-lg transition-shadow cursor-pointer rounded-xl border border-gray-200 shadow-sm hover:border-blue-300"
      onClick={handleClick}
      bodyStyle={{ padding: '16px' }}
    >
      <div className="flex items-center justify-between mb-4">
        <div className="flex items-center space-x-3">
          <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-100">
            {getTypeIcon(product.icon, product.type)}
          </div>
          <div>
            <h3 className="text-lg font-semibold">{product.name}</h3>
            <div className="flex items-center gap-3 mt-1 flex-wrap">
              {product.category && <Badge color="green" text={product.category} />}
              <div className="flex items-center">
                {product.type === "REST_API" ? (
                  <ApiOutlined className="text-blue-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
                ) : (
                  <McpServerIcon className="text-black mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
                )}
                <span className="text-xs text-gray-700">
                  {product.type === "REST_API" ? "REST API" : "MCP Server"}
                </span>
              </div>
              <div className="flex items-center">
                {product.status === "PENDING" ? (
                  <ExclamationCircleFilled className="text-yellow-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
                ) : product.status === "READY" ? (
                  <ClockCircleFilled className="text-blue-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
                ) : (
                  <CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '12px', width: '12px', height: '12px'}} />
                )}
                <span className="text-xs text-gray-700">
                  {product.status === "PENDING" ? "待配置" : product.status === "READY" ? "待发布" : "已发布"}
                </span>
              </div>
            </div>
          </div>
        </div>
        <Dropdown menu={{ items: dropdownItems }} trigger={['click']}>
          <Button
            type="text"
            icon={<MoreOutlined />}
            onClick={(e) => e.stopPropagation()}
          />
        </Dropdown>
      </div>

      <div className="space-y-4">
        {product.description && (
          <p className="text-sm text-gray-600">{product.description}</p>
        )}

      </div>
    </Card>
  )
})

ProductCard.displayName = 'ProductCard'

export default function ApiProducts() {
  const navigate = useNavigate();
  const [apiProducts, setApiProducts] = useState<ApiProduct[]>([]);
  const [filters, setFilters] = useState<{ type?: string, name?: string }>({});
  const [loading, setLoading] = useState(true); // 初始状态为 loading
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 12,
    total: 0,
  });

  const [modalVisible, setModalVisible] = useState(false);
  const [editingProduct, setEditingProduct] = useState<ApiProduct | null>(null);

  // 搜索状态
  const [searchValue, setSearchValue] = useState('');
  const [searchType, setSearchType] = useState<'name' | 'type'>('name');
  const [activeFilters, setActiveFilters] = useState<Array<{ type: string; value: string; label: string }>>([]);

  const fetchApiProducts = useCallback((page = 1, size = 12, queryFilters?: { type?: string, name?: string }) => {
    setLoading(true);
    const params = { page, size, ...(queryFilters || {}) };
    apiProductApi.getApiProducts(params).then((res: any) => {
      const products = res.data.content;
      setApiProducts(products);
      setPagination({
        current: page,
        pageSize: size,
        total: res.data.totalElements || 0,
      });
    }).finally(() => {
      setLoading(false);
    });
  }, []); // 不依赖任何状态,避免无限循环

  useEffect(() => {
    fetchApiProducts(1, 12);
  }, []); // 只在组件初始化时执行一次

  // 产品类型选项
  const typeOptions = [
    { label: 'REST API', value: 'REST_API' },
    { label: 'MCP Server', value: 'MCP_SERVER' },
  ];

  // 搜索类型选项
  const searchTypeOptions = [
    { label: '产品名称', value: 'name' as const },
    { label: '产品类型', value: 'type' as const },
  ];

  // 搜索处理函数
  const handleSearch = () => {
    if (searchValue.trim()) {
      let labelText = '';
      let filterValue = searchValue.trim();
      
      if (searchType === 'name') {
        labelText = `产品名称:${searchValue.trim()}`;
      } else {
        const typeLabel = typeOptions.find(opt => opt.value === searchValue.trim())?.label || searchValue.trim();
        labelText = `产品类型:${typeLabel}`;
      }
      
      const newFilter = { type: searchType, value: filterValue, label: labelText };
      const updatedFilters = activeFilters.filter(f => f.type !== searchType);
      updatedFilters.push(newFilter);
      setActiveFilters(updatedFilters);
      
      const filters: { type?: string, name?: string } = {};
      updatedFilters.forEach(filter => {
        if (filter.type === 'type' || filter.type === 'name') {
          filters[filter.type] = filter.value;
        }
      });
      
      setFilters(filters);
      fetchApiProducts(1, pagination.pageSize, filters);
      setSearchValue('');
    }
  };

  // 移除单个筛选条件
  const removeFilter = (filterType: string) => {
    const updatedFilters = activeFilters.filter(f => f.type !== filterType);
    setActiveFilters(updatedFilters);
    
    const newFilters: { type?: string, name?: string } = {};
    updatedFilters.forEach(filter => {
      if (filter.type === 'type' || filter.type === 'name') {
        newFilters[filter.type] = filter.value;
      }
    });
    
    setFilters(newFilters);
    fetchApiProducts(1, pagination.pageSize, newFilters);
  };

  // 清空所有筛选条件
  const clearAllFilters = () => {
    setActiveFilters([]);
    setFilters({});
    fetchApiProducts(1, pagination.pageSize, {});
  };

  // 处理分页变化
  const handlePaginationChange = (page: number, pageSize: number) => {
    fetchApiProducts(page, pageSize, filters); // 传递当前filters
  };

  // 直接使用服务端返回的列表

  // 优化的导航处理函数
  const handleNavigateToProduct = useCallback((productId: string) => {
    navigate(`/api-products/detail?productId=${productId}`);
  }, [navigate]);

  // 处理创建
  const handleCreate = () => {
    setEditingProduct(null);
    setModalVisible(true);
  };

  // 处理编辑
  const handleEdit = (product: ApiProduct) => {
    setEditingProduct(product);
    setModalVisible(true);
  };

  // 处理模态框成功
  const handleModalSuccess = () => {
    setModalVisible(false);
    setEditingProduct(null);
    fetchApiProducts(pagination.current, pagination.pageSize, filters);
  };

  // 处理模态框取消
  const handleModalCancel = () => {
    setModalVisible(false);
    setEditingProduct(null);
  };

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold tracking-tight">API Products</h1>
          <p className="text-gray-500 mt-2">
            管理和配置您的API产品
          </p>
        </div>
        <Button onClick={handleCreate} type="primary" icon={<PlusOutlined/>}>
          创建 API Product
        </Button>
      </div>

      {/* 搜索和筛选 */}
      <div className="space-y-4">
        {/* 搜索框 */}
        <div className="flex items-center max-w-xl">
          {/* 左侧:搜索类型选择器 */}
          <Select
            value={searchType}
            onChange={setSearchType}
            style={{ 
              width: 120,
              borderTopRightRadius: 0,
              borderBottomRightRadius: 0,
              backgroundColor: '#f5f5f5',
            }}
            className="h-10"
            size="large"
          >
            {searchTypeOptions.map(option => (
              <Select.Option key={option.value} value={option.value}>
                {option.label}
              </Select.Option>
            ))}
          </Select>

          {/* 中间:搜索值输入框或选择框 */}
          {searchType === 'type' ? (
            <Select
              placeholder="请选择产品类型"
              value={searchValue}
              onChange={(value) => {
                setSearchValue(value);
                // 对于类型选择,立即执行搜索
                if (value) {
                  const typeLabel = typeOptions.find(opt => opt.value === value)?.label || value;
                  const labelText = `产品类型:${typeLabel}`;
                  const newFilter = { type: 'type', value, label: labelText };
                  const updatedFilters = activeFilters.filter(f => f.type !== 'type');
                  updatedFilters.push(newFilter);
                  setActiveFilters(updatedFilters);
                  
                  const filters: { type?: string, name?: string } = {};
                  updatedFilters.forEach(filter => {
                    if (filter.type === 'type' || filter.type === 'name') {
                      filters[filter.type] = filter.value;
                    }
                  });
                  
                  setFilters(filters);
                  fetchApiProducts(1, pagination.pageSize, filters);
                  setSearchValue('');
                }
              }}
              style={{ 
                flex: 1,
                borderTopLeftRadius: 0,
                borderBottomLeftRadius: 0,
                borderTopRightRadius: 0,
                borderBottomRightRadius: 0,
              }}
              allowClear
              onClear={clearAllFilters}
              className="h-10"
              size="large"
            >
              {typeOptions.map(option => (
                <Select.Option key={option.value} value={option.value}>
                  {option.label}
                </Select.Option>
              ))}
            </Select>
          ) : (
            <Input
              placeholder="请输入要检索的产品名称"
              value={searchValue}
              onChange={(e) => setSearchValue(e.target.value)}
              style={{ 
                flex: 1,
                borderTopLeftRadius: 0,
                borderBottomLeftRadius: 0,
                borderTopRightRadius: 0,
                borderBottomRightRadius: 0,
              }}
              onPressEnter={handleSearch}
              allowClear
              onClear={() => setSearchValue('')}
              size="large"
              className="h-10"
            />
          )}

          {/* 右侧:搜索按钮 */}
          <Button
            icon={<SearchOutlined />}
            onClick={handleSearch}
            style={{
              borderTopLeftRadius: 0,
              borderBottomLeftRadius: 0,
              width: 48,
            }}
            className="h-10"
            size="large"
          />
        </div>

        {/* 筛选条件标签 */}
        {activeFilters.length > 0 && (
          <div className="flex items-center gap-2">
            <span className="text-sm text-gray-500">筛选条件:</span>
            <Space wrap>
              {activeFilters.map(filter => (
                <Tag
                  key={filter.type}
                  closable
                  onClose={() => removeFilter(filter.type)}
                  style={{
                    backgroundColor: '#f5f5f5',
                    border: '1px solid #d9d9d9',
                    borderRadius: '16px',
                    color: '#666',
                    fontSize: '12px',
                    padding: '4px 12px',
                  }}
                >
                  {filter.label}
                </Tag>
              ))}
            </Space>
            <Button
              type="link"
              size="small"
              onClick={clearAllFilters}
              className="text-blue-500 hover:text-blue-600 text-sm"
            >
              清除筛选条件
            </Button>
          </div>
        )}
      </div>

      {loading ? (
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {Array.from({ length: pagination.pageSize || 12 }).map((_, index) => (
            <div key={index} className="h-full rounded-lg shadow-lg bg-white p-4">
              <div className="flex items-start space-x-4">
                <Skeleton.Avatar size={48} active />
                <div className="flex-1 min-w-0">
                  <div className="flex items-center justify-between mb-2">
                    <Skeleton.Input active size="small" style={{ width: 120 }} />
                    <Skeleton.Input active size="small" style={{ width: 60 }} />
                  </div>
                  <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} />
                  <Skeleton.Input active size="small" style={{ width: '80%', marginBottom: 8 }} />
                  <div className="flex items-center justify-between">
                    <Skeleton.Input active size="small" style={{ width: 60 }} />
                    <Skeleton.Input active size="small" style={{ width: 80 }} />
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
      ) : (
        <>
          <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
            {apiProducts.map((product) => (
              <ProductCard
                key={product.productId}
                product={product}
                onNavigate={handleNavigateToProduct}
                handleRefresh={() => fetchApiProducts(pagination.current, pagination.pageSize, filters)}
                onEdit={handleEdit}
              />
            ))}
          </div>

          {pagination.total > 0 && (
            <div className="flex justify-center mt-6">
              <Pagination
                current={pagination.current}
                pageSize={pagination.pageSize}
                total={pagination.total}
                onChange={handlePaginationChange}
                showSizeChanger
                showQuickJumper
                showTotal={(total) => `共 ${total} 条`}
                pageSizeOptions={['6', '12', '24', '48']}
              />
            </div>
          )}
        </>
      )}

      <ApiProductFormModal
        visible={modalVisible}
        onCancel={handleModalCancel}
        onSuccess={handleModalSuccess}
        productId={editingProduct?.productId}
        initialData={editingProduct || undefined}
      />
    </div>
  )
}

```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/AIGatewayOperator.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.codec.Base64;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.Gateway;
import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
import com.alibaba.apiopenplatform.service.gateway.client.PopGatewayClient;
import com.alibaba.apiopenplatform.service.gateway.client.SLSClient;
import com.alibaba.apiopenplatform.support.consumer.APIGAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
import com.aliyuncs.http.MethodType;
import com.aliyun.sdk.gateway.pop.exception.PopClientException;
import com.aliyun.sdk.service.apig20240327.models.*;
import com.aliyun.sdk.service.sls20201230.models.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

@Service
@Slf4j
public class AIGatewayOperator extends APIGOperator {

    @Override
    public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
        PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());

        Map<String , String> queryParams = MapUtil.<String, String>builder()
                .put("gatewayId", gateway.getGatewayId())
                .put("pageNumber", String.valueOf(page))
                .put("pageSize", String.valueOf(size))
                .build();

        return client.execute("/v1/mcp-servers", MethodType.GET, queryParams, data -> {
            List<APIGMCPServerResult> mcpServers = Optional.ofNullable(data.getJSONArray("items"))
                    .map(items -> items.stream()
                            .map(JSONObject.class::cast)
                            .map(json -> {
                                APIGMCPServerResult result = new APIGMCPServerResult();
                                result.setMcpServerName(json.getStr("name"));
                                result.setMcpServerId(json.getStr("mcpServerId"));
                                result.setMcpRouteId(json.getStr("routeId"));
                                result.setApiId(json.getStr("apiId"));
                                return result;
                            })
                            .collect(Collectors.toList()))
                    .orElse(new ArrayList<>());

            return PageResult.of(mcpServers, page, size, data.getInt("totalSize"));
        });
    }

    public PageResult<? extends GatewayMCPServerResult> fetchMcpServers_V1(Gateway gateway, int page, int size) {
        PageResult<APIResult> apiPage = fetchAPIs(gateway, APIGAPIType.MCP, 0, 1);
        if (apiPage.getTotalElements() == 0) {
            return PageResult.empty(page, size);
        }

        // MCP Server定义在一个API下
        String apiId = apiPage.getContent().get(0).getApiId();
        try {
            PageResult<HttpRoute> routesPage = fetchHttpRoutes(gateway, apiId, page, size);
            if (routesPage.getTotalElements() == 0) {
                return PageResult.empty(page, size);
            }

            return PageResult.<APIGMCPServerResult>builder().build()
                    .mapFrom(routesPage, route -> {
                        APIGMCPServerResult r = new APIGMCPServerResult().convertFrom(route);
                        r.setApiId(apiId);
                        return r;
                    });
        } catch (Exception e) {
            log.error("Error fetching MCP servers", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching MCP servers,Cause:" + e.getMessage());
        }
    }

    @Override
    public String fetchMcpConfig(Gateway gateway, Object conf) {
        APIGRefConfig config = (APIGRefConfig) conf;
        PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());
        String mcpServerId = config.getMcpServerId();
        MCPConfigResult mcpConfig = new MCPConfigResult();

        return client.execute("/v1/mcp-servers/" + mcpServerId, MethodType.GET, null, data -> {
            mcpConfig.setMcpServerName(data.getStr("name"));

            // mcpServer config
            MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
            String path = data.getStr("mcpServerPath");
            String exposedUriPath = data.getStr("exposedUriPath");
            if (StrUtil.isNotBlank(exposedUriPath)) {
                path += exposedUriPath;
            }
            serverConfig.setPath(path);

            JSONArray domains = data.getJSONArray("domainInfos");
            if (domains != null && !domains.isEmpty()) {
                serverConfig.setDomains(domains.stream()
                        .map(JSONObject.class::cast)
                        .map(json -> MCPConfigResult.Domain.builder()
                                .domain(json.getStr("name"))
                                .protocol(Optional.ofNullable(json.getStr("protocol"))
                                        .map(String::toLowerCase)
                                        .orElse(null))
                                .build())
                        .collect(Collectors.toList()));
            }
            mcpConfig.setMcpServerConfig(serverConfig);

            // meta
            MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
            meta.setSource(GatewayType.APIG_AI.name());
            meta.setProtocol(data.getStr("protocol"));
            meta.setCreateFromType(data.getStr("createFromType"));
            mcpConfig.setMeta(meta);

            // tools
            String tools = data.getStr("mcpServerConfig");
            if (StrUtil.isNotBlank(tools)) {
                mcpConfig.setTools(Base64.decodeStr(tools));
            }

            return JSONUtil.toJsonStr(mcpConfig);
        });
    }

    public String fetchMcpConfig_V1(Gateway gateway, Object conf) {
        APIGRefConfig config = (APIGRefConfig) conf;
        HttpRoute httpRoute = fetchHTTPRoute(gateway, config.getApiId(), config.getMcpRouteId());

        MCPConfigResult m = new MCPConfigResult();
        m.setMcpServerName(httpRoute.getName());

        // mcpServer config
        MCPConfigResult.MCPServerConfig c = new MCPConfigResult.MCPServerConfig();
        if (httpRoute.getMatch() != null) {
            c.setPath(httpRoute.getMatch().getPath().getValue());
        }
        if (httpRoute.getDomainInfos() != null) {
            c.setDomains(httpRoute.getDomainInfos().stream()
                    .map(domainInfo -> MCPConfigResult.Domain.builder()
                            .domain(domainInfo.getName())
                            .protocol(Optional.ofNullable(domainInfo.getProtocol())
                                    .map(String::toLowerCase)
                                    .orElse(null))
                            .build())
                    .collect(Collectors.toList()));
        }
        m.setMcpServerConfig(c);

        // meta
        MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
        meta.setSource(GatewayType.APIG_AI.name());

        // tools
        HttpRoute.McpServerInfo mcpServerInfo = httpRoute.getMcpServerInfo();
        boolean fetchTool = true;
        if (mcpServerInfo.getMcpRouteConfig() != null) {
            String protocol = mcpServerInfo.getMcpRouteConfig().getProtocol();
            meta.setCreateFromType(protocol);

            // HTTP转MCP需从插件获取tools配置
            fetchTool = StrUtil.equalsIgnoreCase(protocol, "HTTP");
        }

        if (fetchTool) {
            String toolSpec = fetchMcpTools(gateway, config.getMcpRouteId());
            if (StrUtil.isNotBlank(toolSpec)) {
                m.setTools(toolSpec);
                // 默认为HTTP转MCP
                if (StrUtil.isBlank(meta.getCreateFromType())) {
                    meta.setCreateFromType("HTTP");
                }
            }
        }

        m.setMeta(meta);
        return JSONUtil.toJsonStr(m);
    }

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

    @Override
    public String getDashboard(Gateway gateway, String type) {
        SLSClient ticketClient = new SLSClient(gateway.getApigConfig(), true);
        String ticket = null;
        try {
            CreateTicketResponse response = ticketClient.execute(c -> {
                CreateTicketRequest request = CreateTicketRequest.builder().build();
                try {
                    return c.createTicket(request).get();
                } catch (InterruptedException | ExecutionException e) {
                    throw new RuntimeException(e);
                }
            });
            ticket = response.getBody().getTicket();
        } catch (Exception e) {
            log.error("Error fetching API", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching createTicket API,Cause:" + e.getMessage());
        }
        SLSClient client = new SLSClient(gateway.getApigConfig(), false);
        String projectName = null;
        try {
            ListProjectResponse response = client.execute(c -> {
                ListProjectRequest request = ListProjectRequest.builder().projectName("product").build();
                try {
                    return c.listProject(request).get();
                } catch (InterruptedException | ExecutionException e) {
                    throw new RuntimeException(e);
                }
            });
            projectName = response.getBody().getProjects().get(0).getProjectName();
        } catch (Exception e) {
            log.error("Error fetching Project", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Project,Cause:" + e.getMessage());
        }
        String region = gateway.getApigConfig().getRegion();
        String gatewayId = gateway.getGatewayId();
        String dashboardId = "";
        String gatewayFilter = "";
        if (type.equals("Portal")) {
            dashboardId = "dashboard-1758009692051-393998";
            gatewayFilter = "";
        } else if (type.equals("MCP")) {
            dashboardId = "dashboard-1757483808537-433375";
            gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
        } else if (type.equals("API")) {
            dashboardId = "dashboard-1756276497392-966932";
            gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
            ;
        }
        String dashboardUrl = String.format("https://sls.console.aliyun.com/lognext/project/%s/dashboard/%s?%s&slsRegion=%s&sls_ticket=%s&isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true", projectName, dashboardId, gatewayFilter, region, ticket);
        log.info("Dashboard URL: {}", dashboardUrl);
        return dashboardUrl;
    }

    public String fetchMcpTools(Gateway gateway, String routeId) {
        APIGClient client = getClient(gateway);

        try {
            CompletableFuture<ListPluginAttachmentsResponse> f = client.execute(c -> {
                ListPluginAttachmentsRequest request = ListPluginAttachmentsRequest.builder()
                        .gatewayId(gateway.getGatewayId())
                        .attachResourceId(routeId)
                        .attachResourceType("GatewayRoute")
                        .pageNumber(1)
                        .pageSize(100)
                        .build();

                return c.listPluginAttachments(request);
            });

            ListPluginAttachmentsResponse response = f.join();
            if (response.getStatusCode() != 200) {
                throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
            }

            for (ListPluginAttachmentsResponseBody.Items item : response.getBody().getData().getItems()) {
                PluginClassInfo classInfo = item.getPluginClassInfo();

                if (!StrUtil.equalsIgnoreCase(classInfo.getName(), "mcp-server")) {
                    continue;
                }

                String pluginConfig = item.getPluginConfig();
                if (StrUtil.isNotBlank(pluginConfig)) {
                    return Base64.decodeStr(pluginConfig);
                }
            }
        } catch (Exception e) {
            log.error("Error fetching Plugin Attachment", e);
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Plugin Attachment,Cause:" + e.getMessage());
        }
        return null;
    }

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

        APIGRefConfig config = (APIGRefConfig) refConfig;
        // MCP Server 授权
        String mcpRouteId = config.getMcpRouteId();

        try {
            // 确认Gateway的EnvId
            String envId = fetchGatewayEnv(gateway);

            CreateConsumerAuthorizationRulesRequest.AuthorizationRules rule = CreateConsumerAuthorizationRulesRequest.AuthorizationRules.builder()
                    .consumerId(consumerId)
                    .expireMode("LongTerm")
                    .resourceType("MCP")
                    .resourceIdentifier(CreateConsumerAuthorizationRulesRequest.ResourceIdentifier.builder()
                            .resourceId(mcpRouteId)
                            .environmentId(envId).build())
                    .build();

            CompletableFuture<CreateConsumerAuthorizationRulesResponse> f = client.execute(c -> {
                        CreateConsumerAuthorizationRulesRequest request = CreateConsumerAuthorizationRulesRequest.builder()
                                .authorizationRules(Collections.singletonList(rule))
                                .build();

                        return c.createConsumerAuthorizationRules(request);
                    }
            );
            CreateConsumerAuthorizationRulesResponse response = f.join();
            if (200 != response.getStatusCode()) {
                throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
            }

            APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
                    .authorizationRuleIds(response.getBody().getData().getConsumerAuthorizationRuleIds())
                    .build();
            return ConsumerAuthConfig.builder()
                    .apigAuthConfig(apigAuthConfig)
                    .build();
        } catch (Exception e) {
            Throwable cause = e.getCause();
            if (cause instanceof PopClientException
                    && "Conflict.ConsumerAuthorizationForbidden".equals(((PopClientException) cause).getErrCode())) {
                return getConsumerAuthorization(gateway, consumerId, mcpRouteId);
            }
            log.error("Error authorizing consumer {} to mcp server {} in AI gateway {}", consumerId, mcpRouteId, gateway.getGatewayId(), e);
            throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to mcp server in AI gateway: " + e.getMessage());
        }
    }

    public ConsumerAuthConfig getConsumerAuthorization(Gateway gateway, String consumerId, String resourceId) {
        APIGClient client = getClient(gateway);

        CompletableFuture<QueryConsumerAuthorizationRulesResponse> f = client.execute(c -> {
            QueryConsumerAuthorizationRulesRequest request = QueryConsumerAuthorizationRulesRequest.builder()
                    .consumerId(consumerId)
                    .resourceId(resourceId)
                    .resourceType("MCP")
                    .build();

            return c.queryConsumerAuthorizationRules(request);
        });
        QueryConsumerAuthorizationRulesResponse response = f.join();

        if (200 != response.getStatusCode()) {
            throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
        }

        QueryConsumerAuthorizationRulesResponseBody.Items items = response.getBody().getData().getItems().get(0);
        APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
                .authorizationRuleIds(Collections.singletonList(items.getConsumerAuthorizationRuleId()))
                .build();

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

```

--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/ProductServiceImpl.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.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
import com.alibaba.apiopenplatform.core.event.ProductDeletingEvent;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.product.*;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.*;
import com.alibaba.apiopenplatform.repository.*;
import com.alibaba.apiopenplatform.service.GatewayService;
import com.alibaba.apiopenplatform.service.PortalService;
import com.alibaba.apiopenplatform.service.ProductService;
import com.alibaba.apiopenplatform.service.NacosService;
import com.alibaba.apiopenplatform.support.enums.ProductStatus;
import com.alibaba.apiopenplatform.support.enums.ProductType;
import com.alibaba.apiopenplatform.support.enums.SourceType;
import com.alibaba.apiopenplatform.support.product.NacosRefConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Page;

import javax.persistence.criteria.*;
import javax.transaction.Transactional;

import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ProductServiceImpl implements ProductService {

    private final ContextHolder contextHolder;

    private final PortalService portalService;

    private final GatewayService gatewayService;

    private final ProductRepository productRepository;

    private final ProductRefRepository productRefRepository;

    private final ProductPublicationRepository publicationRepository;

    private final SubscriptionRepository subscriptionRepository;

    private final ConsumerRepository consumerRepository;

    private final NacosService nacosService;

    private final ApplicationEventPublisher eventPublisher;

    @Override
    public ProductResult createProduct(CreateProductParam param) {
        productRepository.findByNameAndAdminId(param.getName(), contextHolder.getUser())
                .ifPresent(product -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PRODUCT, product.getName()));
                });

        String productId = IdGenerator.genApiProductId();

        Product product = param.convertTo();
        product.setProductId(productId);
        product.setAdminId(contextHolder.getUser());

        // 设置默认的自动审批配置,如果未指定则默认为null(使用平台级别配置)
        if (param.getAutoApprove() != null) {
            product.setAutoApprove(param.getAutoApprove());
        }

        productRepository.save(product);

        return getProduct(productId);
    }

    @Override
    public ProductResult getProduct(String productId) {
        Product product = contextHolder.isAdministrator() ?
                findProduct(productId) :
                findPublishedProduct(contextHolder.getPortal(), productId);

        ProductResult result = new ProductResult().convertFrom(product);

        // 补充Product信息
        fullFillProduct(result);
        return result;
    }

    @Override
    public PageResult<ProductResult> listProducts(QueryProductParam param, Pageable pageable) {
        log.info("zhaoh-test-listProducts-start");
        if (contextHolder.isDeveloper()) {
            param.setPortalId(contextHolder.getPortal());
        }

        Page<Product> products = productRepository.findAll(buildSpecification(param), pageable);
        return new PageResult<ProductResult>().convertFrom(
                products, product -> {
                    ProductResult result = new ProductResult().convertFrom(product);
                    fullFillProduct(result);
                    return result;
                });
    }

    @Override
    public ProductResult updateProduct(String productId, UpdateProductParam param) {
        Product product = findProduct(productId);

        // 更换API产品类型
        if (param.getType() != null && product.getType() != param.getType()) {
            productRefRepository.findFirstByProductId(productId)
                    .ifPresent(productRef -> {
                        throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已关联API");
                    });
        }
        param.update(product);

        // Consumer鉴权配置同步至网关
        Optional.ofNullable(param.getEnableConsumerAuth()).ifPresent(product::setEnableConsumerAuth);

        // 更新自动审批配置
        Optional.ofNullable(param.getAutoApprove()).ifPresent(product::setAutoApprove);

        productRepository.saveAndFlush(product);
        return getProduct(product.getProductId());
    }

    @Override
    public void publishProduct(String productId, String portalId) {
        portalService.existsPortal(portalId);
        if (publicationRepository.findByPortalIdAndProductId(portalId, productId).isPresent()) {
            return;
        }

        Product product = findProduct(productId);
        product.setStatus(ProductStatus.PUBLISHED);

        // 未关联不允许发布
        if (getProductRef(productId) == null) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API");
        }

        ProductPublication productPublication = new ProductPublication();
        productPublication.setPortalId(portalId);
        productPublication.setProductId(productId);

        publicationRepository.save(productPublication);
        productRepository.save(product);
    }

    @Override
    public PageResult<ProductPublicationResult> getPublications(String productId, Pageable pageable) {
        Page<ProductPublication> publications = publicationRepository.findByProductId(productId, pageable);

        return new PageResult<ProductPublicationResult>().convertFrom(
                publications, publication -> {
                    ProductPublicationResult publicationResult = new ProductPublicationResult().convertFrom(publication);
                    PortalResult portal;
                    try {
                        portal = portalService.getPortal(publication.getPortalId());
                    } catch (Exception e) {
                        log.error("Failed to get portal: {}", publication.getPortalId(), e);
                        return null;
                    }

                    publicationResult.setPortalName(portal.getName());
                    publicationResult.setAutoApproveSubscriptions(portal.getPortalSettingConfig().getAutoApproveSubscriptions());

                    return publicationResult;
                });
    }

    @Override
    public void unpublishProduct(String productId, String portalId) {
        portalService.existsPortal(portalId);

        publicationRepository.findByPortalIdAndProductId(portalId, productId)
                .ifPresent(publicationRepository::delete);
    }

    @Override
    public void deleteProduct(String productId) {
        Product Product = findProduct(productId);

        // 下线后删除
        publicationRepository.deleteByProductId(productId);
        productRepository.delete(Product);

        // 异步清理Product资源
        eventPublisher.publishEvent(new ProductDeletingEvent(productId));
    }

    /**
     * 查找产品,如果不存在则抛出异常
     */
    private Product findProduct(String productId) {
        return productRepository.findByProductId(productId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
    }

    @Override
    public void addProductRef(String productId, CreateProductRefParam param) {
        Product product = findProduct(productId);

        // 是否已存在API引用
        productRefRepository.findByProductId(product.getProductId())
                .ifPresent(productRef -> {
                    throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已关联API", Resources.PRODUCT, productId));
                });
        ProductRef productRef = param.convertTo();
        productRef.setProductId(productId);
        syncConfig(product, productRef);

        productRepository.save(product);
        productRefRepository.save(productRef);
    }

    @Override
    public ProductRefResult getProductRef(String productId) {
        return productRefRepository.findFirstByProductId(productId)
                .map(productRef -> new ProductRefResult().convertFrom(productRef))
                .orElse(null);
    }

    @Override
    public void deleteProductRef(String productId) {
        Product product = findProduct(productId);
        product.setStatus(ProductStatus.PENDING);

        ProductRef productRef = productRefRepository.findFirstByProductId(productId)
                .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API"));

        // 已发布的产品不允许解绑
        if (publicationRepository.existsByProductId(productId)) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已发布");
        }

        productRefRepository.delete(productRef);
        productRepository.save(product);
    }

    private void syncConfig(Product product, ProductRef productRef) {
        SourceType sourceType = productRef.getSourceType();

        if (sourceType.isGateway()) {
            GatewayResult gateway = gatewayService.getGateway(productRef.getGatewayId());
            Object config = gateway.getGatewayType().isHigress() ? productRef.getHigressRefConfig() : gateway.getGatewayType().isAdpAIGateway() ? productRef.getAdpAIGatewayRefConfig() : productRef.getApigRefConfig();
            if (product.getType() == ProductType.REST_API) {
                String apiConfig = gatewayService.fetchAPIConfig(gateway.getGatewayId(), config);
                productRef.setApiConfig(apiConfig);
            } else {
                String mcpConfig = gatewayService.fetchMcpConfig(gateway.getGatewayId(), config);
                productRef.setMcpConfig(mcpConfig);
            }
        } else if (sourceType.isNacos()) {
            // 从Nacos获取MCP Server配置
            NacosRefConfig nacosRefConfig = productRef.getNacosRefConfig();
            if (nacosRefConfig != null) {
                String mcpConfig = nacosService.fetchMcpConfig(productRef.getNacosId(), nacosRefConfig);
                productRef.setMcpConfig(mcpConfig);
            }
        }
        product.setStatus(ProductStatus.READY);
        productRef.setEnabled(true);
    }

    private void fullFillProduct(ProductResult product) {
        productRefRepository.findFirstByProductId(product.getProductId())
                .ifPresent(productRef -> {
                    product.setEnabled(productRef.getEnabled());
                    if (StrUtil.isNotBlank(productRef.getApiConfig())) {
                        product.setApiConfig(JSONUtil.toBean(productRef.getApiConfig(), APIConfigResult.class));
                    }

                    // API Config
                    if (StrUtil.isNotBlank(productRef.getMcpConfig())) {
                        product.setMcpConfig(JSONUtil.toBean(productRef.getMcpConfig(), MCPConfigResult.class));
                    }
                    product.setStatus(ProductStatus.READY);
                });

        if (publicationRepository.existsByProductId(product.getProductId())) {
            product.setStatus(ProductStatus.PUBLISHED);
        }
    }

    private Product findPublishedProduct(String portalId, String productId) {
        ProductPublication publication = publicationRepository.findByPortalIdAndProductId(portalId, productId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));

        return findProduct(publication.getProductId());
    }

    private Specification<Product> buildSpecification(QueryProductParam param) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (StrUtil.isNotBlank(param.getPortalId())) {
                Subquery<String> subquery = query.subquery(String.class);
                Root<ProductPublication> publicationRoot = subquery.from(ProductPublication.class);
                subquery.select(publicationRoot.get("productId"))
                        .where(cb.equal(publicationRoot.get("portalId"), param.getPortalId()));
                predicates.add(root.get("productId").in(subquery));
            }

            if (param.getType() != null) {
                predicates.add(cb.equal(root.get("type"), param.getType()));
            }

            if (StrUtil.isNotBlank(param.getCategory())) {
                predicates.add(cb.equal(root.get("category"), param.getCategory()));
            }

            if (param.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), param.getStatus()));
            }

            if (StrUtil.isNotBlank(param.getName())) {
                String likePattern = "%" + param.getName() + "%";
                predicates.add(cb.like(root.get("name"), likePattern));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    @EventListener
    @Async("taskExecutor")
    @Override
    public void handlePortalDeletion(PortalDeletingEvent event) {
        String portalId = event.getPortalId();
        try {
            log.info("Starting to cleanup publications for portal {}", portalId);
            publicationRepository.deleteAllByPortalId(portalId);

            log.info("Completed cleanup publications for portal {}", portalId);
        } catch (Exception e) {
            log.error("Failed to cleanup developers for portal {}: {}", portalId, e.getMessage());
        }
    }

    @Override
    public Map<String, ProductResult> getProducts(List<String> productIds) {
        List<Product> products = productRepository.findByProductIdIn(productIds);
        return products.stream()
                .collect(Collectors.toMap(Product::getProductId, product -> new ProductResult().convertFrom(product)));
    }

    @Override
    public String getProductDashboard(String productId) {
        // 获取产品关联的网关信息
        ProductRef productRef = productRefRepository.findFirstByProductId(productId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));

        if (productRef.getGatewayId() == null) {
            throw new BusinessException(ErrorCode.INVALID_REQUEST, "该产品尚未关联网关服务");
        }
        // 基于产品类型选择Dashboard类型
        Product product = findProduct(productId);
        String dashboardType;
        if (product.getType() == ProductType.MCP_SERVER) {
            dashboardType = "MCP";
        } else {
            // REST_API、HTTP_API 统一走 API 面板
            dashboardType = "API";
        }
        // 通过网关服务获取Dashboard URL
        return gatewayService.getDashboard(productRef.getGatewayId(), dashboardType);
    }

    @Override
    public PageResult<SubscriptionResult> listProductSubscriptions(String productId, QueryProductSubscriptionParam param, Pageable pageable) {
        existsProduct(productId);
        Page<ProductSubscription> subscriptions = subscriptionRepository.findAll(buildProductSubscriptionSpec(productId, param), pageable);

        List<String> consumerIds = subscriptions.getContent().stream()
                .map(ProductSubscription::getConsumerId)
                .collect(Collectors.toList());
        if (CollUtil.isEmpty(consumerIds)) {
            return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
        }

        Map<String, Consumer> consumers = consumerRepository.findByConsumerIdIn(consumerIds)
                .stream()
                .collect(Collectors.toMap(Consumer::getConsumerId, consumer -> consumer));

        return new PageResult<SubscriptionResult>().convertFrom(subscriptions, s -> {
            SubscriptionResult r = new SubscriptionResult().convertFrom(s);
            Consumer consumer = consumers.get(r.getConsumerId());
            if (consumer != null) {
                r.setConsumerName(consumer.getName());
            }
            return r;
        });
    }

    @Override
    public void existsProduct(String productId) {
        productRepository.findByProductId(productId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
    }

    private Specification<ProductSubscription> buildProductSubscriptionSpec(String productId, QueryProductSubscriptionParam param) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            predicates.add(cb.equal(root.get("productId"), productId));

            // 如果是开发者,只能查看自己的Consumer订阅
            if (contextHolder.isDeveloper()) {
                Subquery<String> consumerSubquery = query.subquery(String.class);
                Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);
                consumerSubquery.select(consumerRoot.get("consumerId"))
                        .where(cb.equal(consumerRoot.get("developerId"), contextHolder.getUser()));

                predicates.add(root.get("consumerId").in(consumerSubquery));
            }

            if (param.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), param.getStatus()));
            }

            if (StrUtil.isNotBlank(param.getConsumerName())) {
                Subquery<String> consumerSubquery = query.subquery(String.class);
                Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);

                consumerSubquery.select(consumerRoot.get("consumerId"))
                        .where(cb.like(
                                cb.lower(consumerRoot.get("name")),
                                "%" + param.getConsumerName().toLowerCase() + "%"
                        ));

                predicates.add(root.get("consumerId").in(consumerSubquery));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
}

```
Page 5/7FirstPrevNextLast