This commit is contained in:
mangomqy
2025-11-13 02:54:06 +00:00
commit c5e51ed069
254 changed files with 54901 additions and 0 deletions

View File

@ -0,0 +1,458 @@
# Go vs TypeScript + class-transformer 对比
## 命名约定和自动转换实现
本文档展示 Go 和 TypeScript 如何实现类似的自动类型转换机制。
---
## 📋 命名约定总结
### OpenAPI 规范
| 元素 | 命名约定 | 示例 |
|------|---------|------|
| Fixed Fields | `camelCase` | `operationId`, `requestBody` |
| Schema 名称 | `PascalCase` | `ClusterResponse`, `CreateClusterRequest` |
| Schema 属性 | `snake_case` | `has_ca_data`, `created_at`, `cluster_id` |
### Backend Go
| 元素 | 命名约定 | 示例 |
|------|---------|------|
| 导出变量/字段 | `PascalCase` | `HasCAData`, `CreatedAt` |
| 非导出变量 | `camelCase` | `hasCAData`, `createdAt` |
| 类型名 | `PascalCase` | `ClusterResponse`, `CreateClusterRequest` |
| JSON 标签 | `snake_case` | `json:"has_ca_data"`, `json:"created_at"` |
### Frontend TypeScript
| 元素 | 命名约定 | 示例 |
|------|---------|------|
| 变量 | `camelCase` | `hasCAData`, `createdAt` |
| 类型名 | `PascalCase` | `Cluster`, `CreateClusterRequest` |
| JSON | `snake_case` | `has_ca_data`, `created_at` |
---
## 🔄 自动转换对比
### Go 的实现
```go
// backend/internal/adapter/input/http/dto/cluster_dto.go
package dto
// 类型定义PascalCase
type ClusterResponse struct {
// 导出字段PascalCase
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
// 字段名PascalCase, JSONsnake_case
HasCAData bool `json:"has_ca_data"` // ← struct tag
HasCertData bool `json:"has_cert_data"` // ← struct tag
HasKeyData bool `json:"has_key_data"` // ← struct tag
CAData string `json:"ca_data"` // ← struct tag
CertData string `json:"cert_data"` // ← struct tag
KeyData string `json:"key_data"` // ← struct tag
CreatedAt string `json:"created_at"` // ← struct tag
UpdatedAt string `json:"updated_at"` // ← struct tag
}
// 使用 - Go 自动转换
func GetCluster() ClusterResponse {
cluster := ClusterResponse{
ID: "cluster-123",
Name: "Production",
HasCAData: true, // 内部使用 PascalCase
CAData: "••••••••",
CreatedAt: "2025-11-10",
}
// json.Marshal 自动转换为 snake_case
// {"id":"cluster-123","has_ca_data":true,"ca_data":"••••••••","created_at":"2025-11-10"}
return cluster
}
```
### TypeScript + class-transformer 的实现
```typescript
// frontend/src/api/models/cluster.model.ts
import { Expose } from 'class-transformer';
// 类型定义PascalCase
export class Cluster {
@Expose()
id!: string;
@Expose()
name!: string;
@Expose()
host!: string;
// 字段名camelCase, JSONsnake_case
@Expose({ name: 'has_ca_data' }) // ← @Expose decorator (类似 struct tag)
hasCAData?: boolean;
@Expose({ name: 'has_cert_data' }) // ← @Expose decorator
hasCertData?: boolean;
@Expose({ name: 'has_key_data' }) // ← @Expose decorator
hasKeyData?: boolean;
@Expose({ name: 'ca_data' }) // ← @Expose decorator
caData?: string;
@Expose({ name: 'cert_data' }) // ← @Expose decorator
certData?: string;
@Expose({ name: 'key_data' }) // ← @Expose decorator
keyData?: string;
@Expose({ name: 'created_at' }) // ← @Expose decorator
createdAt!: string;
@Expose({ name: 'updated_at' }) // ← @Expose decorator
updatedAt!: string;
}
// 使用 - TypeScript + class-transformer 自动转换
import { fromJson, toJson } from '@/api/serializer';
function getCluster(): Cluster {
// JSON → 类实例 (snake_case → camelCase)
const apiResponse = {
id: "cluster-123",
name: "Production",
has_ca_data: true, // JSON: snake_case
ca_data: "••••••••",
created_at: "2025-11-10",
};
const cluster = fromJson(Cluster, apiResponse);
// 内部使用 camelCase
console.log(cluster.hasCAData); // true
console.log(cluster.caData); // "••••••••"
console.log(cluster.createdAt); // "2025-11-10"
return cluster;
}
```
---
## 🔍 详细对比
### 1. 结构定义
#### Go
```go
type ClusterResponse struct {
HasCAData bool `json:"has_ca_data"`
CreatedAt string `json:"created_at"`
}
```
#### TypeScript + class-transformer
```typescript
class Cluster {
@Expose({ name: 'has_ca_data' })
hasCAData?: boolean;
@Expose({ name: 'created_at' })
createdAt!: string;
}
```
**对应关系**:
- Go 的 `struct tag` ↔ TypeScript 的 `@Expose` 装饰器
- Go 的 `json:"field_name"` ↔ TypeScript 的 `{ name: 'field_name' }`
---
### 2. JSON 序列化(结构体/类 → JSON
#### Go
```go
cluster := ClusterResponse{
HasCAData: true, // PascalCase
CreatedAt: "2025-11-10",
}
jsonBytes, _ := json.Marshal(cluster)
// 自动转换为: {"has_ca_data":true,"created_at":"2025-11-10"}
```
#### TypeScript + class-transformer
```typescript
const cluster = new Cluster();
cluster.hasCAData = true; // camelCase
cluster.createdAt = "2025-11-10";
const json = toJson(cluster);
// 自动转换为: {has_ca_data: true, created_at: "2025-11-10"}
```
**对应关系**:
- Go 的 `json.Marshal()` ↔ TypeScript 的 `toJson()`
- 都实现了:内部字段名 → JSON snake_case
---
### 3. JSON 反序列化JSON → 结构体/类)
#### Go
```go
jsonStr := `{"has_ca_data":true,"created_at":"2025-11-10"}`
var cluster ClusterResponse
json.Unmarshal([]byte(jsonStr), &cluster)
// 自动映射到 PascalCase 字段
fmt.Println(cluster.HasCAData) // true
fmt.Println(cluster.CreatedAt) // "2025-11-10"
```
#### TypeScript + class-transformer
```typescript
const apiResponse = {
has_ca_data: true,
created_at: "2025-11-10"
};
const cluster = fromJson(Cluster, apiResponse);
// 自动映射到 camelCase 字段
console.log(cluster.hasCAData); // true
console.log(cluster.createdAt); // "2025-11-10"
```
**对应关系**:
- Go 的 `json.Unmarshal()` ↔ TypeScript 的 `fromJson()`
- 都实现了JSON snake_case → 内部字段名
---
## 📊 完整数据流转示例
### Scenario: 创建集群
```
┌─────────────────────────────────────────────────────────────────┐
│ 1. Frontend 组件 (TypeScript camelCase) │
├─────────────────────────────────────────────────────────────────┤
│ const request = new CreateClusterRequest(); │
│ request.name = "Production"; │
│ request.caData = "LS0t..."; // camelCase │
│ request.certData = "LS0t..."; │
└─────────────────────────────────────────────────────────────────┘
↓ toJson(request)
┌─────────────────────────────────────────────────────────────────┐
│ 2. HTTP Request Body (JSON snake_case) │
├─────────────────────────────────────────────────────────────────┤
│ { │
│ "name": "Production", │
│ "ca_data": "LS0t...", // snake_case │
│ "cert_data": "LS0t..." │
│ } │
└─────────────────────────────────────────────────────────────────┘
↓ HTTP POST
┌─────────────────────────────────────────────────────────────────┐
│ 3. Backend Go (PascalCase struct) │
├─────────────────────────────────────────────────────────────────┤
│ type CreateClusterRequest struct { │
│ Name string `json:"name"` │
│ CAData string `json:"ca_data"` // PascalCase │
│ CertData string `json:"cert_data"` │
│ } │
│ │
│ // json.Unmarshal 自动映射 │
│ var req CreateClusterRequest │
│ json.Unmarshal(body, &req) │
│ // req.CAData = "LS0t..." // 自动转换! │
└─────────────────────────────────────────────────────────────────┘
↓ 处理业务逻辑
┌─────────────────────────────────────────────────────────────────┐
│ 4. Backend Response (JSON snake_case) │
├─────────────────────────────────────────────────────────────────┤
│ { │
│ "id": "cluster-123", │
│ "name": "Production", │
│ "has_ca_data": true, // snake_case │
│ "created_at": "2025-11-10T08:00:00Z" │
│ } │
└─────────────────────────────────────────────────────────────────┘
↓ fromJson(Cluster, response)
┌─────────────────────────────────────────────────────────────────┐
│ 5. Frontend 使用 (TypeScript camelCase) │
├─────────────────────────────────────────────────────────────────┤
│ const cluster: Cluster = await createCluster(request); │
│ │
│ // 使用 camelCase │
│ console.log(cluster.hasCAData); // true │
│ console.log(cluster.createdAt); // "2025-11-10T08:00:00Z" │
│ │
│ // 在 React 组件中 │
│ {cluster.hasCAData && <Badge>Has CA</Badge>} │
└─────────────────────────────────────────────────────────────────┘
```
---
## 🎯 关键相似点
| 特性 | Go | TypeScript + class-transformer |
|------|----|---------------------------------|
| **元数据标记** | `struct tags` | `@Expose` 装饰器 |
| **内部命名** | `PascalCase` | `camelCase` |
| **JSON 命名** | `snake_case` | `snake_case` |
| **序列化** | `json.Marshal()` | `toJson()` |
| **反序列化** | `json.Unmarshal()` | `fromJson()` |
| **自动转换** | ✅ 内置支持 | ✅ 通过 class-transformer |
---
## 🔧 实现代码对比
### Go - 完整示例
```go
package dto
import "encoding/json"
type ClusterResponse struct {
ID string `json:"id"`
Name string `json:"name"`
HasCAData bool `json:"has_ca_data"`
CreatedAt string `json:"created_at"`
}
func main() {
// 创建实例
cluster := ClusterResponse{
ID: "123",
Name: "Test",
HasCAData: true,
CreatedAt: "2025-11-10",
}
// 序列化
jsonBytes, _ := json.Marshal(cluster)
// Output: {"id":"123","name":"Test","has_ca_data":true,"created_at":"2025-11-10"}
// 反序列化
var newCluster ClusterResponse
json.Unmarshal(jsonBytes, &newCluster)
// newCluster.HasCAData = true
}
```
### TypeScript - 完整示例
```typescript
import { Expose } from 'class-transformer';
import { fromJson, toJson } from '@/api/serializer';
class Cluster {
@Expose() id!: string;
@Expose() name!: string;
@Expose({ name: 'has_ca_data' }) hasCAData?: boolean;
@Expose({ name: 'created_at' }) createdAt!: string;
}
function main() {
// 创建实例
const cluster = new Cluster();
cluster.id = "123";
cluster.name = "Test";
cluster.hasCAData = true;
cluster.createdAt = "2025-11-10";
// 序列化
const json = toJson(cluster);
// Output: {id:"123",name:"Test",has_ca_data:true,created_at:"2025-11-10"}
// 反序列化
const newCluster = fromJson(Cluster, json);
// newCluster.hasCAData === true
}
```
---
## 📝 OpenAPI 驱动开发
### OpenAPI 规范 → Go
```yaml
# backend/docs/openapi.yaml
components:
schemas:
ClusterResponse: # → type ClusterResponse struct
properties:
id: # → ID string `json:"id"`
type: string
has_ca_data: # → HasCAData bool `json:"has_ca_data"`
type: boolean
created_at: # → CreatedAt string `json:"created_at"`
type: string
```
### OpenAPI 规范 → TypeScript
```yaml
# backend/docs/openapi.yaml
components:
schemas:
ClusterResponse: # → class Cluster
properties:
id: # → @Expose() id!: string
type: string
has_ca_data: # → @Expose({ name: 'has_ca_data' }) hasCAData?: boolean
type: boolean
created_at: # → @Expose({ name: 'created_at' }) createdAt!: string
type: string
```
---
## ✨ 总结
### Go 的优势
- ✅ 内置支持,无需额外库
- ✅ 编译时生成代码
- ✅ 零运行时开销
### TypeScript + class-transformer 的优势
- ✅ 与 Go 相似的开发体验
- ✅ 装饰器语法清晰
- ✅ 类型安全
- ✅ 运行时开销极小
### 共同点
- ✅ 都使用元数据标记字段映射
- ✅ 都实现了自动类型转换
- ✅ 都保持了代码的可读性和可维护性
- ✅ 都支持 OpenAPI 驱动开发
---
**结论**: TypeScript + class-transformer 成功复现了 Go 的 struct tags 机制,为前端开发提供了同样优雅的类型转换体验!
---
**创建日期**: 2025-11-10
**作者**: AI Assistant

