From 0794474c04ec9b2ee79b3588f65e79a325fa76ce Mon Sep 17 00:00:00 2001 From: yangzhaohan Date: Tue, 14 Jul 2026 23:36:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E6=96=B0=E5=A2=9E=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B=E4=B8=8E=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BaseModel 结构体,包含统一主键和时间字段 - 定义 JanusProvider、JanusModel、Channel、APIKey 四个业务模型结构 - 引入 Status 枚举表示启用/禁用状态 - 完成对应数据库建表 SQL 迁移脚本 - 新增 router 包及 API 健康检查路由注册实现 - 优化 main.go,使用 router 包统一路由注册管理 - 补充模型中字段的 GORM 和 JSON 标签规范 --- .claude/plans/sequential-juggling-lecun.md | 249 +++++++++++++++++++++ cmd/server/main.go | 9 +- internal/core/base_model.go | 18 ++ internal/enum/enabled_status.go | 8 + internal/model/api_key.go | 23 ++ internal/model/channel.go | 25 +++ internal/model/model.go | 29 +++ internal/model/provider.go | 17 ++ internal/router/router.go | 10 + sql/v0.0.1.sql | 113 ++++++++++ 10 files changed, 496 insertions(+), 5 deletions(-) create mode 100644 .claude/plans/sequential-juggling-lecun.md create mode 100644 internal/core/base_model.go create mode 100644 internal/enum/enabled_status.go create mode 100644 internal/model/api_key.go create mode 100644 internal/model/channel.go create mode 100644 internal/model/model.go create mode 100644 internal/model/provider.go create mode 100644 internal/router/router.go create mode 100644 sql/v0.0.1.sql diff --git a/.claude/plans/sequential-juggling-lecun.md b/.claude/plans/sequential-juggling-lecun.md new file mode 100644 index 0000000..4863ad2 --- /dev/null +++ b/.claude/plans/sequential-juggling-lecun.md @@ -0,0 +1,249 @@ +# Janus - 完整开发计划 + +> AI API 网关(中转站),Go 语言学习项目。 + +--- + +## 总览(8 阶段) + +| Phase | 主题 | 核心 Go 知识点 | +|-------|------|---------------| +| 1 | 项目骨架 & 基础设施 | go mod、package、Gin、Viper、指针 vs 值 | +| **2** | **数据模型 & Repository** | **struct、GORM tags、interface 隐式满足** | +| 3 | 管理 API CRUD | gin.Context、JSON、binding 验证、统一错误响应 | +| 4 | 代理引擎 | http.Client、io.Reader/Writer、SSE streaming | +| 5 | 负载均衡 | sync.Mutex、RWMutex、加权轮询算法 | +| 6 | 健康检查 & 故障转移 | goroutine、channel、context、熔断器 | +| 7 | 速率限制 & 可观测性 | Gin 中间件链、slog、Token bucket | +| 8 | Dashboard & 容器化 | Vue 3、Docker 多阶段构建、graceful shutdown | + +--- + +# Phase 2:数据模型 & Repository 层 + +## 学习目标 + +与 Java 对照理解: + +| Go 概念 | Java 对照 | 关键差异 | +|---------|-----------|---------| +| struct + GORM tag | `@Entity` + JPA annotation | 无继承,字段是直接嵌入而非注解驱动 | +| interface 隐式满足 | `implements` 关键字 | 不需要显式声明,只要方法签名匹配就自动满足 | +| repository interface + impl | DAO 接口 + 实现类 | 模式相似,但 Go 用隐式满足,无 `@Autowired` | +| `*gorm.DB` | `EntityManager` / `JpaRepository` | GORM 更接近 MyBatis 风格的链式 API | + +## 表结构设计 + +### providers(供应商) + +AI 服务供应商(OpenAI、Anthropic、Azure 等)。 + +```sql +CREATE TABLE providers ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, -- openai / anthropic / azure + base_url VARCHAR(255) NOT NULL, -- API 基础地址 + status SMALLINT NOT NULL DEFAULT 1, -- 1=启用 0=停用 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON COLUMN providers.status IS '1: enabled, 0: disabled'; +``` + +### models(模型) + +每个供应商下可用的 AI 模型。 + +```sql +CREATE TABLE models ( + id BIGSERIAL PRIMARY KEY, + provider_id BIGINT NOT NULL REFERENCES providers(id), + name VARCHAR(100) NOT NULL, -- gpt-4 / claude-3-opus / text-embedding-3 + model_type VARCHAR(50) NOT NULL, -- chat / completion / embedding / image + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(provider_id, name) +); + +COMMENT ON COLUMN models.model_type IS 'chat|completion|embedding|image'; +``` + +### channels(通道) + +路由转发通道,连接 provider + model,支持权重和优先级(Phase 5 负载均衡使用)。 + +```sql +CREATE TABLE channels ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, -- 通道名称(如 "主通道-OpenAI") + provider_id BIGINT NOT NULL REFERENCES providers(id), + model_id BIGINT REFERENCES models(id), -- NULL 表示供应商级别通道(透传) + weight INT NOT NULL DEFAULT 1, -- 负载均衡权重 + priority INT NOT NULL DEFAULT 1, -- 优先级(数字越小越高) + rate_limit INT NOT NULL DEFAULT 60, -- 每分钟请求数上限 + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +### api_keys(API 密钥) + +调用方认证密钥。 + +```sql +CREATE TABLE api_keys ( + id BIGSERIAL PRIMARY KEY, + key VARCHAR(64) NOT NULL UNIQUE, -- 实际密钥字符串 + name VARCHAR(100) NOT NULL, -- 密钥名称/用途描述 + rate_limit INT NOT NULL DEFAULT 60, -- 每分钟请求数上限 + status SMALLINT NOT NULL DEFAULT 1, + expired_at TIMESTAMPTZ, -- NULL = 永不过期 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +--- + +## Go 代码组织 + +### 新增目录结构 + +``` +internal/ +├── model/ # ← 新增:数据模型 +│ ├── provider.go +│ ├── model.go (模型表 struct) +│ ├── channel.go +│ └── api_key.go +├── repository/ # ← 新增:数据访问层 +│ ├── interfaces.go (所有 repository interface 定义) +│ ├── provider_repo.go (GORM 实现) +│ ├── model_repo.go +│ ├── channel_repo.go +│ └── api_key_repo.go +└── database/ + └── database.go # 已有,需增加自动迁移 +``` + +### model 包设计要点 + +每个文件一个 struct,包含: +- GORM tags(`gorm:"column:xxx;type:xxx"`) +- JSON tags(`json:"xxx"`) +- GORM 约定:`ID` 为主键,`CreatedAt`/`UpdatedAt` 自动管理 +- 嵌入 `gorm.Model` 还是手写 → 建议**手写**(学习目的,显式看到字段) + +示例思路(非完整代码): + +```go +type Provider struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"uniqueIndex;size:100;not null" json:"name"` + BaseURL string `gorm:"size:255;not null" json:"base_url"` + Status int16 `gorm:"default:1;not null" json:"status"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} +``` + +### repository 包设计要点 + +**interface 定义**(`interfaces.go`)— 类比 Java 的 DAO 接口: + +```go +type ProviderRepository interface { + Create(ctx context.Context, provider *model.Provider) error + GetByID(ctx context.Context, id int64) (*model.Provider, error) + GetAll(ctx context.Context) ([]*model.Provider, error) + Update(ctx context.Context, provider *model.Provider) error + Delete(ctx context.Context, id int64) error +} +``` + +**GORM 实现**(`provider_repo.go`)— 类比 Java 的 DAO Impl: + +```go +type providerRepository struct { + db *gorm.DB +} + +func NewProviderRepository(db *gorm.DB) ProviderRepository { + return &providerRepository{db: db} +} +``` + +**关键 Go 知识点(与 Java 对比):** +- `providerRepository` 没有 `implements ProviderRepository` 关键字 — 只要方法签名匹配就自动满足 interface,这叫**隐式满足** +- 构造函数 `NewProviderRepository` 返回 `ProviderRepository`(interface 类型),而不是具体类型 — 调用方只依赖 interface,不依赖具体实现 +- 接口定义在哪里?**在 repository 包(使用方附近)**,而不是像 Java 那样单独抽一个 api 模块 + +--- + +## GORM 关联关系 + +- **Model → Provider**:多对一(`BelongsTo`) +- **Channel → Provider**:多对一 +- **Channel → Model**:多对一(可空) + +Phase 2 先用手写外键 + 手动关联查询来理解,后续可以引入 GORM 的 `Preload` / `Association`。 + +--- + +## 数据库迁移 + +在 `internal/database/database.go` 的 `Connect` 函数后增加自动迁移: + +```go +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &model.Provider{}, + &model.Model{}, // 注意命名冲突,Go 里可以用 ModelName + &model.Channel{}, + &model.APIKey{}, + ) +} +``` + +同时创建 `migrations/` 目录下的 SQL 文件作为正式迁移脚本(与自动迁移并存,生产环境用 SQL)。 + +--- + +## 实施步骤 + +按顺序完成,每一步都可以编译验证: + +### Step 1:创建 SQL 迁移脚本 +- 在 `migrations/` 下创建 `001_init.sql`,包含 4 张建表语句 + +### Step 2:创建 model struct +- `internal/model/provider.go` +- `internal/model/model.go`(注意 Go 标准库 `builtin` 没有 model,但包名 model + 类型名 Model 会造成 `model.Model` 有点别扭,可以考虑命名为 `AiModel` 或在调用时用别名,由你决定) +- `internal/model/channel.go` +- `internal/model/api_key.go` + +### Step 3:创建 repository interface +- `internal/repository/interfaces.go` — 4 个 interface 定义 + +### Step 4:创建 repository 实现 +- 每个 repository 一个文件,GORM CRUD 实现 + +### Step 5:集成自动迁移 +- 修改 `database/database.go` 增加 `AutoMigrate` +- 修改 `cmd/server/main.go` 在连接后调用迁移 + +--- + +## 设计决策说明 + +| 决策 | 选择 | 理由 | +|------|------|------| +| 主键类型 | `int64` (BIGSERIAL) | 适合中小规模,比 UUID 性能好,比自增 int 容量大 | +| 时间字段 | `time.Time` + `TIMESTAMPTZ` | GORM 自动管理,带时区避免歧义 | +| 状态字段 | `int16` (SMALLINT) | 扩展性强,可后续加更多状态值 | +| 软删除 | 暂不使用 | GORM 默认支持 `gorm.DeletedAt`,但 Phase 2 先学基础,不引入 | +| model_type | varchar | 比 enum 灵活,Go 端用 const 常量约束 | +| 外键约束 | 数据库级 REFERENCES | GORM 不强制外键,但数据库层保证完整性 | diff --git a/cmd/server/main.go b/cmd/server/main.go index 6dce323..49eb777 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -7,6 +7,7 @@ import ( "synoth.com/janus/internal/config" "synoth.com/janus/internal/database" "synoth.com/janus/internal/handler" + "synoth.com/janus/internal/router" ) func main() { @@ -27,10 +28,8 @@ func main() { panic(err) } - router := gin.Default() - + r := gin.Default() healthHandler := &handler.HealthHandler{Database: db} - router.GET("/api/health", healthHandler.Check) - - router.Run(fmt.Sprintf(":%d", cfg.Server.Port)) + router.SetUp(r, healthHandler) + r.Run(fmt.Sprintf(":%d", cfg.Server.Port)) } diff --git a/internal/core/base_model.go b/internal/core/base_model.go new file mode 100644 index 0000000..bbddd53 --- /dev/null +++ b/internal/core/base_model.go @@ -0,0 +1,18 @@ +package core + +import "time" + +type BaseModel struct { + // ID 数据的ID,雪花ID + ID int64 `gorm:"primaryKey;" json:"id"` + // CreatedAt 创建者ID + CreatedAt int64 `gorm:"not null" json:"created_at"` + // CreatedTime 创建时间 + CreatedTime time.Time `gorm:"autoCreateTime;not null" json:"created_time"` + // UpdatedAt 修改者ID + UpdatedAt int64 `gorm:"not null" json:"updated_at"` + // UpdatedTime 修改时间 + UpdatedTime time.Time `gorm:"autoUpdateTime;not null" json:"updated_time"` + // DelFlag 删除标志(1=已删除,0=未删除) + DelFlag int8 `gorm:"default:0;not null" json:"del_flag"` +} diff --git a/internal/enum/enabled_status.go b/internal/enum/enabled_status.go new file mode 100644 index 0000000..13ad2e5 --- /dev/null +++ b/internal/enum/enabled_status.go @@ -0,0 +1,8 @@ +package enum + +type Status int8 + +const ( + Disabled Status = iota + Enabled +) diff --git a/internal/model/api_key.go b/internal/model/api_key.go new file mode 100644 index 0000000..2148121 --- /dev/null +++ b/internal/model/api_key.go @@ -0,0 +1,23 @@ +package model + +import ( + "time" + + "synoth.com/janus/internal/core" + "synoth.com/janus/internal/enum" +) + +// APIKey 调用方认证密钥 +type APIKey struct { + core.BaseModel + // Key API密钥字符串 + Key string `gorm:"uniqueIndex;size:64;not null" json:"key"` + // Name 密钥名称/用途描述 + Name string `gorm:"size:100;not null" json:"name"` + // RateLimit 每分钟请求数上限 + RateLimit int `gorm:"default:60;not null" json:"rate_limit"` + // Status 密钥状态 + Status enum.Status `gorm:"default:1;not null" json:"status"` + // ExpiredAt 过期时间(NULL=永不过期) + ExpiredAt *time.Time `json:"expired_at"` +} diff --git a/internal/model/channel.go b/internal/model/channel.go new file mode 100644 index 0000000..4adaa35 --- /dev/null +++ b/internal/model/channel.go @@ -0,0 +1,25 @@ +package model + +import ( + "synoth.com/janus/internal/core" + "synoth.com/janus/internal/enum" +) + +// Channel 路由转发通道 +type Channel struct { + core.BaseModel + // Name 通道名称 + Name string `gorm:"size:100;not null" json:"name"` + // ProviderID 供应商ID + ProviderID int64 `gorm:"not null" json:"provider_id"` + // ModelID 模型ID(NULL表示供应商级别通道) + ModelID *int64 `json:"model_id"` + // Weight 负载均衡权重 + Weight int `gorm:"default:1;not null" json:"weight"` + // Priority 优先级(数字越小越高) + Priority int `gorm:"default:1;not null" json:"priority"` + // RateLimit 每分钟请求数上限 + RateLimit int `gorm:"default:60;not null" json:"rate_limit"` + // Status 通道状态 + Status enum.Status `gorm:"default:1;not null" json:"status"` +} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..9d7dec0 --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,29 @@ +package model + +import ( + "synoth.com/janus/internal/core" + "synoth.com/janus/internal/enum" +) + +// Type 模型类型 +type Type string + +// JanusModel 供应商下可用的AI模型 +type JanusModel struct { + core.BaseModel + // ProviderID 供应商ID + ProviderID int64 `gorm:"not null" json:"provider_id"` + // Name 模型名称 + Name string `gorm:"not null" json:"name"` + // 模型类型 + ModelType Type `gorm:"not null" json:"model_type"` + // 模型状态 + Status enum.Status `gorm:"default:1;not null" json:"status"` +} + +const ( + Chat Type = "chat" + Completion Type = "completion" + Embedding Type = "embedding" + Image Type = "image" +) diff --git a/internal/model/provider.go b/internal/model/provider.go new file mode 100644 index 0000000..59c2af0 --- /dev/null +++ b/internal/model/provider.go @@ -0,0 +1,17 @@ +package model + +import ( + "synoth.com/janus/internal/core" + "synoth.com/janus/internal/enum" +) + +// JanusProvider 服务提供商 +type JanusProvider struct { + core.BaseModel + // Name 服务供应商名称 + Name string `gorm:"uniqueIndex;size:100;not null" json:"name"` + // BaseURL 服务地址 + BaseURL string `gorm:"size:255;not null" json:"base_url"` + // Status 服务状态(1=启用,0=停用) + Status enum.Status `gorm:"default:1;not null" json:"status"` +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..83b4010 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,10 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "synoth.com/janus/internal/handler" +) + +func SetUp(router *gin.Engine, h *handler.HealthHandler) { + router.GET("/api/health", h.Check) +} diff --git a/sql/v0.0.1.sql b/sql/v0.0.1.sql new file mode 100644 index 0000000..c98a886 --- /dev/null +++ b/sql/v0.0.1.sql @@ -0,0 +1,113 @@ +CREATE TABLE public.janus_providers +( + id bigint NOT NULL, + name varchar(100) NULL, + base_url varchar(255) NULL, + status int4 DEFAULT 0 NULL, + created_at bigint NOT NULL, + created_time timestamp NOT NULL, + updated_at bigint NULL, + updated_time timestamp NULL, + del_flag int4 DEFAULT 0 NOT NULL, + CONSTRAINT janus_providers_pk PRIMARY KEY (id) +); +COMMENT ON TABLE public.janus_providers IS 'AI 服务供应商(OpenAI、Anthropic、Azure 等)'; + +COMMENT ON COLUMN public.janus_providers.id IS '主键ID'; +COMMENT ON COLUMN public.janus_providers.name IS '服务供应商名称'; +COMMENT ON COLUMN public.janus_providers.base_url IS '服务地址'; +COMMENT ON COLUMN public.janus_providers.status IS '服务状态(1=启用,0=停用)'; +COMMENT ON COLUMN public.janus_providers.created_at IS '创建者'; +COMMENT ON COLUMN public.janus_providers.created_time IS '创建时间'; +COMMENT ON COLUMN public.janus_providers.updated_at IS '修改者'; +COMMENT ON COLUMN public.janus_providers.updated_time IS '修改时间'; +COMMENT ON COLUMN public.janus_providers.del_flag IS '删除标志(1=已删除,0=未删除)'; + +CREATE TABLE public.janus_models +( + id bigint NOT NULL, + provider_id bigint NOT NULL, + name varchar not null, + model_type varchar not null, + status int4 default 0 not null, + created_at bigint NOT NULL, + created_time timestamp NOT NULL, + updated_at bigint NULL, + updated_time timestamp NULL, + del_flag int4 DEFAULT 0 NOT NULL, + CONSTRAINT janus_models_pk PRIMARY KEY (id) +); +COMMENT ON TABLE public.janus_models IS '供应商下可用的AI模型'; + +COMMENT ON COLUMN public.janus_models.id IS '主键ID'; +COMMENT ON COLUMN public.janus_models.provider_id IS '供应商ID'; +COMMENT ON COLUMN public.janus_models.name IS '模型名称'; +COMMENT ON COLUMN public.janus_models.model_type IS '模型类型(chat/completion/embedding/image)'; +COMMENT ON COLUMN public.janus_models.status IS '模型状态(1=启用,0=停用)'; +COMMENT ON COLUMN public.janus_models.created_at IS '创建者'; +COMMENT ON COLUMN public.janus_models.created_time IS '创建时间'; +COMMENT ON COLUMN public.janus_models.updated_at IS '修改者'; +COMMENT ON COLUMN public.janus_models.updated_time IS '修改时间'; +COMMENT ON COLUMN public.janus_models.del_flag IS '删除标志(1=已删除,0=未删除)'; + +CREATE TABLE public.janus_channels +( + id bigint NOT NULL, + name varchar NOT NULL, + provider_id bigint NOT NULL, + model_id bigint NULL, + weight int4 DEFAULT 1 NOT NULL, + priority int4 DEFAULT 1 NOT NULL, + rate_limit int4 DEFAULT 60 NOT NULL, + status int4 DEFAULT 0 NULL, + created_at bigint NOT NULL, + created_time timestamp NOT NULL, + updated_at bigint NULL, + updated_time timestamp NULL, + del_flag int4 DEFAULT 0 NOT NULL, + CONSTRAINT janus_channels_pk PRIMARY KEY (id) +); +COMMENT ON TABLE public.janus_channels IS '路由转发通道(连接供应商和模型,支持权重和优先级)'; + +COMMENT ON COLUMN public.janus_channels.id IS '主键ID'; +COMMENT ON COLUMN public.janus_channels.name IS '通道名称'; +COMMENT ON COLUMN public.janus_channels.provider_id IS '供应商ID'; +COMMENT ON COLUMN public.janus_channels.model_id IS '模型ID(NULL表示供应商级别通道)'; +COMMENT ON COLUMN public.janus_channels.weight IS '负载均衡权重'; +COMMENT ON COLUMN public.janus_channels.priority IS '优先级(数字越小越高)'; +COMMENT ON COLUMN public.janus_channels.rate_limit IS '每分钟请求数上限'; +COMMENT ON COLUMN public.janus_channels.status IS '通道状态(1=启用,0=停用)'; +COMMENT ON COLUMN public.janus_channels.created_at IS '创建者'; +COMMENT ON COLUMN public.janus_channels.created_time IS '创建时间'; +COMMENT ON COLUMN public.janus_channels.updated_at IS '修改者'; +COMMENT ON COLUMN public.janus_channels.updated_time IS '修改时间'; +COMMENT ON COLUMN public.janus_channels.del_flag IS '删除标志(1=已删除,0=未删除)'; + +CREATE TABLE public.janus_api_keys +( + id bigint NOT NULL, + key varchar NOT NULL, + name varchar NOT NULL, + rate_limit int4 DEFAULT 60 NOT NULL, + status int4 DEFAULT 0 NULL, + expired_at timestamp NULL, + created_at bigint NOT NULL, + created_time timestamp NOT NULL, + updated_at bigint NULL, + updated_time timestamp NULL, + del_flag int4 DEFAULT 0 NOT NULL, + CONSTRAINT janus_api_keys_pk PRIMARY KEY (id) +); +COMMENT ON TABLE public.janus_api_keys IS '调用方认证密钥'; + +COMMENT ON COLUMN public.janus_api_keys.id IS '主键ID'; +COMMENT ON COLUMN public.janus_api_keys.key IS 'API密钥字符串'; +COMMENT ON COLUMN public.janus_api_keys.name IS '密钥名称/用途描述'; +COMMENT ON COLUMN public.janus_api_keys.rate_limit IS '每分钟请求数上限'; +COMMENT ON COLUMN public.janus_api_keys.status IS '密钥状态(1=启用,0=停用)'; +COMMENT ON COLUMN public.janus_api_keys.expired_at IS '过期时间(NULL=永不过期)'; +COMMENT ON COLUMN public.janus_api_keys.created_at IS '创建者'; +COMMENT ON COLUMN public.janus_api_keys.created_time IS '创建时间'; +COMMENT ON COLUMN public.janus_api_keys.updated_at IS '修改者'; +COMMENT ON COLUMN public.janus_api_keys.updated_time IS '修改时间'; +COMMENT ON COLUMN public.janus_api_keys.del_flag IS '删除标志(1=已删除,0=未删除)' \ No newline at end of file