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