View File

@ -0,0 +1,339 @@
# OCDP 命名约定对照表
## 快速参考
| 层级 | 变量/属性 | 类型名 | JSON 字段 |
|-----|----------|-------|----------|
| **Backend Go** | 导出: `PascalCase`<br>不导出: `camelCase` | `PascalCase` | `snake_case` |
| **OpenAPI Schema** | `snake_case` | `PascalCase` | `snake_case` |
| **Frontend Generated** | `snake_case` (引号) | `PascalCase` | `snake_case` |
| **Frontend Internal** | `camelCase` | `PascalCase` | `snake_case` |
---
## 详细说明
### 1. Backend (Go)
```go
// 文件: backend/internal/adapter/input/http/dto/cluster_dto.go
type ClusterResponse struct {
ID string `json:"id"` // 导出字段: PascalCase, JSON: snake_case
Name string `json:"name"`
HasCAData bool `json:"has_ca_data"` // Go: PascalCase → JSON: snake_case
CreatedAt string `json:"created_at"`
}
// 内部变量
func example() {
var clusterId string // 不导出: camelCase
var clusterName string
}
```
**规则**:
- ✅ 导出变量/字段: `PascalCase` (首字母大写)
- ✅ 不导出变量/字段: `camelCase` (首字母小写)
- ✅ 类型名: `PascalCase`
- ✅ JSON 标签: `snake_case`
---
### 2. OpenAPI 规范 (openapi.yaml)
```yaml
# 文件: backend/docs/openapi.yaml
components:
schemas:
ClusterResponse: # Schema 名称: PascalCase
type: object
properties:
id: # 属性: snake_case
type: string
name:
type: string
has_ca_data: # 属性: snake_case (与 Go JSON 标签一致)
type: boolean
created_at: # 属性: snake_case
type: string
```
**规则**:
- ✅ 固定字段 (operationId, paths, etc.): `camelCase`
- ✅ Schema 本身: `PascalCase`
- ✅ Schema 下面的属性: `snake_case`
---
### 3. Frontend TypeScript - 生成的 API Client
```typescript
// 文件: frontend/src/api/generated/models/cluster-response.ts
// 自动生成,不要手动修改
export interface ClusterResponse {
'id'?: string; // 属性: snake_case (加引号)
'name'?: string;
'has_ca_data'?: boolean; // 保持 snake_case与 JSON 一致
'created_at'?: string;
}
```
**规则**:
- ✅ 类型名: `PascalCase`
- ✅ 属性: `snake_case` (带引号)
- ⚠️ 不要手动修改生成的文件
---
### 4. Frontend TypeScript - 内部类型
```typescript
// 文件: frontend/src/core/types/index.ts
// 前端内部使用的类型定义
export interface Cluster {
id: string; // 内部变量: camelCase
name: string;
hasCAData?: boolean; // camelCase (前端惯例)
hasCertData?: boolean;
createdAt: string; // camelCase
updatedAt: string;
}
// API 请求类型 (保持与后端一致)
export interface CreateClusterRequest {
name: string;
host: string;
ca_data: string; // JSON 字段: snake_case
cert_data: string; // 与后端 API 保持一致
key_data: string;
}
```
**规则**:
- ✅ 内部变量: `camelCase`
- ✅ 类型名: `PascalCase`
- ✅ JSON 序列化 (API 通信): `snake_case`
---
## 数据流转示例
### 完整的请求-响应流程
```
┌─────────────────────────────────────────────────────────────┐
│ 1. 前端组件 (camelCase) │
├─────────────────────────────────────────────────────────────┤
│ const cluster = { │
│ name: "Production", │
│ hasCAData: true, // camelCase │
│ createdAt: "2025-11-10" │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. API Request Body (snake_case JSON) │
├─────────────────────────────────────────────────────────────┤
│ { │
│ "name": "Production", │
│ "ca_data": "LS0t...", // snake_case │
│ "cert_data": "LS0t..." │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. Backend Go 结构体 (PascalCase) │
├─────────────────────────────────────────────────────────────┤
│ type CreateClusterRequest struct { │
│ Name string `json:"name"` │
│ CAData string `json:"ca_data"` // Go: PascalCase │
│ CertData string `json:"cert_data"` // JSON: snake_case │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. API Response JSON (snake_case) │
├─────────────────────────────────────────────────────────────┤
│ { │
│ "id": "cluster-123", │
│ "name": "Production", │
│ "has_ca_data": true, // snake_case │
│ "created_at": "2025-11-10T08:00:00Z" │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. 前端接收 (可以保持 snake_case 或转换为 camelCase) │
├─────────────────────────────────────────────────────────────┤
│ // 选项 A: 直接使用生成的类型 (snake_case) │
│ const cluster: ClusterResponse = response; │
│ console.log(cluster.has_ca_data); │
│ │
│ // 选项 B: 转换为内部类型 (camelCase) │
│ const cluster: Cluster = { │
│ id: response.id, │
│ hasCAData: response.has_ca_data, // 转换 │
│ createdAt: response.created_at // 转换 │
│ }; │
└─────────────────────────────────────────────────────────────┘
```
---
## 命名转换对照
### 常见字段名转换
| Go (PascalCase) | JSON (snake_case) | TS Generated | TS Internal (camelCase) |
|----------------|------------------|--------------|------------------------|
| `ID` | `id` | `'id'` | `id` |
| `Name` | `name` | `'name'` | `name` |
| `ClusterID` | `cluster_id` | `'cluster_id'` | `clusterId` |
| `RegistryID` | `registry_id` | `'registry_id'` | `registryId` |
| `HasCAData` | `has_ca_data` | `'has_ca_data'` | `hasCAData` |
| `CAData` | `ca_data` | `'ca_data'` | `caData` |
| `CertData` | `cert_data` | `'cert_data'` | `certData` |
| `KeyData` | `key_data` | `'key_data'` | `keyData` |
| `CreatedAt` | `created_at` | `'created_at'` | `createdAt` |
| `UpdatedAt` | `updated_at` | `'updated_at'` | `updatedAt` |
---
## 重新生成 OpenAPI Client
### 安装依赖
```bash
# 安装 Java (如果尚未安装)
sudo apt-get install openjdk-11-jdk
# 安装 OpenAPI Generator CLI (全局)
npm install -g @openapitools/openapi-generator-cli
```
### 生成命令
```bash
# 方式 1: 使用项目根目录的 Makefile (推荐)
cd /home/mango/workspace/ocdp-go
make openapi-gen-frontend
# 方式 2: 使用前端目录的 npm 脚本
cd /home/mango/workspace/ocdp-go/frontend
npm run openapi-gen
# 方式 3: 直接运行 (如果需要自定义参数)
cd /home/mango/workspace/ocdp-go
openapi-generator-cli generate \
-i backend/docs/openapi.yaml \
-g typescript-axios \
-o frontend/src/api/generated \
--additional-properties=supportsES6=true,withSeparateModelsAndApi=true,apiPackage=api,modelPackage=models
```
### 文件权限问题解决
如果遇到权限问题 (文件属于 root):
```bash
# 修改生成文件的所有权
sudo chown -R $USER:$USER frontend/src/api/generated
# 然后重新生成
make openapi-gen-frontend
```
---
## 最佳实践
### ✅ 推荐做法
1. **使用生成的类型进行 API 通信**
```typescript
import type { ClusterResponse } from "@/api/generated";
const clusters = await apiRequest<ClusterResponse[]>("/v1/clusters");
```
2. **统一使用 apiRequest helper**
```typescript
import { apiRequest } from "@/shared/utils/api-helpers";
// 自动处理认证、错误、token 刷新
```
3. **后端修改 OpenAPI 后,重新生成前端 client**
```bash
make openapi-gen-frontend
```
### ❌ 避免的做法
1. **不要手动修改生成的代码**
```typescript
// ❌ 不要修改 /frontend/src/api/generated/ 下的文件
// 这些文件会在重新生成时被覆盖
```
2. **不要直接使用 fetch**
```typescript
// ❌ 不推荐
const response = await fetch("/api/v1/clusters");
// ✅ 推荐
const clusters = await apiRequest("/v1/clusters");
```
3. **避免混淆命名约定**
```typescript
// ❌ 不要在 API 请求中使用 camelCase
const request = {
name: "Test",
caData: "xxx", // 错误! 应该是 ca_data
};
// ✅ 正确
const request = {
name: "Test",
ca_data: "xxx", // 与后端 JSON 标签一致
};
```
---
## 快速检查清单
### Backend (Go)
- [ ] 导出字段使用 `PascalCase`
- [ ] JSON 标签使用 `snake_case`
- [ ] 更新 OpenAPI 规范与代码保持一致
### OpenAPI 规范
- [ ] Schema 名称使用 `PascalCase`
- [ ] 属性使用 `snake_case`
- [ ] 与后端 DTO 的 JSON 标签一致
### Frontend
- [ ] 从 OpenAPI 重新生成 client
- [ ] 使用生成的类型进行 API 通信
- [ ] 内部类型可以使用 `camelCase` (可选)
- [ ] 使用 `apiRequest` helper
---
## 相关文档
- [API Client 详细说明](./frontend/API_CLIENT_CONVENTIONS.md)
- [OpenAPI 规范](./backend/docs/openapi.yaml)
- [前端 API Helper](./frontend/src/shared/utils/api-helpers.ts)
---
**更新日期**: 2025-11-10

