ocdp v1
This commit is contained in:
458
docs/development/go-vs-typescript.md
Normal file
458
docs/development/go-vs-typescript.md
Normal 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, JSON:snake_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, JSON:snake_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
|
||||
|
||||
|
||||
339
docs/development/naming-conventions.md
Normal file
339
docs/development/naming-conventions.md
Normal 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
|
||||
|
||||
348
docs/development/specification.md
Normal file
348
docs/development/specification.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user