This is page 2 of 7. Use http://codebase.md/higress-group/himarket?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/dto/params/gateway/QueryAPIGParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.gateway; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Data public class QueryAPIGParam implements InputConverter<APIGConfig> { @NotBlank(message = "网关region不能为空") private String region; @NotNull(message = "网关类型不能为空") private GatewayType gatewayType; @NotBlank(message = "accessKey不能为空") private String accessKey; @NotBlank(message = "secretKey不能为空") private String secretKey; } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/developer/CreateDeveloperParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.developer; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.Developer; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; @Data @NoArgsConstructor public class CreateDeveloperParam implements InputConverter<Developer> { @NotBlank(message = "用户名不能为空") @Size(max = 64, message = "用户名长度不能超过64个字符") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 32, message = "密码长度应为6-32位") private String password; @Size(max = 256, message = "头像url长度不能超过256个字符") private String avatarUrl; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/BaseEntity.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.entity; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDateTime; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Data public class BaseEntity implements Serializable { @CreatedDate @Column(name = "created_at", updatable = false, columnDefinition = "datetime(3)") private LocalDateTime createAt; @LastModifiedDate @Column(name = "updated_at", columnDefinition = "datetime(3)") private LocalDateTime updatedAt; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/ProductPublicationRepository.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.repository; import com.alibaba.apiopenplatform.entity.ProductPublication; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.Optional; public interface ProductPublicationRepository extends BaseRepository<ProductPublication, Long> { Page<ProductPublication> findByPortalId(String portalId, Pageable pageable); Optional<ProductPublication> findByPortalIdAndProductId(String portalId, String productId); Page<ProductPublication> findByProductId(String productId, Pageable pageable); void deleteByProductId(String productId); void deleteAllByPortalId(String portalId); boolean existsByProductId(String productId); } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/ProductRefRepository.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.repository; import com.alibaba.apiopenplatform.entity.ProductRef; import com.alibaba.apiopenplatform.support.enums.SourceType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; /** * API Reference Repository */ @Repository public interface ProductRefRepository extends JpaRepository<ProductRef, Long>, JpaSpecificationExecutor<ProductRef> { Optional<ProductRef> findByProductId(String productId); Optional<ProductRef> findFirstByProductId(String productId); boolean existsByGatewayId(String gatewayId); } ``` -------------------------------------------------------------------------------- /portal-dal/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-dal</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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.14.0-rc1</version> </dependency> </dependencies> </project> ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/AuthResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import cn.hutool.core.annotation.Alias; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Data; @Data @Builder public class AuthResult { @Alias("access_token") @JsonProperty("access_token") private String accessToken; @Alias("token_type") @JsonProperty("token_type") @Builder.Default private String tokenType = "Bearer"; @Alias("expires_in") @JsonProperty("expires_in") private Long expiresIn; public static AuthResult of(String accessToken, Long expiresIn) { return AuthResult.builder() .accessToken(accessToken) .expiresIn(expiresIn) .build(); } } ``` -------------------------------------------------------------------------------- /deploy/helm/templates/himarket-admin-deployment.yaml: -------------------------------------------------------------------------------- ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: himarket-admin labels: app: himarket-admin spec: replicas: {{ .Values.admin.replicaCount }} selector: matchLabels: app: himarket-admin template: metadata: labels: app: himarket-admin spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "himarket.serviceAccountName" . }} containers: - name: admin image: "{{ .Values.hub }}/{{ .Values.admin.image.repository }}:{{ .Values.admin.image.tag}}" imagePullPolicy: {{ .Values.admin.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.admin.serverPort }} protocol: TCP envFrom: - configMapRef: name: himarket-admin {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/APIGMCPServerResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import com.aliyun.sdk.service.apig20240327.models.HttpRoute; import lombok.Data; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data public class APIGMCPServerResult extends GatewayMCPServerResult implements OutputConverter<APIGMCPServerResult, HttpRoute> { private String apiId; private String mcpServerId; private String mcpRouteId; @Override public APIGMCPServerResult convertFrom(HttpRoute httpRoute) { APIGMCPServerResult r = OutputConverter.super.convertFrom(httpRoute); r.setMcpServerName(httpRoute.getName()); r.setMcpRouteId(httpRoute.getRouteId()); return r; } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/package.json: -------------------------------------------------------------------------------- ```json { "name": "portal-admin", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", "start": "node server.js", "serve": "npm run build && npm start", "serve-live": "npx live-server dist --port=3000 --entry-file=index.html" }, "dependencies": { "@babel/runtime": "^8.0.0-alpha.17", "antd": "^5.15.7", "axios": "^1.7.9", "clsx": "^2.1.1", "helmet": "^7.1.0", "js-yaml": "^4.1.0", "monaco-editor": "^0.52.2", "postcss": "^8.5.6", "react": "^18.0.0", "react-dom": "^18.0.0", "react-markdown": "^10.1.0", "react-markdown-editor-lite": "^1.3.4", "react-monaco-editor": "^0.59.0", "react-router-dom": "^6.28.0", "remark-gfm": "^4.0.1", "swagger-ui-react": "^5.29.0", "tailwind-merge": "^3.3.1", "terser": "^5.43.1" }, "resolutions": { "@babel/runtime": "^8.0.0-alpha.17" }, "devDependencies": { "@eslint/js": "^9.30.1", "@types/js-yaml": "^4.0.9", "@types/node": "^24.3.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.3", "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.21", "compression": "^1.8.0", "cors": "^2.8.5", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "express": "^4.21.2", "globals": "^16.3.0", "path": "^0.12.7", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "typescript": "^5.6.3", "url": "^0.11.4", "vite": "^4.5.14" } } ``` -------------------------------------------------------------------------------- /deploy/helm/templates/himarket-frontend-deployment.yaml: -------------------------------------------------------------------------------- ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: himarket-frontend labels: app: himarket-frontend spec: replicas: {{ .Values.frontend.replicaCount }} selector: matchLabels: app: himarket-frontend template: metadata: labels: app: himarket-frontend spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "himarket.serviceAccountName" . }} containers: - name: frontend image: "{{ .Values.hub }}/{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag}}" imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.frontend.serverPort }} protocol: TCP envFrom: - configMapRef: name: himarket-frontend {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/consumer/CreateCredentialParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.consumer; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.ConsumerCredential; import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig; import com.alibaba.apiopenplatform.support.consumer.HmacConfig; import com.alibaba.apiopenplatform.support.consumer.JwtConfig; import lombok.Data; import javax.validation.constraints.AssertTrue; @Data public class CreateCredentialParam implements InputConverter<ConsumerCredential> { private ApiKeyConfig apiKeyConfig; private HmacConfig hmacConfig; private JwtConfig jwtConfig; @AssertTrue(message = "凭证信息不能为空") private boolean isValid() { return apiKeyConfig != null || hmacConfig != null || jwtConfig != null; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/nacos/CreateNacosParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.nacos; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.NacosInstance; import lombok.Data; import javax.validation.constraints.NotBlank; /** * 创建Nacos实例参数 * */ @Data public class CreateNacosParam implements InputConverter<NacosInstance> { @NotBlank(message = "Nacos名称不能为空") private String nacosName; @NotBlank(message = "服务器地址不能为空") private String serverUrl; /** * 可选:客户端指定的 nacosId,若为空则由服务端生成 */ private String nacosId; // namespace removed from create param as it's no longer stored on instance private String username; private String password; private String accessKey; private String secretKey; private String description; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/support/enums/GatewayType.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.support.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Getter public enum GatewayType { /** * 云原生API网关 */ APIG_API("API"), /** * AI网关 */ APIG_AI("AI"), /** * ADP AI网关 */ ADP_AI_GATEWAY("ADP_AI_GATEWAY"), /** * Higress */ HIGRESS("Higress"), ; private final String type; public boolean isHigress() { return this == HIGRESS; } public boolean isAPIG() { return this == APIG_API || this == APIG_AI || this == ADP_AI_GATEWAY; } public boolean isAIGateway() { return this == APIG_AI || this == ADP_AI_GATEWAY; } public boolean isAdpAIGateway() { return this == ADP_AI_GATEWAY; } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/BaseRepository.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.repository; import com.alibaba.apiopenplatform.entity.ProductPublication; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.lang.NonNull; import java.util.Collection; import java.util.List; /** * 基础数据访问接口,提供通用的数据库操作方法 * * @param <D> 实体类型(Domain/Entity) * @param <I> 主键类型(ID) */ @NoRepositoryBean public interface BaseRepository<D, I> extends JpaRepository<D, I>, JpaSpecificationExecutor<D> { /** * 根据ID集合批量查询实体列表 * * @param ids * @param sort * @return */ List<D> findAllByIdIn(@NonNull Collection<I> ids, @NonNull Sort sort); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/controller/OidcController.java: -------------------------------------------------------------------------------- ```java package com.alibaba.apiopenplatform.controller; import com.alibaba.apiopenplatform.dto.result.AuthResult; import com.alibaba.apiopenplatform.dto.result.IdpResult; import com.alibaba.apiopenplatform.service.OidcService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @Slf4j @RestController @RequestMapping("/developers/oidc") @RequiredArgsConstructor public class OidcController { private final OidcService oidcService; @GetMapping("/authorize") public void authorize(@RequestParam String provider, @RequestParam(defaultValue = "/api/v1") String apiPrefix, HttpServletRequest request, HttpServletResponse response) throws IOException { String authUrl = oidcService.buildAuthorizationUrl(provider, apiPrefix, request); log.info("Redirecting to OIDC authorization URL: {}", authUrl); response.sendRedirect(authUrl); } @GetMapping("/callback") public AuthResult callback(@RequestParam String code, @RequestParam String state, HttpServletRequest request, HttpServletResponse response) { return oidcService.handleCallback(code, state, request, response); } @GetMapping("/providers") public List<IdpResult> getProviders() { return oidcService.getAvailableProviders(); } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Administrator.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.entity; import javax.persistence.*; import java.util.Date; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; /** * 管理员实体类,映射管理员账号信息 * */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "administrator", uniqueConstraints = { @UniqueConstraint(columnNames = {"adminId"}), @UniqueConstraint(columnNames = {"username"}) }) public class Administrator extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 64) private String adminId; @Column(nullable = false, unique = true, length = 64) private String username; @Column(nullable = false) private String passwordHash; } ``` -------------------------------------------------------------------------------- /portal-bootstrap/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-bootstrap</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-server</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- OkHttp for RestTemplate --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.7.18</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/AsyncConfig.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 org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @Configuration @EnableAsync public class AsyncConfig { @Bean("taskExecutor") public Executor getTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("TaskExecutor-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/DeveloperRepository.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.repository; import com.alibaba.apiopenplatform.entity.Developer; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; import java.util.List; public interface DeveloperRepository extends BaseRepository<Developer, Long> { Optional<Developer> findByDeveloperId(String developerId); Optional<Developer> findByUsername(String username); List<Developer> findByPortalId(String portalId); Optional<Developer> findByPortalIdAndUsername(String portalId, String username); Optional<Developer> findByPortalIdAndEmail(String portalId, String email); Optional<Developer> findByDeveloperIdAndPortalId(String developerId, String portalId); Page<Developer> findByPortalId(String portalId, Pageable pageable); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/MseNacosResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.aliyun.mse20190531.models.ListClustersResponseBody.ListClustersResponseBodyData; import lombok.Data; @Data public class MseNacosResult { private String instanceId; private String name; private String serverIntranetEndpoint; private String serverInternetEndpoint; private String version; public static MseNacosResult fromListClustersResponseBodyData(ListClustersResponseBodyData cluster) { MseNacosResult nacosResult = new MseNacosResult(); nacosResult.setName(cluster.getClusterAliasName()); nacosResult.setVersion(cluster.getVersionCode()); nacosResult.setInstanceId(cluster.getInstanceId()); nacosResult.setServerIntranetEndpoint(cluster.getIntranetDomain()); nacosResult.setServerInternetEndpoint(cluster.getInternetDomain()); return nacosResult; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/ProductRefResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import com.alibaba.apiopenplatform.entity.ProductRef; import com.alibaba.apiopenplatform.support.enums.SourceType; import com.alibaba.apiopenplatform.support.product.APIGRefConfig; import com.alibaba.apiopenplatform.support.product.HigressRefConfig; import com.alibaba.apiopenplatform.support.product.NacosRefConfig; import lombok.Data; @Data public class ProductRefResult implements OutputConverter<ProductRefResult, ProductRef> { private String productId; private SourceType sourceType; private String gatewayId; private APIGRefConfig apigRefConfig; // 新增:ADP AI 网关引用配置(与 APIGRefConfig 结构一致) private APIGRefConfig adpAIGatewayRefConfig; private HigressRefConfig higressRefConfig; private String nacosId; private NacosRefConfig nacosRefConfig; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/ProductRepository.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.repository; import org.springframework.stereotype.Repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.Collection; import java.util.List; import java.util.Optional; import com.alibaba.apiopenplatform.entity.Product; @Repository public interface ProductRepository extends BaseRepository<Product, Long> { Optional<Product> findByProductId(String productId); Optional<Product> findByProductIdAndAdminId(String productId, String adminId); Optional<Product> findByNameAndAdminId(String name, String adminId); Page<Product> findByProductIdIn(Collection<String> productIds, Pageable pageable); List<Product> findByProductIdIn(Collection<String> productIds); Page<Product> findByAdminId(String adminId, Pageable pageable); Page<Product> findByCategory(String category, Pageable pageable); } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/ConsumerRepository.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.repository; import com.alibaba.apiopenplatform.entity.Consumer; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.Collection; import java.util.List; import java.util.Optional; public interface ConsumerRepository extends BaseRepository<Consumer, Long> { Optional<Consumer> findByConsumerId(String consumerId); Optional<Consumer> findByDeveloperIdAndConsumerId(String developerId, String consumerId); Optional<Consumer> findByDeveloperIdAndName(String developerId, String name); Page<Consumer> findByDeveloperId(String developerId, Pageable pageable); Page<Consumer> findByPortalId(String portalId, Pageable pageable); List<Consumer> findAllByDeveloperId(String developerId); void deleteAllByDeveloperId(String developerId); List<Consumer> findByConsumerIdIn(Collection<String> consumerIds); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/product/UpdateProductParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.product; import cn.hutool.core.util.StrUtil; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.Product; import com.alibaba.apiopenplatform.support.enums.ProductType; import com.alibaba.apiopenplatform.support.product.ProductIcon; import lombok.Data; import javax.validation.constraints.AssertTrue; @Data public class UpdateProductParam implements InputConverter<Product> { private String name; private String description; private ProductType type; private Boolean enableConsumerAuth; private String document; private ProductIcon icon; private String category; private Boolean autoApprove; @AssertTrue(message = "Icon大小不能超过16KB") public boolean checkIcon() { if (icon == null || StrUtil.isBlank(icon.getValue())) { return true; } return icon.getValue().length() < 16 * 1024; } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/types/api-product.ts: -------------------------------------------------------------------------------- ```typescript export interface ApiProductConfig { spec: string; meta: { source: string; type: string; } } // 产品图标类型 export interface ProductIcon { type: 'URL' | 'BASE64'; value: 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; } } // API 配置相关类型 export interface RestAPIItem { apiId: string; apiName: string; } export interface HigressMCPItem { mcpServerName: string; fromGatewayType: 'HIGRESS'; } export interface NacosMCPItem { mcpServerName: string; fromGatewayType: 'NACOS'; namespaceId: string; } export interface APIGAIMCPItem { mcpServerName: string; fromGatewayType: 'APIG_AI' | 'ADP_AI_GATEWAY'; mcpRouteId: string; mcpServerId?: string; apiId?: string; type?: string; } export type ApiItem = RestAPIItem | HigressMCPItem | APIGAIMCPItem | NacosMCPItem; // 关联服务配置 export interface LinkedService { productId: string; gatewayId?: string; nacosId?: string; sourceType: 'GATEWAY' | 'NACOS'; apigRefConfig?: RestAPIItem | APIGAIMCPItem; higressRefConfig?: HigressMCPItem; nacosRefConfig?: NacosMCPItem; adpAIGatewayRefConfig?: APIGAIMCPItem; } export interface ApiProduct { productId: string; name: string; description: string; type: 'REST_API' | 'MCP_SERVER'; category: string; status: 'PENDING' | 'READY' | 'PUBLISHED' | string; createAt: string; enableConsumerAuth?: boolean; autoApprove?: boolean; apiConfig?: ApiProductConfig; mcpConfig?: ApiProductMcpConfig; document?: string; icon?: ProductIcon | null; } ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/RestTemplateConfig.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 lombok.RequiredArgsConstructor; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.util.concurrent.TimeUnit; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(OkHttpClient okHttpClient) { // 使用OkHttp作为RestTemplate的底层客户端 return new RestTemplate(new OkHttp3ClientHttpRequestFactory(okHttpClient)); } @Bean public OkHttpClient okHttpClient() { return new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) .build(); } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/product/CreateProductRefParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.product; import cn.hutool.core.util.StrUtil; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.ProductRef; import com.alibaba.apiopenplatform.support.enums.SourceType; import com.alibaba.apiopenplatform.support.product.APIGRefConfig; import com.alibaba.apiopenplatform.support.product.HigressRefConfig; import com.alibaba.apiopenplatform.support.product.NacosRefConfig; import lombok.Data; import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotNull; @Data public class CreateProductRefParam implements InputConverter<ProductRef> { @NotNull(message = "数据源类型不能为空") private SourceType sourceType; private String gatewayId; private String nacosId; private APIGRefConfig apigRefConfig; // 新增:ADP AI 网关引用配置(与 APIGRefConfig 结构一致) private APIGRefConfig adpAIGatewayRefConfig; private HigressRefConfig higressRefConfig; private NacosRefConfig nacosRefConfig; } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/ProductResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import com.alibaba.apiopenplatform.entity.Product; import com.alibaba.apiopenplatform.support.enums.ProductStatus; import com.alibaba.apiopenplatform.support.enums.ProductType; import com.alibaba.apiopenplatform.support.product.ProductIcon; import lombok.Data; import java.time.LocalDateTime; @Data public class ProductResult implements OutputConverter<ProductResult, Product> { private String productId; private String name; private String description; private ProductStatus status = ProductStatus.PENDING; private Boolean enableConsumerAuth = false; private ProductType type; private String document; private ProductIcon icon; private String category; private Boolean autoApprove; private LocalDateTime createAt; private LocalDateTime updatedAt; private APIConfigResult apiConfig; private MCPConfigResult mcpConfig; private Boolean enabled; } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/console/NacosTypeSelector.tsx: -------------------------------------------------------------------------------- ```typescript import { Modal, Radio, Button, Space } from 'antd' import { useState } from 'react' export type NacosImportType = 'OPEN_SOURCE' | 'MSE' interface NacosTypeSelectorProps { visible: boolean onCancel: () => void onSelect: (type: NacosImportType) => void } export default function NacosTypeSelector({ visible, onCancel, onSelect }: NacosTypeSelectorProps) { const [selectedType, setSelectedType] = useState<NacosImportType>('MSE') const handleConfirm = () => { onSelect(selectedType) } const handleCancel = () => { setSelectedType('OPEN_SOURCE') onCancel() } return ( <Modal title="选择 Nacos 类型" open={visible} onCancel={handleCancel} footer={[ <Button key="cancel" onClick={handleCancel}> 取消 </Button>, <Button key="confirm" type="primary" onClick={handleConfirm}> 确定 </Button> ]} width={500} > <div className="py-4"> <Radio.Group value={selectedType} onChange={(e) => setSelectedType(e.target.value)} className="w-full" > <Space direction="vertical" className="w-full"> <Radio value="MSE" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">MSE Nacos</div> <div className="text-sm text-gray-500">通过阿里云 MSE 账号授权后选择实例导入</div> </div> </Radio> <Radio value="OPEN_SOURCE" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">开源 Nacos</div> <div className="text-sm text-gray-500">使用已有自建/开源 Nacos 地址登录创建</div> </div> </Radio> </Space> </Radio.Group> </div> </Modal> ) } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/PortalDomain.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.entity; import com.alibaba.apiopenplatform.support.enums.DomainType; import com.alibaba.apiopenplatform.support.enums.ProtocolType; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; @Entity @Table(name = "portal_domain", uniqueConstraints = { @UniqueConstraint(columnNames = {"domain"}, name = "uk_domain") } ) @Data @EqualsAndHashCode(callSuper = true) public class PortalDomain extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "portal_id", length = 64, nullable = false) private String portalId; @Column(name = "domain", length = 128, nullable = false) private String domain; @Enumerated(EnumType.STRING) @Column(name = "type", length = 32, nullable = false) private DomainType type = DomainType.DEFAULT; @Column(name = "protocol", length = 32, nullable = false) @Enumerated(EnumType.STRING) private ProtocolType protocol = ProtocolType.HTTP; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/DeveloperExternalIdentity.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.entity; import javax.persistence.*; import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "developer_external_identity", uniqueConstraints = { @UniqueConstraint(columnNames = {"provider", "subject"}) }) public class DeveloperExternalIdentity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "developer_id", referencedColumnName = "developerId", nullable = false) private Developer developer; @Column(nullable = false, length = 32) private String provider; @Column(nullable = false, length = 128) private String subject; @Column(length = 128) private String displayName; @Column(length = 32) private DeveloperAuthType authType; @Column(columnDefinition = "json") private String rawInfoJson; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/ConsumerRef.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.entity; import com.alibaba.apiopenplatform.converter.GatewayConfigConverter; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; import lombok.*; import javax.persistence.*; @Entity @Table(name = "consumer_ref") @Data @Builder @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class ConsumerRef extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "consumer_id", length = 64, nullable = false) private String consumerId; @Column(name = "gateway_type", length = 32, nullable = false) @Enumerated(EnumType.STRING) private GatewayType gatewayType; @Column(name = "gw_consumer_id", length = 64, nullable = false) private String gwConsumerId; @Column(name = "gateway_config", columnDefinition = "json", nullable = false) @Convert(converter = GatewayConfigConverter.class) private GatewayConfig gatewayConfig; } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/PortalFormModal.tsx: -------------------------------------------------------------------------------- ```typescript import { Modal, Form, Input, message } from 'antd' import { useEffect } from 'react' import { portalApi } from '@/lib/api' import { Portal } from '@/types' interface PortalFormModalProps { visible: boolean onCancel: () => void onSuccess: () => void portal: Portal | null } export default function PortalFormModal({ visible, onCancel, onSuccess, portal, }: PortalFormModalProps) { const [form] = Form.useForm() useEffect(() => { if (visible && portal) { form.setFieldsValue({ name: portal.name, description: portal.description || '', }) } }, [visible, portal, form]) const handleOk = async () => { try { const values = await form.validateFields() if (!portal) return await portalApi.updatePortal(portal.portalId, { name: values.name, description: values.description, portalSettingConfig: portal.portalSettingConfig, portalDomainConfig: portal.portalDomainConfig, portalUiConfig: portal.portalUiConfig, }) message.success('Portal信息更新成功') form.resetFields() onSuccess() } catch (error) { message.error('更新失败,请稍后重试') } } const handleCancel = () => { form.resetFields() onCancel() } return ( <Modal title="编辑Portal" open={visible} onOk={handleOk} onCancel={handleCancel} okText="保存" cancelText="取消" width={600} destroyOnClose > <Form form={form} layout="vertical"> <Form.Item name="name" label="Portal名称" rules={[{ required: true, message: "请输入Portal名称" }]} > <Input placeholder="请输入Portal名称" /> </Form.Item> <Form.Item name="description" label="描述" > <Input.TextArea rows={3} placeholder="请输入Portal描述" /> </Form.Item> </Form> </Modal> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/constant/Resources.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.constant; public class Resources { public static final String ADMINISTRATOR = "Administrator"; public static final String PORTAL = "Portal"; public static final String PORTAL_SETTING = "PortalSetting"; public static final String PORTAL_UI = "PortalUI"; public static final String PORTAL_DOMAIN = "PortalDomain"; public static final String OAUTH2_CONFIG = "OAuth2Config"; public static final String PUBLIC_KEY = "PublicKey"; public static final String OIDC_CONFIG = "OidcConfig"; public static final String PRODUCT = "Product"; public static final String PRODUCT_SETTING = "ProductSetting"; public static final String DEVELOPER = "Developer"; public static final String CONSUMER = "Consumer"; public static final String CONSUMER_CREDENTIAL = "ConsumerCredential"; public static final String GATEWAY = "Gateway"; public static final String PRODUCT_REF = "ProductRef"; public static final String NACOS_INSTANCE = "NacosInstance"; public static final String SUBSCRIPTION = "Subscription"; } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/bin/replace_var.py: -------------------------------------------------------------------------------- ```python #!/usr/bin/python # -*- coding: utf-8 -*- from jinja2 import Template import sys import os import traceback import json def readFileTrimspace(filepath): try: result = list() is_yaml = 'yaml' in filepath or 'yml' in filepath if os.path.isfile(filepath): f = open(filepath, 'r') for line in f.readlines(): line = (line.strip('\n') if is_yaml else line.strip()) if not len(line): continue result.append(line) return "\n".join(result) except: traceback.print_exc() return "" def readFile(filepath): try: if not os.path.isfile(filepath): print("The input [%s] is not a file" % filepath) return "" if os.path.isfile(filepath): try: f = open(filepath, 'r') return f.read() finally: if f: f.close() except: traceback.print_exc() return "" def writeFile(filepath, content): try: open(filepath, 'w+').write(content) except: traceback.print_exc() def dockerenv2json(): envJson = {} with open('/.dockerenv') as ifs: data = ifs.read() assert len(data) > 2, 'invalid dockerenv' envlist = json.loads(data) for env in envlist: indexOfEqual = env.index('=') if indexOfEqual > 0: key = env[0: indexOfEqual] value = env[indexOfEqual + 1:] envJson[key] = value return envJson def createVarMap(filepath): map = os.environ # add more param to map return map def main(): filepath = sys.argv[1] if len(sys.argv) > 1 else '' t = Template(readFile(filepath)) map = createVarMap(filepath) ret = t.render(map) writeFile(filepath, ret) if __name__ == "__main__": reload(sys) sys.setdefaultencoding('utf-8') main() ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/bin/replace_var.py: -------------------------------------------------------------------------------- ```python #!/usr/bin/python # -*- coding: utf-8 -*- from jinja2 import Template import sys import os import traceback import json def readFileTrimspace(filepath): try: result = list() is_yaml = 'yaml' in filepath or 'yml' in filepath if os.path.isfile(filepath): f = open(filepath, 'r') for line in f.readlines(): line = (line.strip('\n') if is_yaml else line.strip()) if not len(line): continue result.append(line) return "\n".join(result) except: traceback.print_exc() return "" def readFile(filepath): try: if not os.path.isfile(filepath): print("The input [%s] is not a file" % filepath) return "" if os.path.isfile(filepath): try: f = open(filepath, 'r') return f.read() finally: if f: f.close() except: traceback.print_exc() return "" def writeFile(filepath, content): try: open(filepath, 'w+').write(content) except: traceback.print_exc() def dockerenv2json(): envJson = {} with open('/.dockerenv') as ifs: data = ifs.read() assert len(data) > 2, 'invalid dockerenv' envlist = json.loads(data) for env in envlist: indexOfEqual = env.index('=') if indexOfEqual > 0: key = env[0: indexOfEqual] value = env[indexOfEqual + 1:] envJson[key] = value return envJson def createVarMap(filepath): map = os.environ # add more param to map return map def main(): filepath = sys.argv[1] if len(sys.argv) > 1 else '' t = Template(readFile(filepath)) map = createVarMap(filepath) ret = t.render(map) writeFile(filepath, ret) if __name__ == "__main__": reload(sys) sys.setdefaultencoding('utf-8') main() ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/FilterConfig.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.ContextHolder; import com.alibaba.apiopenplatform.filter.PortalResolvingFilter; import com.alibaba.apiopenplatform.service.PortalService; import lombok.RequiredArgsConstructor; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @Configuration @RequiredArgsConstructor public class FilterConfig { private final PortalService portalService; private final ContextHolder contextHolder; @Bean public FilterRegistrationBean<PortalResolvingFilter> portalResolvingFilter() { FilterRegistrationBean<PortalResolvingFilter> registrationBean = new FilterRegistrationBean<>(); PortalResolvingFilter filter = new PortalResolvingFilter(portalService, contextHolder); registrationBean.setFilter(filter); registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); registrationBean.addUrlPatterns("/*"); return registrationBean; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/AdpMCPServerResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; @EqualsAndHashCode(callSuper = true) @Data public class AdpMCPServerResult extends GatewayMCPServerResult { private String gwInstanceId; @JsonProperty("name") private String name; private String description; private List<String> domains; private List<Service> services; private ConsumerAuthInfo consumerAuthInfo; private String rawConfigurations; private String type; private String dsn; private String dbType; private String upstreamPathPrefix; /** * 确保 mcpServerName 字段被正确设置 */ public void setName(String name) { this.name = name; // 同时设置父类的 mcpServerName 字段 this.setMcpServerName(name); } @Data public static class Service { private String name; private Integer port; private String version; private Integer weight; } @Data public static class ConsumerAuthInfo { private String type; private Boolean enable; private List<String> allowedConsumers; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/product/CreateProductParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.product; import cn.hutool.core.util.StrUtil; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.Product; import com.alibaba.apiopenplatform.support.enums.ProductType; import com.alibaba.apiopenplatform.support.product.ProductIcon; import lombok.Data; import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Data public class CreateProductParam implements InputConverter<Product> { @NotBlank(message = "API产品名称不能为空") @Size(max = 50, message = "API产品名称长度不能超过50个字符") private String name; @Size(max = 256, message = "API产品描述长度不能超过256个字符") private String description; @NotNull(message = "API产品类型不能为空") private ProductType type; private String document; private ProductIcon icon; private String category; private Boolean autoApprove; @AssertTrue(message = "Icon大小不能超过16KB") public boolean checkIcon() { if (icon == null || StrUtil.isBlank(icon.getValue())) { return true; } return icon.getValue().length() < 16 * 1024; } } ``` -------------------------------------------------------------------------------- /deploy/helm/templates/himarket-server-deployment.yaml: -------------------------------------------------------------------------------- ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: himarket-server labels: app: himarket-server spec: replicas: {{ .Values.server.replicaCount }} selector: matchLabels: app: himarket-server template: metadata: labels: app: himarket-server spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "himarket.serviceAccountName" . }} containers: - name: server image: "{{ .Values.hub }}/{{ .Values.server.image.repository }}:{{ .Values.server.image.tag}}" imagePullPolicy: {{ .Values.server.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.server.serverPort }} protocol: TCP envFrom: - configMapRef: name: himarket-server # 非敏感配置 {{- if .Values.mysql.enabled }} - secretRef: name: himarket-server-secret # 内置MySQL的敏感配置 {{- end }} {{/* {{- with .Values.livenessProbe }}*/}} {{/* livenessProbe:*/}} {{/* {{- toYaml . | nindent 12 }}*/}} {{/* {{- end }}*/}} {{/* {{- with .Values.readinessProbe }}*/}} {{/* readinessProbe:*/}} {{/* {{- toYaml . | nindent 12 }}*/}} {{/* {{- end }}*/}} {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.volumes }} volumes: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/NacosInstance.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.entity; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; /** * Nacos实例实体 */ @EqualsAndHashCode(callSuper = true) @Entity @Table(name = "nacos_instance", uniqueConstraints = { @UniqueConstraint(columnNames = {"nacos_id"}, name = "uk_nacos_id"), }) @Data public class NacosInstance extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "nacos_name", length = 64, nullable = false) private String nacosName; @Column(name = "nacos_id", length = 64, nullable = false) private String nacosId; @Column(name = "admin_id", length = 64, nullable = false) private String adminId; @Column(name = "server_url", length = 256, nullable = false) private String serverUrl; @Column(name = "username", length = 64) private String username; @Column(name = "password", length = 128) private String password; @Column(name = "access_key", length = 128) private String accessKey; @Column(name = "secret_key", length = 256) private String secretKey; @Column(name = "description", length = 512) private String description; } ``` -------------------------------------------------------------------------------- /portal-bootstrap/src/main/java/com/alibaba/apiopenplatform/config/PageConfig.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 org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @Configuration public class PageConfig { @Bean public PageableHandlerMethodArgumentResolver pageableResolver() { PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); // 默认分页和排序 resolver.setFallbackPageable(PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "createAt"))); // 页码从1开始 resolver.setOneIndexedParameters(true); return resolver; } @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(pageableResolver()); } }; } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Developer.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.entity; import javax.persistence.*; import java.util.Date; import com.alibaba.apiopenplatform.support.enums.DeveloperAuthType; import com.alibaba.apiopenplatform.support.enums.DeveloperStatus; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "developer", uniqueConstraints = { @UniqueConstraint(columnNames = {"developerId"}), @UniqueConstraint(columnNames = {"portalId", "username"}) }) public class Developer extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 64) private String developerId; @Column(length = 64) private String username; @Column() private String passwordHash; @Column(length = 128) private String email; @Column(nullable = false, length = 64) private String portalId; @Column(length = 256) private String avatarUrl; @Column(nullable = false, length = 16) @Enumerated(EnumType.STRING) private DeveloperStatus status; @Column(length = 16) @Enumerated(EnumType.STRING) private DeveloperAuthType authType; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Consumer.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.entity; import com.alibaba.apiopenplatform.support.enums.ConsumerStatus; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.ColumnDefault; import javax.persistence.*; @Entity @Table(name = "consumer", uniqueConstraints = { @UniqueConstraint(columnNames = {"consumer_id"}, name = "uk_consumer_id"), @UniqueConstraint(columnNames = {"name", "portal_id", "developer_id"}, name = "uk_name_portal_developer") }) @Data @EqualsAndHashCode(callSuper = true) public class Consumer extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "consumer_id", length = 64, nullable = false) private String consumerId; @Column(name = "name", length = 64, nullable = false) private String name; @Column(name = "description", length = 256) private String description; // @Enumerated(EnumType.STRING) // @Column(name = "status", length = 32, nullable = false) // private ConsumerStatus status; @Column(name = "portal_id", length = 64, nullable = false) private String portalId; @Column(name = "developer_id", length = 64, nullable = false) private String developerId; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/ConsumerCredential.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.entity; import com.alibaba.apiopenplatform.converter.ApiKeyConfigConverter; import com.alibaba.apiopenplatform.converter.HmacConfigConverter; import com.alibaba.apiopenplatform.converter.JwtConfigConverter; import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig; import com.alibaba.apiopenplatform.support.consumer.HmacConfig; import com.alibaba.apiopenplatform.support.consumer.JwtConfig; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "consumer_credential", uniqueConstraints = { @UniqueConstraint(columnNames = {"consumer_id"}, name = "uk_consumer_id") }) @Data public class ConsumerCredential extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "consumer_id", nullable = false) private String consumerId; @Column(name = "apikey_config", columnDefinition = "json") @Convert(converter = ApiKeyConfigConverter.class) private ApiKeyConfig apiKeyConfig; @Column(name = "hmac_config", columnDefinition = "json") @Convert(converter = HmacConfigConverter.class) private HmacConfig hmacConfig; @Column(name = "jwt_config", columnDefinition = "json") @Convert(converter = JwtConfigConverter.class) private JwtConfig jwtConfig; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/support/common/Encryptor.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.support.common; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.symmetric.AES; import cn.hutool.extra.spring.SpringUtil; import lombok.extern.slf4j.Slf4j; @Slf4j public class Encryptor { private static String ROOT_KEY; private static AES getAes() { if (StrUtil.isBlank(ROOT_KEY)) { ROOT_KEY = SpringUtil.getProperty("encryption.root-key"); } if (StrUtil.isBlank(ROOT_KEY)) { throw new RuntimeException("Encryption root key is not set"); } return SecureUtil.aes(ROOT_KEY.getBytes(CharsetUtil.CHARSET_UTF_8)); } public static String encrypt(String value) { if (StrUtil.isBlank(value)) { return value; } try { return getAes().encryptHex(value); } catch (Exception e) { log.error("Encrypt failed: {}", e.getMessage()); return value; } } public static String decrypt(String value) { if (StrUtil.isBlank(value)) { return value; } try { return getAes().decryptStr(value); } catch (Exception e) { log.error("Decrypt failed: {}", e.getMessage()); return value; } } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/nginx.conf: -------------------------------------------------------------------------------- ``` user nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; # Load dynamic modules. See /usr/share/nginx/README.dynamic. include /usr/share/nginx/modules/*.conf; events { worker_connections 1024; } http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay off; gzip on; gzip_http_version 1.0; gzip_comp_level 2; gzip_proxied any; gzip_types text/plain text/css application/javascript text/xml application/xml+rss; keepalive_timeout 65; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; client_max_body_size 80M; server_tokens off; include /etc/nginx/mime.types; default_type application/octet-stream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; # HTTP Server server { # Port to listen on, can also be set in IP:PORT format listen 8000; # compression-webpack-plugin 配置 gzip on; gzip_min_length 1k; gzip_comp_level 9; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary on; # 配置禁用 gzip 条件,支持正则,此处表示 ie6 及以下不启用 gzip(因为ie低版本不支持) gzip_disable "MSIE [1-6]\."; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers Content-Type; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; include "/etc/nginx/default.d/*.conf"; location /status { stub_status on; access_log off; allow 127.0.0.1; deny all; } } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/nginx.conf: -------------------------------------------------------------------------------- ``` user nginx; worker_processes auto; error_log /var/log/nginx/error.log; pid /run/nginx.pid; # Load dynamic modules. See /usr/share/nginx/README.dynamic. include /usr/share/nginx/modules/*.conf; events { worker_connections 1024; } http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; tcp_nodelay off; gzip on; gzip_http_version 1.0; gzip_comp_level 2; gzip_proxied any; gzip_types text/plain text/css application/javascript text/xml application/xml+rss; keepalive_timeout 65; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; client_max_body_size 80M; server_tokens off; include /etc/nginx/mime.types; default_type application/octet-stream; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include # for more information. include /etc/nginx/conf.d/*.conf; # HTTP Server server { # Port to listen on, can also be set in IP:PORT format listen 8000; # compression-webpack-plugin 配置 gzip on; gzip_min_length 1k; gzip_comp_level 9; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary on; # 配置禁用 gzip 条件,支持正则,此处表示 ie6 及以下不启用 gzip(因为ie低版本不支持) gzip_disable "MSIE [1-6]\."; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers Content-Type; add_header Access-Control-Allow-Methods GET,POST,OPTIONS; include "/etc/nginx/default.d/*.conf"; location /status { stub_status on; access_log off; allow 127.0.0.1; deny all; } } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/ProductSubscription.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.entity; import com.alibaba.apiopenplatform.converter.ConsumerAuthConfigConverter; import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig; import com.alibaba.apiopenplatform.support.enums.SubscriptionStatus; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; @Entity @Table(name = "product_subscription", uniqueConstraints = { @UniqueConstraint(columnNames = {"product_id", "consumer_id"}, name = "uk_product_consumer") }) @Data @EqualsAndHashCode(callSuper = true) public class ProductSubscription extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "product_id", length = 64, nullable = false) private String productId; @Column(name = "consumer_id", length = 64, nullable = false) private String consumerId; @Column(name = "developer_id", length = 64) private String developerId; @Column(name = "portal_id", length = 64) private String portalId; @Enumerated(EnumType.STRING) @Column(name = "status", length = 32, nullable = false) private SubscriptionStatus status; @Column(name = "consumer_auth_config", columnDefinition = "json") @Convert(converter = ConsumerAuthConfigConverter.class) private ConsumerAuthConfig consumerAuthConfig; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/repository/ConsumerRefRepository.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.repository; import com.alibaba.apiopenplatform.entity.ConsumerRef; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.GatewayConfig; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import javax.swing.text.html.Option; import java.util.List; import java.util.Optional; @Repository public interface ConsumerRefRepository extends JpaRepository<ConsumerRef, Long>, JpaSpecificationExecutor<ConsumerRef> { List<ConsumerRef> findAllByConsumerId(String consumerId); @Query("SELECT c FROM ConsumerRef c WHERE c.consumerId = :consumerId AND c.gatewayType = :gatewayType AND c.gatewayConfig = :gatewayConfig") @Deprecated Optional<ConsumerRef> findConsumerRef(@Param("consumerId") String consumerId, @Param("gatewayType") GatewayType gatewayType, @Param("gatewayConfig") GatewayConfig gatewayConfig); Optional<ConsumerRef> findByGwConsumerId(String gwConsumerId); List<ConsumerRef> findAllByConsumerIdAndGatewayType(String consumerId, GatewayType gatewayType); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/MCPConfigResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import lombok.Builder; import lombok.Data; import lombok.Getter; import java.util.List; @Data public class MCPConfigResult { protected String mcpServerName; protected MCPServerConfig mcpServerConfig; protected String tools; protected McpMetadata meta; @Data public static class McpMetadata { /** * 来源 * AI网关/Higress/Nacos */ private String source; /** * 服务类型 * AI网关:HTTP(HTTP转MCP)/MCP(MCP直接代理) * Higress:OPEN_API(OpenAPI转MCP)/DIRECT_ROUTE(直接路由)/DATABASE(数据库) */ private String createFromType; /** * HTTP/SSE */ private String protocol; } @Data public static class MCPServerConfig { /** * for gateway */ private String path; private List<Domain> domains; /** * for nacos */ private Object rawConfig; private String transportMode = MCPTransportMode.REMOTE.getMode(); } @Data @Builder public static class Domain { private String domain; private String protocol; } @Getter public enum MCPTransportMode { LOCAL("Local"), REMOTE("Remote"); private final String mode; MCPTransportMode(String mode) { this.mode = mode; } } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/pages/Home.tsx: -------------------------------------------------------------------------------- ```typescript import { Button, Card, Typography } from "antd"; import { Link } from "react-router-dom"; import { Layout } from "../components/Layout"; import { useEffect } from "react"; import { getTokenFromCookie } from "../lib/utils"; const { Title, Paragraph } = Typography; function HomePage() { useEffect(() => { const params = new URLSearchParams(window.location.search); const fromCookie = params.get("fromCookie"); const token = getTokenFromCookie(); if (fromCookie && token) { localStorage.setItem("access_token", token); } }, []); return ( <Layout> <div className="text-center"> <Title level={1} className="text-6xl font-bold text-gray-900 mb-6"> HiMarket AI 开放平台 </Title> <Paragraph className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto"> 低成本接入企业级AI能力,助力业务快速创新 </Paragraph> <Link to="/apis"> <Button type="primary" size="large" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-3 text-lg" > Get started </Button> </Link> </div> <div className="mt-16"> <Card className="bg-gradient-to-r from-purple-600 via-blue-600 to-purple-800 border-0"> <div className="relative overflow-hidden"> <div className="absolute inset-0 bg-gradient-to-r from-purple-600 via-blue-600 to-purple-800 opacity-90"></div> <div className="absolute inset-0 grid grid-cols-8 gap-4"> {Array.from({ length: 32 }, (_, i) => ( <div key={i} className="bg-white/10 rounded-full aspect-square opacity-30"></div> ))} </div> <div className="relative z-10 h-64 flex items-center justify-center"> <div className="text-white text-center"> <Title level={2} className="text-3xl font-bold mb-4 text-white"> 探索 AI API 服务 </Title> <Paragraph className="text-purple-100 text-lg"> 丰富多样的 AI 能力,助您打造智能应用 </Paragraph> </div> </div> </div> </Card> </div> </Layout> ); } export default HomePage; ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/factory/HTTPClientFactory.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.factory; import lombok.extern.slf4j.Slf4j; import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.util.concurrent.TimeUnit; @Slf4j public class HTTPClientFactory { public static RestTemplate createRestTemplate() { OkHttpClient okHttpClient = okHttpClient(); // 使用OkHttp作为RestTemplate的底层客户端 return new RestTemplate(new OkHttp3ClientHttpRequestFactory(okHttpClient)); } public static OkHttpClient okHttpClient() { return new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) .build(); } public static void closeClient(RestTemplate restTemplate) { try { if (restTemplate != null) { ClientHttpRequestFactory factory = restTemplate.getRequestFactory(); if (factory instanceof OkHttp3ClientHttpRequestFactory) { ((OkHttp3ClientHttpRequestFactory) factory).destroy(); } } } catch (Exception e) { log.error("Error closing RestTemplate", e); } } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/params/gateway/ImportGatewayParam.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.params.gateway; import cn.hutool.core.util.StrUtil; import com.alibaba.apiopenplatform.dto.converter.InputConverter; import com.alibaba.apiopenplatform.entity.Gateway; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.alibaba.apiopenplatform.support.gateway.AdpAIGatewayConfig; import com.alibaba.apiopenplatform.support.gateway.HigressConfig; import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Data public class ImportGatewayParam implements InputConverter<Gateway> { @NotBlank(message = "网关名称不能为空") private String gatewayName; private String description; @NotNull(message = "网关类型不能为空") private GatewayType gatewayType; private String gatewayId; private APIGConfig apigConfig; private AdpAIGatewayConfig adpAIGatewayConfig; private HigressConfig higressConfig; @AssertTrue(message = "网关配置无效") private boolean isGatewayConfigValid() { return (gatewayType.isAPIG() && !gatewayType.equals(GatewayType.ADP_AI_GATEWAY) && apigConfig != null && StrUtil.isNotBlank(gatewayId)) || (gatewayType.equals(GatewayType.ADP_AI_GATEWAY) && adpAIGatewayConfig != null && StrUtil.isNotBlank(gatewayId)) || (gatewayType.isHigress() && higressConfig != null); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/aliyunThemeToken.ts: -------------------------------------------------------------------------------- ```typescript export default { "colorPrimary": "#0064c8", "colorPrimaryBg": "#F0F7FF", "colorPrimaryBgHover": "#CAE3FD", "colorPrimaryBorder": "#90C0EF", "colorPrimaryBorderHover": "#589ADB", "colorPrimaryHover": "#2A7DD1", "colorPrimaryActive": "#0057AD", "colorPrimaryTextHover": "#2A7DD1", "colorPrimaryText": "#0064c8", "colorPrimaryTextActive": "#0057AD", "fontSize": 12, "borderRadius": 2, "fontSizeSM": 12, "lineHeight": 1.5, "lineHeightSM": 1.5, "wireframe": true, "colorInfo": "#0064c8", "colorBgBase": "#ffffff", "colorText": "rgba(0, 0, 0, 0.90)", "colorTextSecondary": "rgba(0, 0, 0, 0.80)", "colorTextTertiary": "rgba(0, 0, 0, 0.50)", "colorTextQuaternary": "rgba(0, 0, 0, 0.20)", "colorBorder": "#d9d9d9", "colorBorderSecondary": "#E5E5E5", "colorFillQuaternary": "#F7F7F7", "colorFillTertiary": "#F7F7F7", "colorFillSecondary": "#E5E5E5", "colorFill": "#E5E5E5", "colorBgLayout": "#F7F7F7", "colorBgSpotlight": "ffffff", "colorSuccess": "#23b066", "colorSuccessBg": "#EBFFF6", "colorSuccessBgHover": "#D1F4E1", "colorSuccessBorder": "#90DEB5", "colorSuccessActive": "#159953", "colorSuccessTextHover": "#159953", "colorSuccessTextActive": "#159953", "colorSuccessText": "#23B066", "colorWarning": "#f98e1a", "colorWarningBorder": "#FFCD96", "colorWarningActive": "#CF7412", "colorWarningTextActive": "#CF7412", "colorWarningBorderHover": "#F7A854", "colorWarningHover": "#F7A854", "colorError": "#e84738", "colorErrorBg": "#FFECEB", "colorErrorBgHover": "#FCCECA", "colorErrorBorder": "#F7AAA3", "colorErrorTextActive": "#C43123", "colorErrorActive": "#C43123", "colorErrorTextHover": "#ED675A", "colorErrorHover": "#ED675A", "colorInfoActive": "#0057AD", "colorInfoBg": "#F0F7FF", "colorInfoBorder": "#90C0EF", "colorInfoTextActive": "#0057AD", "colorInfoHover": "#2A7DD1", "colorInfoBorderHover": "#2A7DD1", "colorInfoTextHover": "#2A7DD1", "fontSizeHeading2": 24, "fontSizeHeading3": 20, "fontSizeHeading4": 16, "marginXXS": 4, "boxShadow": " 0 3px 8px 0 rgba(0, 0, 0, 0.06), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) ", "boxShadowSecondary": " 0 3px 8px 0 rgba(0, 0, 0, 0.18), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) " } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/aliyunThemeToken.ts: -------------------------------------------------------------------------------- ```typescript export default { "colorPrimary": "#0064c8", "colorPrimaryBg": "#F0F7FF", "colorPrimaryBgHover": "#CAE3FD", "colorPrimaryBorder": "#90C0EF", "colorPrimaryBorderHover": "#589ADB", "colorPrimaryHover": "#2A7DD1", "colorPrimaryActive": "#0057AD", "colorPrimaryTextHover": "#2A7DD1", "colorPrimaryText": "#0064c8", "colorPrimaryTextActive": "#0057AD", "fontSize": 12, "borderRadius": 2, "fontSizeSM": 12, "lineHeight": 1.5, "lineHeightSM": 1.5, "wireframe": true, "colorInfo": "#0064c8", "colorBgBase": "#ffffff", "colorText": "rgba(0, 0, 0, 0.90)", "colorTextSecondary": "rgba(0, 0, 0, 0.80)", "colorTextTertiary": "rgba(0, 0, 0, 0.50)", "colorTextQuaternary": "rgba(0, 0, 0, 0.20)", "colorBorder": "#d9d9d9", "colorBorderSecondary": "#E5E5E5", "colorFillQuaternary": "#F7F7F7", "colorFillTertiary": "#F7F7F7", "colorFillSecondary": "#E5E5E5", "colorFill": "#E5E5E5", "colorBgLayout": "#F7F7F7", "colorBgSpotlight": "ffffff", "colorSuccess": "#23b066", "colorSuccessBg": "#EBFFF6", "colorSuccessBgHover": "#D1F4E1", "colorSuccessBorder": "#90DEB5", "colorSuccessActive": "#159953", "colorSuccessTextHover": "#159953", "colorSuccessTextActive": "#159953", "colorSuccessText": "#23B066", "colorWarning": "#f98e1a", "colorWarningBorder": "#FFCD96", "colorWarningActive": "#CF7412", "colorWarningTextActive": "#CF7412", "colorWarningBorderHover": "#F7A854", "colorWarningHover": "#F7A854", "colorError": "#e84738", "colorErrorBg": "#FFECEB", "colorErrorBgHover": "#FCCECA", "colorErrorBorder": "#F7AAA3", "colorErrorTextActive": "#C43123", "colorErrorActive": "#C43123", "colorErrorTextHover": "#ED675A", "colorErrorHover": "#ED675A", "colorInfoActive": "#0057AD", "colorInfoBg": "#F0F7FF", "colorInfoBorder": "#90C0EF", "colorInfoTextActive": "#0057AD", "colorInfoHover": "#2A7DD1", "colorInfoBorderHover": "#2A7DD1", "colorInfoTextHover": "#2A7DD1", "fontSizeHeading2": 24, "fontSizeHeading3": 20, "fontSizeHeading4": 16, "marginXXS": 4, "boxShadow": " 0 3px 8px 0 rgba(0, 0, 0, 0.06), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) ", "boxShadowSecondary": " 0 3px 8px 0 rgba(0, 0, 0, 0.18), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) " } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/utils/IdGenerator.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.core.utils; import cn.hutool.core.lang.ObjectId; /** * ID生成器 * <p> * 格式为: prefix + 24位字符串 * <p> * 支持的ID类型: * - 门户ID: portal-xxxxxx * - API产品ID: api-xxxxxx * - 开发者ID: dev-xxxxxx * - 管理员ID: admin-xxxxxx * <p> * 注意: * - API ID由网关同步,不在此生成 * */ public class IdGenerator { private static final String PORTAL_PREFIX = "portal-"; private static final String API_PRODUCT_PREFIX = "product-"; private static final String DEVELOPER_PREFIX = "dev-"; private static final String CONSUMER_PREFIX = "consumer-"; private static final String ADMINISTRATOR_PREFIX = "admin-"; private static final String NACOS_PREFIX = "nacos-"; private static final String HIGRESS_PREFIX = "higress-"; public static String genHigressGatewayId() { return HIGRESS_PREFIX + ObjectId.next(); } public static String genPortalId() { return PORTAL_PREFIX + ObjectId.next(); } public static String genApiProductId() { return API_PRODUCT_PREFIX + ObjectId.next(); } public static String genDeveloperId() { return DEVELOPER_PREFIX + ObjectId.next(); } public static String genConsumerId() { return CONSUMER_PREFIX + ObjectId.next(); } public static String genAdministratorId() { return ADMINISTRATOR_PREFIX + ObjectId.next(); } public static String genNacosId() { return NACOS_PREFIX + ObjectId.next(); } public static String genIdWithPrefix(String prefix) { return prefix + ObjectId.next(); } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/client/PopGatewayClient.java: -------------------------------------------------------------------------------- ```java package com.alibaba.apiopenplatform.service.gateway.client; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonResponse; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.MethodType; import com.aliyuncs.http.ProtocolType; import com.aliyuncs.profile.DefaultProfile; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.function.Function; /** * @author zh * 通用SDK客户端,解决OpenAPI未开放问题 */ @Slf4j public class PopGatewayClient extends GatewayClient { private final APIGConfig config; private final IAcsClient client; public PopGatewayClient(APIGConfig config) { this.config = config; this.client = createClient(config); } private IAcsClient createClient(APIGConfig config) { DefaultProfile profile = DefaultProfile.getProfile( config.getRegion(), config.getAccessKey(), config.getSecretKey()); return new DefaultAcsClient(profile); } @Override public void close() { client.shutdown(); } public <E> E execute(String uri, MethodType methodType, Map<String, String> queryParams, Function<JSONObject, E> converter) { // CommonRequest CommonRequest request = new CommonRequest(); request.setSysProtocol(ProtocolType.HTTPS); request.setSysDomain(getAPIGEndpoint(config.getRegion())); request.setSysVersion("2024-03-27"); request.setSysUriPattern(uri); request.setSysMethod(methodType); // Query Parameters if (queryParams != null) { for (Map.Entry<String, String> entry : queryParams.entrySet()) { request.putQueryParameter(entry.getKey(), entry.getValue()); } } try { CommonResponse response = client.getCommonResponse(request); JSONObject data = JSONUtil.parseObj(response.getData()) .getJSONObject("data"); return converter.apply(data); } catch (ClientException e) { log.error("Error executing Pop request", e); throw new RuntimeException(e); } } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/PortalResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import cn.hutool.core.collection.CollUtil; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import com.alibaba.apiopenplatform.entity.Portal; import com.alibaba.apiopenplatform.entity.PortalDomain; import com.alibaba.apiopenplatform.support.enums.DomainType; import com.alibaba.apiopenplatform.support.enums.ProtocolType; import com.alibaba.apiopenplatform.support.portal.PortalSettingConfig; import com.alibaba.apiopenplatform.support.portal.PortalUiConfig; import lombok.Data; import java.util.List; import java.util.stream.Collectors; @Data public class PortalResult implements OutputConverter<PortalResult, Portal> { private String portalId; private String name; private String description; private String adminId; private PortalSettingConfig portalSettingConfig; private PortalUiConfig portalUiConfig; private List<PortalDomainConfig> portalDomainConfig; @Override public PortalResult convertFrom(Portal source) { OutputConverter.super.convertFrom(source); if (CollUtil.isNotEmpty(source.getPortalDomains())) { portalDomainConfig = source.getPortalDomains().stream().map(domain -> new PortalDomainConfig().convertFrom(domain)).collect(Collectors.toList()); } return this; } @Data static class PortalDomainConfig implements OutputConverter<PortalDomainConfig, PortalDomain> { private String domain; private DomainType type; private ProtocolType protocol = ProtocolType.HTTP; } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/types/portal.ts: -------------------------------------------------------------------------------- ```typescript export interface AuthCodeConfig { clientId: string; clientSecret: string; scopes: string; authorizationEndpoint: string; tokenEndpoint: string; userInfoEndpoint: string; jwkSetUri: string; // OIDC issuer地址(用于自动发现模式) issuer?: string; // 可选的身份映射配置 identityMapping?: IdentityMapping; } export interface OidcConfig { provider: string; name: string; logoUrl?: string | null; enabled: boolean; grantType: 'AUTHORIZATION_CODE'; authCodeConfig: AuthCodeConfig; identityMapping?: IdentityMapping; } // 第三方认证相关类型定义 export enum AuthenticationType { OIDC = 'OIDC', OAUTH2 = 'OAUTH2' } export enum GrantType { AUTHORIZATION_CODE = 'AUTHORIZATION_CODE', JWT_BEARER = 'JWT_BEARER' } export enum PublicKeyFormat { PEM = 'PEM', JWK = 'JWK' } export interface PublicKeyConfig { kid: string; format: PublicKeyFormat; algorithm: string; value: string; } export interface JwtBearerConfig { publicKeys: PublicKeyConfig[]; } export interface IdentityMapping { userIdField?: string | null; userNameField?: string | null; emailField?: string | null; customFields?: { [key: string]: string } | null; } // OAuth2配置(使用现有格式) export interface OAuth2Config { provider: string; name: string; enabled: boolean; grantType: GrantType; jwtBearerConfig?: JwtBearerConfig; identityMapping?: IdentityMapping; } // 为了UI显示方便,给配置添加类型标识的联合类型 export type ThirdPartyAuthConfig = | (OidcConfig & { type: AuthenticationType.OIDC }) | (OAuth2Config & { type: AuthenticationType.OAUTH2 }) export interface PortalSettingConfig { builtinAuthEnabled: boolean; oidcAuthEnabled: boolean; autoApproveDevelopers: boolean; autoApproveSubscriptions: boolean; frontendRedirectUrl: string; // 第三方认证配置(分离存储) oidcConfigs?: OidcConfig[]; oauth2Configs?: OAuth2Config[]; } export interface PortalUiConfig { logo: string | null; icon: string | null; } export interface PortalDomainConfig { domain: string; type: string; protocol: string; } export interface Portal { portalId: string; name: string; title: string; description: string; adminId: string; portalSettingConfig: PortalSettingConfig; portalUiConfig: PortalUiConfig; portalDomainConfig: PortalDomainConfig[]; } export interface Developer { portalId: string; developerId: string; username: string; status: string; avatarUrl?: string; createAt: string; } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Portal.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.entity; import com.alibaba.apiopenplatform.converter.PortalSettingConfigConverter; import com.alibaba.apiopenplatform.converter.PortalUiConfigConverter; import com.alibaba.apiopenplatform.support.portal.PortalSettingConfig; import com.alibaba.apiopenplatform.support.portal.PortalUiConfig; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = "portal", uniqueConstraints = { @UniqueConstraint(columnNames = {"portal_id"}, name = "uk_portal_id"), @UniqueConstraint(columnNames = {"name", "admin_id"}, name = "uk_name_admin_id") }) @Data public class Portal extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "portal_id", length = 64, nullable = false) private String portalId; @Column(name = "name", length = 64, nullable = false) private String name; @Column(name = "description", length = 256) private String description; @Column(name = "admin_id", length = 64) private String adminId; @Column(name = "portal_setting_config", columnDefinition = "json") @Convert(converter = PortalSettingConfigConverter.class) private PortalSettingConfig portalSettingConfig; @Column(name = "portal_ui_config", columnDefinition = "json") @Convert(converter = PortalUiConfigConverter.class) private PortalUiConfig portalUiConfig; @Transient private List<PortalDomain> portalDomains = new ArrayList<>(); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/security/DeveloperAuthenticationProvider.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.service.DeveloperService; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collections; @Component @RequiredArgsConstructor public class DeveloperAuthenticationProvider implements AuthenticationProvider { private final DeveloperService developerService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString(); try { developerService.login(username, password); GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_DEVELOPER"); return new UsernamePasswordAuthenticationToken(username, null, Collections.singletonList(authority)); } catch (Exception e) { throw new BadCredentialsException("用户名或密码错误"); } } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/console/GatewayTypeSelector.tsx: -------------------------------------------------------------------------------- ```typescript import { Modal, Radio, Button, Space } from 'antd' import { useState } from 'react' import { GatewayType } from '@/types' import { GATEWAY_TYPE_LABELS } from '@/lib/constant' interface GatewayTypeSelectorProps { visible: boolean onCancel: () => void onSelect: (type: GatewayType) => void } export default function GatewayTypeSelector({ visible, onCancel, onSelect }: GatewayTypeSelectorProps) { const [selectedType, setSelectedType] = useState<GatewayType>('APIG_API') const handleConfirm = () => { onSelect(selectedType) } const handleCancel = () => { setSelectedType('APIG_API') onCancel() } return ( <Modal title="选择网关类型" open={visible} onCancel={handleCancel} footer={[ <Button key="cancel" onClick={handleCancel}> 取消 </Button>, <Button key="confirm" type="primary" onClick={handleConfirm}> 确定 </Button> ]} width={500} > <div className="py-4"> <Radio.Group value={selectedType} onChange={(e) => setSelectedType(e.target.value)} className="w-full" > <Space direction="vertical" className="w-full"> <Radio value="APIG_API" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">{GATEWAY_TYPE_LABELS.APIG_API}</div> <div className="text-sm text-gray-500">阿里云 API 网关服务</div> </div> </Radio> <Radio value="APIG_AI" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">{GATEWAY_TYPE_LABELS.APIG_AI}</div> <div className="text-sm text-gray-500">阿里云 AI 网关服务</div> </div> </Radio> <Radio value="HIGRESS" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">{GATEWAY_TYPE_LABELS.HIGRESS}</div> <div className="text-sm text-gray-500">Higress 云原生网关</div> </div> </Radio> <Radio value="ADP_AI_GATEWAY" className="w-full p-3 border rounded-lg hover:bg-gray-50"> <div className="ml-2"> <div className="font-medium">{GATEWAY_TYPE_LABELS.ADP_AI_GATEWAY}</div> <div className="text-sm text-gray-500">专有云 AI 网关服务</div> </div> </Radio> </Space> </Radio.Group> </div> </Modal> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/NacosNamespaceResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import lombok.Data; import lombok.EqualsAndHashCode; /** * Nacos 命名空间结果 */ @EqualsAndHashCode(callSuper = false) @Data public class NacosNamespaceResult implements OutputConverter<NacosNamespaceResult, Object> { private String namespaceId; private String namespaceName; private String namespaceDesc; @Override public NacosNamespaceResult convertFrom(Object source) { // 兼容不同SDK类型的命名空间对象,尽可能抽取常见字段 if (source == null) { return this; } try { // 优先通过常见getter获取 String id = invokeGetter(source, "getNamespaceId", "getNamespace", "getId"); String name = invokeGetter(source, "getNamespaceShowName", "getNamespaceName", "getName"); String desc = invokeGetter(source, "getNamespaceDesc", "getDescription", "getDesc"); this.namespaceId = id != null ? id : this.namespaceId; this.namespaceName = name != null ? name : this.namespaceName; this.namespaceDesc = desc != null ? desc : this.namespaceDesc; } catch (Exception ignore) { // 回退到通用属性复制 OutputConverter.super.convertFrom(source); } return this; } private String invokeGetter(Object obj, String... methods) { for (String m : methods) { try { java.lang.reflect.Method method = obj.getClass().getMethod(m); Object val = method.invoke(obj); if (val != null) { return String.valueOf(val); } } catch (Exception e) { // ignore and continue } } return null; } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Product.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.entity; import com.alibaba.apiopenplatform.converter.ProductIconConverter; import com.alibaba.apiopenplatform.support.enums.ProductStatus; import com.alibaba.apiopenplatform.support.enums.ProductType; import com.alibaba.apiopenplatform.support.product.ProductIcon; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = "product", uniqueConstraints = { @UniqueConstraint(columnNames = {"product_id"}, name = "uk_product_id"), @UniqueConstraint(columnNames = {"name"}, name = "uk_name") }) @Data public class Product extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "product_id", length = 64, nullable = false) private String productId; @Column(name = "admin_id", length = 64) private String adminId; @Column(name = "name", length = 64, nullable = false) private String name; @Column(name = "type", length = 64) @Enumerated(EnumType.STRING) private ProductType type; @Column(name = "description", length = 256) private String description; @Column(name = "enable_consumer_auth") private Boolean enableConsumerAuth; @Column(name = "document", columnDefinition = "longtext") private String document; @Column(name = "icon", columnDefinition = "json") @Convert(converter = ProductIconConverter.class) private ProductIcon icon; @Column(name = "category", length = 64) private String category; @Column(name = "status", length = 64) @Enumerated(EnumType.STRING) private ProductStatus status = ProductStatus.PENDING; @Column(name = "auto_approve") private Boolean autoApprove; } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/client/APIGClient.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.service.gateway.client; import com.alibaba.apiopenplatform.core.exception.BusinessException; import com.alibaba.apiopenplatform.core.exception.ErrorCode; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.aliyun.auth.credentials.Credential; import com.aliyun.auth.credentials.provider.StaticCredentialProvider; import com.aliyun.sdk.service.apig20240327.AsyncClient; import darabonba.core.client.ClientOverrideConfiguration; import lombok.extern.slf4j.Slf4j; import java.util.function.Function; @Slf4j public class APIGClient extends GatewayClient { private final AsyncClient apigClient; public APIGClient(APIGConfig config) { this.apigClient = createClient(config); } @Override public void close() { if (apigClient != null) { apigClient.close(); } } public <E> E execute(Function<AsyncClient, E> function) { try { return function.apply(apigClient); } catch (Exception e) { log.error("Error executing APIG request", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); } } private AsyncClient createClient(APIGConfig config) { // noinspection AklessInspection StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() .accessKeyId(config.getAccessKey()) .accessKeySecret(config.getSecretKey()) .build()); return AsyncClient.builder() .credentialsProvider(provider) .overrideConfiguration( ClientOverrideConfiguration.create() .setEndpointOverride(getAPIGEndpoint(config.getRegion())) ).build(); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/portal/PortalDashboard.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useEffect, useState } from 'react' import { Card, Spin, Button, Space } from 'antd' import { ReloadOutlined, DashboardOutlined } from '@ant-design/icons' import { portalApi } from '@/lib/api' import type { Portal } from '@/types' interface PortalDashboardProps { portal: Portal } export const PortalDashboard: React.FC<PortalDashboardProps> = ({ portal }) => { const [dashboardUrl, setDashboardUrl] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [fallback, setFallback] = useState(false) const fetchDashboardUrl = async () => { if (!portal.portalId) return setLoading(true) setError('') try { const res = await portalApi.getPortalDashboard(portal.portalId, 'Portal') if (!res?.data) { setFallback(true) } else { setDashboardUrl(res.data) } } catch (e: any) { setError(e?.response?.data?.message || '获取监控面板失败') setFallback(true) } finally { setLoading(false) } } useEffect(() => { fetchDashboardUrl() }, [portal.portalId]) if (loading) { return ( <div className="flex items-center justify-center h-64"> <Spin size="large" /> </div> ) } if (fallback || !dashboardUrl || error) { return ( <div className="p-6"> <div className="w-full h-[600px] flex items-center justify-center text-gray-500"> Dashboard 发布中,敬请期待 </div> <div className="mt-4 text-right"> <Button onClick={fetchDashboardUrl} loading={loading}>刷新</Button> </div> </div> ) } return ( <div className="p-6 space-y-6"> <div className="flex items-center justify-between"> <div> <h2 className="text-2xl font-bold flex items-center gap-2"> <DashboardOutlined className="text-blue-500" /> Dashboard 监控面板 </h2> <p className="text-gray-500 mt-2">实时监控 {portal.name} 的访问与性能</p> </div> <Space> <Button icon={<ReloadOutlined />} onClick={fetchDashboardUrl} loading={loading}>刷新</Button> </Space> </div> <Card title="监控面板" className="w-full"> <div className="w-full h-[600px] border rounded-lg overflow-hidden"> <iframe src={dashboardUrl} title={`${portal.name} Dashboard`} className="w-full h-full border-0" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox" onError={() => setFallback(true)} /> </div> </Card> </div> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/client/SLSClient.java: -------------------------------------------------------------------------------- ```java package com.alibaba.apiopenplatform.service.gateway.client; import com.aliyun.sdk.service.sls20201230.*; import com.aliyun.auth.credentials.Credential; import com.aliyun.auth.credentials.provider.StaticCredentialProvider; import darabonba.core.client.ClientOverrideConfiguration; import lombok.extern.slf4j.Slf4j; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.alibaba.apiopenplatform.core.exception.BusinessException; import com.alibaba.apiopenplatform.core.exception.ErrorCode; import java.util.function.Function; @Slf4j public class SLSClient { private final AsyncClient slsClient; public SLSClient(APIGConfig config,boolean forTicket) { if (forTicket) { this.slsClient = createTicketClient(config); } else { this.slsClient = createClient(config); } } public void close() { if (slsClient != null) { slsClient.close(); } } public <E> E execute(Function<AsyncClient, E> function) { try { return function.apply(slsClient); } catch (Exception e) { log.error("Error executing SLS request", e); throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage()); } } private AsyncClient createClient(APIGConfig config) { // noinspection AklessInspection StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() .accessKeyId(config.getAccessKey()) .accessKeySecret(config.getSecretKey()) .build()); String endpoint = String.format("%s.log.aliyuncs.com", config.getRegion()); return AsyncClient.builder() .region(config.getRegion()) .credentialsProvider(provider) .overrideConfiguration( ClientOverrideConfiguration.create() .setEndpointOverride(endpoint) ).build(); } private AsyncClient createTicketClient(APIGConfig config) { StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() .accessKeyId(config.getAccessKey()) .accessKeySecret(config.getSecretKey()) .build()); return AsyncClient.builder() .region("cn-shanghai") .credentialsProvider(provider) .overrideConfiguration( ClientOverrideConfiguration.create() .setEndpointOverride("cn-shanghai.log.aliyuncs.com") ).build(); } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/Gateway.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.entity; import com.alibaba.apiopenplatform.converter.APIGConfigConverter; import com.alibaba.apiopenplatform.converter.AdpAIGatewayConfigConverter; import com.alibaba.apiopenplatform.converter.HigressConfigConverter; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.alibaba.apiopenplatform.support.gateway.AdpAIGatewayConfig; import com.alibaba.apiopenplatform.support.gateway.HigressConfig; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = "gateway", uniqueConstraints = { @UniqueConstraint(columnNames = {"gateway_id"}, name = "uk_gateway_id"), }) @Data public class Gateway extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "gateway_name", length = 64, nullable = false) private String gatewayName; @Enumerated(EnumType.STRING) @Column(name = "gateway_type", length = 32, nullable = false) private GatewayType gatewayType; @Column(name = "gateway_id", length = 64, nullable = false) private String gatewayId; @Column(name = "admin_id", length = 64, nullable = false) private String adminId; @Convert(converter = APIGConfigConverter.class) @Column(name = "apig_config", columnDefinition = "json") private APIGConfig apigConfig; @Convert(converter = AdpAIGatewayConfigConverter.class) @Column(name = "adp_ai_gateway_config", columnDefinition = "json") private AdpAIGatewayConfig adpAIGatewayConfig; @Convert(converter = HigressConfigConverter.class) @Column(name = "higress_config", columnDefinition = "json") private HigressConfig higressConfig; } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/constant/JwtConstants.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.constant; public class JwtConstants { // region JWT Header /** * 算法字段 */ public static final String HEADER_ALG = "alg"; /** * 类型字段 */ public static final String HEADER_TYP = "typ"; /** * 密钥ID字段 */ public static final String HEADER_KID = "kid"; // endregion // region JWT Payload public static final String PAYLOAD_PROVIDER = "provider"; /** * 过期时间 */ public static final String PAYLOAD_EXP = "exp"; /** * 签发时间 */ public static final String PAYLOAD_IAT = "iat"; /** * JWT唯一标识 */ public static final String PAYLOAD_JTI = "jti"; /** * 签发者 */ public static final String PAYLOAD_ISS = "iss"; /** * 主题 */ public static final String PAYLOAD_SUB = "sub"; /** * 受众 */ public static final String PAYLOAD_AUD = "aud"; /** * 门户ID */ public static final String PAYLOAD_PORTAL = "portal"; // endregion // region 自定义Payload /** * 用户ID(默认身份映射字段) */ public static final String PAYLOAD_USER_ID = "userId"; /** * 用户名(默认身份映射字段) */ public static final String PAYLOAD_USER_NAME = "name"; /** * 邮箱(默认身份映射字段) */ public static final String PAYLOAD_EMAIL = "email"; // endregion // region OAuth2相关常量 /** * JWT Bearer Grant类型 */ public static final String JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; /** * Token类型 */ public static final String TOKEN_TYPE_BEARER = "Bearer"; /** * 默认Token过期时间(秒) */ public static final int DEFAULT_TOKEN_EXPIRES_IN = 3600; /** * JWT Token类型 */ public static final String JWT_TOKEN_TYPE = "JWT"; // endregion } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/console/ImportHigressModal.tsx: -------------------------------------------------------------------------------- ```typescript import { useState } from 'react' import { Button, Modal, Form, Input, message } from 'antd' import { gatewayApi } from '@/lib/api' interface ImportHigressModalProps { visible: boolean onCancel: () => void onSuccess: () => void } export default function ImportHigressModal({ visible, onCancel, onSuccess }: ImportHigressModalProps) { const [form] = Form.useForm() const [loading, setLoading] = useState(false) const handleSubmit = async (values: any) => { setLoading(true) try { // 构建请求参数,将 apiOptions 改为 apiConfig const requestData = { gatewayName: values.gatewayName, description: values.description, gatewayType: 'HIGRESS', higressConfig: { address: values.address, username: values.username, password: values.password, } } await gatewayApi.importGateway(requestData) message.success('导入成功!') handleCancel() onSuccess() } catch (error: any) { // message.error(error.response?.data?.message || '导入失败!') } finally { setLoading(false) } } const handleCancel = () => { form.resetFields() onCancel() } return ( <Modal title="导入 Higress 网关" open={visible} onCancel={handleCancel} footer={null} width={600} > <Form form={form} layout="vertical" onFinish={handleSubmit} preserve={false} > <Form.Item label="网关名称" name="gatewayName" rules={[{ required: true, message: '请输入网关名称' }]} > <Input placeholder="请输入网关名称" /> </Form.Item> <Form.Item label="描述" name="description" > <Input.TextArea placeholder="请输入网关描述(可选)" rows={3} /> </Form.Item> <Form.Item label="服务地址" name="address" rules={[{ required: true, message: '请输入服务地址' }]} > <Input placeholder="例如:higress.example.com" /> </Form.Item> <Form.Item label="用户名" name="username" // rules={[{ required: true, message: '请输入用户名' }]} > <Input placeholder="请输入用户名" /> </Form.Item> <Form.Item label="密码" name="password" > <Input.Password placeholder="请输入密码" /> </Form.Item> <div className="flex justify-end space-x-2 pt-4"> <Button onClick={handleCancel}> 取消 </Button> <Button type="primary" htmlType="submit" loading={loading}> 导入 </Button> </div> </Form> </Modal> ) } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/AdpGatewayInstanceResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific * language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import lombok.Data; import java.util.List; /** * ADP网关实例列表响应结果 */ @Data public class AdpGatewayInstanceResult { private Integer code; private String msg; private String message; private AdpGatewayInstanceData data; @Data public static class AdpGatewayInstanceData { private List<AdpGatewayInstance> records; private Integer total; private Integer size; private Integer current; } @Data public static class AdpGatewayInstance { private Integer status; private String gwInstanceId; private String name; private String deployClusterNamespace; private String deployClusterCode; private List<AccessMode> accessMode; private String deployClusterName; private String k8sServiceName; private String createTime; private String modifyTime; private String tid; private String vpcId; private String regionId; private String zoneId; private String deployMode; private String edasAppId; private String edasNamespaceId; private String k8sClusterId; private String k8sNamespace; private String instanceClass; private String edasAppInfos; private String department; private String resourceGroup; private String ingressClassName; private String brokerEngineType; private String brokerEngineVersion; private String deployClusterAttribute; private String vSwitchId; } @Data public static class AccessMode { private List<String> ips; private List<String> ports; private String accessModeType; private String loadBalancerNetworkType; private String loadBalancerAddressType; private String serviceName; private List<String> externalIps; private List<String> clusterIp; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/PageResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.domain.Page; import java.util.ArrayList; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PageResult<T> implements OutputConverter<PageResult<T>, Page<T>> { private List<T> content; private int number; private int size; private long totalElements; public <S> PageResult<T> mapFrom(PageResult<S> source, Function<S, T> mapper) { setContent(source.getContent().stream() .map(mapper) .collect(Collectors.toList())); setSize(source.getSize()); setNumber(source.getNumber()); setTotalElements(source.getTotalElements()); return this; } public <S> PageResult<T> convertFrom(Page<S> source, Function<S, T> mapper) { setContent(source.getContent().stream() .map(mapper) .collect(Collectors.toList())); setSize(source.getSize()); // 由Pageable转换时修正 setNumber(source.getNumber() + 1); setTotalElements(source.getTotalElements()); return this; } public static <T> PageResult<T> empty(int pageNumber, int pageSize) { return PageResult.<T>builder() .content(new ArrayList<>()) .number(pageNumber) .size(pageSize) .totalElements(0) .build(); } public static <T> PageResult<T> of(List<T> content, int pageNumber, int pageSize, long total) { return PageResult.<T>builder() .content(content) .number(pageNumber) .size(pageSize) .totalElements(total) .build(); } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/advice/ResponseAdvice.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.advice; import cn.hutool.json.JSONUtil; import com.alibaba.apiopenplatform.core.response.Response; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * 统一响应处理 * <p> * 用于封装接口响应数据为统一格式: * { * "code": "Success", * "message": "操作成功", * "data": T * } * <p> * 以下情况不会被包装: * 1. 返回值已经是 {@link ResponseEntity} * 2. 返回值已经是 {@link Response} * */ @RestControllerAdvice @Slf4j public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // 排除Swagger相关路径 Class<?> declaringClass = returnType.getDeclaringClass(); if (declaringClass.getName().contains("org.springdoc") || declaringClass.getName().contains("springfox.documentation")) { return false; } return !returnType.getParameterType().equals(ResponseEntity.class) && !returnType.getParameterType().equals(Response.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 设置成功响应码 response.setStatusCode(HttpStatus.OK); if (body instanceof String) { return JSONUtil.toJsonStr(Response.ok(body)); } return Response.ok(body); } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/entity/ProductRef.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.entity; import com.alibaba.apiopenplatform.converter.APIGRefConfigConverter; import com.alibaba.apiopenplatform.converter.HigressRefConfigConverter; import com.alibaba.apiopenplatform.converter.NacosRefConfigConverter; import com.alibaba.apiopenplatform.support.enums.SourceType; import com.alibaba.apiopenplatform.support.product.APIGRefConfig; import com.alibaba.apiopenplatform.support.product.HigressRefConfig; import com.alibaba.apiopenplatform.support.product.NacosRefConfig; import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.*; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = "product_ref") @Data public class ProductRef extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "product_id", length = 64, nullable = false) private String productId; @Column(name = "gateway_id", length = 64) private String gatewayId; @Column(name = "apig_ref_config", columnDefinition = "json") @Convert(converter = APIGRefConfigConverter.class) private APIGRefConfig apigRefConfig; @Column(name = "adp_ai_gateway_ref_config", columnDefinition = "json") @Convert(converter = APIGRefConfigConverter.class) private APIGRefConfig adpAIGatewayRefConfig; @Column(name = "higress_ref_config", columnDefinition = "json") @Convert(converter = HigressRefConfigConverter.class) private HigressRefConfig higressRefConfig; @Column(name = "nacos_id", length = 64) private String nacosId; @Column(name = "nacos_ref_config", columnDefinition = "json") @Convert(converter = NacosRefConfigConverter.class) private NacosRefConfig nacosRefConfig; @Column(name = "source_type", length = 32) @Enumerated(EnumType.STRING) private SourceType sourceType; @Column(name = "api_config", columnDefinition = "json") private String apiConfig; @Column(name = "mcp_config", columnDefinition = "json") private String mcpConfig; @Column(name = "enabled") private Boolean enabled; } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/pages/Register.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState } from 'react' import { Link, useNavigate, useLocation } from 'react-router-dom' import api from '../lib/api' import { Form, Input, Button, Alert } from 'antd' const Register: React.FC = () => { const [loading, setLoading] = useState(false) const [error, setError] = useState('') const navigate = useNavigate() const location = useLocation() const searchParams = new URLSearchParams(location.search) const portalId = searchParams.get('portalId') || '' const handleRegister = async (values: { username: string; password: string; confirmPassword: string }) => { setError('') if (!values.username || !values.password || !values.confirmPassword) { setError('请填写所有字段') return } if (values.password !== values.confirmPassword) { setError('两次输入的密码不一致') return } setLoading(true) try { await api.post('/admins/init', { username: values.username, password: values.password }) navigate('/login') } 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">注册 AI Portal</h2> <Form className="w-full flex flex-col gap-4" layout="vertical" onFinish={handleRegister} > <Form.Item name="username" rules={[{ required: true, message: '请输入账号' }]} > <Input placeholder="账号" autoComplete="username" size="large" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]} > <Input.Password placeholder="密码" autoComplete="new-password" size="large" /> </Form.Item> <Form.Item name="confirmPassword" rules={[{ required: true, message: '请确认密码' }]} > <Input.Password placeholder="确认密码" autoComplete="new-password" 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 className="mt-6 text-gray-400 text-sm text-center w-full"> 已有账号?<Link to="/login" className="text-indigo-500 hover:underline ml-1">登录</Link> </div> </div> </div> ) } export default Register ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/service/PortalService.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.portal.*; import com.alibaba.apiopenplatform.dto.result.PageResult; import com.alibaba.apiopenplatform.dto.result.PortalResult; import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam; import com.alibaba.apiopenplatform.dto.result.SubscriptionResult; import org.springframework.data.domain.Pageable; public interface PortalService { /** * 创建门户 * * @param param * @return */ PortalResult createPortal(CreatePortalParam param); /** * 查询门户 * * @param portalId * @return */ PortalResult getPortal(String portalId); /** * 检查门户是否存在 * * @param portalId */ void existsPortal(String portalId); /** * 查询门户列表 * * @param pageable * @return */ PageResult<PortalResult> listPortals(Pageable pageable); /** * 更新门户 * * @param portalId * @param param * @return */ PortalResult updatePortal(String portalId, UpdatePortalParam param); /** * 删除门户 * * @param portalId */ void deletePortal(String portalId); /** * 根据请求域名解析门户 * * @param domain * @return */ String resolvePortal(String domain); /** * 为门户绑定域名 * * @param portalId * @param param * @return */ PortalResult bindDomain(String portalId, BindDomainParam param); /** * 删除门户绑定域名 * * @param portalId * @param domain * @return */ PortalResult unbindDomain(String portalId, String domain); /** * 获取门户上的API产品订阅列表 * * @param portalId 门户ID * @param param 查询参数 * @param pageable 分页参数 * @return PageResult of SubscriptionResult */ PageResult<SubscriptionResult> listSubscriptions(String portalId, QuerySubscriptionParam param, Pageable pageable); /** * 获取默认门户 * * @return */ String getDefaultPortal(); /** * 获取门户的Dashboard监控面板URL * * @param portalId 门户ID * @return Dashboard URL */ String getDashboard(String portalId); } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/components/Layout.tsx: -------------------------------------------------------------------------------- ```typescript import type { ReactNode } from "react"; import { Skeleton } from "antd"; import { Navigation } from "./Navigation"; interface LayoutProps { children: ReactNode; className?: string; loading?: boolean; } export function Layout({ children, className = "", loading = false }: LayoutProps) { return ( <div className={`min-h-screen bg-[#f4f4f6] ${className}`}> <Navigation loading={loading} /> <main className="pt-4"> <div className="w-full mx-auto px-4 sm:px-6 lg:px-8 py-8"> {loading ? ( <div className="space-y-8"> {/* 页面标题骨架屏 */} <div className="text-center mb-8"> <Skeleton.Input active size="large" style={{ width: 300, height: 48, margin: '0 auto 16px' }} /> <Skeleton.Input active size="small" style={{ width: '80%', height: 24, margin: '0 auto' }} /> </div> {/* 搜索框骨架屏 */} <div className="flex justify-center mb-8"> <div className="relative w-full max-w-2xl"> <Skeleton.Input active size="large" style={{ width: '100%', height: 40 }} /> </div> </div> {/* 子标题骨架屏 */} <div className="mb-6"> <Skeleton.Input active size="small" style={{ width: 200, height: 32 }} /> </div> {/* 内容区域骨架屏 */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> {Array.from({ length: 6 }).map((_, index) => ( <div key={index} className="h-full rounded-lg shadow-lg bg-white p-4"> <div className="flex items-start space-x-4"> <Skeleton.Avatar size={48} active /> <div className="flex-1 min-w-0"> <div className="flex items-center justify-between mb-2"> <Skeleton.Input active size="small" style={{ width: 120 }} /> <Skeleton.Input active size="small" style={{ width: 60 }} /> </div> <Skeleton.Input active size="small" style={{ width: 80, marginBottom: 8 }} /> <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} /> <Skeleton.Input active size="small" style={{ width: '100%', marginBottom: 12 }} /> <div className="flex items-center justify-between"> <Skeleton.Input active size="small" style={{ width: 60 }} /> <Skeleton.Input active size="small" style={{ width: 80 }} /> </div> </div> </div> </div> ))} </div> </div> ) : ( children )} </div> </main> {/* <Footer /> */} </div> ); } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/dto/result/GatewayResult.java: -------------------------------------------------------------------------------- ```java /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package com.alibaba.apiopenplatform.dto.result; import com.alibaba.apiopenplatform.dto.converter.OutputConverter; import com.alibaba.apiopenplatform.entity.Gateway; import com.alibaba.apiopenplatform.support.enums.GatewayType; import com.alibaba.apiopenplatform.support.gateway.APIGConfig; import com.alibaba.apiopenplatform.support.gateway.AdpAIGatewayConfig; import com.alibaba.apiopenplatform.support.gateway.HigressConfig; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class GatewayResult implements OutputConverter<GatewayResult, Gateway> { private String gatewayId; private GatewayType gatewayType; private String gatewayName; private APIGConfigResult apigConfig; private AdpAIGatewayConfigResult adpAIGatewayConfig; private HigressConfigResult higressConfig; private LocalDateTime createAt; @Override public GatewayResult convertFrom(Gateway source) { OutputConverter.super.convertFrom(source); if (source.getGatewayType().isAPIG() && !source.getGatewayType().equals(GatewayType.ADP_AI_GATEWAY)) { setApigConfig(new APIGConfigResult().convertFrom(source.getApigConfig())); } else if (source.getGatewayType().equals(GatewayType.ADP_AI_GATEWAY)) { setAdpAIGatewayConfig(new AdpAIGatewayConfigResult().convertFrom(source.getAdpAIGatewayConfig())); } else { setHigressConfig(new HigressConfigResult().convertFrom(source.getHigressConfig())); } return this; } @Data public static class APIGConfigResult implements OutputConverter<APIGConfigResult, APIGConfig> { private String region; } @Data public static class AdpAIGatewayConfigResult implements OutputConverter<AdpAIGatewayConfigResult, AdpAIGatewayConfig> { private String baseUrl; private Integer port; private String authSeed; } @Data public static class HigressConfigResult implements OutputConverter<HigressConfigResult, HigressConfig> { private String address; private String username; } } ``` -------------------------------------------------------------------------------- /portal-server/src/main/java/com/alibaba/apiopenplatform/core/advice/ExceptionAdvice.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.advice; import com.alibaba.apiopenplatform.core.exception.BusinessException; import com.alibaba.apiopenplatform.core.exception.ErrorCode; import com.alibaba.apiopenplatform.core.response.Response; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.stream.Collectors; /** * 全局异常处理 * <p> * 处理三类异常: * 1. 业务异常 {@link BusinessException}: 业务异常 * 2. 参数校验异常 {@link MethodArgumentNotValidException}: 请求参数校验不通过 * 3. 系统异常 {@link Exception}: 非预期的系统异常 * <p> * 所有异常都会被转换为统一的响应格式: * { * "code": "错误码", * "message": "错误信息", * "data": null * } * */ @Slf4j @RestControllerAdvice public class ExceptionAdvice { @ExceptionHandler(BusinessException.class) public ResponseEntity<Response<Void>> handleBusinessException(BusinessException e) { return ResponseEntity .status(e.getStatus()) .body(Response.fail(e.getCode(), e.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Response<Void>> handleParamVerifyException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) .collect(Collectors.joining("; ")); log.error("Validation failed", e); return ResponseEntity .status(ErrorCode.INVALID_PARAMETER.getStatus()) .body(Response.fail(ErrorCode.INVALID_PARAMETER.name(), message)); } @ExceptionHandler(Exception.class) public ResponseEntity<Response<Void>> handleSystemException(Exception e) { log.error("System error", e); return ResponseEntity .status(ErrorCode.INTERNAL_ERROR.getStatus()) .body(Response.fail( ErrorCode.INTERNAL_ERROR.name(), ErrorCode.INTERNAL_ERROR.getMessage(e.getMessage()) )); } } ``` -------------------------------------------------------------------------------- /portal-dal/src/main/java/com/alibaba/apiopenplatform/converter/JsonConverter.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.converter; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.json.JSONUtil; import com.alibaba.apiopenplatform.support.common.Encrypted; import com.alibaba.apiopenplatform.support.common.Encryptor; import lombok.extern.slf4j.Slf4j; import javax.persistence.AttributeConverter; import java.lang.reflect.Field; @Slf4j public abstract class JsonConverter<T> implements AttributeConverter<T, String> { private final Class<T> type; protected JsonConverter(Class<T> type) { this.type = type; } @Override public String convertToDatabaseColumn(T attribute) { if (attribute == null) { return null; } T clonedAttribute = cloneAndEncrypt(attribute); return JSONUtil.toJsonStr(clonedAttribute); } @Override public T convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } T attribute = JSONUtil.toBean(dbData, type); decrypt(attribute); return attribute; } private T cloneAndEncrypt(T original) { // Clone避免JPA更新数据 T cloned = JSONUtil.toBean(JSONUtil.toJsonStr(original), type); handleEncryption(cloned, true); return cloned; } private void decrypt(T attribute) { handleEncryption(attribute, false); } private void handleEncryption(Object obj, boolean isEncrypt) { if (obj == null) { return; } BeanUtil.descForEach(obj.getClass(), pd -> { Field field = pd.getField(); if (field == null) { return; } Object value = ReflectUtil.getFieldValue(obj, field); if (value == null) { return; } // 处理需要加密/解密的字段 if (field.isAnnotationPresent(Encrypted.class) && value instanceof String) { String result = isEncrypt ? Encryptor.encrypt((String) value) : Encryptor.decrypt((String) value); ReflectUtil.setFieldValue(obj, field, result); } else if (!ClassUtil.isSimpleValueType(value.getClass())) { handleEncryption(value, isEncrypt); } }); } } ``` -------------------------------------------------------------------------------- /portal-web/api-portal-admin/src/components/api-product/ApiProductDashboard.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState, useEffect } from 'react'; import { Card, Spin, Button, Space } from 'antd'; import { ReloadOutlined, DashboardOutlined } from '@ant-design/icons'; import { apiProductApi } from '@/lib/api'; import type { ApiProduct } from '@/types/api-product'; interface ApiProductDashboardProps { apiProduct: ApiProduct; } export const ApiProductDashboard: React.FC<ApiProductDashboardProps> = ({ apiProduct }) => { const [dashboardUrl, setDashboardUrl] = useState<string>(''); const [loading, setLoading] = useState(false); const [error, setError] = useState<string>(''); const [fallback, setFallback] = useState<boolean>(false); // 获取Dashboard URL const fetchDashboardUrl = async () => { if (!apiProduct.productId) return; setLoading(true); setError(''); try { // 直接调用产品的dashboard接口获取监控面板URL const response = await apiProductApi.getProductDashboard(apiProduct.productId); if (!response?.data) { setFallback(true); } else { setDashboardUrl(response.data); } } catch (err: any) { setError(err?.response?.data?.message || '获取监控面板失败'); setFallback(true); } finally { setLoading(false); } }; useEffect(() => { if (apiProduct.productId) { fetchDashboardUrl(); } }, [apiProduct.productId]); const handleRefresh = () => { fetchDashboardUrl(); }; if (loading) { return ( <div className="flex items-center justify-center h-64"> <Spin size="large" /> </div> ); } if (fallback || !dashboardUrl || error) { return ( <div className="p-6"> <div className="w-full h-[600px] flex items-center justify-center text-gray-500"> Dashboard 发布中,敬请期待 </div> <div className="mt-4 text-right"> <Button onClick={handleRefresh} loading={loading}>刷新</Button> </div> </div> ); } return ( <div className="p-6 space-y-6"> {/* 标题和操作 */} <div className="flex items-center justify-between"> <div> <h2 className="text-2xl font-bold flex items-center gap-2"> <DashboardOutlined className="text-blue-500" /> Dashboard 监控面板 </h2> <p className="text-gray-500 mt-2"> 实时监控 {apiProduct.name} 的API调用情况和性能指标 </p> </div> <Space> <Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={loading} > 刷新 </Button> </Space> </div> {/* Dashboard嵌入区域 */} <Card title="监控面板" className="w-full"> <div className="w-full h-[600px] border rounded-lg overflow-hidden"> {dashboardUrl ? ( <iframe src={dashboardUrl} title={`${apiProduct.name} Dashboard`} className="w-full h-full border-0" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox" onError={() => setFallback(true)} /> ) : ( <div className="flex items-center justify-center h-full text-gray-500"> 加载监控面板中... </div> )} </div> </Card> </div> ); }; ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/pages/Register.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Form, Input, Button, Card, message } from 'antd' import { UserOutlined, LockOutlined } from '@ant-design/icons' import api from '../lib/api' const Register: React.FC = () => { const [loading, setLoading] = useState(false) const navigate = useNavigate() // const location = useLocation() // const searchParams = new URLSearchParams(location.search) // const portalId = searchParams.get('portalId') || '' const handleRegister = async (values: { username: string; password: string; confirmPassword: string }) => { setLoading(true) try { // 这里需要根据实际API调整 await api.post('/developers', { username: values.username, password: values.password, }) message.success('注册成功!') // 注册成功后跳转到登录页 navigate('/login') } catch { message.error('注册失败,请重试') } finally { setLoading(false) } } 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">注册 AI Portal - 前台</h2> </div> <Form name="register" onFinish={handleRegister} autoComplete="off" layout="vertical" size="large" > <Form.Item name="username" rules={[ { required: true, message: '请输入账号' }, { min: 3, message: '账号至少3个字符' } ]} > <Input prefix={<UserOutlined />} placeholder="账号" autoComplete="username" /> </Form.Item> <Form.Item name="password" rules={[ { required: true, message: '请输入密码' }, { min: 6, message: '密码至少6个字符' } ]} > <Input.Password prefix={<LockOutlined />} placeholder="密码" autoComplete="new-password" /> </Form.Item> <Form.Item name="confirmPassword" dependencies={['password']} rules={[ { required: true, message: '请确认密码' }, ({ getFieldValue }) => ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve() } return Promise.reject(new Error('两次输入的密码不一致')) }, }), ]} > <Input.Password prefix={<LockOutlined />} placeholder="确认密码" autoComplete="new-password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading} className="w-full" size="large" > {loading ? '注册中...' : '注册'} </Button> </Form.Item> </Form> <div className="text-center text-gray-500"> 已有账号?<Link to="/login" className="text-blue-500 hover:underline">登录</Link> </div> </Card> </div> ) } export default Register ``` -------------------------------------------------------------------------------- /portal-web/api-portal-frontend/src/components/Navigation.tsx: -------------------------------------------------------------------------------- ```typescript import { Link, useLocation } from "react-router-dom"; import { Skeleton } from "antd"; import { UserInfo } from "./UserInfo"; interface NavigationProps { loading?: boolean; } export function Navigation({ loading = false }: NavigationProps) { const location = useLocation(); const isActive = (path: string) => { if (path === '/') { return location.pathname === '/'; } return location.pathname.startsWith(path); }; const getNavLinkClass = (path: string) => { const baseClass = "font-medium transition-colors"; return isActive(path) ? `${baseClass} text-blue-600 border-b-2 border-blue-600 pb-1` : `${baseClass} text-gray-700 hover:text-gray-900`; }; return ( <nav className="bg-[#f4f4f6] sticky top-0 z-50"> <div className="w-full mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between items-center h-16"> <div className="flex items-center"> {loading ? ( <div className="flex items-center space-x-2"> <Skeleton.Avatar size={32} active /> <Skeleton.Input active size="small" style={{ width: 120, height: 24 }} /> </div> ) : ( <Link to="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity"> <div className="w-8 h-8 rounded-full flex items-center justify-center"> {/* LOGO区域 */} <img src="/logo.png" alt="logo" className="w-6 h-6" style={{ display: "block" }} /> </div> <span className="text-xl font-bold text-gray-900">HiMarket</span> </Link> )} </div> <div className="hidden md:flex items-center space-x-8 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"> {loading ? ( <> <Skeleton.Input active size="small" style={{ width: 100, height: 20 }} /> <Skeleton.Input active size="small" style={{ width: 60, height: 20 }} /> <Skeleton.Input active size="small" style={{ width: 60, height: 20 }} /> <Skeleton.Input active size="small" style={{ width: 60, height: 20 }} /> </> ) : ( <> <Link to="/getting-started" className={getNavLinkClass('/getting-started')} > Getting Started </Link> <Link to="/apis" className={getNavLinkClass('/apis')} > APIs </Link> <Link to="/mcp" className={getNavLinkClass('/mcp')} > MCP </Link> </> )} </div> <div className="flex items-center space-x-4"> {/* <div className="hidden sm:block"> <Input placeholder="Search" prefix={<SearchOutlined className="text-gray-400" />} className="w-48 lg:w-64" size="middle" /> </div> */} {loading ? ( <Skeleton.Avatar size={32} active /> ) : ( <UserInfo /> )} </div> </div> </div> </nav> ); } ```