This is page 3 of 7. Use http://codebase.md/higress-group/himarket?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .cursor │ └── rules │ ├── api-style.mdc │ └── project-architecture.mdc ├── .gitignore ├── build.sh ├── deploy │ ├── docker │ │ ├── docker-compose.yml │ │ └── Docker部署说明.md │ └── helm │ ├── Chart.yaml │ ├── Helm部署说明.md │ ├── templates │ │ ├── _helpers.tpl │ │ ├── himarket-admin-cm.yaml │ │ ├── himarket-admin-deployment.yaml │ │ ├── himarket-admin-service.yaml │ │ ├── himarket-frontend-cm.yaml │ │ ├── himarket-frontend-deployment.yaml │ │ ├── himarket-frontend-service.yaml │ │ ├── himarket-server-cm.yaml │ │ ├── himarket-server-deployment.yaml │ │ ├── himarket-server-service.yaml │ │ ├── mysql.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── LICENSE ├── NOTICE ├── pom.xml ├── portal-bootstrap │ ├── Dockerfile │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── alibaba │ │ │ └── apiopenplatform │ │ │ ├── config │ │ │ │ ├── AsyncConfig.java │ │ │ │ ├── FilterConfig.java │ │ │ │ ├── PageConfig.java │ │ │ │ ├── RestTemplateConfig.java │ │ │ │ ├── SecurityConfig.java │ │ │ │ └── SwaggerConfig.java │ │ │ ├── filter │ │ │ │ └── PortalResolvingFilter.java │ │ │ └── PortalApplication.java │ │ └── resources │ │ └── application.yaml │ └── test │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ └── integration │ └── AdministratorAuthIntegrationTest.java ├── portal-dal │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ ├── converter │ │ ├── AdpAIGatewayConfigConverter.java │ │ ├── APIGConfigConverter.java │ │ ├── APIGRefConfigConverter.java │ │ ├── ApiKeyConfigConverter.java │ │ ├── ConsumerAuthConfigConverter.java │ │ ├── GatewayConfigConverter.java │ │ ├── HigressConfigConverter.java │ │ ├── HigressRefConfigConverter.java │ │ ├── HmacConfigConverter.java │ │ ├── JsonConverter.java │ │ ├── JwtConfigConverter.java │ │ ├── NacosRefConfigConverter.java │ │ ├── PortalSettingConfigConverter.java │ │ ├── PortalUiConfigConverter.java │ │ └── ProductIconConverter.java │ ├── entity │ │ ├── Administrator.java │ │ ├── BaseEntity.java │ │ ├── Consumer.java │ │ ├── ConsumerCredential.java │ │ ├── ConsumerRef.java │ │ ├── Developer.java │ │ ├── DeveloperExternalIdentity.java │ │ ├── Gateway.java │ │ ├── NacosInstance.java │ │ ├── Portal.java │ │ ├── PortalDomain.java │ │ ├── Product.java │ │ ├── ProductPublication.java │ │ ├── ProductRef.java │ │ └── ProductSubscription.java │ ├── repository │ │ ├── AdministratorRepository.java │ │ ├── BaseRepository.java │ │ ├── ConsumerCredentialRepository.java │ │ ├── ConsumerRefRepository.java │ │ ├── ConsumerRepository.java │ │ ├── DeveloperExternalIdentityRepository.java │ │ ├── DeveloperRepository.java │ │ ├── GatewayRepository.java │ │ ├── NacosInstanceRepository.java │ │ ├── PortalDomainRepository.java │ │ ├── PortalRepository.java │ │ ├── ProductPublicationRepository.java │ │ ├── ProductRefRepository.java │ │ ├── ProductRepository.java │ │ └── SubscriptionRepository.java │ └── support │ ├── common │ │ ├── Encrypted.java │ │ ├── Encryptor.java │ │ └── User.java │ ├── consumer │ │ ├── AdpAIAuthConfig.java │ │ ├── APIGAuthConfig.java │ │ ├── ApiKeyConfig.java │ │ ├── ConsumerAuthConfig.java │ │ ├── HigressAuthConfig.java │ │ ├── HmacConfig.java │ │ └── JwtConfig.java │ ├── enums │ │ ├── APIGAPIType.java │ │ ├── ConsumerAuthType.java │ │ ├── ConsumerStatus.java │ │ ├── CredentialMode.java │ │ ├── DeveloperAuthType.java │ │ ├── DeveloperStatus.java │ │ ├── DomainType.java │ │ ├── GatewayType.java │ │ ├── GrantType.java │ │ ├── HigressAPIType.java │ │ ├── JwtAlgorithm.java │ │ ├── ProductIconType.java │ │ ├── ProductStatus.java │ │ ├── ProductType.java │ │ ├── ProtocolType.java │ │ ├── PublicKeyFormat.java │ │ ├── SourceType.java │ │ ├── SubscriptionStatus.java │ │ └── UserType.java │ ├── gateway │ │ ├── AdpAIGatewayConfig.java │ │ ├── APIGConfig.java │ │ ├── GatewayConfig.java │ │ └── HigressConfig.java │ ├── portal │ │ ├── AuthCodeConfig.java │ │ ├── IdentityMapping.java │ │ ├── JwtBearerConfig.java │ │ ├── OAuth2Config.java │ │ ├── OidcConfig.java │ │ ├── PortalSettingConfig.java │ │ ├── PortalUiConfig.java │ │ └── PublicKeyConfig.java │ └── product │ ├── APIGRefConfig.java │ ├── HigressRefConfig.java │ ├── NacosRefConfig.java │ └── ProductIcon.java ├── portal-server │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── alibaba │ └── apiopenplatform │ ├── controller │ │ ├── AdministratorController.java │ │ ├── ConsumerController.java │ │ ├── DeveloperController.java │ │ ├── GatewayController.java │ │ ├── NacosController.java │ │ ├── OAuth2Controller.java │ │ ├── OidcController.java │ │ ├── PortalController.java │ │ └── ProductController.java │ ├── core │ │ ├── advice │ │ │ ├── ExceptionAdvice.java │ │ │ └── ResponseAdvice.java │ │ ├── annotation │ │ │ ├── AdminAuth.java │ │ │ ├── AdminOrDeveloperAuth.java │ │ │ └── DeveloperAuth.java │ │ ├── constant │ │ │ ├── CommonConstants.java │ │ │ ├── IdpConstants.java │ │ │ ├── JwtConstants.java │ │ │ └── Resources.java │ │ ├── event │ │ │ ├── DeveloperDeletingEvent.java │ │ │ ├── PortalDeletingEvent.java │ │ │ └── ProductDeletingEvent.java │ │ ├── exception │ │ │ ├── BusinessException.java │ │ │ └── ErrorCode.java │ │ ├── response │ │ │ └── Response.java │ │ ├── security │ │ │ ├── ContextHolder.java │ │ │ ├── DeveloperAuthenticationProvider.java │ │ │ └── JwtAuthenticationFilter.java │ │ └── utils │ │ ├── IdGenerator.java │ │ ├── PasswordHasher.java │ │ └── TokenUtil.java │ ├── dto │ │ ├── converter │ │ │ ├── InputConverter.java │ │ │ ├── NacosToGatewayToolsConverter.java │ │ │ └── OutputConverter.java │ │ ├── params │ │ │ ├── admin │ │ │ │ ├── AdminCreateParam.java │ │ │ │ ├── AdminLoginParam.java │ │ │ │ └── ResetPasswordParam.java │ │ │ ├── consumer │ │ │ │ ├── CreateConsumerParam.java │ │ │ │ ├── CreateCredentialParam.java │ │ │ │ ├── CreateSubscriptionParam.java │ │ │ │ ├── QueryConsumerParam.java │ │ │ │ ├── QuerySubscriptionParam.java │ │ │ │ └── UpdateCredentialParam.java │ │ │ ├── developer │ │ │ │ ├── CreateDeveloperParam.java │ │ │ │ ├── CreateExternalDeveloperParam.java │ │ │ │ ├── DeveloperLoginParam.java │ │ │ │ ├── QueryDeveloperParam.java │ │ │ │ ├── UnbindExternalIdentityParam.java │ │ │ │ ├── UpdateDeveloperParam.java │ │ │ │ └── UpdateDeveloperStatusParam.java │ │ │ ├── gateway │ │ │ │ ├── ImportGatewayParam.java │ │ │ │ ├── QueryAdpAIGatewayParam.java │ │ │ │ ├── QueryAPIGParam.java │ │ │ │ └── QueryGatewayParam.java │ │ │ ├── nacos │ │ │ │ ├── CreateNacosParam.java │ │ │ │ ├── QueryNacosNamespaceParam.java │ │ │ │ ├── QueryNacosParam.java │ │ │ │ └── UpdateNacosParam.java │ │ │ ├── portal │ │ │ │ ├── BindDomainParam.java │ │ │ │ ├── CreatePortalParam.java │ │ │ │ └── UpdatePortalParam.java │ │ │ └── product │ │ │ ├── CreateProductParam.java │ │ │ ├── CreateProductRefParam.java │ │ │ ├── PublishProductParam.java │ │ │ ├── QueryProductParam.java │ │ │ ├── QueryProductSubscriptionParam.java │ │ │ ├── UnPublishProductParam.java │ │ │ └── UpdateProductParam.java │ │ └── result │ │ ├── AdminResult.java │ │ ├── AdpGatewayInstanceResult.java │ │ ├── AdpMcpServerListResult.java │ │ ├── AdpMCPServerResult.java │ │ ├── APIConfigResult.java │ │ ├── APIGMCPServerResult.java │ │ ├── APIResult.java │ │ ├── AuthResult.java │ │ ├── ConsumerCredentialResult.java │ │ ├── ConsumerResult.java │ │ ├── DeveloperResult.java │ │ ├── GatewayMCPServerResult.java │ │ ├── GatewayResult.java │ │ ├── HigressMCPServerResult.java │ │ ├── IdpResult.java │ │ ├── IdpState.java │ │ ├── IdpTokenResult.java │ │ ├── MCPConfigResult.java │ │ ├── MCPServerResult.java │ │ ├── MseNacosResult.java │ │ ├── NacosMCPServerResult.java │ │ ├── NacosNamespaceResult.java │ │ ├── NacosResult.java │ │ ├── PageResult.java │ │ ├── PortalResult.java │ │ ├── ProductPublicationResult.java │ │ ├── ProductRefResult.java │ │ ├── ProductResult.java │ │ └── SubscriptionResult.java │ └── service │ ├── AdministratorService.java │ ├── AdpAIGatewayService.java │ ├── ConsumerService.java │ ├── DeveloperService.java │ ├── gateway │ │ ├── AdpAIGatewayOperator.java │ │ ├── AIGatewayOperator.java │ │ ├── APIGOperator.java │ │ ├── client │ │ │ ├── AdpAIGatewayClient.java │ │ │ ├── APIGClient.java │ │ │ ├── GatewayClient.java │ │ │ ├── HigressClient.java │ │ │ ├── PopGatewayClient.java │ │ │ └── SLSClient.java │ │ ├── factory │ │ │ └── HTTPClientFactory.java │ │ ├── GatewayOperator.java │ │ └── HigressOperator.java │ ├── GatewayService.java │ ├── IdpService.java │ ├── impl │ │ ├── AdministratorServiceImpl.java │ │ ├── ConsumerServiceImpl.java │ │ ├── DeveloperServiceImpl.java │ │ ├── GatewayServiceImpl.java │ │ ├── IdpServiceImpl.java │ │ ├── NacosServiceImpl.java │ │ ├── OAuth2ServiceImpl.java │ │ ├── OidcServiceImpl.java │ │ ├── PortalServiceImpl.java │ │ └── ProductServiceImpl.java │ ├── NacosService.java │ ├── OAuth2Service.java │ ├── OidcService.java │ ├── PortalService.java │ └── ProductService.java ├── portal-web │ ├── api-portal-admin │ │ ├── .env │ │ ├── .gitignore │ │ ├── bin │ │ │ ├── replace_var.py │ │ │ └── start.sh │ │ ├── Dockerfile │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── nginx.conf │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── proxy.conf │ │ ├── public │ │ │ ├── logo.png │ │ │ └── vite.svg │ │ ├── README.md │ │ ├── src │ │ │ ├── aliyunThemeToken.ts │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── assets │ │ │ │ └── react.svg │ │ │ ├── components │ │ │ │ ├── api-product │ │ │ │ │ ├── ApiProductApiDocs.tsx │ │ │ │ │ ├── ApiProductDashboard.tsx │ │ │ │ │ ├── ApiProductFormModal.tsx │ │ │ │ │ ├── ApiProductLinkApi.tsx │ │ │ │ │ ├── ApiProductOverview.tsx │ │ │ │ │ ├── ApiProductPolicy.tsx │ │ │ │ │ ├── ApiProductPortal.tsx │ │ │ │ │ ├── ApiProductUsageGuide.tsx │ │ │ │ │ ├── SwaggerUIWrapper.css │ │ │ │ │ └── SwaggerUIWrapper.tsx │ │ │ │ ├── common │ │ │ │ │ ├── AdvancedSearch.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── console │ │ │ │ │ ├── GatewayTypeSelector.tsx │ │ │ │ │ ├── ImportGatewayModal.tsx │ │ │ │ │ ├── ImportHigressModal.tsx │ │ │ │ │ ├── ImportMseNacosModal.tsx │ │ │ │ │ └── NacosTypeSelector.tsx │ │ │ │ ├── icons │ │ │ │ │ └── McpServerIcon.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ ├── LayoutWrapper.tsx │ │ │ │ ├── portal │ │ │ │ │ ├── PortalConsumers.tsx │ │ │ │ │ ├── PortalDashboard.tsx │ │ │ │ │ ├── PortalDevelopers.tsx │ │ │ │ │ ├── PortalDomain.tsx │ │ │ │ │ ├── PortalFormModal.tsx │ │ │ │ │ ├── PortalOverview.tsx │ │ │ │ │ ├── PortalPublishedApis.tsx │ │ │ │ │ ├── PortalSecurity.tsx │ │ │ │ │ ├── PortalSettings.tsx │ │ │ │ │ ├── PublicKeyManager.tsx │ │ │ │ │ └── ThirdPartyAuthManager.tsx │ │ │ │ └── subscription │ │ │ │ └── SubscriptionListModal.tsx │ │ │ ├── contexts │ │ │ │ └── LoadingContext.tsx │ │ │ ├── index.css │ │ │ ├── lib │ │ │ │ ├── api.ts │ │ │ │ ├── constant.ts │ │ │ │ └── utils.ts │ │ │ ├── main.tsx │ │ │ ├── pages │ │ │ │ ├── ApiProductDetail.tsx │ │ │ │ ├── ApiProducts.tsx │ │ │ │ ├── Dashboard.tsx │ │ │ │ ├── GatewayConsoles.tsx │ │ │ │ ├── Login.tsx │ │ │ │ ├── NacosConsoles.tsx │ │ │ │ ├── PortalDetail.tsx │ │ │ │ ├── Portals.tsx │ │ │ │ └── Register.tsx │ │ │ ├── routes │ │ │ │ └── index.tsx │ │ │ ├── types │ │ │ │ ├── api-product.ts │ │ │ │ ├── consumer.ts │ │ │ │ ├── gateway.ts │ │ │ │ ├── index.ts │ │ │ │ ├── portal.ts │ │ │ │ ├── shims-js-yaml.d.ts │ │ │ │ └── subscription.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── api-portal-frontend │ ├── .env │ ├── .gitignore │ ├── .husky │ │ └── pre-commit │ ├── bin │ │ ├── replace_var.py │ │ └── start.sh │ ├── Dockerfile │ ├── eslint.config.js │ ├── index.html │ ├── nginx.conf │ ├── package.json │ ├── postcss.config.js │ ├── proxy.conf │ ├── public │ │ ├── favicon.ico │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── MCP.png │ │ ├── MCP.svg │ │ └── vite.svg │ ├── README.md │ ├── src │ │ ├── aliyunThemeToken.ts │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ ├── aliyun.png │ │ │ ├── github.png │ │ │ ├── google.png │ │ │ └── react.svg │ │ ├── components │ │ │ ├── consumer │ │ │ │ ├── ConsumerBasicInfo.tsx │ │ │ │ ├── CredentialManager.tsx │ │ │ │ ├── index.ts │ │ │ │ └── SubscriptionManager.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── ProductHeader.tsx │ │ │ ├── SwaggerUIWrapper.css │ │ │ ├── SwaggerUIWrapper.tsx │ │ │ └── UserInfo.tsx │ │ ├── index.css │ │ ├── lib │ │ │ ├── api.ts │ │ │ ├── statusUtils.ts │ │ │ └── utils.ts │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── ApiDetail.tsx │ │ │ ├── Apis.tsx │ │ │ ├── Callback.tsx │ │ │ ├── ConsumerDetail.tsx │ │ │ ├── Consumers.tsx │ │ │ ├── GettingStarted.tsx │ │ │ ├── Home.tsx │ │ │ ├── Login.tsx │ │ │ ├── Mcp.tsx │ │ │ ├── McpDetail.tsx │ │ │ ├── OidcCallback.tsx │ │ │ ├── Profile.tsx │ │ │ ├── Register.tsx │ │ │ └── Test.css │ │ ├── router.tsx │ │ ├── types │ │ │ ├── consumer.ts │ │ │ └── index.ts │ │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── README.md ``` # Files -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/ProductService.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; import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent; import com.alibaba.apiopenplatform.dto.params.product.*; import com.alibaba.apiopenplatform.dto.result.*; import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Map; public interface ProductService { /** * 创建API产品 * * @param param * @return */ ProductResult createProduct(CreateProductParam param); /** * 查询API产品 * * @param productId * @return */ ProductResult getProduct(String productId); /** * 查询API产品列表 * * @param param * @param pageable * @return */ PageResult<ProductResult> listProducts(QueryProductParam param, Pageable pageable); /** * 更新门户 * * @param productId * @param param * @return */ ProductResult updateProduct(String productId, UpdateProductParam param); /** * 发布API产品 * * @param productId * @param portalId * @return */ void publishProduct(String productId, String portalId); /** * 获取API产品的发布信息 * * @param productId * @param pageable * @return */ PageResult<ProductPublicationResult> getPublications(String productId, Pageable pageable); /** * 下线产品 * * @param productId * @param portalId * @return */ void unpublishProduct(String productId, String portalId); /** * 删除产品 * * @param productId */ void deleteProduct(String productId); /** * API产品引用API或MCP Server * * @param productId * @param param */ void addProductRef(String productId, CreateProductRefParam param); /** * 查询API产品引用的资源 * * @param productId * @return */ ProductRefResult getProductRef(String productId); /** * 删除API产品的引用 * * @param productId */ void deleteProductRef(String productId); /** * 清理门户资源 * * @param event */ void handlePortalDeletion(PortalDeletingEvent event); Map<String, ProductResult> getProducts(List<String> productIds); /** * 获取API产品的Dashboard监控面板URL * * @param productId * @return Dashboard URL */ String getProductDashboard(String productId); /** * 获取API产品的订阅信息 * * @param productId * @param param * @param pageable * @return */ PageResult<SubscriptionResult> listProductSubscriptions(String productId, QueryProductSubscriptionParam param, Pageable pageable); /** * 检查API产品是否存在 * * @param productId * @return */ void existsProduct(String productId); } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/pages/Test.css: -------------------------------------------------------------------------------- ```css .test-container { padding: 2rem; max-width: 800px; margin: 0 auto; font-family: 'Arial', sans-serif; } .test-container h1 { text-align: center; color: #333; margin-bottom: 2rem; } .demo-section { margin-bottom: 3rem; padding: 1.5rem; border: 1px solid #e0e0e0; border-radius: 8px; background: #fafafa; } .demo-section h2 { color: #555; margin-bottom: 1rem; font-size: 1.2rem; } .text-flow-container { text-align: center; padding: 2rem 0; } .text-flow-container.large { padding: 3rem 0; } .text-flow { display: inline-block; margin: 0 1rem; /* font-weight: bold; */ position: relative; } /* 基础流光效果 - 红色到白色 */ .text-flow-primary { background-image: linear-gradient( to right, #ff0000, #ffffff 12.5%, #ff0000 25%, #ffffff 37.5%, #ff0000 50%, #ff0000 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 400% 100%; animation: light 2s infinite linear; } /* 次要流光效果 - 黑色到白色 */ .text-flow-secondary { background-image: linear-gradient( to right, #000000, #000000 50%, #ffffff 62.5%, #000000 75%, #ffffff 87.5%, #000000 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 800% 100%; animation: light 2s infinite linear; } .text-flow-grey { background-image: linear-gradient( to right, #4b5563, #ffffff 12.5%, #4b5563 25%, #ffffff 37.5%, #4b5563 50%, #4b5563 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 400% 100%; animation: light 2s infinite linear; } /* 蓝色流光效果 */ .text-flow-blue { background-image: linear-gradient( to right, #0066cc, #ffffff 12.5%, #0066cc 25%, #ffffff 37.5%, #0066cc 50%, #0066cc 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 400% 100%; animation: light 2s infinite linear; } /* 绿色流光效果 */ .text-flow-green { background-image: linear-gradient( to right, #00cc00, #ffffff 12.5%, #00cc00 25%, #ffffff 37.5%, #00cc00 50%, #00cc00 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 400% 100%; animation: light 2s infinite linear; } /* 紫色流光效果 */ .text-flow-purple { background-image: linear-gradient( to right, #6600cc, #ffffff 12.5%, #6600cc 25%, #ffffff 37.5%, #6600cc 50%, #6600cc 100% ); -webkit-text-fill-color: transparent; -webkit-background-clip: text; background-clip: text; background-size: 400% 100%; animation: light 2s infinite linear; } /* 大字体流光效果 */ .text-flow-container.large .text-flow { font-size: 3.5rem; margin: 0 1.5rem; } /* 慢速动画 */ .text-flow.slow { /* animation: light 4s infinite linear; */ } /* 快速动画 */ .text-flow.fast { animation: light 1s infinite linear; } /* 流光动画关键帧 */ @keyframes light { 0% { background-position: 0 0; } 100% { background-position: -100% 0; } } /* 兼容性处理 */ @-webkit-keyframes light { 0% { background-position: 0 0; } 100% { background-position: -100% 0; } } /* 响应式设计 */ @media (max-width: 768px) { .test-container { padding: 1rem; } .text-flow { font-size: 1.5rem; margin: 0 0.5rem; } .text-flow-container.large .text-flow { font-size: 2.5rem; margin: 0 1rem; } } ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/filter/PortalResolvingFilter.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.filter; import cn.hutool.core.util.StrUtil; import com.alibaba.apiopenplatform.core.security.ContextHolder; import com.alibaba.apiopenplatform.service.PortalService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; @Slf4j @RequiredArgsConstructor public class PortalResolvingFilter extends OncePerRequestFilter { private final PortalService portalService; private final ContextHolder contextHolder; @Override protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws ServletException, IOException { try { String origin = request.getHeader("Origin"); String host = request.getHeader("Host"); String xForwardedHost = request.getHeader("X-Forwarded-Host"); String xRealIp = request.getHeader("X-Real-IP"); String xForwardedFor = request.getHeader("X-Forwarded-For"); String domain = null; if (origin != null) { try { URI uri = new URI(origin); domain = uri.getHost(); } catch (Exception ignored) { } } log.info("域名解析调试 - Origin: {}, Host: {}, X-Forwarded-Host: {}, ServerName: {}, X-Real-IP: {}, X-Forwarded-For: {}", origin, host, xForwardedHost, request.getServerName(), xRealIp, xForwardedFor); if (domain == null) { // 优先使用Host头,如果没有则使用ServerName if (host != null && !host.isEmpty()) { domain = host.split(":")[0]; // 去掉端口号 } else { domain = request.getServerName(); } } String portalId = portalService.resolvePortal(domain); if (StrUtil.isNotBlank(portalId)) { contextHolder.savePortal(portalId); log.info("Resolved portal for domain: {} with portalId: {}", domain, portalId); } else { log.info("No portal found for domain: {}", domain); String defaultPortalId = portalService.getDefaultPortal(); if (StrUtil.isNotBlank(defaultPortalId)) { contextHolder.savePortal(defaultPortalId); log.info("Use default portal: {}", defaultPortalId); } } chain.doFilter(request, response); } finally { contextHolder.clearPortal(); } } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/GatewayService.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; import com.alibaba.apiopenplatform.dto.params.gateway.ImportGatewayParam; import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam; import com.alibaba.apiopenplatform.dto.params.gateway.QueryGatewayParam; import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult; import com.alibaba.apiopenplatform.dto.result.*; import com.alibaba.apiopenplatform.entity.Consumer; import com.alibaba.apiopenplatform.entity.ConsumerCredential; import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig; import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; import org.springframework.data.domain.Pageable; public interface GatewayService { /** * 获取APIG Gateway列表 * * @param param * @param page * @param size * @return */ PageResult<GatewayResult> fetchGateways(QueryAPIGParam param, int page, int size); /** * 导入Gateway * * @param param */ void importGateway(ImportGatewayParam param); GatewayResult getGateway(String gatewayId); /** * 获取导入的Gateway列表 * * @param param * @param pageable * @return */ PageResult<GatewayResult> listGateways(QueryGatewayParam param, Pageable pageable); /** * 删除Gateway * * @param gatewayId */ void deleteGateway(String gatewayId); /** * 拉取网关API列表 * * @param gatewayId * @param apiType * @param page * @param size * @return */ PageResult<APIResult> fetchAPIs(String gatewayId, String apiType, int page, int size); PageResult<APIResult> fetchHTTPAPIs(String gatewayId, int page, int size); PageResult<APIResult> fetchRESTAPIs(String gatewayId, int page, int size); PageResult<APIResult> fetchRoutes(String gatewayId, int page, int size); PageResult<GatewayMCPServerResult> fetchMcpServers(String gatewayId, int page, int size); String fetchAPIConfig(String gatewayId, Object config); String fetchMcpConfig(String gatewayId, Object conf); String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config); void updateConsumer(String gwConsumerId, ConsumerCredential credential, GatewayConfig config); void deleteConsumer(String gwConsumerId, GatewayConfig config); /** * 检查消费者是否存在于网关中 * @param gwConsumerId 网关消费者ID * @param config 网关配置 * @return 是否存在 */ boolean isConsumerExists(String gwConsumerId, GatewayConfig config); ConsumerAuthConfig authorizeConsumer(String gatewayId, String gwConsumerId, ProductRefResult productRef); void revokeConsumerAuthorization(String gatewayId, String gwConsumerId, ConsumerAuthConfig config); GatewayConfig getGatewayConfig(String gatewayId); /** * 获取仪表板URL * * @return 仪表板URL */ String getDashboard(String gatewayId, String type); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/ConsumerService.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; import com.alibaba.apiopenplatform.dto.params.consumer.QueryConsumerParam; import com.alibaba.apiopenplatform.dto.params.consumer.CreateConsumerParam; import com.alibaba.apiopenplatform.dto.result.ConsumerResult; import com.alibaba.apiopenplatform.dto.result.PageResult; import com.alibaba.apiopenplatform.dto.result.ConsumerCredentialResult; import com.alibaba.apiopenplatform.dto.params.consumer.CreateCredentialParam; import com.alibaba.apiopenplatform.dto.params.consumer.UpdateCredentialParam; import com.alibaba.apiopenplatform.dto.result.SubscriptionResult; import com.alibaba.apiopenplatform.dto.params.consumer.CreateSubscriptionParam; import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam; import org.springframework.data.domain.Pageable; public interface ConsumerService { /** * 创建Consumer * * @param param * @return */ ConsumerResult createConsumer(CreateConsumerParam param); /** * 获取Consumer列表 * * @param param * @param pageable * @return */ PageResult<ConsumerResult> listConsumers(QueryConsumerParam param, Pageable pageable); /** * 查询Consumer * * @param consumerId * @return */ ConsumerResult getConsumer(String consumerId); /** * 删除Consumer * * @param consumerId */ void deleteConsumer(String consumerId); /** * 创建Consumer凭证 * * @param consumerId * @param param */ void createCredential(String consumerId, CreateCredentialParam param); /** * 获取Consumer凭证 * * @param consumerId * @return */ ConsumerCredentialResult getCredential(String consumerId); /** * 更新Consumer凭证 * * @param consumerId * @param param */ void updateCredential(String consumerId, UpdateCredentialParam param); /** * 删除Consumer凭证 * * @param consumerId Consumer ID */ void deleteCredential(String consumerId); /** * 订阅API产品 * * @param consumerId * @param param * @return */ SubscriptionResult subscribeProduct(String consumerId, CreateSubscriptionParam param); /** * 取消订阅 * * @param consumerId * @param productId */ void unsubscribeProduct(String consumerId, String productId); /** * 获取Consumer的订阅列表 * * @param consumerId * @param param * @param pageable * @return */ PageResult<SubscriptionResult> listSubscriptions(String consumerId, QuerySubscriptionParam param, Pageable pageable); /** * 取消订阅API产品 * * @param consumerId * @param productId */ void deleteSubscription(String consumerId, String productId); /** * 审批订阅API产品 * * @param consumerId * @param productId */ SubscriptionResult approveSubscription(String consumerId, String productId); } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/types/index.ts: -------------------------------------------------------------------------------- ```typescript // 与 Admin 端保持一致的 API 产品配置接口 export interface ApiProductConfig { spec: string; meta: { source: string; type: string; } } export interface ApiProductMcpConfig { mcpServerName: string; tools: string; meta: { source: string; mcpServerName: string; mcpServerConfig: any; fromType: string; protocol?: string; } mcpServerConfig: { path: string; domains: { domain: string; protocol: string; }[]; rawConfig?: unknown; } } export interface ApiProduct { productId: string; name: string; description: string; type: 'REST_API' | 'MCP_SERVER'; category: string; status: 'PENDING' | 'READY' | 'PUBLISHED' | string; createAt: string; createdAt?: string; // 兼容字段 enableConsumerAuth?: boolean; autoApprove?: boolean; apiConfig?: ApiProductConfig; mcpConfig?: ApiProductMcpConfig; document?: string; icon?: ProductIcon | null; // 向后兼容 apiSpec?: string; } export const ProductType = { REST_API: 'REST_API', MCP_SERVER: 'MCP_SERVER', } as const; export type ProductType = typeof ProductType[keyof typeof ProductType]; // 产品状态枚举 export const ProductStatus = { ENABLE: 'ENABLE', DISABLE: 'DISABLE', } as const; export type ProductStatus = typeof ProductStatus[keyof typeof ProductStatus]; // 产品分类 export const ProductCategory = { OFFICIAL: 'official', COMMUNITY: 'community', CUSTOM: 'custom', } as const; export type ProductCategory = typeof ProductCategory[keyof typeof ProductCategory]; // 基础产品接口 export interface BaseProduct { productId: string; name: string; description: string; status: ProductStatus; enableConsumerAuth: boolean | null; autoApprove?: boolean; type: ProductType; document: string | null; icon: ProductIcon | null; category: ProductCategory; productType: ProductType; productName: string; mcpConfig: any; updatedAt: string; lastUpdated: string; } // REST API 产品 export interface RestApiProduct extends BaseProduct { apiSpec: string | null; mcpSpec: null; } // MCP Server 产品 // @ts-ignore export interface McpServerProduct extends BaseProduct { apiSpec: null; mcpSpec?: McpServerConfig; // 保持向后兼容 mcpConfig?: McpConfig; // 新的nacos格式 enabled?: boolean; } // 联合类型 export type Product = RestApiProduct | McpServerProduct; // 产品图标类型(与 Admin 端保持一致) export interface ProductIcon { type: 'URL' | 'BASE64'; value: string; } // API 响应结构 export interface ApiResponse<T> { code: string; message: string | null; data: T; } // 分页响应结构 export interface PaginatedResponse<T> { content: T[]; totalElements: number; totalPages: number; size: number; number: number; first: boolean; last: boolean; } // MCP 配置解析后的结构 (旧格式,保持向后兼容) export interface McpServerConfig { mcpRouteId?: string; mcpServerName?: string; fromType?: string; fromGatewayType?: string; domains?: Array<{ domain: string; protocol: string; }>; mcpServerConfig?: string; // YAML配置字符串 enabled?: boolean; server?: { name: string; config: Record<string, unknown>; allowTools: string[]; }; tools?: Array<{ name: string; description: string; args: Array<{ name: string; description: string; type: string; required: boolean; position: string; default?: string; enum?: string[]; }>; requestTemplate: { url: string; method: string; headers: Array<{ key: string; value: string; }>; }; responseTemplate: { body: string; }; }>; } // 新的nacos格式MCP配置 export interface McpConfig { mcpServerName: string; mcpServerConfig: { path: string; domains: Array<{ domain: string; protocol: string; }>; rawConfig?: string; }; tools: string; // YAML格式的tools配置字符串 meta: { source: string; fromType: string; protocol?: string; }; } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/security/ContextHolder.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.core.security; import cn.hutool.core.util.EnumUtil; import com.alibaba.apiopenplatform.core.constant.CommonConstants; import com.alibaba.apiopenplatform.support.enums.UserType; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @Component public class ContextHolder { private final ThreadLocal<String> portalContext = new ThreadLocal<>(); public String getPortal() { return portalContext.get(); } public void savePortal(String portalId) { portalContext.set(portalId); } public void clearPortal() { portalContext.remove(); } /** * 获取当前认证用户ID * * @return */ public String getUser() { Authentication authentication = getAuthenticationFromContext(); Object principal = authentication.getPrincipal(); if (principal instanceof String) { return (String) principal; } throw new AuthenticationCredentialsNotFoundException("User ID not found in authentication"); } /** * 获取当前认证用户类型 * * @return 用户类型 * @throws AuthenticationException 如果用户未认证或类型无效 */ private UserType getCurrentUserType() { Authentication authentication = getAuthenticationFromContext(); return authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .filter(authority -> authority.startsWith(CommonConstants.ROLE_PREFIX)) .map(authority -> authority.substring(5)) .map(role -> EnumUtil.likeValueOf(UserType.class, role)) .findFirst() .orElseThrow(() -> new AuthenticationCredentialsNotFoundException("User type not found in authentication")); } public boolean isAdministrator() { try { return getCurrentUserType() == UserType.ADMIN; } catch (AuthenticationException e) { return false; } } public boolean isDeveloper() { try { return getCurrentUserType() == UserType.DEVELOPER; } catch (AuthenticationException e) { return false; } } /** * 获取当前认证信息 * * @return */ private Authentication getAuthenticationFromContext() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) { throw new AuthenticationCredentialsNotFoundException("No authenticated user found"); } return authentication; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/DeveloperService.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; import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent; 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.support.enums.DeveloperStatus; import org.springframework.data.domain.Pageable; import javax.servlet.http.HttpServletRequest; public interface DeveloperService { /** * 开发者注册 * * @param param * @return */ AuthResult registerDeveloper(CreateDeveloperParam param); /** * 创建开发者 * * @param param * @return */ DeveloperResult createDeveloper(CreateDeveloperParam param); /** * 开发者登录 * * @param username * @param password * @return */ AuthResult login(String username, String password); /** * 校验Developer * * @param developerId */ void existsDeveloper(String developerId); /** * 获取外部开发者详情 * * @param provider * @param subject * @return */ DeveloperResult getExternalDeveloper(String provider, String subject); /** * 外部账号创建开发者 * * @param param * @return */ DeveloperResult createExternalDeveloper(CreateExternalDeveloperParam param); /** * 删除开发者账号(删除账号及所有外部身份) * * @param developerId */ void deleteDeveloper(String developerId); /** * 查询开发者详情 * * @param developerId * @return */ DeveloperResult getDeveloper(String developerId); /** * 查询门户下的开发者列表 * * @param param * @param pageable * @return */ PageResult<DeveloperResult> listDevelopers(QueryDeveloperParam param, Pageable pageable); /** * 设置开发者状态 * * @param developerId * @param status * @return */ void setDeveloperStatus(String developerId, DeveloperStatus status); /** * 开发者修改密码 * * @param developerId * @param oldPassword * @param newPassword * @return */ boolean resetPassword(String developerId, String oldPassword, String newPassword); /** * 开发者更新个人信息 * * @param param * @return */ boolean updateProfile(UpdateDeveloperParam param); /** * 清理门户资源 * * @param event */ void handlePortalDeletion(PortalDeletingEvent event); /** * 开发者登出 * * @param request HTTP请求 */ void logout(HttpServletRequest request); /** * 获取当前登录开发者信息 * * @return 开发者信息 */ DeveloperResult getCurrentDeveloperInfo(); /** * 当前开发者修改密码 * * @param oldPassword 旧密码 * @param newPassword 新密码 * @return 是否成功 */ boolean changeCurrentDeveloperPassword(String oldPassword, String newPassword); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/PortalController.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.controller; import com.alibaba.apiopenplatform.core.annotation.AdminAuth; 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.service.PortalService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @RestController @RequestMapping("/portals") @Slf4j @Validated @Tag(name = "门户管理") @AdminAuth @RequiredArgsConstructor public class PortalController { private final PortalService portalService; @Operation(summary = "创建门户") @PostMapping public PortalResult createPortal(@Valid @RequestBody CreatePortalParam param) { return portalService.createPortal(param); } @Operation(summary = "获取门户详情") @GetMapping("/{portalId}") public PortalResult getPortal(@PathVariable String portalId) { return portalService.getPortal(portalId); } @Operation(summary = "获取门户列表") @GetMapping public PageResult<PortalResult> listPortals(Pageable pageable) { return portalService.listPortals(pageable); } @Operation(summary = "更新门户信息") @PutMapping("/{portalId}") public PortalResult updatePortal(@PathVariable String portalId, @Valid @RequestBody UpdatePortalParam param) { return portalService.updatePortal(portalId, param); } @Operation(summary = "删除门户") @DeleteMapping("/{portalId}") public void deletePortal(@PathVariable String portalId) { portalService.deletePortal(portalId); } @Operation(summary = "绑定域名") @PostMapping("/{portalId}/domains") public PortalResult bindDomain(@PathVariable String portalId, @Valid @RequestBody BindDomainParam param) { return portalService.bindDomain(portalId, param); } @Operation(summary = "解绑域名") @DeleteMapping("/{portalId}/domains/{domain}") public PortalResult unbindDomain(@PathVariable String portalId, @PathVariable String domain) { return portalService.unbindDomain(portalId, domain); } @Operation(summary = "获取门户上的API产品订阅列表") @GetMapping("/{portalId}/subscriptions") public PageResult<SubscriptionResult> listSubscriptions(@PathVariable String portalId, QuerySubscriptionParam param, Pageable pageable) { return portalService.listSubscriptions(portalId, param, pageable); } @Operation(summary = "获取门户Dashboard监控面板URL") @GetMapping("/{portalId}/dashboard") public String getDashboard(@PathVariable String portalId, @RequestParam(required = false, defaultValue = "Portal") String type) { return portalService.getDashboard(portalId); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/assets/react.svg: -------------------------------------------------------------------------------- ``` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/assets/react.svg: -------------------------------------------------------------------------------- ``` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/AdministratorServiceImpl.java: -------------------------------------------------------------------------------- ```java /* * 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.StrUtil; import com.alibaba.apiopenplatform.core.constant.Resources; import com.alibaba.apiopenplatform.core.security.ContextHolder; import com.alibaba.apiopenplatform.core.utils.TokenUtil; import com.alibaba.apiopenplatform.dto.result.AdminResult; import com.alibaba.apiopenplatform.dto.result.AuthResult; import com.alibaba.apiopenplatform.entity.Administrator; import com.alibaba.apiopenplatform.repository.AdministratorRepository; import com.alibaba.apiopenplatform.service.AdministratorService; import com.alibaba.apiopenplatform.core.utils.PasswordHasher; import com.alibaba.apiopenplatform.core.utils.IdGenerator; import lombok.RequiredArgsConstructor; 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; @Service @RequiredArgsConstructor @Transactional public class AdministratorServiceImpl implements AdministratorService { private final AdministratorRepository administratorRepository; private final ContextHolder contextHolder; @Override public AuthResult login(String username, String password) { Administrator admin = administratorRepository.findByUsername(username) .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.ADMINISTRATOR, username)); if (!PasswordHasher.verify(password, admin.getPasswordHash())) { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误"); } String token = TokenUtil.generateAdminToken(admin.getAdminId()); return AuthResult.of(token, TokenUtil.getTokenExpiresIn()); } @Override public boolean needInit() { return administratorRepository.count() == 0; } @Override public AdminResult initAdmin(String username, String password) { Administrator admin = Administrator.builder() .adminId(generateAdminId()) .username(username) .passwordHash(PasswordHasher.hash(password)) .build(); administratorRepository.save(admin); return new AdminResult().convertFrom(admin); } @Override public AdminResult getAdministrator() { Administrator administrator = findAdministrator(contextHolder.getUser()); return new AdminResult().convertFrom(administrator); } @Override @Transactional public void resetPassword(String oldPassword, String newPassword) { Administrator admin = findAdministrator(contextHolder.getUser()); if (!PasswordHasher.verify(oldPassword, admin.getPasswordHash())) { throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误"); } admin.setPasswordHash(PasswordHasher.hash(newPassword)); administratorRepository.save(admin); } private String generateAdminId() { return IdGenerator.genAdministratorId(); } private Administrator findAdministrator(String adminId) { return administratorRepository.findByAdminId(adminId) .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.ADMINISTRATOR, adminId)); } } ``` -------------------------------------------------------------------------------- /deploy/helm/templates/mysql.yaml: -------------------------------------------------------------------------------- ```yaml {{- if .Values.mysql.enabled }} {{- $existingSecret := (lookup "v1" "Secret" .Release.Namespace "mysql-secret") }} {{- $rootPassword := "" }} {{- $userPassword := "" }} {{- if $existingSecret }} {{- $rootPassword = (index $existingSecret.data "MYSQL_ROOT_PASSWORD" | b64dec) }} {{- $userPassword = (index $existingSecret.data "MYSQL_PASSWORD" | b64dec) }} {{- else }} {{- if .Values.mysql.auth.rootPassword }} {{- $rootPassword = .Values.mysql.auth.rootPassword }} {{- else }} {{- $rootPassword = randAlphaNum 16 }} {{- end }} {{- if .Values.mysql.auth.password }} {{- $userPassword = .Values.mysql.auth.password }} {{- else }} {{- $userPassword = randAlphaNum 16 }} {{- end }} {{- end }} --- # MySQL Secret: 存储敏感的数据库凭据(自动生成随机密码) apiVersion: v1 kind: Secret metadata: name: mysql-secret type: Opaque stringData: MYSQL_ROOT_PASSWORD: {{ $rootPassword | quote }} MYSQL_DATABASE: {{ .Values.mysql.auth.database | quote }} MYSQL_USER: {{ .Values.mysql.auth.username | quote }} MYSQL_PASSWORD: {{ $userPassword | quote }} --- # HiMarket Server Secret: 应用专用敏感配置(使用相同的密码) apiVersion: v1 kind: Secret metadata: name: himarket-server-secret labels: app: himarket-server type: Opaque stringData: # 使用相同的 MySQL 密码变量,确保一致性 DB_HOST: "mysql-headless-svc" DB_PORT: "3306" DB_NAME: {{ .Values.mysql.auth.database | quote }} DB_USERNAME: {{ .Values.mysql.auth.username | quote }} DB_PASSWORD: {{ $userPassword | quote }} --- # MySQL Headless Service: 为 StatefulSet 提供稳定的网络域 apiVersion: v1 kind: Service metadata: name: mysql-headless-svc spec: ports: - port: 3306 name: mysql clusterIP: None selector: app: mysql --- # MySQL External Service: 暴露数据库给外部访问(可选) {{- if .Values.mysql.service.external.enabled }} apiVersion: v1 kind: Service metadata: name: mysql-external-svc spec: type: {{ .Values.mysql.service.external.type }} ports: - port: 3306 targetPort: 3306 protocol: TCP selector: app: mysql {{- end }} --- # MySQL StatefulSet: 部署 MySQL 应用 apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql labels: app: mysql spec: replicas: {{ .Values.mysql.replicaCount }} selector: matchLabels: app: mysql serviceName: "mysql-headless-svc" template: metadata: labels: app: mysql spec: serviceAccountName: {{ include "himarket.serviceAccountName" . }} containers: - name: mysql image: "{{ .Values.hub }}/{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}" imagePullPolicy: {{ .Values.mysql.image.pullPolicy }} ports: - containerPort: 3306 name: mysql envFrom: - secretRef: name: mysql-secret volumeMounts: - name: mysql-data mountPath: /var/lib/mysql {{- with .Values.mysql.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} # 健康检查 livenessProbe: exec: command: - mysqladmin - ping - -h - localhost - -u{{ .Values.mysql.auth.username }} - -p{{ $userPassword }} initialDelaySeconds: 60 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: exec: command: - mysql - -h - localhost - -u{{ .Values.mysql.auth.username }} - -p{{ $userPassword }} - -e - "SELECT 1" initialDelaySeconds: 1 periodSeconds: 1 timeoutSeconds: 5 volumeClaimTemplates: - metadata: name: mysql-data spec: accessModes: - {{ .Values.mysql.persistence.accessMode }} resources: requests: storage: {{ .Values.mysql.persistence.size }} {{- if .Values.mysql.persistence.storageClass }} {{- if (eq "-" .Values.mysql.persistence.storageClass) }} storageClassName: "" {{- else }} storageClassName: {{ .Values.mysql.persistence.storageClass | quote }} {{- end }} {{- end }} {{- end }} ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/NacosController.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.controller; import com.alibaba.apiopenplatform.core.annotation.AdminAuth; import com.alibaba.apiopenplatform.dto.params.nacos.CreateNacosParam; import com.alibaba.apiopenplatform.dto.params.nacos.QueryNacosParam; import com.alibaba.apiopenplatform.dto.params.nacos.UpdateNacosParam; import com.alibaba.apiopenplatform.dto.result.MseNacosResult; import com.alibaba.apiopenplatform.dto.result.NacosMCPServerResult; import com.alibaba.apiopenplatform.dto.result.NacosNamespaceResult; import com.alibaba.apiopenplatform.dto.result.NacosResult; import com.alibaba.apiopenplatform.dto.result.PageResult; import com.alibaba.apiopenplatform.service.NacosService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @Tag(name = "Nacos资源管理", description = "Nacos实例管理与能力市场统一控制器") @RestController @RequestMapping("/nacos") @RequiredArgsConstructor @AdminAuth public class NacosController { private final NacosService nacosService; @Operation(summary = "获取Nacos实例列表", description = "分页获取Nacos实例列表") @GetMapping public PageResult<NacosResult> listNacosInstances(Pageable pageable) { return nacosService.listNacosInstances(pageable); } @Operation(summary = "从阿里云MSE获取Nacos集群列表") @GetMapping("/mse") public PageResult<MseNacosResult> fetchNacos(@Valid QueryNacosParam param, Pageable pageable) { return nacosService.fetchNacos(param, pageable); } @Operation(summary = "获取Nacos实例详情", description = "根据ID获取Nacos实例详细信息") @GetMapping("/{nacosId}") public NacosResult getNacosInstance(@PathVariable String nacosId) { return nacosService.getNacosInstance(nacosId); } @Operation(summary = "创建Nacos实例", description = "创建新的Nacos实例") @PostMapping public void createNacosInstance(@RequestBody @Valid CreateNacosParam param) { nacosService.createNacosInstance(param); } @Operation(summary = "更新Nacos实例", description = "更新指定Nacos实例信息") @PutMapping("/{nacosId}") public void updateNacosInstance(@PathVariable String nacosId, @RequestBody @Valid UpdateNacosParam param) { nacosService.updateNacosInstance(nacosId, param); } @Operation(summary = "删除Nacos实例", description = "删除指定的Nacos实例") @DeleteMapping("/{nacosId}") public void deleteNacosInstance(@PathVariable String nacosId) { nacosService.deleteNacosInstance(nacosId); } @Operation(summary = "获取Nacos中的MCP Server列表", description = "获取指定Nacos实例中的MCP Server列表,可按命名空间过滤") @GetMapping("/{nacosId}/mcp-servers") public PageResult<NacosMCPServerResult> fetchMcpServers(@PathVariable String nacosId, @RequestParam(value = "namespaceId", required = false) String namespaceId, Pageable pageable) throws Exception { return nacosService.fetchMcpServers(nacosId, namespaceId, pageable); } @Operation(summary = "获取指定Nacos实例的命名空间列表") @GetMapping("/{nacosId}/namespaces") public PageResult<NacosNamespaceResult> fetchNamespaces(@PathVariable String nacosId, Pageable pageable) throws Exception { return nacosService.fetchNamespaces(nacosId, pageable); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/pages/ConsumerDetail.tsx: -------------------------------------------------------------------------------- ```typescript import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { Layout } from "../components/Layout"; import { Alert, Tabs } from "antd"; import { ArrowLeftOutlined } from "@ant-design/icons"; import api from "../lib/api"; import { ConsumerBasicInfo, CredentialManager, SubscriptionManager } from "../components/consumer"; import type { Consumer, Subscription } from "../types/consumer"; import type { ApiResponse } from "../types"; function ConsumerDetailPage() { const { consumerId } = useParams(); const navigate = useNavigate(); const [subscriptionsLoading, setSubscriptionsLoading] = useState(false); const [error, setError] = useState(''); const [consumer, setConsumer] = useState<Consumer>(); const [subscriptions, setSubscriptions] = useState<Subscription[]>([]); const [activeTab, setActiveTab] = useState('basic'); useEffect(() => { if (!consumerId) return; const fetchConsumerDetail = async () => { try { const response: ApiResponse<Consumer> = await api.get(`/consumers/${consumerId}`); if (response?.code === "SUCCESS" && response?.data) { setConsumer(response.data); } } catch (error) { console.error('获取消费者详情失败:', error); setError('加载失败,请稍后重试'); } }; const fetchSubscriptions = async () => { setSubscriptionsLoading(true); try { const response: ApiResponse<{content: Subscription[], totalElements: number}> = await api.get(`/consumers/${consumerId}/subscriptions`); if (response?.code === "SUCCESS" && response?.data) { // 从分页数据中提取实际的订阅数组 const subscriptionsData = response.data.content || []; setSubscriptions(subscriptionsData); } } catch (error) { console.error('获取订阅列表失败:', error); } finally { setSubscriptionsLoading(false); } }; const loadData = async () => { try { await Promise.all([ fetchConsumerDetail(), fetchSubscriptions() ]); } finally { // 不设置loading状态,避免闪烁 } }; loadData(); }, [consumerId]); if (error) { return ( <Layout> <Alert message="加载失败" description={error} type="error" showIcon className="my-8" /> </Layout> ); } return ( <Layout> {consumer ? ( <> {/* 消费者头部 - 返回按钮 + 消费者名称 */} <div className="mb-2"> <div className="flex items-center gap-2"> <ArrowLeftOutlined className="text-gray-500 hover:text-gray-700 cursor-pointer" style={{ fontSize: '20px', fontWeight: 'normal' }} onClick={() => navigate('/consumers')} /> <span className="text-2xl font-normal text-gray-500"> {consumer.name} </span> </div> </div> <Tabs activeKey={activeTab} onChange={setActiveTab}> <Tabs.TabPane tab="基本信息" key="basic"> <ConsumerBasicInfo consumer={consumer} /> <div className="mt-6"> <CredentialManager consumerId={consumerId!} /> </div> </Tabs.TabPane> <Tabs.TabPane tab="订阅列表" key="authorization"> <SubscriptionManager consumerId={consumerId!} subscriptions={subscriptions} onSubscriptionsChange={async () => { // 重新获取订阅列表 if (consumerId) { setSubscriptionsLoading(true); try { const response: ApiResponse<{content: Subscription[], totalElements: number}> = await api.get(`/consumers/${consumerId}/subscriptions`); if (response?.code === "SUCCESS" && response?.data) { // 从分页数据中提取实际的订阅数组 const subscriptionsData = response.data.content || []; setSubscriptions(subscriptionsData); } } catch (error) { console.error('获取订阅列表失败:', error); } finally { setSubscriptionsLoading(false); } } }} loading={subscriptionsLoading} /> </Tabs.TabPane> </Tabs> </> ) : ( <div className="flex items-center justify-center h-64"> <div className="text-gray-500">加载中...</div> </div> )} </Layout> ); } export default ConsumerDetailPage; ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/security/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.core.security; import com.alibaba.apiopenplatform.core.constant.CommonConstants; import com.alibaba.apiopenplatform.core.utils.TokenUtil; import com.alibaba.apiopenplatform.support.common.User; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { // 白名单路径 private static final String[] WHITELIST_PATHS = { "/admins/init", "/admins/need-init", "/admins/login", "/developers", "/developers/login", "/developers/authorize", "/developers/callback", "/developers/providers", "/developers/oidc/authorize", "/developers/oidc/callback", "/developers/oidc/providers", "/developers/oauth2/token", "/portal/swagger-ui.html", "/portal/swagger-ui/**", "/portal/v3/api-docs/**", "/favicon.ico", "/error" }; @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain) throws IOException, ServletException { // 检查是否是白名单路径 if (isWhitelistPath(request.getRequestURI())) { chain.doFilter(request, response); return; } try { String token = TokenUtil.getTokenFromRequest(request); if (token != null) { // 检查token是否被撤销 if (TokenUtil.isTokenRevoked(token)) { log.debug("Token已被撤销: {}", token); SecurityContextHolder.clearContext(); } else { try { authenticateRequest(token); } catch (Exception e) { log.debug("Token认证失败: {}", e.getMessage()); SecurityContextHolder.clearContext(); } } } } catch (Exception e) { log.debug("Token处理异常: {}", e.getMessage()); SecurityContextHolder.clearContext(); } chain.doFilter(request, response); } private boolean isWhitelistPath(String requestURI) { for (String whitelistPath : WHITELIST_PATHS) { if (whitelistPath.endsWith("/**")) { // 处理通配符路径 String basePath = whitelistPath.substring(0, whitelistPath.length() - 2); if (requestURI.startsWith(basePath)) { return true; } } else if (requestURI.equals(whitelistPath)) { return true; } } return false; } private void authenticateRequest(String token) { User user = TokenUtil.parseUser(token); // 设置认证信息 String role = CommonConstants.ROLE_PREFIX + user.getUserType().name(); Authentication authentication = new UsernamePasswordAuthenticationToken( user.getUserId(), null, Collections.singletonList(new SimpleGrantedAuthority(role)) ); SecurityContextHolder.getContext().setAuthentication(authentication); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/pages/Login.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import api from "../lib/api"; import { authApi } from '@/lib/api' import { Form, Input, Button, Alert } from "antd"; const Login: React.FC = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [isRegister, setIsRegister] = useState<boolean | null>(null); // null 表示正在加载 const navigate = useNavigate(); // 页面加载时检查权限 useEffect(() => { const checkAuth = async () => { try { const response = await authApi.getNeedInit(); // 替换为你的权限接口 setIsRegister(response.data === true); // 根据接口返回值决定是否显示注册表单 } catch (err) { setIsRegister(false); // 默认显示登录表单 } }; checkAuth(); }, []); // 登录表单提交 const handleLogin = async (values: { username: string; password: string }) => { setLoading(true); setError(""); try { const response = await api.post("/admins/login", { username: values.username, password: values.password, }); const accessToken = response.data.access_token; localStorage.setItem('access_token', accessToken); localStorage.setItem('userInfo', JSON.stringify(response.data)); navigate('/portals'); } catch { setError("账号或密码错误"); } finally { setLoading(false); } }; // 注册表单提交 const handleRegister = async (values: { username: string; password: string; confirmPassword: string }) => { setLoading(true); setError(""); if (values.password !== values.confirmPassword) { setError("两次输入的密码不一致"); setLoading(false); return; } try { const response = await api.post("/admins/init", { username: values.username, password: values.password, }); if (response.data.adminId) { setIsRegister(false); // 初始化成功后切换到登录状态 } } catch { setError("初始化失败,请重试"); } finally { setLoading(false); } }; return ( <div className="flex items-center justify-center min-h-screen bg-white"> <div className="bg-white p-8 rounded-xl shadow-2xl w-full max-w-md flex flex-col items-center border border-gray-100"> {/* Logo */} <div className="mb-4"> <img src="/logo.png" alt="Logo" className="w-16 h-16 mx-auto mb-4" /> </div> <h2 className="text-2xl font-bold mb-6 text-gray-900 text-center"> {isRegister ? "注册Admin账号" : "登录HiMarket-后台"} </h2> {/* 登录表单 */} {!isRegister && ( <Form className="w-full flex flex-col gap-4" layout="vertical" onFinish={handleLogin} > <Form.Item name="username" rules={[{ required: true, message: "请输入账号" }]} > <Input placeholder="账号" size="large" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: "请输入密码" }]} > <Input.Password placeholder="密码" size="large" /> </Form.Item> {error && <Alert message={error} type="error" showIcon className="mb-2" />} <Form.Item> <Button type="primary" htmlType="submit" className="w-full" loading={loading} size="large" > 登录 </Button> </Form.Item> </Form> )} {/* 注册表单 */} {isRegister && ( <Form className="w-full flex flex-col gap-4" layout="vertical" onFinish={handleRegister} > <Form.Item name="username" rules={[{ required: true, message: "请输入账号" }]} > <Input placeholder="账号" size="large" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: "请输入密码" }]} > <Input.Password placeholder="密码" size="large" /> </Form.Item> <Form.Item name="confirmPassword" rules={[{ required: true, message: "请确认密码" }]} > <Input.Password placeholder="确认密码" size="large" /> </Form.Item> {error && <Alert message={error} type="error" showIcon className="mb-2" />} <Form.Item> <Button type="primary" htmlType="submit" className="w-full" loading={loading} size="large" > 初始化 </Button> </Form.Item> </Form> )} </div> </div> ); }; export default Login; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/console/ImportMseNacosModal.tsx: -------------------------------------------------------------------------------- ```typescript import { useState } from 'react' import { Button, Table, Modal, Form, Input, message } from 'antd' import { nacosApi } from '@/lib/api' interface MseNacosItem { instanceId: string name: string serverIntranetEndpoint?: string serverInternetEndpoint?: string version?: string } interface ImportMseNacosModalProps { visible: boolean onCancel: () => void // 将选中的 MSE Nacos 信息带入创建表单 onPrefill: (values: { nacosName?: string serverUrl?: string serverInternetEndpoint?: string serverIntranetEndpoint?: string username?: string password?: string accessKey?: string secretKey?: string description?: string nacosId?: string }) => void } export default function ImportMseNacosModal({ visible, onCancel, onPrefill }: ImportMseNacosModalProps) { const [importForm] = Form.useForm() const [loading, setLoading] = useState(false) const [list, setList] = useState<MseNacosItem[]>([]) const [selected, setSelected] = useState<MseNacosItem | null>(null) const [auth, setAuth] = useState({ regionId: '', accessKey: '', secretKey: '' }) const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }) const fetchMseNacos = async (values: any, page = 0, size = 10) => { setLoading(true) try { const res = await nacosApi.getMseNacos({ ...values, page, size }) setList(res.data?.content || []) setPagination({ current: page + 1, pageSize: size, total: res.data?.totalElements || 0 }) } catch (e: any) { // message.error(e?.response?.data?.message || '获取 MSE Nacos 列表失败') } finally { setLoading(false) } } const handleImport = async () => { if (!selected) { message.warning('请选择一个 Nacos 实例') return } // 将关键信息带出到创建表单,供用户补充 onPrefill({ nacosName: selected.name, serverUrl: selected.serverInternetEndpoint || selected.serverIntranetEndpoint, serverInternetEndpoint: selected.serverInternetEndpoint, serverIntranetEndpoint: selected.serverIntranetEndpoint, accessKey: auth.accessKey, secretKey: auth.secretKey, nacosId: selected.instanceId, }) handleCancel() } const handleCancel = () => { setSelected(null) setList([]) setPagination({ current: 1, pageSize: 10, total: 0 }) importForm.resetFields() onCancel() } return ( <Modal title="导入 MSE Nacos 实例" open={visible} onCancel={handleCancel} footer={null} width={800}> <Form form={importForm} layout="vertical" preserve={false}> {list.length === 0 && ( <div className="mb-4"> <h3 className="text-lg font-medium mb-3">认证信息</h3> <Form.Item label="Region" name="regionId" 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) => { setAuth(values) fetchMseNacos(values) }) }} loading={loading} > 获取实例列表 </Button> </div> )} {list.length > 0 && ( <div className="mb-4"> <h3 className="text-lg font-medium mb-3">选择 Nacos 实例</h3> <Table rowKey="instanceId" columns={[ { title: '实例ID', dataIndex: 'instanceId' }, { title: '名称', dataIndex: 'name' }, { title: '版本', dataIndex: 'version' }, ]} dataSource={list} rowSelection={{ type: 'radio', selectedRowKeys: selected ? [selected.instanceId] : [], onChange: (_selectedRowKeys, selectedRows) => setSelected(selectedRows[0]), }} pagination={{ current: pagination.current, pageSize: pagination.pageSize, total: pagination.total, onChange: (page, pageSize) => fetchMseNacos(auth, page - 1, pageSize), showSizeChanger: true, showQuickJumper: true, showTotal: (total) => `共 ${total} 条`, }} size="small" /> </div> )} {selected && ( <div className="text-right"> <Button type="primary" onClick={handleImport}> 导入 </Button> </div> )} </Form> </Modal> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/GatewayController.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.controller; import com.alibaba.apiopenplatform.core.annotation.AdminAuth; 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.GatewayMCPServerResult; import com.alibaba.apiopenplatform.dto.result.*; import com.alibaba.apiopenplatform.service.GatewayService; import com.alibaba.apiopenplatform.service.AdpAIGatewayService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @Tag(name = "网关资源管理") @RestController @RequestMapping("/gateways") @RequiredArgsConstructor @AdminAuth public class GatewayController { private final GatewayService gatewayService; private final AdpAIGatewayService adpAIGatewayService; @Operation(summary = "获取APIG Gateway列表") @GetMapping("/apig") public PageResult<GatewayResult> fetchGateways(@Valid QueryAPIGParam param, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "500") int size) { return gatewayService.fetchGateways(param, page, size); } @Operation(summary = "获取ADP AI Gateway列表") @PostMapping("/adp") public PageResult<GatewayResult> fetchAdpGateways(@RequestBody @Valid QueryAdpAIGatewayParam param, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "500") int size) { return adpAIGatewayService.fetchGateways(param, page, size); } @Operation(summary = "获取导入的Gateway列表") @GetMapping public PageResult<GatewayResult> listGateways(QueryGatewayParam param, Pageable pageable) { return gatewayService.listGateways(param, pageable); } @Operation(summary = "导入Gateway") @PostMapping public void importGateway(@RequestBody @Valid ImportGatewayParam param) { gatewayService.importGateway(param); } @Operation(summary = "删除Gateway") @DeleteMapping("/{gatewayId}") public void deleteGateway(@PathVariable String gatewayId) { gatewayService.deleteGateway(gatewayId); } @Operation(summary = "获取REST API列表") @GetMapping("/{gatewayId}/rest-apis") public PageResult<APIResult> fetchRESTAPIs(@PathVariable String gatewayId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "500") int size) { return gatewayService.fetchRESTAPIs(gatewayId, page, size); } // @Operation(summary = "获取API列表") // @GetMapping("/{gatewayId}/apis") // public PageResult<APIResult> fetchAPIs(@PathVariable String gatewayId, // @RequestParam String apiType, // Pageable pageable) { // return gatewayService.fetchAPIs(gatewayId, apiType, pageable); // } @Operation(summary = "获取MCP Server列表") @GetMapping("/{gatewayId}/mcp-servers") public PageResult<GatewayMCPServerResult> fetchMcpServers(@PathVariable String gatewayId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "500") int size) { return gatewayService.fetchMcpServers(gatewayId, page, size); } @Operation(summary = "获取仪表板URL") @GetMapping("/{gatewayId}/dashboard") public String getDashboard(@PathVariable String gatewayId, @RequestParam(required = false, defaultValue = "API") String type) { return gatewayService.getDashboard(gatewayId, type); } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/GatewayOperator.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 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.*; import com.alibaba.apiopenplatform.service.gateway.client.APIGClient; import com.alibaba.apiopenplatform.service.gateway.client.GatewayClient; import com.alibaba.apiopenplatform.service.gateway.client.HigressClient; import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j public abstract class GatewayOperator<T> { private final Map<String, GatewayClient> clientCache = new ConcurrentHashMap<>(); abstract public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size); abstract public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size); abstract public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size); abstract public String fetchAPIConfig(Gateway gateway, Object config); abstract public String fetchMcpConfig(Gateway gateway, Object conf); abstract public PageResult<GatewayResult> fetchGateways(Object param, int page, int size); abstract public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config); abstract public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config); abstract public void deleteConsumer(String consumerId, GatewayConfig config); /** * 检查消费者是否存在于网关中 * @param consumerId 消费者ID * @param config 网关配置 * @return 是否存在 */ abstract public boolean isConsumerExists(String consumerId, GatewayConfig config); abstract public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig); abstract public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig); abstract public APIResult fetchAPI(Gateway gateway, String apiId); abstract public GatewayType getGatewayType(); /** * 获取网关控制台仪表盘链接 * @param gateway 网关实体 * @return 仪表盘访问链接 */ abstract public String getDashboard(Gateway gateway,String type); @SuppressWarnings("unchecked") protected T getClient(Gateway gateway) { String clientKey = gateway.getGatewayType().isAPIG() ? gateway.getApigConfig().buildUniqueKey() : gateway.getHigressConfig().buildUniqueKey(); return (T) clientCache.computeIfAbsent( clientKey, key -> createClient(gateway) ); } // @SuppressWarnings("unchecked") // protected T getClient(Gateway gateway) { // String clientKey = gateway.getGatewayType().isAPIG() ? // gateway.getApigConfig().buildUniqueKey() : gateway.getHigressConfig().buildUniqueKey(); // return (T) clientCache.computeIfAbsent( // clientKey, // key -> createClient(gateway) // ); // } /** * 创建网关客户端 */ private GatewayClient createClient(Gateway gateway) { switch (gateway.getGatewayType()) { case APIG_API: case APIG_AI: return new APIGClient(gateway.getApigConfig()); case HIGRESS: return new HigressClient(gateway.getHigressConfig()); default: throw new BusinessException(ErrorCode.INTERNAL_ERROR, "No factory found for gateway type: " + gateway.getGatewayType()); } } /** * 移除网关客户端 */ public void removeClient(String instanceId) { GatewayClient client = clientCache.remove(instanceId); try { client.close(); } catch (Exception e) { log.error("Error closing client for instance: {}", instanceId, e); } } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/ProductController.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.controller; import com.alibaba.apiopenplatform.core.annotation.AdminAuth; import com.alibaba.apiopenplatform.core.annotation.AdminOrDeveloperAuth; import com.alibaba.apiopenplatform.dto.params.product.*; import com.alibaba.apiopenplatform.dto.params.product.CreateProductRefParam; import com.alibaba.apiopenplatform.dto.result.*; import com.alibaba.apiopenplatform.service.ProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @Tag(name = "API产品管理", description = "提供API产品的创建、更新、删除、查询、订阅等管理功能") @RestController @RequestMapping("/products") @Slf4j @RequiredArgsConstructor public class ProductController { private final ProductService productService; @Operation(summary = "创建API产品") @PostMapping @AdminAuth public ProductResult createProduct(@RequestBody @Valid CreateProductParam param) { return productService.createProduct(param); } @Operation(summary = "获取API产品列表") @GetMapping public PageResult<ProductResult> listProducts(QueryProductParam param, Pageable pageable) { return productService.listProducts(param, pageable); } @Operation(summary = "获取API产品详情") @GetMapping("/{productId}") public ProductResult getProduct(@PathVariable String productId) { return productService.getProduct(productId); } @Operation(summary = "更新API产品") @PutMapping("/{productId}") @AdminAuth public ProductResult updateProduct(@PathVariable String productId, @RequestBody @Valid UpdateProductParam param) { return productService.updateProduct(productId, param); } @Operation(summary = "发布API产品") @PostMapping("/{productId}/publications/{portalId}") @AdminAuth public void publishProduct(@PathVariable String productId, @PathVariable String portalId) { productService.publishProduct(productId, portalId); } @Operation(summary = "获取API产品的发布信息") @GetMapping("/{productId}/publications") @AdminAuth public PageResult<ProductPublicationResult> getPublications(@PathVariable String productId, Pageable pageable) { return productService.getPublications(productId, pageable); } @Operation(summary = "下线API产品") @DeleteMapping("/{productId}/publications/{portalId}") @AdminAuth public void unpublishProduct(@PathVariable String productId, @PathVariable String portalId) { productService.unpublishProduct(productId, portalId); } @Operation(summary = "删除API产品") @DeleteMapping("/{productId}") @AdminAuth public void deleteProduct(@PathVariable String productId) { productService.deleteProduct(productId); } @Operation(summary = "API产品关联API或MCP Server") @PostMapping("/{productId}/ref") @AdminAuth public void addProductRef(@PathVariable String productId, @RequestBody @Valid CreateProductRefParam param) throws Exception { productService.addProductRef(productId, param); } @Operation(summary = "获取API产品关联的API或MCP Server") @GetMapping("/{productId}/ref") public ProductRefResult getProductRef(@PathVariable String productId) { return productService.getProductRef(productId); } @Operation(summary = "删除API产品关联的API或MCP Server") @DeleteMapping("/{productId}/ref") @AdminAuth public void deleteProductRef(@PathVariable String productId) { productService.deleteProductRef(productId); } @Operation(summary = "获取API产品的Dashboard监控面板URL") @GetMapping("/{productId}/dashboard") public String getProductDashboard(@PathVariable String productId) { return productService.getProductDashboard(productId); } @Operation(summary = "获取产品的订阅列表") @GetMapping("/{productId}/subscriptions") @AdminOrDeveloperAuth public PageResult<SubscriptionResult> listProductSubscriptions( @PathVariable String productId, QueryProductSubscriptionParam param, Pageable pageable) { return productService.listProductSubscriptions(productId, param, pageable); } } ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/SecurityConfig.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.config; import com.alibaba.apiopenplatform.core.security.JwtAuthenticationFilter; import com.alibaba.apiopenplatform.core.utils.TokenUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.*; import com.alibaba.apiopenplatform.core.security.DeveloperAuthenticationProvider; import org.springframework.http.HttpMethod; /** * Spring Security安全配置,集成JWT认证与接口权限控制,支持管理员和开发者多用户体系 * */ @Configuration @RequiredArgsConstructor @Slf4j @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final DeveloperAuthenticationProvider developerAuthenticationProvider; // Auth相关 private static final String[] AUTH_WHITELIST = { "/admins/init", "/admins/need-init", "/admins/login", "/developers", "/developers/login", "/developers/authorize", "/developers/callback", "/developers/providers", "/developers/oidc/authorize", "/developers/oidc/callback", "/developers/oidc/providers", "/developers/oauth2/token" }; // Swagger API文档相关 private static final String[] SWAGGER_WHITELIST = { "/portal/swagger-ui.html", "/portal/swagger-ui/**", "/portal/v3/api-docs/**" }; // 系统路径白名单 private static final String[] SYSTEM_WHITELIST = { "/favicon.ico", "/error" }; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) .csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() // OPTIONS请求放行 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 认证相关接口放行 .antMatchers(AUTH_WHITELIST).permitAll() // Swagger相关接口放行 .antMatchers(SWAGGER_WHITELIST).permitAll() // 系统路径放行 .antMatchers(SYSTEM_WHITELIST).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .authenticationProvider(developerAuthenticationProvider); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration corsConfig = new CorsConfiguration(); corsConfig.setAllowedOriginPatterns(Collections.singletonList("*")); corsConfig.setAllowedMethods(Collections.singletonList("*")); corsConfig.setAllowedHeaders(Collections.singletonList("*")); corsConfig.setAllowCredentials(true); corsConfig.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", corsConfig); return source; } } ``` -------------------------------------------------------------------------------- /portal-server/pom.xml: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.alibaba.himarket</groupId> <artifactId>himarket</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>portal-server</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba.himarket</groupId> <artifactId>portal-dal</artifactId> <version>1.0-SNAPSHOT</version> <exclusions> <exclusion> <artifactId>checker-qual</artifactId> <groupId>org.checkerframework</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-mail</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <exclusions> <exclusion> <artifactId>org.jacoco.agent</artifactId> <groupId>org.jacoco</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-apig20240327</artifactId> <exclusions> <exclusion> <artifactId>annotations</artifactId> <groupId>org.jetbrains</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-maintainer-client</artifactId> <version>3.0.2</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>mse20190531</artifactId> <exclusions> <exclusion> <artifactId>dom4j</artifactId> <groupId>org.dom4j</groupId> </exclusion> <exclusion> <artifactId>credentials-java</artifactId> <groupId>com.aliyun</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-sls20201230</artifactId> <version>4.0.11</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <exclusions> <exclusion> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk16</artifactId> </exclusion> </exclusions> </dependency> <!-- SnakeYAML for JSON to YAML conversion --> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.0</version> </dependency> </dependencies> </project> ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/Layout.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState, useEffect } from 'react' import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom' import { GlobalOutlined, AppstoreOutlined, DesktopOutlined, UserOutlined, MenuOutlined, SettingOutlined } from '@ant-design/icons' import { Button } from 'antd' import { isAuthenticated, removeToken } from '../lib/utils' interface NavigationItem { name: string cn: string href: string icon: React.ComponentType<any> children?: NavigationItem[] } const Layout: React.FC = () => { const location = useLocation() const navigate = useNavigate() const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false) const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false) useEffect(() => { // 检查 cookie 中的 token 来判断登录状态 const checkAuthStatus = () => { const hasToken = isAuthenticated() setIsLoggedIn(hasToken) } checkAuthStatus() // 监听 storage 变化(当其他标签页登录/登出时) window.addEventListener('storage', checkAuthStatus) return () => { window.removeEventListener('storage', checkAuthStatus) } }, []) useEffect(() => { // 进入详情页自动折叠侧边栏 if (location.pathname.startsWith('/portals/detail') || location.pathname.startsWith('/api-products/detail')) { setSidebarCollapsed(true) } else { setSidebarCollapsed(false) } }, [location.pathname]) const navigation: NavigationItem[] = [ { name: 'Portal', cn: '门户', href: '/portals', icon: GlobalOutlined }, { name: 'API Products', cn: 'API产品', href: '/api-products', icon: AppstoreOutlined }, { name: '实例管理', cn: '实例管理', href: '/consoles', icon: SettingOutlined, children: [ { name: 'Nacos实例', cn: 'Nacos实例', href: '/consoles/nacos', icon: DesktopOutlined }, { name: '网关实例', cn: '网关实例', href: '/consoles/gateway', icon: DesktopOutlined }, ] }, ] const toggleSidebar = () => { setSidebarCollapsed(!sidebarCollapsed) } const handleLogout = () => { removeToken() setIsLoggedIn(false) navigate('/login') } const isMenuActive = (item: NavigationItem): boolean => { if (location.pathname === item.href) return true if (item.children) { return item.children.some(child => location.pathname === child.href) } return false } const renderMenuItem = (item: NavigationItem, level: number = 0) => { const Icon = item.icon const isActive = isMenuActive(item) const hasChildren = item.children && item.children.length > 0 return ( <div key={item.name}> <Link to={item.href} className={`flex items-center mt-2 px-3 py-3 rounded-lg transition-colors duration-150 ${ level > 0 ? 'ml-4' : '' } ${ isActive && !hasChildren ? 'bg-gray-100 text-black font-semibold' : 'text-gray-500 hover:text-black hover:bg-gray-50' }`} title={sidebarCollapsed ? item.name : ''} > <Icon className="mr-3 h-5 w-5 flex-shrink-0" /> {!sidebarCollapsed && ( <div className="flex flex-col flex-1"> <span className="text-base leading-none">{item.name}</span> </div> )} </Link> {!sidebarCollapsed && hasChildren && ( <div className="ml-2"> {item.children!.map(child => renderMenuItem(child, level + 1))} </div> )} </div> ) } return ( <div className="min-h-screen bg-background"> {/* 顶部导航栏 */} <header className="w-full h-16 flex items-center justify-between px-8 bg-white border-b shadow-sm"> <div className="flex items-center space-x-2"> <div className="bg-white"> <Button type="text" icon={<MenuOutlined />} onClick={toggleSidebar} className="hover:bg-gray-100" /> </div> <span className="text-2xl font-bold">HiMarket</span> </div> {/* 顶部右侧用户信息或登录按钮 */} {isLoggedIn ? ( <div className="flex items-center space-x-2"> <UserOutlined className="mr-2 text-lg" /> <span>admin</span> <button onClick={handleLogout} className="ml-2 px-2 py-1 rounded bg-gray-200 hover:bg-gray-300" > 退出 </button> </div> ) : ( <button onClick={() => navigate('/login')} className="flex items-center px-4 py-2 rounded bg-black text-white hover:bg-gray-800"> <UserOutlined className="mr-2" /> 登录 </button> )} </header> <div className="flex"> {/* 侧边栏 */} <aside className={`bg-white border-r min-h-screen pt-8 transition-all duration-300 ${ sidebarCollapsed ? 'w-16' : 'w-64' }`}> <nav className="flex flex-col space-y-2 px-4"> {navigation.map(item => renderMenuItem(item))} </nav> </aside> {/* 主内容区域 */} <div className="flex-1 min-h-screen overflow-hidden"> <main className="p-8 w-full max-w-full overflow-x-hidden"> <Outlet /> </main> </div> </div> </div> ) } export default Layout ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/components/SwaggerUIWrapper.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import SwaggerUI from 'swagger-ui-react'; import 'swagger-ui-react/swagger-ui.css'; import './SwaggerUIWrapper.css'; import * as yaml from 'js-yaml'; import { message } from 'antd'; interface SwaggerUIWrapperProps { apiSpec: string; } export const SwaggerUIWrapper: React.FC<SwaggerUIWrapperProps> = ({ apiSpec }) => { // 直接解析原始规范,不进行重新构建 let swaggerSpec: any; try { // 尝试解析YAML格式 try { swaggerSpec = yaml.load(apiSpec); } catch { // 如果YAML解析失败,尝试JSON格式 swaggerSpec = JSON.parse(apiSpec); } if (!swaggerSpec || !swaggerSpec.paths) { throw new Error('Invalid OpenAPI specification'); } // 为没有tags的操作添加默认标签,避免显示"default" Object.keys(swaggerSpec.paths).forEach(path => { const pathItem = swaggerSpec.paths[path]; Object.keys(pathItem).forEach(method => { const operation = pathItem[method]; if (operation && typeof operation === 'object' && !operation.tags) { operation.tags = ['接口列表']; } }); }); } catch (error) { console.error('OpenAPI规范解析失败:', error); return ( <div className="text-center text-gray-500 py-8 bg-gray-50 rounded-lg"> <p>无法解析OpenAPI规范</p> <div className="text-sm text-gray-400 mt-2"> 请检查API配置格式是否正确 </div> <div className="text-xs text-gray-400 mt-1"> 错误详情: {error instanceof Error ? error.message : String(error)} </div> </div> ); } return ( <div className="swagger-ui-wrapper"> <SwaggerUI spec={swaggerSpec} docExpansion="list" displayRequestDuration={true} tryItOutEnabled={true} filter={false} defaultModelsExpandDepth={0} defaultModelExpandDepth={0} displayOperationId={true} supportedSubmitMethods={['get', 'post', 'put', 'delete', 'patch', 'head', 'options']} deepLinking={false} requestInterceptor={(request: any) => { console.log('Request:', request); return request; }} responseInterceptor={(response: any) => { console.log('Response:', response); return response; }} onComplete={() => { console.log('Swagger UI loaded'); // 添加服务器复制功能 - 使用requestAnimationFrame优化性能 const addCopyButton = () => { const serversContainer = document.querySelector('.swagger-ui .servers'); if (serversContainer && !serversContainer.querySelector('.copy-server-btn')) { const copyBtn = document.createElement('button'); copyBtn.className = 'copy-server-btn'; copyBtn.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> </svg> `; copyBtn.title = '复制服务器地址'; copyBtn.style.cssText = ` position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: transparent; border: none; border-radius: 4px; padding: 6px 8px; cursor: pointer; color: #666; transition: all 0.2s; z-index: 10; display: flex; align-items: center; justify-content: center; `; // 添加hover效果 copyBtn.addEventListener('mouseenter', () => { copyBtn.style.background = '#f5f5f5'; copyBtn.style.color = '#1890ff'; }); copyBtn.addEventListener('mouseleave', () => { copyBtn.style.background = 'transparent'; copyBtn.style.color = '#666'; }); copyBtn.addEventListener('click', () => { const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement; if (serverSelect && serverSelect.value) { navigator.clipboard.writeText(serverSelect.value) .then(() => { message.success('服务器地址已复制到剪贴板', 1); }) .catch(() => { // 降级到传统复制方法 const textArea = document.createElement('textarea'); textArea.value = serverSelect.value; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); message.success('服务器地址已复制到剪贴板', 1); }); } }); serversContainer.appendChild(copyBtn); // 调整服务器选择框的padding const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement; if (serverSelect) { serverSelect.style.paddingRight = '50px'; } } }; // 立即尝试添加按钮,如果失败则在下一帧重试 addCopyButton(); if (!document.querySelector('.swagger-ui .servers .copy-server-btn')) { requestAnimationFrame(addCopyButton); } }} /> </div> ); }; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/api-product/SwaggerUIWrapper.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import SwaggerUI from 'swagger-ui-react'; import 'swagger-ui-react/swagger-ui.css'; import './SwaggerUIWrapper.css'; import * as yaml from 'js-yaml'; import { message } from 'antd'; import { copyToClipboard } from '@/lib/utils'; interface SwaggerUIWrapperProps { apiSpec: string; } export const SwaggerUIWrapper: React.FC<SwaggerUIWrapperProps> = ({ apiSpec }) => { // 直接解析原始规范,不进行重新构建 let swaggerSpec: any; try { // 尝试解析YAML格式 try { swaggerSpec = yaml.load(apiSpec); } catch { // 如果YAML解析失败,尝试JSON格式 swaggerSpec = JSON.parse(apiSpec); } if (!swaggerSpec || !swaggerSpec.paths) { throw new Error('Invalid OpenAPI specification'); } // 为没有tags的操作添加默认标签,避免显示"default" Object.keys(swaggerSpec.paths).forEach(path => { const pathItem = swaggerSpec.paths[path]; Object.keys(pathItem).forEach(method => { const operation = pathItem[method]; if (operation && typeof operation === 'object' && !operation.tags) { operation.tags = ['接口列表']; } }); }); } catch (error) { console.error('OpenAPI规范解析失败:', error); return ( <div className="text-center text-gray-500 py-8 bg-gray-50 rounded-lg"> <p>无法解析OpenAPI规范</p> <div className="text-sm text-gray-400 mt-2"> 请检查API配置格式是否正确 </div> <div className="text-xs text-gray-400 mt-1"> 错误详情: {error instanceof Error ? error.message : String(error)} </div> </div> ); } return ( <div className="swagger-ui-wrapper"> <SwaggerUI spec={swaggerSpec} docExpansion="list" displayRequestDuration={true} tryItOutEnabled={true} filter={false} showRequestHeaders={true} showCommonExtensions={true} defaultModelsExpandDepth={0} defaultModelExpandDepth={0} displayOperationId={true} enableCORS={true} supportedSubmitMethods={['get', 'post', 'put', 'delete', 'patch', 'head', 'options']} deepLinking={false} showMutatedRequest={true} requestInterceptor={(request: any) => { console.log('Request:', request); return request; }} responseInterceptor={(response: any) => { console.log('Response:', response); return response; }} onComplete={() => { console.log('Swagger UI loaded'); // 添加服务器复制功能 setTimeout(() => { const serversContainer = document.querySelector('.swagger-ui .servers'); if (serversContainer && !serversContainer.querySelector('.copy-server-btn')) { const copyBtn = document.createElement('button'); copyBtn.className = 'copy-server-btn'; copyBtn.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/> </svg> `; copyBtn.title = '复制服务器地址'; copyBtn.style.cssText = ` position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: transparent; border: none; border-radius: 4px; padding: 6px 8px; cursor: pointer; color: #666; transition: all 0.2s; z-index: 10; display: flex; align-items: center; justify-content: center; `; copyBtn.addEventListener('click', async () => { const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement; if (serverSelect && serverSelect.value) { try { await copyToClipboard(serverSelect.value); message.success('服务器地址已复制到剪贴板', 1); } catch { message.error('复制失败,请手动复制'); } } }); // 添加hover效果 copyBtn.addEventListener('mouseenter', () => { copyBtn.style.background = '#f5f5f5'; copyBtn.style.color = '#1890ff'; }); copyBtn.addEventListener('mouseleave', () => { copyBtn.style.background = 'transparent'; copyBtn.style.color = '#666'; }); serversContainer.appendChild(copyBtn); // 调整服务器选择框的padding const serverSelect = serversContainer.querySelector('select') as HTMLSelectElement; if (serverSelect) { serverSelect.style.paddingRight = '50px'; } } }, 1000); }} syntaxHighlight={{ activated: true, theme: 'agate' }} requestSnippetsEnabled={true} requestSnippets={{ generators: { 'curl_bash': { title: 'cURL (bash)', syntax: 'bash' }, 'curl_powershell': { title: 'cURL (PowerShell)', syntax: 'powershell' }, 'curl_cmd': { title: 'cURL (CMD)', syntax: 'bash' } } }} /> </div> ); }; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/pages/Dashboard.tsx: -------------------------------------------------------------------------------- ```typescript import { Card, Row, Col, Statistic, Progress, Table } from 'antd' import { EyeOutlined, UserOutlined, ApiOutlined, GlobalOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons' const mockRecentActivity = [ { key: '1', action: 'Portal访问', description: 'Company Portal被访问了1250次', time: '2小时前', user: '[email protected]' }, { key: '2', action: 'API调用', description: 'Payment API被调用了8765次', time: '4小时前', user: '[email protected]' }, { key: '3', action: '新用户注册', description: '新开发者注册了账户', time: '6小时前', user: '[email protected]' } ] export default function Dashboard() { const activityColumns = [ { title: '操作', dataIndex: 'action', key: 'action', }, { title: '描述', dataIndex: 'description', key: 'description', }, { title: '用户', dataIndex: 'user', key: 'user', }, { title: '时间', dataIndex: 'time', key: 'time', }, ] return ( <div className="space-y-6"> <div> <h1 className="text-3xl font-bold tracking-tight">仪表板</h1> <p className="text-gray-500 mt-2"> 欢迎使用HiMarket管理系统 </p> </div> {/* 统计卡片 */} <Row gutter={[16, 16]}> <Col xs={24} sm={12} lg={6}> <Card> <Statistic title="Portal访问量" value={1250} prefix={<EyeOutlined />} valueStyle={{ color: '#3f8600' }} suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />} /> </Card> </Col> <Col xs={24} sm={12} lg={6}> <Card> <Statistic title="注册用户" value={45} prefix={<UserOutlined />} valueStyle={{ color: '#1890ff' }} suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />} /> </Card> </Col> <Col xs={24} sm={12} lg={6}> <Card> <Statistic title="API调用" value={8765} prefix={<ApiOutlined />} valueStyle={{ color: '#722ed1' }} suffix={<ArrowUpOutlined style={{ fontSize: '14px' }} />} /> </Card> </Col> <Col xs={24} sm={12} lg={6}> <Card> <Statistic title="活跃Portal" value={3} prefix={<GlobalOutlined />} valueStyle={{ color: '#fa8c16' }} suffix={<ArrowDownOutlined style={{ fontSize: '14px' }} />} /> </Card> </Col> </Row> {/* 详细信息 */} <Row gutter={[16, 16]}> <Col xs={24} lg={12}> <Card title="系统状态" className="h-full"> <div className="space-y-4"> <div> <div className="flex justify-between mb-2"> <span>系统负载</span> <span className="text-blue-600">75%</span> </div> <Progress percent={75} strokeColor="#1890ff" /> </div> <div> <div className="flex justify-between mb-2"> <span>API响应时间</span> <span className="text-green-600">245ms</span> </div> <Progress percent={85} strokeColor="#52c41a" /> </div> <div> <div className="flex justify-between mb-2"> <span>错误率</span> <span className="text-red-600">0.12%</span> </div> <Progress percent={1.2} strokeColor="#ff4d4f" /> </div> </div> </Card> </Col> <Col xs={24} lg={12}> <Card title="快速操作" className="h-full"> <div className="space-y-4"> <div className="grid grid-cols-2 gap-4"> <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer"> <GlobalOutlined className="text-2xl text-blue-500 mb-2" /> <div className="font-medium">创建Portal</div> <div className="text-sm text-gray-500">新建开发者门户</div> </div> <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer"> <ApiOutlined className="text-2xl text-green-500 mb-2" /> <div className="font-medium">发布API</div> <div className="text-sm text-gray-500">发布新的API产品</div> </div> <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer"> <UserOutlined className="text-2xl text-purple-500 mb-2" /> <div className="font-medium">管理用户</div> <div className="text-sm text-gray-500">管理开发者账户</div> </div> <div className="text-center p-4 border rounded-lg hover:bg-gray-50 cursor-pointer"> <EyeOutlined className="text-2xl text-orange-500 mb-2" /> <div className="font-medium">查看统计</div> <div className="text-sm text-gray-500">查看使用统计</div> </div> </div> </div> </Card> </Col> </Row> {/* 最近活动 */} <Card title="最近活动"> <Table columns={activityColumns} dataSource={mockRecentActivity} rowKey="key" pagination={false} size="small" /> </Card> </div> ) } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/PortalDomain.tsx: -------------------------------------------------------------------------------- ```typescript import {Card, Button, Table, Modal, Form, Input, Select, message, Space} from 'antd' import {PlusOutlined, ExclamationCircleOutlined} from '@ant-design/icons' import {useState} from 'react' import {Portal} from '@/types' import {portalApi} from '@/lib/api' interface PortalDomainProps { portal: Portal onRefresh?: () => void } export function PortalDomain({portal, onRefresh}: PortalDomainProps) { const [domainModalVisible, setDomainModalVisible] = useState(false) const [domainForm] = Form.useForm() const [domainLoading, setDomainLoading] = useState(false) const handleAddDomain = () => { setDomainModalVisible(true) } const handleDomainModalOk = async () => { try { setDomainLoading(true) const values = await domainForm.validateFields() await portalApi.bindDomain(portal.portalId, { domain: values.domain, type: 'CUSTOM', protocol: values.protocol }) message.success('域名绑定成功') setDomainModalVisible(false) domainForm.resetFields() onRefresh?.() } 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('解绑域名失败') } }, }) } const domainColumns = [ { title: '域名', dataIndex: 'domain', key: 'domain', }, { title: '协议', dataIndex: 'protocol', key: 'protocol', render: (protocol: string) => protocol?.toUpperCase() || 'HTTP' }, { title: '类型', dataIndex: 'type', key: 'type', render: (type: string) => type === 'CUSTOM' ? '自定义域名' : '系统域名' }, { title: '操作', key: 'action', render: (_: any, record: any) => ( <Space> {record.type === 'CUSTOM' ? ( <Button type="link" danger size="small" onClick={() => handleDeleteDomain(record.domain)} > 解绑 </Button> ) : ( <span className="text-gray-400 text-sm">-</span> )} </Space> ), }, ] 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> <Space> <Button type="primary" icon={<PlusOutlined/>} onClick={handleAddDomain}> 绑定域名 </Button> </Space> </div> <Card> <div className="space-y-6"> {/* 域名列表内容 */} <div> <Table columns={domainColumns} dataSource={portal.portalDomainConfig || []} rowKey="domain" pagination={false} size="small" locale={{ emptyText: '暂无绑定域名' }} /> </div> </div> </Card> {/* 域名绑定模态框 */} <Modal title="绑定域名" open={domainModalVisible} onOk={handleDomainModalOk} onCancel={handleDomainModalCancel} confirmLoading={domainLoading} destroyOnClose > <Form form={domainForm} layout="vertical" initialValues={{ protocol: 'HTTP' }}> <Form.Item name="domain" label="域名" rules={[{ required: true, message: '请输入要绑定的域名' }]} > <Input placeholder="例如:example.com" /> </Form.Item> <Form.Item name="protocol" label="协议" rules={[{ required: true, message: '请选择协议' }]} > <Select placeholder="请选择协议"> <Select.Option value="HTTPS">HTTPS</Select.Option> <Select.Option value="HTTP">HTTP</Select.Option> </Select> </Form.Item> </Form> </Modal> </div> ) } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/pages/Login.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Form, Input, Button, Card, Divider, message } from "antd"; import { UserOutlined, LockOutlined } from "@ant-design/icons"; import api, { getOidcProviders, type IdpResult } from "../lib/api"; import aliyunIcon from "../assets/aliyun.png"; import githubIcon from "../assets/github.png"; import googleIcon from "../assets/google.png"; import { AxiosError } from "axios"; const oidcIcons: Record<string, React.ReactNode> = { google: <img src={googleIcon} alt="Google" className="w-5 h-5 mr-2" />, github: <img src={githubIcon} alt="GitHub" className="w-6 h-6 mr-2" />, aliyun: <img src={aliyunIcon} alt="Aliyun" className="w-6 h-6 mr-2" />, }; const Login: React.FC = () => { const [providers, setProviders] = useState<IdpResult[]>([]); const [loading, setLoading] = useState(false); const navigate = useNavigate(); useEffect(() => { // 使用OidcController的接口获取OIDC提供商 getOidcProviders() .then((response: any) => { console.log('OIDC providers response:', response); // 处理不同的响应格式 let providersData: IdpResult[]; if (Array.isArray(response)) { providersData = response; } else if (response && Array.isArray(response.data)) { providersData = response.data; } else if (response && response.data) { console.warn('Unexpected response format:', response); providersData = []; } else { providersData = []; } console.log('Processed providers data:', providersData); setProviders(providersData); }) .catch((error) => { console.error('Failed to fetch OIDC providers:', error); setProviders([]); }); }, []); // 账号密码登录 const handlePasswordLogin = async (values: { username: string; password: string }) => { setLoading(true); try { const res = await api.post("/developers/login", { username: values.username, password: values.password, }); // 登录成功后跳转到首页并携带access_token if (res && res.data && res.data.access_token) { message.success('登录成功!', 1); localStorage.setItem('access_token', res.data.access_token) navigate('/') } else { message.error("登录失败,未获取到access_token"); } } catch (error) { if (error instanceof AxiosError) { message.error(error.response?.data.message || "登录失败,请检查账号密码是否正确"); } else { message.error("登录失败"); } } finally { setLoading(false); } }; // 跳转到 OIDC 授权 - 对接OidcController const handleOidcLogin = (provider: string) => { // 获取API前缀配置 const apiPrefix = api.defaults.baseURL || '/api/v1'; // 构建授权URL - 对接 /developers/oidc/authorize const authUrl = new URL(`${window.location.origin}${apiPrefix}/developers/oidc/authorize`); authUrl.searchParams.set('provider', provider); console.log('Redirecting to OIDC authorization:', authUrl.toString()); // 跳转到OIDC授权服务器 window.location.href = authUrl.toString(); }; return ( <div className="flex items-center justify-center min-h-screen bg-gray-50"> <Card className="w-full max-w-md shadow-lg"> {/* Logo */} <div className="text-center mb-6"> <img src="/logo.png" alt="Logo" className="w-16 h-16 mx-auto mb-4" /> <h2 className="text-2xl font-bold text-gray-900">登录HiMarket-前台</h2> </div> {/* 账号密码登录表单 */} <Form name="login" onFinish={handlePasswordLogin} autoComplete="off" layout="vertical" size="large" > <Form.Item name="username" rules={[ { required: true, message: '请输入账号' } ]} > <Input prefix={<UserOutlined />} placeholder="账号" autoComplete="username" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, message: '请输入密码' } ]} > <Input.Password prefix={<LockOutlined />} placeholder="密码" autoComplete="current-password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading} className="w-full" size="large" > {loading ? "登录中..." : "登录"} </Button> </Form.Item> </Form> {/* 分隔线 */} <Divider plain>或</Divider> {/* OIDC 登录按钮 */} <div className="flex flex-col gap-3"> {!Array.isArray(providers) || providers.length === 0 ? ( <div className="text-gray-400 text-center">暂无可用第三方登录</div> ) : ( providers.map((provider) => ( <Button key={provider.provider} onClick={() => handleOidcLogin(provider.provider)} className="w-full flex items-center justify-center" size="large" icon={oidcIcons[provider.provider.toLowerCase()] || <span>🆔</span>} > 使用{provider.name || provider.provider}登录 </Button> )) )} </div> {/* 底部提示 */} <div className="mt-6 text-center text-gray-500"> 没有账号? <Link to="/register" className="text-blue-500 hover:underline ml-1"> 注册 </Link> </div> </Card> </div> ); }; export default Login; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/PortalConsumers.tsx: -------------------------------------------------------------------------------- ```typescript import { Card, Table, Badge, Button, Space, Avatar, Tag, Input } from 'antd' import { SearchOutlined, UserAddOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons' import { useState } from 'react' import { Portal, DeveloperStats } from '@/types' import { formatDateTime } from '@/lib/utils' interface PortalConsumersProps { portal: Portal } const mockConsumers: DeveloperStats[] = [ { id: "1", name: "企业A", email: "[email protected]", status: "active", plan: "premium", joinedAt: "2025-01-01T10:00:00Z", lastActive: "2025-01-08T15:30:00Z", apiCalls: 15420, subscriptions: 3 }, { id: "2", name: "企业B", email: "[email protected]", status: "active", plan: "standard", joinedAt: "2025-01-02T11:00:00Z", lastActive: "2025-01-08T14:20:00Z", apiCalls: 8765, subscriptions: 2 }, { id: "3", name: "企业C", email: "[email protected]", status: "inactive", plan: "basic", joinedAt: "2025-01-03T12:00:00Z", lastActive: "2025-01-05T09:15:00Z", apiCalls: 1200, subscriptions: 1 } ] export function PortalConsumers({ portal }: PortalConsumersProps) { const [consumers, setConsumers] = useState<DeveloperStats[]>(mockConsumers) const [searchText, setSearchText] = useState('') const filteredConsumers = consumers.filter(consumer => consumer.name.toLowerCase().includes(searchText.toLowerCase()) || consumer.email.toLowerCase().includes(searchText.toLowerCase()) ) const getPlanColor = (plan: string) => { switch (plan) { case 'premium': return 'gold' case 'standard': return 'blue' case 'basic': return 'green' default: return 'default' } } const getPlanText = (plan: string) => { switch (plan) { case 'premium': return '高级版' case 'standard': return '标准版' case 'basic': return '基础版' default: return plan } } const columns = [ { title: '消费者', dataIndex: 'name', key: 'name', render: (name: string, record: DeveloperStats) => ( <div className="flex items-center space-x-3"> <Avatar className="bg-green-500"> {name.charAt(0).toUpperCase()} </Avatar> <div> <div className="font-medium">{name}</div> <div className="text-sm text-gray-500">{record.email}</div> </div> </div> ), }, { title: '状态', dataIndex: 'status', key: 'status', render: (status: string) => ( <Badge status={status === 'active' ? 'success' : 'default'} text={status === 'active' ? '活跃' : '非活跃'} /> ) }, { title: '套餐', dataIndex: 'plan', key: 'plan', render: (plan: string) => ( <Tag color={getPlanColor(plan)}> {getPlanText(plan)} </Tag> ) }, { title: 'API调用', dataIndex: 'apiCalls', key: 'apiCalls', render: (calls: number) => calls.toLocaleString() }, { title: '订阅数', dataIndex: 'subscriptions', key: 'subscriptions', render: (subscriptions: number) => subscriptions.toLocaleString() }, { title: '加入时间', dataIndex: 'joinedAt', key: 'joinedAt', render: (date: string) => formatDateTime(date) }, { title: '最后活跃', dataIndex: 'lastActive', key: 'lastActive', render: (date: string) => formatDateTime(date) }, { title: '操作', key: 'action', render: (_: any, record: DeveloperStats) => ( <Space size="middle"> <Button type="link" icon={<EditOutlined />}> 编辑 </Button> <Button type="link" danger icon={<DeleteOutlined />}> 删除 </Button> </Space> ), }, ] 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> <Button type="primary" icon={<UserAddOutlined />}> 添加消费者 </Button> </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={filteredConsumers} rowKey="id" pagination={false} /> </Card> {/* <Card title="消费者统计"> <div className="grid grid-cols-4 gap-4"> <div className="text-center"> <div className="text-2xl font-bold text-blue-600">{consumers.length}</div> <div className="text-sm text-gray-500">总消费者</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-green-600"> {consumers.filter(c => c.status === 'active').length} </div> <div className="text-sm text-gray-500">活跃消费者</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-purple-600"> {consumers.reduce((sum, c) => sum + c.apiCalls, 0).toLocaleString()} </div> <div className="text-sm text-gray-500">总API调用</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-orange-600"> {consumers.reduce((sum, c) => sum + c.subscriptions, 0)} </div> <div className="text-sm text-gray-500">总订阅数</div> </div> </div> </Card> */} </div> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/ConsumerController.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.controller; import com.alibaba.apiopenplatform.core.annotation.AdminAuth; import com.alibaba.apiopenplatform.core.annotation.DeveloperAuth; import com.alibaba.apiopenplatform.core.annotation.AdminOrDeveloperAuth; import com.alibaba.apiopenplatform.dto.params.consumer.CreateCredentialParam; import com.alibaba.apiopenplatform.dto.params.consumer.QueryConsumerParam; import com.alibaba.apiopenplatform.dto.params.consumer.CreateConsumerParam; import com.alibaba.apiopenplatform.dto.params.consumer.UpdateCredentialParam; import com.alibaba.apiopenplatform.dto.params.consumer.CreateSubscriptionParam; import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam; import com.alibaba.apiopenplatform.dto.result.ConsumerCredentialResult; import com.alibaba.apiopenplatform.dto.result.ConsumerResult; import com.alibaba.apiopenplatform.dto.result.PageResult; import com.alibaba.apiopenplatform.dto.result.SubscriptionResult; import com.alibaba.apiopenplatform.service.ConsumerService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @Tag(name = "Consumer管理", description = "提供Consumer注册、审批、产品订阅等管理功能") @RestController @RequestMapping("/consumers") @RequiredArgsConstructor @Validated public class ConsumerController { private final ConsumerService consumerService; @Operation(summary = "获取Consumer列表") @GetMapping public PageResult<ConsumerResult> listConsumers(QueryConsumerParam param, Pageable pageable) { return consumerService.listConsumers(param, pageable); } @Operation(summary = "获取Consumer") @GetMapping("/{consumerId}") public ConsumerResult getConsumer(@PathVariable String consumerId) { return consumerService.getConsumer(consumerId); } @Operation(summary = "注册Consumer") @PostMapping @DeveloperAuth public ConsumerResult createConsumer(@RequestBody @Valid CreateConsumerParam param) { return consumerService.createConsumer(param); } @Operation(summary = "删除Consumer") @DeleteMapping("/{consumerId}") public void deleteDevConsumer(@PathVariable String consumerId) { consumerService.deleteConsumer(consumerId); } @Operation(summary = "生成Consumer凭证") @PostMapping("/{consumerId}/credentials") @DeveloperAuth public void createCredential(@PathVariable String consumerId, @RequestBody @Valid CreateCredentialParam param) { consumerService.createCredential(consumerId, param); } @Operation(summary = "获取Consumer凭证信息") @GetMapping("/{consumerId}/credentials") @DeveloperAuth public ConsumerCredentialResult getCredential(@PathVariable String consumerId) { return consumerService.getCredential(consumerId); } @Operation(summary = "更新Consumer凭证") @PutMapping("/{consumerId}/credentials") @DeveloperAuth public void updateCredential(@PathVariable String consumerId, @RequestBody @Valid UpdateCredentialParam param) { consumerService.updateCredential(consumerId, param); } @Operation(summary = "删除Consumer凭证") @DeleteMapping("/{consumerId}/credentials") @DeveloperAuth public void deleteCredential(@PathVariable String consumerId) { consumerService.deleteCredential(consumerId); } @Operation(summary = "订阅API产品") @PostMapping("/{consumerId}/subscriptions") @DeveloperAuth public SubscriptionResult subscribeProduct(@PathVariable String consumerId, @RequestBody @Valid CreateSubscriptionParam param) { return consumerService.subscribeProduct(consumerId, param); } @Operation(summary = "获取Consumer的订阅列表") @GetMapping("/{consumerId}/subscriptions") @AdminOrDeveloperAuth public PageResult<SubscriptionResult> listSubscriptions(@PathVariable String consumerId, QuerySubscriptionParam param, Pageable pageable) { return consumerService.listSubscriptions(consumerId, param, pageable); } @Operation(summary = "取消订阅") @DeleteMapping("/{consumerId}/subscriptions/{productId}") public void deleteSubscription(@PathVariable String consumerId, @PathVariable String productId) { consumerService.unsubscribeProduct(consumerId, productId); } @Operation(summary = "审批订阅申请") @PatchMapping("/{consumerId}/subscriptions/{productId}") @AdminAuth public SubscriptionResult approveSubscription(@PathVariable String consumerId, @PathVariable String productId) { return consumerService.approveSubscription(consumerId, productId); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/common/AdvancedSearch.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState, useEffect } from 'react'; import { Select, Input, Button, Tag, Space } from 'antd'; import { SearchOutlined, CloseOutlined } from '@ant-design/icons'; // import './AdvancedSearch.css'; const { Option } = Select; export interface SearchParam { label: string; name: string; placeholder: string; type?: 'input' | 'select'; optionList?: Array<{ label: string; value: string }>; } interface AdvancedSearchProps { searchParamsList: SearchParam[]; onSearch: (searchName: string, searchValue: string) => void; onClear?: () => void; className?: string; } export const AdvancedSearch: React.FC<AdvancedSearchProps> = ({ searchParamsList, onSearch, onClear, className = '' }) => { const [activeSearchName, setActiveSearchName] = useState<string>(''); const [activeSearchValue, setActiveSearchValue] = useState<string>(''); const [tagList, setTagList] = useState<Array<SearchParam & { value: string }>>([]); const [isInitialized, setIsInitialized] = useState<boolean>(false); useEffect(() => { // 防止初始化时自动触发搜索 if (isInitialized && activeSearchName) { setActiveSearchValue(''); // 清空输入框 setTagList([]); // 清空关联标签 onSearch(activeSearchName, ''); } }, [activeSearchName, isInitialized]); // 移除 onSearch 避免无限循环 useEffect(() => { if (searchParamsList.length > 0) { setActiveSearchName(searchParamsList[0].name); setIsInitialized(true); // 标记为已初始化 } }, [searchParamsList]); const handleSearch = () => { if (activeSearchValue.trim()) { // 添加到标签列表 const currentParam = searchParamsList.find(item => item.name === activeSearchName); if (currentParam) { const newTag = { ...currentParam, value: activeSearchValue }; setTagList(prev => { const filtered = prev.filter(tag => tag.name !== activeSearchName); return [...filtered, newTag]; }); } onSearch(activeSearchName, activeSearchValue); setActiveSearchValue(''); } }; const handleClearOne = (tagName: string) => { setTagList(prev => prev.filter(tag => tag.name !== tagName)); onSearch(tagName, ''); }; const handleClearAll = () => { setTagList([]); if (onClear) { onClear(); } }; const handleSelectOne = (tagName: string) => { const tag = tagList.find(t => t.name === tagName); if (tag) { setActiveSearchName(tagName); setActiveSearchValue(tag.value); } }; const getCurrentParam = () => { return searchParamsList.find(item => item.name === activeSearchName); }; const currentParam = getCurrentParam(); return ( <div className={`flex flex-col gap-4 ${className}`}> {/* 搜索控件 */} <div className="flex items-center"> {/* 左侧:搜索字段选择器 */} <Select value={activeSearchName} onChange={setActiveSearchName} style={{ width: 120, borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: 'none' }} className="h-10" size="large" > {searchParamsList.map(item => ( <Option key={item.name} value={item.name}> {item.label} </Option> ))} </Select> {/* 中间:搜索值输入框 */} {currentParam?.type === 'select' ? ( <Select placeholder={currentParam.placeholder} value={activeSearchValue} onChange={(value) => { setActiveSearchValue(value); // 自动触发搜索 if (value) { onSearch(activeSearchName, value); } }} style={{ width: 400, borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }} allowClear onClear={() => { setActiveSearchValue(''); onClear?.(); }} className="h-10" size="large" > {currentParam.optionList?.map(item => ( <Option key={item.value} value={item.value}> {item.label} </Option> ))} </Select> ) : ( <Input placeholder={currentParam?.placeholder} value={activeSearchValue} onChange={(e) => setActiveSearchValue(e.target.value)} style={{ width: 400, borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }} onPressEnter={handleSearch} allowClear onClear={() => setActiveSearchValue('')} size="large" className="h-10" suffix={ <Button type="text" icon={<SearchOutlined />} onClick={handleSearch} size="small" className="h-8 w-8 flex items-center justify-center" /> } /> )} </div> {/* 搜索标签 */} {tagList.length > 0 && ( <div className="mt-4"> <div className="flex items-center gap-2 mb-2"> <span className="text-sm text-gray-500">已选择的筛选条件:</span> <Button type="link" size="small" onClick={handleClearAll} className="text-gray-400 hover:text-gray-600" > 清除全部 </Button> </div> <Space wrap> {tagList.map(tag => ( <Tag key={tag.name} closable onClose={() => handleClearOne(tag.name)} onClick={() => handleSelectOne(tag.name)} className="cursor-pointer" color={tag.name === activeSearchName ? 'blue' : 'default'} > {tag.label}:{tag.value} </Tag> ))} </Space> </div> )} </div> ); }; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/subscription/SubscriptionListModal.tsx: -------------------------------------------------------------------------------- ```typescript import { Modal, Table, Badge, message, Button, Popconfirm } from 'antd'; import { useEffect, useState } from 'react'; import { Subscription } from '@/types/subscription'; import { portalApi } from '@/lib/api'; import { formatDateTime } from '@/lib/utils'; interface SubscriptionListModalProps { visible: boolean; consumerId: string; consumerName: string; onCancel: () => void; } export function SubscriptionListModal({ visible, consumerId, consumerName, onCancel }: SubscriptionListModalProps) { const [subscriptions, setSubscriptions] = useState<Subscription[]>([]); const [loading, setLoading] = useState(false); const [actionLoading, setActionLoading] = useState<string | null>(null); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showQuickJumper: true, showTotal: (total: number) => `共 ${total} 条` }); useEffect(() => { if (visible && consumerId) { fetchSubscriptions(); } }, [visible, consumerId, pagination.current, pagination.pageSize]); const fetchSubscriptions = () => { setLoading(true); portalApi.getConsumerSubscriptions(consumerId, { page: pagination.current - 1, // 后端从0开始 size: pagination.pageSize }).then((res) => { setSubscriptions(res.data.content || []); setPagination(prev => ({ ...prev, total: res.data.totalElements || 0 })); }).catch((err) => { message.error('获取订阅列表失败'); }).finally(() => { setLoading(false); }); }; const handleTableChange = (paginationInfo: any) => { setPagination(prev => ({ ...prev, current: paginationInfo.current, pageSize: paginationInfo.pageSize })); }; const handleApproveSubscription = async (subscription: Subscription) => { setActionLoading(`${subscription.consumerId}-${subscription.productId}-approve`); try { await portalApi.approveSubscription(subscription.consumerId, subscription.productId); message.success('审批通过成功'); fetchSubscriptions(); // 重新获取数据 } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || '审批失败'; message.error(`审批失败: ${errorMessage}`); } finally { setActionLoading(null); } }; const handleDeleteSubscription = async (subscription: Subscription) => { setActionLoading(`${subscription.consumerId}-${subscription.productId}-delete`); try { await portalApi.deleteSubscription(subscription.consumerId, subscription.productId); message.success('删除订阅成功'); fetchSubscriptions(); // 重新获取数据 } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || '删除订阅失败'; message.error(`删除订阅失败: ${errorMessage}`); } finally { setActionLoading(null); } }; const columns = [ { title: '产品名称', dataIndex: 'productName', key: 'productName', render: (productName: string) => ( <div> <div className="font-medium">{productName || '未知产品'}</div> </div> ) }, { title: '产品类型', dataIndex: 'productType', key: 'productType', render: (productType: string) => ( <Badge color={productType === 'REST_API' ? 'blue' : 'purple'} text={productType === 'REST_API' ? 'REST API' : 'MCP Server'} /> ) }, { title: '订阅状态', dataIndex: 'status', key: 'status', render: (status: string) => ( <Badge status={status === 'APPROVED' ? 'success' : 'processing'} text={status === 'APPROVED' ? '已通过' : '待审批'} /> ) }, { title: '订阅时间', dataIndex: 'createAt', key: 'createAt', render: (date: string) => formatDateTime(date) }, { title: '更新时间', dataIndex: 'updatedAt', key: 'updatedAt', render: (date: string) => formatDateTime(date) }, { title: '操作', key: 'action', width: 120, render: (_: any, record: Subscription) => { const loadingKey = `${record.consumerId}-${record.productId}`; const isApproving = actionLoading === `${loadingKey}-approve`; const isDeleting = actionLoading === `${loadingKey}-delete`; if (record.status === 'PENDING') { return ( <Button type="primary" size="small" loading={isApproving} onClick={() => handleApproveSubscription(record)} > 审批通过 </Button> ); } else if (record.status === 'APPROVED') { return ( <Popconfirm title="确定要删除这个订阅吗?" description="删除后将无法恢复" onConfirm={() => handleDeleteSubscription(record)} okText="确定" cancelText="取消" > <Button type="default" size="small" danger loading={isDeleting} > 删除订阅 </Button> </Popconfirm> ); } return null; } } ]; const pendingCount = subscriptions.filter(s => s.status === 'PENDING').length; const approvedCount = subscriptions.filter(s => s.status === 'APPROVED').length; return ( <Modal title={ <div> <div className="text-lg font-semibold">订阅列表 - {consumerName}</div> <div className="text-sm text-gray-500 mt-1"> 待审批: <Badge count={pendingCount} style={{ backgroundColor: '#faad14' }} /> | 已通过: <Badge count={approvedCount} style={{ backgroundColor: '#52c41a' }} /> </div> </div> } open={visible} onCancel={onCancel} footer={null} width={1000} destroyOnClose > <Table columns={columns} dataSource={subscriptions} rowKey="subscriptionId" loading={loading} pagination={pagination} onChange={handleTableChange} locale={{ emptyText: '暂无订阅记录' }} /> </Modal> ); } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/api-product/ApiProductOverview.tsx: -------------------------------------------------------------------------------- ```typescript import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Row, Col, Statistic, Button, message } from 'antd' import { ApiOutlined, GlobalOutlined, TeamOutlined, EditOutlined, CheckCircleFilled, MinusCircleFilled, CopyOutlined, ExclamationCircleFilled, ClockCircleFilled } from '@ant-design/icons' import type { ApiProduct } from '@/types/api-product' import { getServiceName, formatDateTime, copyToClipboard } from '@/lib/utils' import { apiProductApi } from '@/lib/api' interface ApiProductOverviewProps { apiProduct: ApiProduct linkedService: any | null onEdit: () => void } export function ApiProductOverview({ apiProduct, linkedService, onEdit }: ApiProductOverviewProps) { const [portalCount, setPortalCount] = useState(0) const [subscriberCount] = useState(0) const navigate = useNavigate() useEffect(() => { if (apiProduct.productId) { fetchPublishedPortals() } }, [apiProduct.productId]) const fetchPublishedPortals = async () => { try { const res = await apiProductApi.getApiProductPublications(apiProduct.productId) setPortalCount(res.data.content?.length || 0) } catch (error) { } finally { } } return ( <div className="p-6 space-y-6"> <div> <h1 className="text-2xl font-bold mb-2">概览</h1> <p className="text-gray-600">API产品概览</p> </div> {/* 基本信息 */} <Card title="基本信息" extra={ <Button type="primary" icon={<EditOutlined />} onClick={onEdit} > 编辑 </Button> } > <div> <div className="grid grid-cols-6 gap-8 items-center pt-0 pb-2"> <span className="text-xs text-gray-600">产品名称:</span> <span className="col-span-2 text-xs text-gray-900">{apiProduct.name}</span> <span className="text-xs text-gray-600">产品ID:</span> <div className="col-span-2 flex items-center gap-2"> <span className="text-xs text-gray-700">{apiProduct.productId}</span> <Button type="text" size="small" icon={<CopyOutlined />} onClick={async () => { try { await copyToClipboard(apiProduct.productId); message.success('产品ID已复制'); } catch { message.error('复制失败,请手动复制'); } }} className="h-auto p-1 min-w-0" /> </div> </div> <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2"> <span className="text-xs text-gray-600">类型:</span> <span className="col-span-2 text-xs text-gray-900"> {apiProduct.type === 'REST_API' ? 'REST API' : 'MCP Server'} </span> <span className="text-xs text-gray-600">状态:</span> <div className="col-span-2 flex items-center"> {apiProduct.status === "PENDING" ? ( <ExclamationCircleFilled className="text-yellow-500 mr-2" style={{fontSize: '10px'}} /> ) : apiProduct.status === "READY" ? ( <ClockCircleFilled className="text-blue-500 mr-2" style={{fontSize: '10px'}} /> ) : ( <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} /> )} <span className="text-xs text-gray-900"> {apiProduct.status === "PENDING" ? "待配置" : apiProduct.status === "READY" ? "待发布" : "已发布"} </span> </div> </div> <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2"> <span className="text-xs text-gray-600">自动审批订阅:</span> <div className="col-span-2 flex items-center"> {apiProduct.autoApprove === true ? ( <CheckCircleFilled className="text-green-500 mr-2" style={{fontSize: '10px'}} /> ) : ( <MinusCircleFilled className="text-gray-400 mr-2" style={{fontSize: '10px'}} /> )} <span className="text-xs text-gray-900"> {apiProduct.autoApprove === true ? '已开启' : '已关闭'} </span> </div> <span className="text-xs text-gray-600">创建时间:</span> <span className="col-span-2 text-xs text-gray-700">{formatDateTime(apiProduct.createAt)}</span> </div> {apiProduct.description && ( <div className="grid grid-cols-6 gap-8 pt-2 pb-2"> <span className="text-xs text-gray-600">描述:</span> <span className="col-span-5 text-xs text-gray-700 leading-relaxed"> {apiProduct.description} </span> </div> )} </div> </Card> {/* 统计数据 */} <Row gutter={[16, 16]}> <Col xs={24} sm={12} lg={8}> <Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => { navigate(`/api-products/detail?productId=${apiProduct.productId}&tab=portal`) }} > <Statistic title="发布的门户" value={portalCount} prefix={<GlobalOutlined className="text-blue-500" />} valueStyle={{ color: '#1677ff', fontSize: '24px' }} /> </Card> </Col> <Col xs={24} sm={12} lg={8}> <Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => { navigate(`/api-products/detail?productId=${apiProduct.productId}&tab=link-api`) }} > <Statistic title="关联API" value={getServiceName(linkedService) || '未关联'} prefix={<ApiOutlined className="text-blue-500" />} valueStyle={{ color: '#1677ff', fontSize: '24px' }} /> </Card> </Col> <Col xs={24} sm={12} lg={8}> <Card className="hover:shadow-md transition-shadow"> <Statistic title="订阅用户" value={subscriberCount} prefix={<TeamOutlined className="text-blue-500" />} valueStyle={{ color: '#1677ff', fontSize: '24px' }} /> </Card> </Col> </Row> </div> ) } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/PortalSecurity.tsx: -------------------------------------------------------------------------------- ```typescript import {Card, Form, Switch, Divider, message} from 'antd' import {useMemo} from 'react' import {Portal, ThirdPartyAuthConfig, AuthenticationType, OidcConfig, OAuth2Config} from '@/types' import {portalApi} from '@/lib/api' import {ThirdPartyAuthManager} from './ThirdPartyAuthManager' interface PortalSecurityProps { portal: Portal onRefresh?: () => void } export function PortalSecurity({portal, onRefresh}: PortalSecurityProps) { const [form] = Form.useForm() const handleSave = async () => { try { 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('安全设置保存成功') onRefresh?.() } catch (error) { message.error('保存安全设置失败') } } const handleSettingUpdate = () => { // 立即更新配置 handleSave() } // 第三方认证配置保存函数 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 } } // 合并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]) return ( <div className="p-6 space-y-6"> <div> <h1 className="text-2xl font-bold mb-2">Portal安全配置</h1> <p className="text-gray-600">配置Portal的认证与审批方式</p> </div> <Form form={form} layout="vertical" initialValues={{ builtinAuthEnabled: portal.portalSettingConfig?.builtinAuthEnabled, oidcAuthEnabled: portal.portalSettingConfig?.oidcAuthEnabled, autoApproveDevelopers: portal.portalSettingConfig?.autoApproveDevelopers, autoApproveSubscriptions: portal.portalSettingConfig?.autoApproveSubscriptions, frontendRedirectUrl: portal.portalSettingConfig?.frontendRedirectUrl, }} > <Card> <div className="space-y-6"> {/* 基本安全配置标题 */} <h3 className="text-lg font-medium">基本安全配置</h3> {/* 基本安全设置内容 */} <div className="grid grid-cols-2 gap-6"> <Form.Item name="builtinAuthEnabled" label="账号密码登录" valuePropName="checked" > <Switch onChange={() => handleSettingUpdate()} /> </Form.Item> <Form.Item name="autoApproveDevelopers" label="开发者自动审批" valuePropName="checked" > <Switch onChange={() => handleSettingUpdate()} /> </Form.Item> <Form.Item name="autoApproveSubscriptions" label="订阅自动审批" valuePropName="checked" > <Switch onChange={() => handleSettingUpdate()} /> </Form.Item> </div> <Divider /> {/* 第三方认证管理器 - 内部已有标题,不需要重复添加 */} <ThirdPartyAuthManager configs={thirdPartyAuthConfigs} onSave={handleSaveThirdPartyAuth} /> </div> </Card> </Form> </div> ) } ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- ``` <?xml version="1.0" encoding="UTF-8"?> <!-- ~ 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. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.alibaba.himarket</groupId> <artifactId>himarket</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>himarket</name> <description>HiMarket AI OPEN Platform</description> <url>https://github.com/higress-group/himarket</url> <licenses> <license> <name>Apache License, Version 2.0</name> <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> <distribution>repo</distribution> </license> </licenses> <modules> <module>portal-dal</module> <module>portal-server</module> <module>portal-bootstrap</module> </modules> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.7.18</spring-boot.version> <mybatis.version>2.3.1</mybatis.version> <mariadb.version>3.4.1</mariadb.version> <hutool.version>5.8.32</hutool.version> <bouncycastle.version>1.78</bouncycastle.version> <springdoc.version>1.7.0</springdoc.version> <apigsdk.version>4.0.10</apigsdk.version> <msesdk.version>7.21.0</msesdk.version> <aliyunsdk.version>4.4.6</aliyunsdk.version> <okhttp.version>4.12.0</okhttp.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> </properties> <!-- Dependency Management --> <dependencyManagement> <dependencies> <!-- Spring Boot Dependencies --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- MariaDB Driver --> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <version>${mariadb.version}</version> </dependency> <!-- Hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <!-- Spring Boot Starter Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Starter OAuth2 Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-apig20240327</artifactId> <version>${apigsdk.version}</version> </dependency> <!-- 阿里云 MSE SDK --> <dependency> <groupId>com.aliyun</groupId> <artifactId>mse20190531</artifactId> <version>${msesdk.version}</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>${aliyunsdk.version}</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-maintainer-client</artifactId> <version>3.0.2</version> </dependency> <!-- Bouncy Castle Provider --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>${bouncycastle.version}</version> </dependency> </dependencies> </dependencyManagement> <!-- Build Configuration --> <build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> </plugins> </pluginManagement> </build> </project> ```