View File

@ -0,0 +1,348 @@
# 📋 OCDP 开发规范
本文档定义了 OCDP 项目的开发规范和架构要求。
## 🎯 整体架构 (Full Stack)
### 1. OpenAPI 驱动开发
采用 OpenAPI 规范来驱动前后端开发,确保 API 契约的一致性。
**优势**
- API 设计优先,前后端并行开发
- 自动生成类型安全的代码
- 文档和代码永远同步
- 减少沟通成本
**实践**
```bash
# 1. 设计 API (编辑 backend/docs/openapi.yaml)
# 2. 验证规范
make openapi-validate
# 3. 生成代码
make openapi-gen
# 4. 实现功能
```
### 2. Docker Compose 部署
使用 Docker Compose 进行整个应用的部署。新版的 Docker 已经将 Compose 集成到 Docker 里面了,所以使用 `docker compose`(带空格)而非旧版的 `docker-compose`(带连字符)。
**部署服务**
- PostgreSQL - 数据持久化
- Redis - 缓存和会话
- Backend - Go 后端服务
- Frontend - React 前端应用
- Nginx - 反向代理(生产环境)
## 🎨 前端规范 (Frontend)
### 1. 纯函数渲染
**要求**:使用纯函数进行组件渲染,避免不必要的副作用。
**原则**
- 组件应该是可预测的(相同输入→相同输出)
- 避免在渲染过程中修改外部状态
- 使用 `useEffect` 等 Hook 处理副作用
- 保持组件的可测试性
**示例**
```typescript
// ✅ 好的实践 - 纯函数组件
interface Props {
name: string;
count: number;
}
const UserCard = ({ name, count }: Props) => {
// 纯函数:只依赖 props不修改外部状态
return (
<div>
<h3>{name}</h3>
<p>Count: {count}</p>
</div>
);
};
// ✅ 副作用在 useEffect 中处理
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
// 副作用API 调用)在这里处理
fetchUsers().then(setUsers);
}, []);
return users.map(user => <UserCard {...user} />);
};
// ❌ 不好的实践 - 在渲染中产生副作用
const BadComponent = () => {
// 不要在这里调用 API 或修改外部状态
globalState.count++; // ❌ 副作用
fetchData(); // ❌ 副作用
return <div>Bad</div>;
};
```
### 2. 技术栈
- **框架**: React 18+ (使用 Hooks)
- **语言**: TypeScript 5+
- **构建工具**: Vite
- **样式**: Tailwind CSS
- **路由**: React Router 6+
- **状态管理**: React Context + Hooks
- **API 客户端**: 从 OpenAPI 自动生成
## 🔧 后端规范 (Backend)
### 1. 六边形架构 (Hexagonal Architecture)
后端采用六边形架构(也称为端口和适配器架构),将业务逻辑与技术实现解耦。
**核心目录结构**
```
backend/internal/
├── domain/ # 领域层 - 业务逻辑核心
│ ├── entity/ # 领域实体
│ ├── service/ # 领域服务
│ └── repository/ # 仓库接口(端口)
├── application/ # 应用层 - 用例编排
│ └── usecase/ # 用例实现
└── adapter/ # 适配器层 - 技术实现
├── input/ # 输入适配器
│ └── http/ # HTTP REST API
└── output/ # 输出适配器
├── persistence/
│ ├── mock/ # Mock 实现
│ └── postgres/ # PostgreSQL 实现
├── oci/ # OCI Registry 客户端
└── helm/ # Helm SDK 封装
```
**职责划分**
- **Domain 层**:纯业务逻辑,不依赖任何框架或外部库
- **Application 层**:编排 Domain 层的服务,实现具体的用例
- **Adapter 层**处理所有技术细节HTTP、数据库、第三方 API
### 2. Mock Adapter 实现
**要求**:除了实现 ports 的 adapters 外,还要做 mock。Mock 的是 adapter 的行为反应而非假数据。
**Mock 原则**
- ✅ 模拟真实 adapter 的行为
- ✅ 可以注入真实数据
- ✅ 可以通过调用接口自行加入数据
- ✅ 使用内存来模拟 adapter 的交互
- ❌ 不是返回固定的假数据
**示例**
```go
// Mock Repository - 模拟真实的数据库行为
type RegistryRepositoryMock struct {
registries map[string]*entity.Registry // 内存存储
mu sync.RWMutex
}
func (r *RegistryRepositoryMock) Create(ctx context.Context, registry *entity.Registry) error {
r.mu.Lock()
defer r.mu.Unlock()
// 模拟真实行为:检查重复、生成 ID、加密等
if _, exists := r.registries[registry.ID]; exists {
return errors.New("registry already exists")
}
r.registries[registry.ID] = registry
return nil
}
func (r *RegistryRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Registry, error) {
r.mu.RLock()
defer r.mu.RUnlock()
registry, exists := r.registries[id]
if !exists {
return nil, errors.New("registry not found")
}
return registry, nil
}
```
### 3. Makefile 支持
**要求**:采用 Makefile 来支持 mock 启动以及 real 启动。
**命令规范**
```makefile
# 开发模式Mock Adapter
run-mock:
@echo "Starting backend with Mock adapters..."
MODE=mock go run cmd/api/main.go
# 生产模式Real Adapter
run-real:
@echo "Starting backend with Real adapters..."
MODE=real go run cmd/api/main.go
# 运行测试
test:
go test -v ./...
# 生成代码
generate:
go generate ./...
```
**使用方式**
```bash
# 开发模式(无需数据库)
make run-mock
# 生产模式(需要 PostgreSQL
make run-real
```
### 4. 技术栈
- **语言**: Go 1.21+
- **Web 框架**: Gin (轻量、高性能)
- **ORM**: GORM (可选,用于 PostgreSQL adapter)
- **OCI 客户端**: ORAS Go SDK v2
- **Helm 客户端**: Helm SDK v3
- **K8s 客户端**: client-go
## 📐 架构原则
### 1. 依赖方向
```
Adapter → Application → Domain
(技术) (编排) (业务)
```
- Domain 层不依赖任何外部库(除了标准库)
- Application 层依赖 Domain 层
- Adapter 层依赖 Application 和 Domain 层
### 2. 端口和适配器
**端口Port**:接口定义,在 Domain 层
```go
// domain/repository/registry_repository.go
type RegistryRepository interface {
Create(ctx context.Context, registry *entity.Registry) error
GetByID(ctx context.Context, id string) (*entity.Registry, error)
List(ctx context.Context) ([]*entity.Registry, error)
}
```
**适配器Adapter**:接口实现,在 Adapter 层
```go
// adapter/output/persistence/mock/registry_repository_mock.go
type RegistryRepositoryMock struct {
// Mock 实现
}
// adapter/output/persistence/postgres/registry_repository_postgres.go
type RegistryRepositoryPostgres struct {
// PostgreSQL 实现
}
```
### 3. 依赖注入
使用构造函数注入依赖:
```go
// 创建 Mock 模式的应用
func NewMockApp() *App {
// 创建 Mock Repository
registryRepo := mock.NewRegistryRepositoryMock()
// 创建 Service注入 Repository
registryService := service.NewRegistryService(registryRepo)
// 创建 Handler注入 Service
registryHandler := handler.NewRegistryHandler(registryService)
return &App{
RegistryHandler: registryHandler,
}
}
// 创建 Production 模式的应用
func NewProductionApp(db *gorm.DB) *App {
// 创建 PostgreSQL Repository
registryRepo := postgres.NewRegistryRepositoryPostgres(db)
// ... 其他相同
}
```
## 🔄 开发工作流
### 1. 功能开发流程
```bash
# 1. 设计 API
vim backend/docs/openapi.yaml
# 2. 生成代码
make openapi-gen
# 3. 实现 Domain 层
vim backend/internal/domain/service/xxx_service.go
# 4. 实现 Mock Adapter
vim backend/internal/adapter/output/persistence/mock/xxx_mock.go
# 5. 实现 Handler
vim backend/internal/adapter/input/http/handler/xxx_handler.go
# 6. 启动测试
make run-mock
# 7. 实现前端
vim frontend/src/features/xxx/pages/XxxPage.tsx
# 8. 集成测试
make dev
# 9. 实现 Production Adapter
vim backend/internal/adapter/output/persistence/postgres/xxx_postgres.go
# 10. 部署测试
docker compose up
```
### 2. 测试策略
- **单元测试**Domain 层和 Service 层(使用 Mock Repository
- **集成测试**:使用 Mock Adapter 测试完整流程
- **E2E 测试**:使用真实 Adapter 测试生产环境
## 📚 参考文档
- [后端六边形架构详解](../../backend/HEXAGONAL_ARCHITECTURE.md)
- [OpenAPI 规范](../../backend/docs/openapi.yaml)
- [Docker 部署指南](../deployment/docker-guide.md)
- [安全实现方案](../security/security-implementation.md)
---
**版本**: 1.0
**最后更新**: 2025-11-07