common

package
v0.0.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 26, 2026 License: BSD-2-Clause Imports: 6 Imported by: 0

README

Common - 公共基础接口

定义 5 层依赖注入架构的基础接口,规范各层的行为契约和生命周期管理,并提供类型转换工具函数。交互层包含 4 种组件类型。

概述

common 包是框架的核心基础模块,定义了标准化的分层架构接口,所有业务组件都必须实现对应的基础接口。通过接口约束,确保架构的一致性和可维护性。

架构层次

┌─────────────────────────────────────────────────────────────┐
│                    交互层 (Interaction Layer)              │
│  Controller/Middleware/Listener/Scheduler               │
│  (HTTP 请求、MQ 消息、定时任务的统一处理层)                 │
├─────────────────────────────────────────────────────────────┤
│                       Service                              │
│                    (业务逻辑层)                              │
├─────────────────────────────────────────────────────────────┤
│                     Repository                              │
│                    (数据访问层)                              │
├─────────────────────────────────────────────────────────────┤
│                       Entity                                │
│                    (数据实体层)                              │
└─────────────────────────────────────────────────────────────┘
                        ↑
             ┌──────────┴──────────┐
             │  内置管理器层 (Manager Layer) │
             │ (基础设施管理器)     │
             └─────────────────────┘
     configmgr、databasemgr、loggermgr、cachemgr、
      lockmgr、limitermgr、mqmgr、telemetrymgr、schedulermgr

特性

  • 标准接口定义 - 定义 Entity、Manager、Repository、Service、Controller、Middleware、Listener、Scheduler 的标准接口
  • 实体基类 - 提供 3 种预定义的实体基类,支持 CUID2 ID 自动生成和时间戳自动填充
  • 生命周期管理 - 提供统一的 OnStart 和 OnStop 钩子方法
  • 命名规范 - 每层接口要求实现对应的名称方法,便于调试和日志
  • 依赖注入支持 - 为容器提供标准接口类型,支持类型安全的依赖注入
  • 类型转换工具 - 提供安全的类型转换函数,避免 panic 并支持默认值
  • HTTP 状态码常量 - 定义完整的 HTTP 状态码常量,便于统一使用
  • 5层架构规范 - 明确各层的职责边界和依赖关系,确保架构清晰

快速开始

使用实体基类(推荐)

框架提供了 3 种预定义的实体基类,支持 CUID2 ID 自动生成和时间戳自动填充:

import "github.com/lite-lake/litecore-go/common"

// 方式 1:最常用的基类(ID + 创建时间 + 更新时间)
type Message struct {
    common.BaseEntityWithTimestamps  // 自动生成 ID、CreatedAt、UpdatedAt
    Nickname  string `gorm:"type:varchar(20);not null"`
    Content   string `gorm:"type:varchar(500);not null"`
    Status    string `gorm:"type:varchar(20);default:'pending'"`
}

func (m *Message) EntityName() string {
    return "Message"
}

func (m *Message) TableName() string {
    return "messages"
}

func (m *Message) GetId() string {
    return m.ID  // ID 由基类提供,类型为 string
}

var _ common.IBaseEntity = (*Message)(nil)
// 方式 2:仅需要 ID 和创建时间(如日志实体)
type AuditLog struct {
    common.BaseEntityWithCreatedAt  // 自动生成 ID、CreatedAt
    Action  string
    Details string
}

var _ common.IBaseEntity = (*AuditLog)(nil)
// 方式 3:只需要 ID(如配置表)
type SystemConfig struct {
    common.BaseEntityOnlyID  // 自动生成 ID
    Key   string
    Value string
}

var _ common.IBaseEntity = (*SystemConfig)(nil)

基类特性

  • CUID2 ID:25 位字符串,时间有序、高唯一性、分布式安全
  • 数据库存储:varchar(32),预留更多兼容空间
  • 自动填充:通过 GORM Hook 自动设置 ID 和时间戳
  • 类型安全:ID 类型为 string,避免类型转换

注意事项

  • GORM 不会自动调用嵌入结构体的 Hook,必须手动调用父类 Hook
  • Repository 中查询 ID 时使用 Where("id = ?", id) 而不是 First(entity, id)
自定义实体(不使用基类)

如果需要自定义 ID 生成逻辑或不使用时间戳,可以手动定义实体:

import "github.com/lite-lake/litecore-go/common"

// 定义实体,实现 IBaseEntity 接口
type User struct {
    ID   string `gorm:"primaryKey"`
    Name string
}

func (u *User) EntityName() string {
    return "User"
}

func (u *User) TableName() string {
    return "users"
}

func (u *User) GetId() string {
    return u.ID
}

// 定义服务,实现 IBaseService 接口
type UserService struct {
    Config    configmgr.IConfigManager    `inject:""`
    LoggerMgr loggermgr.ILoggerManager   `inject:""`
}

func (s *UserService) ServiceName() string {
    return "UserService"
}

func (s *UserService) OnStart() error {
    return nil
}

func (s *UserService) OnStop() error {
    return nil
}

// 定义控制器,实现 IBaseController 接口
type UserController struct {
    Service   UserService                `inject:""`
    LoggerMgr loggermgr.ILoggerManager   `inject:""`
}

func (c *UserController) ControllerName() string {
    return "UserController"
}

func (c *UserController) GetRouter() string {
    return "/api/users [GET]"
}

func (c *UserController) Handle(ctx *gin.Context) {
    ctx.JSON(common.HTTPStatusOK, gin.H{"message": "success"})
}

// 定义中间件,实现 IBaseMiddleware 接口
type AuthMiddleware struct {
    Service   UserService                `inject:""`
}

func (m *AuthMiddleware) MiddlewareName() string {
    return "AuthMiddleware"
}

func (m *AuthMiddleware) Order() int {
    return 100
}

func (m *AuthMiddleware) Wrapper() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        // 中间件逻辑
        ctx.Next()
    }
}

func (m *AuthMiddleware) OnStart() error {
    return nil
}

func (m *AuthMiddleware) OnStop() error {
    return nil
}

// 定义监听器,实现 IBaseListener 接口
type UserCreatedListener struct {
    LoggerMgr loggermgr.ILoggerManager   `inject:""`
}

func (l *UserCreatedListener) ListenerName() string {
    return "UserCreatedListener"
}

func (l *UserCreatedListener) GetQueue() string {
    return "user.created"
}

func (l *UserCreatedListener) GetSubscribeOptions() []common.ISubscribeOption {
    return nil
}

func (l *UserCreatedListener) Handle(ctx context.Context, msg common.IMessageListener) error {
    return nil
}

func (l *UserCreatedListener) OnStart() error {
    return nil
}

func (l *UserCreatedListener) OnStop() error {
    return nil
}

// 定义定时器,实现 IBaseScheduler 接口
type CleanupScheduler struct {
    Service   UserService                `inject:""`
}

func (s *CleanupScheduler) SchedulerName() string {
    return "CleanupScheduler"
}

func (s *CleanupScheduler) GetRule() string {
    return "0 0 2 * * *"
}

func (s *CleanupScheduler) GetTimezone() string {
    return "Asia/Shanghai"
}

func (s *CleanupScheduler) OnTick(tickID int64) error {
    return nil
}

func (s *CleanupScheduler) OnStart() error {
    return nil
}

func (s *CleanupScheduler) OnStop() error {
    return nil
}

核心接口

IBaseEntity - 实体层接口

定义数据实体的标准接口,所有实体必须实现:

type IBaseEntity interface {
    EntityName() string  // 返回实体名称,用于标识和调试
    TableName() string   // 返回数据库表名
    GetId() string       // 返回实体唯一标识
}

命名规范

  • 实体结构体使用 PascalCase(如 MessageUser
  • 方法返回实体名称(如 "Message"
实体基类(BaseEntity)

框架提供了 3 种预定义的实体基类,支持 CUID2 ID 自动生成和时间戳自动填充:

1. BaseEntityOnlyID - 仅 ID

适用场景:无需时间戳的实体(如配置表、字典表)

type BaseEntityOnlyID struct {
    ID string `gorm:"type:varchar(32);primarykey" json:"id"`
}

func (b *BaseEntityOnlyID) BeforeCreate(tx *gorm.DB) error {
    if b.ID == "" {
        newID, err := id.NewCUID2()
        if err != nil {
            return err
        }
        b.ID = newID
    }
    return nil
}
2. BaseEntityWithCreatedAt - ID + 创建时间

适用场景:只需要记录创建时间的实体(如日志、审计记录)

type BaseEntityWithCreatedAt struct {
    BaseEntityOnlyID
    CreatedAt time.Time `gorm:"type:timestamp;not null" json:"created_at"`
}

func (b *BaseEntityWithCreatedAt) BeforeCreate(tx *gorm.DB) error {
    if err := b.BaseEntityOnlyID.BeforeCreate(tx); err != nil {
        return err
    }
    if b.CreatedAt.IsZero() {
        b.CreatedAt = time.Now()
    }
    return nil
}
3. BaseEntityWithTimestamps - ID + 创建时间 + 更新时间(最常用)

适用场景:需要追踪创建和修改时间的实体

type BaseEntityWithTimestamps struct {
    BaseEntityWithCreatedAt
    UpdatedAt time.Time `gorm:"type:timestamp;not null" json:"updated_at"`
}

func (b *BaseEntityWithTimestamps) BeforeCreate(tx *gorm.DB) error {
    if err := b.BaseEntityWithCreatedAt.BeforeCreate(tx); err != nil {
        return err
    }
    if b.UpdatedAt.IsZero() {
        b.UpdatedAt = time.Now()
    }
    return nil
}

func (b *BaseEntityWithTimestamps) BeforeUpdate(tx *gorm.DB) error {
    b.UpdatedAt = time.Now()
    return nil
}

使用示例

// 使用 BaseEntityWithTimestamps(最常用)
type Message struct {
    common.BaseEntityWithTimestamps
    Nickname string `gorm:"type:varchar(20);not null" json:"nickname"`
    Content  string `gorm:"type:varchar(500);not null" json:"content"`
    Status   string `gorm:"type:varchar(20);default:'pending'" json:"status"`
}

func (m *Message) EntityName() string {
    return "Message"
}

func (m *Message) TableName() string {
    return "messages"
}

func (m *Message) GetId() string {
    return m.ID
}

var _ common.IBaseEntity = (*Message)(nil)

Repository 层无需修改

func (r *messageRepositoryImpl) Create(message *entities.Message) error {
    db := r.Manager.DB()
    return db.Create(message).Error  // Hook 自动填充 ID、CreatedAt、UpdatedAt
}

Service 层代码简化

func (s *messageServiceImpl) CreateMessage(nickname, content string) (*entities.Message, error) {
    // 验证逻辑...

    message := &entities.Message{
        Nickname: nickname,
        Content:  content,
        Status:   "pending",
        // 不再需要手动设置 CreatedAt 和 UpdatedAt
    }

    if err := s.Repository.Create(message); err != nil {
        return nil, fmt.Errorf("failed to create message: %w", err)
    }

    return message, nil
}

重要注意事项

  1. GORM Hook 继承:GORM 不会自动调用嵌入结构体的 Hook,必须手动调用父类的 Hook 方法
  2. ID 类型:使用 string 类型,CUID2 生成 25 位字符串,数据库存储 varchar(32)
  3. Repository 查询:使用 Where("id = ?", id) 而不是 First(entity, id)
  4. 并发安全:CUID2 生成器可以在 goroutine 中并发使用

性能考虑

  • CUID2 生成比自增 ID 慢(约 10μs vs < 1μs)
  • 批量插入 1000 条记录可能需要 10ms 的额外开销
  • 如果性能是关键因素,可以考虑:
    • 使用 goroutine 并发生成 ID
    • 缓存一批预生成的 ID
    • 对关键表保留自增 ID(不使用基类)
IBaseManager - 管理器层接口

定义资源管理器的标准接口,提供健康检查和生命周期管理:

type IBaseManager interface {
    ManagerName() string  // 返回管理器名称
    Health() error        // 检查健康状态
    OnStart() error       // 启动时触发
    OnStop() error        // 停止时触发
}

注意:Manager 组件位于 manager/ 目录下,由引擎自动初始化和注入。常见管理器包括:

  • configmgr - 配置管理
  • databasemgr - 数据库管理
  • loggermgr - 日志管理
  • cachemgr - 缓存管理
  • lockmgr - 锁管理
  • limitermgr - 限流管理
  • mqmgr - 消息队列管理
  • telemetrymgr - 可观测性管理
IBaseRepository - 存储库层接口

定义数据访问层的标准接口:

type IBaseRepository interface {
    RepositoryName() string  // 返回存储库名称
    OnStart() error          // 启动时触发
    OnStop() error           // 停止时触发
}

命名规范

  • 接口使用 I + 功能名 + Repository(如 IMessageRepository
  • 实现使用小驼峰 + Impl 后缀(如 messageRepositoryImpl
  • RepositoryName 返回实现类的名称(如 "MessageRepository"
IBaseService - 服务层接口

定义业务逻辑层的标准接口:

type IBaseService interface {
    ServiceName() string  // 返回服务名称
    OnStart() error      // 启动时触发
    OnStop() error       // 停止时触发
}

命名规范

  • 接口使用 I + 功能名 + Service(如 IMessageService
  • 实现使用小驼峰 + Impl 后缀(如 messageServiceImpl
  • ServiceName 返回实现类的名称(如 "MessageService"
IBaseController - 控制器层接口

定义 HTTP 处理层的标准接口:

type IBaseController interface {
    ControllerName() string              // 返回控制器名称
    GetRouter() string                   // 返回路由定义(OpenAPI @Router 规范)
    Handle(ctx *gin.Context)             // 处理请求
}

命名规范

  • 接口使用 I + 功能名 + Controller(如 IMsgCreateController
  • 实现使用小驼峰 + Impl 后缀(如 msgCreateControllerImpl
  • ControllerName 返回实现类的名称(如 "msgCreateControllerImpl"
  • GetRouter 返回路由格式:/path [METHOD](如 /api/messages [POST]
IBaseMiddleware - 中间件层接口

定义中间件的标准接口:

type IBaseMiddleware interface {
    MiddlewareName() string        // 返回中间件名称
    Order() int                   // 返回执行顺序,数值越小越先执行
    Wrapper() gin.HandlerFunc     // 返回 Gin 中间件函数
    OnStart() error               // 启动时触发
    OnStop() error                // 停止时触发
}

命名规范

  • 接口使用 I + 功能名 + Middleware(如 IAuthMiddleware
  • 实现使用小驼峰 + Impl 后缀(如 authMiddlewareImpl
  • MiddlewareName 返回中间件名称(如 "AuthMiddleware"
  • Order 返回执行顺序,数值越小越先执行(如 100)
IBaseListener - 监听器层接口

定义消息监听器的标准接口,用于处理消息队列事件:

type IBaseListener interface {
    ListenerName() string                    // 返回监听器名称
    GetQueue() string                        // 返回监听的队列名称
    GetSubscribeOptions() []ISubscribeOption // 返回订阅选项
    Handle(ctx context.Context, msg IMessageListener) error  // 处理消息
    OnStart() error                          // 启动时触发
    OnStop() error                           // 停止时触发
}

IMessageListener 接口

type IMessageListener interface {
    ID() string              // 获取消息 ID
    Body() []byte           // 获取消息体
    Headers() map[string]any // 获取消息头
}

命名规范

  • 接口使用 I + 功能名 + Listener(如 IMessageCreatedListener
  • 实现使用小驼峰 + Impl 后缀(如 messageCreatedListenerImpl
  • ListenerName 返回监听器名称(如 "MessageCreatedListener"
  • GetQueue 返回队列名称(如 "message.created"
  • Handle 方法处理消息,返回 error 会触发 Nack
IBaseScheduler - 定时器层接口

定义定时任务的标准接口,用于处理周期性任务:

type IBaseScheduler interface {
    SchedulerName() string  // 返回定时器名称
    GetRule() string       // 返回 Crontab 定时规则(6 段式)
    GetTimezone() string   // 返回时区(空字符串使用服务器本地时间)
    OnTick(tickID int64) error  // 定时触发时调用
    OnStart() error       // 启动时触发
    OnStop() error        // 停止时触发
}

命名规范

  • 接口使用 I + 功能名 + Scheduler(如 ICleanupScheduler
  • 实现使用小驼峰 + Impl 后缀(如 cleanupSchedulerImpl
  • SchedulerName 返回定时器名称(如 "cleanupScheduler"
  • GetRule 返回 Crontab 规则(如 "0 0 2 * * *" 表示每天凌晨 2 点)
  • GetTimezone 返回时区(如 "Asia/Shanghai""UTC"
  • OnTick 方法接收 tickID(Unix 时间戳秒级)

依赖规则

各层之间有明确的依赖关系:

  • Entity 层:无依赖
  • Repository 层:可依赖 Entity、Manager
  • Service 层:可依赖 Repository、Entity、Manager、其他 Service
  • 交互层 (Controller/Middleware/Listener/Scheduler):可依赖 Service、Manager

原则:上层可以依赖下层,下层不能依赖上层。交互层统一处理 HTTP 请求、MQ 消息和定时任务,必须通过 Service 层访问数据。

依赖注入

所有基础接口都支持依赖注入,使用 inject:"" 标签:

type UserServiceImpl struct {
    // 内置管理器(由引擎自动注入)
    Config     configmgr.IConfigManager    `inject:""`
    LoggerMgr  loggermgr.ILoggerManager   `inject:""`
    DBManager  databasemgr.IDatabaseManager `inject:""`

    // 业务依赖
    UserRepo   IUserRepository            `inject:""`
    CacheMgr   cachemgr.ICacheManager     `inject:""`
}

生命周期管理

实现 IBaseManager、IBaseRepository、IBaseService、IBaseMiddleware 接口的组件,会在以下时机调用生命周期方法:

  1. OnStart - 服务器启动时调用,用于初始化资源(如连接数据库、加载缓存等)
  2. OnStop - 服务器停止时调用,用于清理资源(如关闭连接、刷新缓存等)

生命周期方法返回 error,如果初始化失败会阻止服务器启动。

类型转换工具

提供安全的类型转换函数,用于从 any 类型中获取特定类型的值:

// GetString 从 any 类型中安全获取字符串值
func GetString(value any) (string, error)

// GetStringOrDefault 从 any 类型中安全获取字符串值,失败时返回默认值
func GetStringOrDefault(value any, defaultValue string) string

// GetMap 从 any 类型中安全获取 map[string]any 值
func GetMap(value any) (map[string]any, error)

// GetMapOrDefault 从 any 类型中安全获取 map[string]any 值,失败时返回默认值
func GetMapOrDefault(value any, defaultValue map[string]any) map[string]any

使用示例:

// 从配置中获取字符串值
name, err := common.GetString(config["name"])
if err != nil {
    log.Error("无效的名称配置")
}

// 带默认值的字符串获取
timeout := common.GetStringOrDefault(config["timeout"], "30s")

// 从配置中获取 map 值
settings, err := common.GetMap(config["settings"])
if err != nil {
    log.Error("无效的设置配置")
}

HTTP 状态码常量

定义完整的 HTTP 状态码常量,便于统一使用:

const (
    HTTPStatusContinue                    = 100
    HTTPStatusOK                          = 200
    HTTPStatusCreated                     = 201
    HTTPStatusNoContent                   = 204
    HTTPStatusMovedPermanently            = 301
    HTTPStatusBadRequest                  = 400
    HTTPStatusUnauthorized                = 401
    HTTPStatusForbidden                   = 403
    HTTPStatusNotFound                    = 404
    HTTPStatusInternalServerError         = 500
    HTTPStatusServiceUnavailable          = 503
    // ... 更多状态码
)

使用示例:

ctx.JSON(common.HTTPStatusOK, gin.H{"message": "success"})
ctx.JSON(common.HTTPStatusNotFound, gin.H{"error": "not found"})
ctx.JSON(common.HTTPStatusInternalServerError, gin.H{"error": "internal error"})

5 层架构统一接口规范

接口命名规律
层级 接口命名 实现命名 示例
Entity 不需要单独接口 PascalCase Message
Repository I + 功能名 + Repository 小驼峰 + Impl IMessageRepository / messageRepositoryImpl
Service I + 功能名 + Service 小驼峰 + Impl IMessageService / messageServiceImpl
Controller I + 功能名 + Controller 小驼峰 + Impl IMsgCreateController / msgCreateControllerImpl
Middleware I + 功能名 + Middleware 小驼峰 + Impl IAuthMiddleware / authMiddlewareImpl
Listener I + 功能名 + Listener 小驼峰 + Impl IMessageCreatedListener / messageCreatedListenerImpl
Scheduler I + 功能名 + Scheduler 小驼峰 + Impl ICleanupScheduler / cleanupSchedulerImpl

说明:交互层包含 4 种组件类型(Controller/Middleware/Listener/Scheduler),它们位于同一架构层级,职责是处理不同类型的外部交互。

接口方法统一规范
接口类型 名称方法 生命周期方法 特殊方法
IBaseEntity EntityName() - TableName(), GetId()
IBaseManager ManagerName() OnStart(), OnStop() Health()
IBaseRepository RepositoryName() OnStart(), OnStop() -
IBaseService ServiceName() OnStart(), OnStop() -
IBaseController ControllerName() - GetRouter(), Handle()
IBaseMiddleware MiddlewareName() OnStart(), OnStop() Order(), Wrapper()
IBaseListener ListenerName() OnStart(), OnStop() GetQueue(), GetSubscribeOptions(), Handle()
IBaseScheduler SchedulerName() OnStart(), OnStop() GetRule(), GetTimezone(), OnTick()
依赖注入规范

所有组件统一使用 inject:"" 标签进行依赖注入:

type messageServiceImpl struct {
    // 内置管理器(由引擎自动注入)
    Config    configmgr.IConfigManager    `inject:""`
    LoggerMgr loggermgr.ILoggerManager   `inject:""`
    DBManager databasemgr.IDatabaseManager `inject:""`

    // 业务依赖(手动注入到容器)
    Repository IMessageRepository `inject:""`
    CacheMgr  cachemgr.ICacheManager `inject:""`
}
接口编译时检查

所有实现都应在文件末尾添加编译时接口检查:

var _ IMessageService = (*messageServiceImpl)(nil)
var _ common.IBaseService = (*messageServiceImpl)(nil)
组件注册规范
  1. Entity:在 entity_container.go 中注册
  2. Repository:在 repository_container.go 中注册
  3. Service:在 service_container.go 中注册
  4. 交互层组件
    • Controller:在 controller_container.go 中注册
    • Middleware:在 middleware_container.go 中注册
    • Listener:在 listener_container.go 中注册
    • Scheduler:在 scheduler_container.go 中注册
层级职责边界
层级 职责 允许依赖
Entity 数据模型定义
Repository 数据访问、持久化 Entity、Manager
Service 业务逻辑、编排 Repository、Entity、Manager、其他 Service
交互层 (Controller) HTTP 请求处理、响应 Service、Manager
交互层 (Middleware) 请求预处理、后处理 Service、Manager
交互层 (Listener) 消息队列事件处理 Service、Manager
交互层 (Scheduler) 定时任务执行 Service、Manager
生命周期方法规范

OnStart 方法用于:

  • 初始化资源(连接数据库、连接缓存等)
  • 预热缓存
  • 注册定时任务
  • 启动消息监听

OnStop 方法用于:

  • 关闭数据库连接
  • 刷新缓存数据
  • 停止定时任务
  • 取消消息订阅
  • 释放其他资源

生命周期方法返回 error,如果初始化失败会阻止服务器启动。

快速开发模板
// 接口定义
type IXxxService interface {
    common.IBaseService
    // 业务方法
    DoSomething() error
}

// 实现结构体
type xxxServiceImpl struct {
    Config    configmgr.IConfigManager    `inject:""`
    LoggerMgr loggermgr.ILoggerManager   `inject:""`
}

// 构造函数
func NewXxxService() IXxxService {
    return &xxxServiceImpl{}
}

// 实现 IBaseService
func (s *xxxServiceImpl) ServiceName() string { return "XxxService" }
func (s *xxxServiceImpl) OnStart() error { return nil }
func (s *xxxServiceImpl) OnStop() error { return nil }

// 实现业务方法
func (s *xxxServiceImpl) DoSomething() error {
    return nil
}

// 编译时接口检查
var _ IXxxService = (*xxxServiceImpl)(nil)
var _ common.IBaseService = (*xxxServiceImpl)(nil)

最佳实践

  1. 接口实现 - 确保所有组件实现对应的基础接口(以 I 开头)
  2. 命名规范 - 使用结构体类型名作为名称方法返回值
  3. 生命周期 - 在 OnStart 中初始化资源,在 OnStop 中清理资源
  4. 依赖关系 - 严格遵循分层架构的依赖规则
  5. 错误处理 - 生命周期方法中的错误应该被正确处理和传播
  6. 类型转换 - 使用 common 包提供的类型转换工具函数,避免直接类型断言导致的 panic
  7. HTTP 状态码 - 使用 common 包定义的 HTTP 状态码常量,保持代码一致性

实际应用示例

参考 samples/messageboard 目录下的完整示例:

// entities/message_entity.go(使用基类)
type Message struct {
    common.BaseEntityWithTimestamps
    Nickname  string `gorm:"type:varchar(20);not null"`
    Content   string `gorm:"type:varchar(500);not null"`
    Status    string `gorm:"type:varchar(20);default:'pending'"`
}

func (m *Message) EntityName() string { return "Message" }
func (m *Message) TableName() string { return "messages" }
func (m *Message) GetId() string { return m.ID }

var _ common.IBaseEntity = (*Message)(nil)

// repositories/message_repository.go
type IMessageRepository interface {
    common.IBaseRepository
    Create(message *entities.Message) error
    GetByID(id string) (*entities.Message, error)  // ID 类型改为 string
}

type messageRepositoryImpl struct {
    Config  configmgr.IConfigManager    `inject:""`
    Manager databasemgr.IDatabaseManager `inject:""`
}

func NewMessageRepository() IMessageRepository {
    return &messageRepositoryImpl{}
}

func (r *messageRepositoryImpl) RepositoryName() string { return "MessageRepository" }
func (r *messageRepositoryImpl) OnStart() error { return nil }
func (r *messageRepositoryImpl) OnStop() error { return nil }

func (r *messageRepositoryImpl) Create(message *entities.Message) error {
    db := r.Manager.DB()
    return db.Create(message).Error
}

func (r *messageRepositoryImpl) GetByID(id string) (*entities.Message, error) {
    db := r.Manager.DB()
    var message entities.Message
    err := db.Where("id = ?", id).First(&message).Error  // 使用 Where 查询
    if err != nil {
        return nil, err
    }
    return &message, nil
}

var _ IMessageRepository = (*messageRepositoryImpl)(nil)

// services/message_service.go
type IMessageService interface {
    common.IBaseService
    CreateMessage(nickname, content string) (*entities.Message, error)  // ID 类型改为 string
    GetApprovedMessages() ([]*entities.Message, error)
    UpdateMessageStatus(id string, status string) error  // ID 类型改为 string
    DeleteMessage(id string) error  // ID 类型改为 string
}

type messageServiceImpl struct {
    Config     configmgr.IConfigManager     `inject:""`
    Repository IMessageRepository           `inject:""`
    LoggerMgr  loggermgr.ILoggerManager    `inject:""`
}

func NewMessageService() IMessageService {
    return &messageServiceImpl{}
}

func (s *messageServiceImpl) ServiceName() string { return "MessageService" }
func (s *messageServiceImpl) OnStart() error { return nil }
func (s *messageServiceImpl) OnStop() error { return nil }

func (s *messageServiceImpl) CreateMessage(nickname, content string) (*entities.Message, error) {
    // 验证逻辑...

    message := &entities.Message{
        Nickname: nickname,
        Content:  content,
        Status:   "pending",
        // 不再需要手动设置 CreatedAt 和 UpdatedAt,由 Hook 自动填充
    }

    if err := s.Repository.Create(message); err != nil {
        s.LoggerMgr.Ins().Error("Failed to create message", "nickname", nickname, "error", err)
        return nil, fmt.Errorf("failed to create message: %w", err)
    }

    s.LoggerMgr.Ins().Info("Message created successfully", "id", message.ID, "nickname", message.Nickname)
    return message, nil
}

func (s *messageServiceImpl) UpdateMessageStatus(id string, status string) error {
    // ID 类型为 string,直接使用
    message, err := s.Repository.GetByID(id)
    if err != nil {
        return fmt.Errorf("message not found: %w", err)
    }

    if err := s.Repository.UpdateStatus(id, status); err != nil {
        return fmt.Errorf("failed to update message status: %w", err)
    }

    s.LoggerMgr.Ins().Info("Message status updated successfully", "id", id, "status", status)
    return nil
}

func (s *messageServiceImpl) DeleteMessage(id string) error {
    // ID 类型为 string,直接使用
    message, err := s.Repository.GetByID(id)
    if err != nil {
        return fmt.Errorf("message not found: %w", err)
    }

    if err := s.Repository.Delete(id); err != nil {
        return fmt.Errorf("failed to delete message: %w", err)
    }

    s.LoggerMgr.Ins().Info("Message deleted successfully", "id", id)
    return nil
}

var _ IMessageService = (*messageServiceImpl)(nil)

// controllers/msg_create_controller.go
type IMsgCreateController interface {
    common.IBaseController
}

type msgCreateControllerImpl struct {
    MessageService IMessageService         `inject:""`
    LoggerMgr      loggermgr.ILoggerManager `inject:""`
}

func NewMsgCreateController() IMsgCreateController {
    return &msgCreateControllerImpl{}
}

func (c *msgCreateControllerImpl) ControllerName() string { return "msgCreateControllerImpl" }
func (c *msgCreateControllerImpl) GetRouter() string { return "/api/messages [POST]" }

func (c *msgCreateControllerImpl) Handle(ctx *gin.Context) {
    var req dtos.CreateMessageRequest
    if err := ctx.ShouldBindJSON(&req); err != nil {
        c.LoggerMgr.Ins().Error("Failed to create message: parameter binding error", "error", err)
        ctx.JSON(common.HTTPStatusBadRequest, dtos.ErrorResponse(common.HTTPStatusBadRequest, err.Error()))
        return
    }

    message, err := c.MessageService.CreateMessage(req.Nickname, req.Content)
    if err != nil {
        c.LoggerMgr.Ins().Error("Failed to create message", "nickname", req.Nickname, "error", err)
        ctx.JSON(common.HTTPStatusBadRequest, dtos.ErrorResponse(common.HTTPStatusBadRequest, err.Error()))
        return
    }

    c.LoggerMgr.Ins().Info("Message created successfully", "id", message.ID)
    ctx.JSON(common.HTTPStatusOK, dtos.SuccessResponse("留言提交成功,等待审核", gin.H{
        "id": message.ID,
    }))
}

var _ IMsgCreateController = (*msgCreateControllerImpl)(nil)

// controllers/msg_delete_controller.go
type IMsgDeleteController interface {
    common.IBaseController
}

type msgDeleteControllerImpl struct {
    MessageService IMessageService         `inject:""`
    LoggerMgr      loggermgr.ILoggerManager `inject:""`
}

func NewMsgDeleteController() IMsgDeleteController {
    return &msgDeleteControllerImpl{}
}

func (c *msgDeleteControllerImpl) ControllerName() string { return "msgDeleteControllerImpl" }
func (c *msgDeleteControllerImpl) GetRouter() string { return "/api/admin/messages/:id/delete [POST]" }

func (c *msgDeleteControllerImpl) Handle(ctx *gin.Context) {
    id := ctx.Param("id")  // ID 类型为 string,直接使用,无需解析

    if err := c.MessageService.DeleteMessage(id); err != nil {
        c.LoggerMgr.Ins().Error("Failed to delete message", "id", id, "error", err)
        ctx.JSON(common.HTTPStatusBadRequest, dtos.ErrorResponse(common.HTTPStatusBadRequest, err.Error()))
        return
    }

    c.LoggerMgr.Ins().Info("Message deleted successfully", "id", id)
    ctx.JSON(common.HTTPStatusOK, dtos.SuccessWithMessage("删除成功"))
}

var _ IMsgDeleteController = (*msgDeleteControllerImpl)(nil)

// middlewares/auth_middleware.go
type IAuthMiddleware interface {
    common.IBaseMiddleware
}

type authMiddlewareImpl struct {
    Service   IMessageService `inject:""`
}

func NewAuthMiddleware() IAuthMiddleware {
    return &authMiddlewareImpl{}
}

func (m *authMiddlewareImpl) MiddlewareName() string { return "AuthMiddleware" }
func (m *authMiddlewareImpl) Order() int { return 100 }
func (m *authMiddlewareImpl) Wrapper() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        // 中间件逻辑
        ctx.Next()
    }
}
func (m *authMiddlewareImpl) OnStart() error { return nil }
func (m *authMiddlewareImpl) OnStop() error { return nil }

var _ IAuthMiddleware = (*authMiddlewareImpl)(nil)

// listeners/message_created_listener.go
type IMessageCreatedListener interface {
    common.IBaseListener
}

type messageCreatedListenerImpl struct {
    LoggerMgr loggermgr.ILoggerManager `inject:""`
}

func NewMessageCreatedListener() IMessageCreatedListener {
    return &messageCreatedListenerImpl{}
}

func (l *messageCreatedListenerImpl) ListenerName() string { return "MessageCreatedListener" }
func (l *messageCreatedListenerImpl) GetQueue() string { return "message.created" }
func (l *messageCreatedListenerImpl) GetSubscribeOptions() []common.ISubscribeOption { return nil }
func (l *messageCreatedListenerImpl) Handle(ctx context.Context, msg common.IMessageListener) error { return nil }
func (l *messageCreatedListenerImpl) OnStart() error { return nil }
func (l *messageCreatedListenerImpl) OnStop() error { return nil }

var _ IMessageCreatedListener = (*messageCreatedListenerImpl)(nil)

// schedulers/cleanup_scheduler.go
type ICleanupScheduler interface {
    common.IBaseScheduler
}

type cleanupSchedulerImpl struct {
    MessageService IMessageService `inject:""`
}

func NewCleanupScheduler() ICleanupScheduler {
    return &cleanupSchedulerImpl{}
}

func (s *cleanupSchedulerImpl) SchedulerName() string { return "cleanupScheduler" }
func (s *cleanupSchedulerImpl) GetRule() string { return "0 0 2 * * *" }
func (s *cleanupSchedulerImpl) GetTimezone() string { return "Asia/Shanghai" }
func (s *cleanupSchedulerImpl) OnTick(tickID int64) error { return nil }
func (s *cleanupSchedulerImpl) OnStart() error { return nil }
func (s *cleanupSchedulerImpl) OnStop() error { return nil }

var _ ICleanupScheduler = (*cleanupSchedulerImpl)(nil)

与其他包的关系

  • manager/ - Manager 组件实现 IBaseManager 接口,作为基础设施层提供各种能力
  • container/ - 依赖注入容器使用 common 包定义的接口类型进行类型安全的依赖注入
  • component/ - 业务组件实现 IBaseEntity、IBaseRepository、IBaseService、IBaseController、IBaseMiddleware 接口
  • util/ - 工具函数包提供特定功能的工具函数,common 包提供通用的类型转换函数
  • server/ - 服务器引擎负责管理所有组件的生命周期(OnStart/OnStop),并按规则调度 Listener 和 Scheduler

Documentation

Overview

Package common 提供七层架构的基础接口定义,规范 Entity、Manager、Repository、Service、Controller、Middleware 和 ConfigMgr 的行为契约。

核心特性:

  • 七层架构基础接口:定义各层的基础接口类型,确保架构一致性
  • 生命周期管理:提供统一的 OnStart 和 OnStop 钩子方法
  • 命名规范:每层接口要求实现对应的名称方法,便于调试和日志
  • 行为契约:通过接口定义各层的核心行为,建立分层依赖关系
  • 依赖注入支持:为依赖注入容器提供标准接口类型

基本用法:

// 实现 Entity 接口
type User struct {
	ID   string `gorm:"primaryKey"`
	Name string
}

func (u *User) EntityName() string { return "User" }
func (u *User) TableName() string { return "users" }
func (u *User) GetId() string { return u.ID }

// 实现 Service 接口
type UserService struct {}

func (s *UserService) ServiceName() string { return "UserService" }
func (s *UserService) OnStart() error { return nil }
func (s *UserService) OnStop() error { return nil }

接口层次:

各层之间有明确的依赖关系,从低到高依次为:
ConfigMgr → Entity → Manager → Repository → Service → Controller/Middleware
上层可以依赖下层,下层不能依赖上层。

Index

Constants

View Source
const (
	HTTPStatusContinue           = 100 // HTTP/1.1: Continue
	HTTPStatusSwitchingProtocols = 101 // HTTP/1.1: Switching Protocols

	HTTPStatusOK                   = 200 // HTTP/1.1: OK
	HTTPStatusCreated              = 201 // HTTP/1.1: Created
	HTTPStatusAccepted             = 202 // HTTP/1.1: Accepted
	HTTPStatusNonAuthoritativeInfo = 203 // HTTP/1.1: Non-Authoritative Information
	HTTPStatusNoContent            = 204 // HTTP/1.1: No Content
	HTTPStatusResetContent         = 205 // HTTP/1.1: Reset Content
	HTTPStatusPartialContent       = 206 // HTTP/1.1: Partial Content

	HTTPStatusMultipleChoices   = 300 // HTTP/1.1: Multiple Choices
	HTTPStatusMovedPermanently  = 301 // HTTP/1.1: Moved Permanently
	HTTPStatusFound             = 302 // HTTP/1.1: Found
	HTTPStatusSeeOther          = 303 // HTTP/1.1: See Other
	HTTPStatusNotModified       = 304 // HTTP/1.1: Not Modified
	HTTPStatusUseProxy          = 305 // HTTP/1.1: Use Proxy
	HTTPStatusTemporaryRedirect = 307 // HTTP/1.1: Temporary Redirect
	HTTPStatusPermanentRedirect = 308 // HTTP/1.1: Permanent Redirect

	HTTPStatusBadRequest                  = 400 // HTTP/1.1: Bad Request
	HTTPStatusUnauthorized                = 401 // HTTP/1.1: Unauthorized
	HTTPStatusPaymentRequired             = 402 // HTTP/1.1: Payment Required
	HTTPStatusForbidden                   = 403 // HTTP/1.1: Forbidden
	HTTPStatusNotFound                    = 404 // HTTP/1.1: Not Found
	HTTPStatusMethodNotAllowed            = 405 // HTTP/1.1: Method Not Allowed
	HTTPStatusNotAcceptable               = 406 // HTTP/1.1: Not Acceptable
	HTTPStatusProxyAuthRequired           = 407 // HTTP/1.1: Proxy Authentication Required
	HTTPStatusRequestTimeout              = 408 // HTTP/1.1: Request Timeout
	HTTPStatusConflict                    = 409 // HTTP/1.1: Conflict
	HTTPStatusGone                        = 410 // HTTP/1.1: Gone
	HTTPStatusLengthRequired              = 411 // HTTP/1.1: Length Required
	HTTPStatusPreconditionFailed          = 412 // HTTP/1.1: Precondition Failed
	HTTPStatusPayloadTooLarge             = 413 // HTTP/1.1: Payload Too Large
	HTTPStatusURITooLong                  = 414 // HTTP/1.1: URI Too Long
	HTTPStatusUnsupportedMediaType        = 415 // HTTP/1.1: Unsupported Media Type
	HTTPStatusRangeNotSatisfiable         = 416 // HTTP/1.1: Range Not Satisfiable
	HTTPStatusExpectationFailed           = 417 // HTTP/1.1: Expectation Failed
	HTTPStatusTeapot                      = 418 // HTTP/1.1: I'm a teapot
	HTTPStatusMisdirectedRequest          = 421 // HTTP/1.1: Misdirected Request
	HTTPStatusUnprocessableEntity         = 422 // HTTP/1.1: Unprocessable Entity
	HTTPStatusLocked                      = 423 // HTTP/1.1: Locked
	HTTPStatusFailedDependency            = 424 // HTTP/1.1: Failed Dependency
	HTTPStatusTooEarly                    = 425 // HTTP/1.1: Too Early
	HTTPStatusUpgradeRequired             = 426 // HTTP/1.1: Upgrade Required
	HTTPStatusPreconditionRequired        = 428 // HTTP/1.1: Precondition Required
	HTTPStatusTooManyRequests             = 429 // HTTP/1.1: Too Many Requests
	HTTPStatusRequestHeaderFieldsTooLarge = 431 // HTTP/1.1: Request Header Fields Too Large
	HTTPStatusUnavailableForLegalReasons  = 451 // HTTP/1.1: Unavailable For Legal Reasons

	HTTPStatusInternalServerError           = 500 // HTTP/1.1: Internal Server Error
	HTTPStatusNotImplemented                = 501 // HTTP/1.1: Not Implemented
	HTTPStatusBadGateway                    = 502 // HTTP/1.1: Bad Gateway
	HTTPStatusServiceUnavailable            = 503 // HTTP/1.1: Service Unavailable
	HTTPStatusGatewayTimeout                = 504 // HTTP/1.1: Gateway Timeout
	HTTPStatusHTTPVersionNotSupported       = 505 // HTTP/1.1: HTTP Version Not Supported
	HTTPStatusVariantAlsoNegotiates         = 506 // HTTP/1.1: Variant Also Negotiates
	HTTPStatusInsufficientStorage           = 507 // HTTP/1.1: Insufficient Storage
	HTTPStatusLoopDetected                  = 508 // HTTP/1.1: Loop Detected
	HTTPStatusNotExtended                   = 510 // HTTP/1.1: Not Extended
	HTTPStatusNetworkAuthenticationRequired = 511 // HTTP/1.1: Network Authentication Required
)

Variables

This section is empty.

Functions

func GetMap added in v0.0.6

func GetMap(value any) (map[string]any, error)

GetMap 从 any 类型中安全获取 map[string]any 值

func GetMapOrDefault added in v0.0.6

func GetMapOrDefault(value any, defaultValue map[string]any) map[string]any

GetMapOrDefault 从 any 类型中安全获取 map[string]any 值,失败时返回默认值

func GetString added in v0.0.6

func GetString(value any) (string, error)

GetString 从 any 类型中安全获取字符串值

func GetStringOrDefault added in v0.0.6

func GetStringOrDefault(value any, defaultValue string) string

GetStringOrDefault 从 any 类型中安全获取字符串值,失败时返回默认值

Types

type BaseEntityOnlyID added in v0.0.11

type BaseEntityOnlyID struct {
	ID string `gorm:"type:varchar(32);primarykey" json:"id"`
}

func (*BaseEntityOnlyID) BeforeCreate added in v0.0.11

func (b *BaseEntityOnlyID) BeforeCreate(tx *gorm.DB) error

type BaseEntityWithCreatedAt added in v0.0.11

type BaseEntityWithCreatedAt struct {
	BaseEntityOnlyID
	CreatedAt time.Time `gorm:"type:timestamp;not null" json:"created_at"`
}

func (*BaseEntityWithCreatedAt) BeforeCreate added in v0.0.11

func (b *BaseEntityWithCreatedAt) BeforeCreate(tx *gorm.DB) error

type BaseEntityWithTimestamps added in v0.0.11

type BaseEntityWithTimestamps struct {
	BaseEntityWithCreatedAt
	UpdatedAt time.Time `gorm:"type:timestamp;not null" json:"updated_at"`
}

func (*BaseEntityWithTimestamps) BeforeCreate added in v0.0.11

func (b *BaseEntityWithTimestamps) BeforeCreate(tx *gorm.DB) error

func (*BaseEntityWithTimestamps) BeforeUpdate added in v0.0.11

func (b *BaseEntityWithTimestamps) BeforeUpdate(tx *gorm.DB) error

type IBaseController

type IBaseController interface {
	// ControllerName 返回当前控制器的类名
	ControllerName() string
	// GetRouter 返回当前控制器的路由
	// 路由格式同OpenAPI @Router 规范
	// 如 `/aaa/bbb [GET]`; `/aaa/bbb [POST]`
	GetRouter() string
	// Handle 处理当前控制器的请求
	Handle(ctx *gin.Context)
}

IBaseController 基础控制器接口 所有 Controller 类必须继承此接口并实现相关方法 用于定义基础控制器的规范,包括路由和处理函数。

type IBaseEntity

type IBaseEntity interface {
	// EntityName 返回当前实体实现的类名
	// 用于标识和调试实体实例
	EntityName() string

	// TableName 返回当前实体的表名
	// 用于数据库操作
	TableName() string

	// GetId 返回实体的唯一标识
	// 用于实体的索引和检索
	GetId() string
}

IBaseEntity 实体基类接口 所有 Entity 类必须继承此接口并实现 EntityName 和 GetId 方法 系统通过此接口判断是否符合标准实体定义

type IBaseListener added in v0.0.7

type IBaseListener interface {
	// ListenerName 返回监听器名称
	// 格式:xxxListenerImpl(小驼峰,带 Impl 后缀)
	ListenerName() string

	// GetQueue 返回监听的队列名称
	// 返回值示例:"message.created", "user.registered"
	GetQueue() string

	// GetSubscribeOptions 返回订阅选项
	// 可配置是否持久化、是否自动确认、并发消费者数量等
	GetSubscribeOptions() []ISubscribeOption

	// Handle 处理队列消息
	// ctx: 上下文
	// msg: 消息对象,包含 ID、Body、Headers
	// 返回: 处理错误(返回 error 会触发 Nack)
	Handle(ctx context.Context, msg IMessageListener) error

	// OnStart 在服务器启动时触发
	OnStart() error
	// OnStop 在服务器停止时触发
	OnStop() error
}

IBaseListener 基础监听器接口 所有 Listener 类必须继承此接口并实现相关方法 用于定义监听器的基础行为和契约

type IBaseManager

type IBaseManager interface {
	// ManagerName 返回管理器名称
	ManagerName() string
	// Health 检查管理器健康状态
	Health() error
	// OnStart 在服务器启动时触发
	OnStart() error
	// OnStop 在服务器停止时触发
	OnStop() error
}

IBaseManager 管理器基础接口定义

type IBaseMiddleware

type IBaseMiddleware interface {
	// MiddlewareName 返回中间件的名称
	MiddlewareName() string
	// Order 返回中间件的执行顺序
	Order() int
	// Wrapper 返回一个中间件函数,用于包装请求处理函数
	Wrapper() gin.HandlerFunc

	// OnStart 在服务器启动时触发
	OnStart() error
	// OnStop 在服务器停止时触发
	OnStop() error
}

IBaseMiddleware 基础中间件接口 所有 Middleware 类必须继承此接口并实现相关方法 用于定义基础中间件的规范,包括名称、执行顺序和包装函数。

type IBaseRepository

type IBaseRepository interface {
	// RepositoryName 返回当前存储库实现的类名
	// 用于标识和调试存储库实例
	RepositoryName() string

	// OnStart 在服务器启动时触发
	OnStart() error
	// OnStop 在服务器停止时触发
	OnStop() error
}

IBaseRepository 存储库基类接口 所有 Repository 类必须继承此接口并实现 RepositoryName 方法 系统通过此接口判断是否符合标准存储层定义

type IBaseScheduler added in v0.0.7

type IBaseScheduler interface {
	// SchedulerName 返回定时器名称
	// 格式:xxxScheduler(小驼峰)
	// 示例:"cleanupScheduler"
	SchedulerName() string

	// GetRule 返回 Crontab 定时规则
	// 使用标准 6 段式格式:秒 分 时 日 月 周
	// 示例:"0 */5 * * * *" 表示每 5 分钟执行一次
	//      "0 0 2 * * *" 表示每天凌晨 2 点执行
	//      "0 0 * * * 1" 表示每周一凌晨执行
	GetRule() string

	// GetTimezone 返回定时器使用的时区
	// 返回空字符串时使用服务器本地时间
	// 支持标准时区名称,如 "Asia/Shanghai", "UTC", "America/New_York"
	// 默认值:空字符串(服务器本地时间)
	GetTimezone() string

	// OnTick 定时触发时调用
	// tickID: 计划执行时间的 Unix 时间戳(秒级),可用于去重或日志追踪
	// 返回: 执行错误(返回 error 不会触发重试,仅记录日志)
	OnTick(tickID int64) error

	// OnStart 在服务器启动时触发
	// 用于初始化定时器状态、连接资源等
	OnStart() error

	// OnStop 在服务器停止时触发
	// 用于清理资源、保存状态等
	OnStop() error
}

IBaseScheduler 基础定时器接口 所有 Scheduler 类必须继承此接口并实现相关方法 用于定义定时器的基础行为和契约

type IBaseService

type IBaseService interface {
	// ServiceName 返回当前服务实现的类名
	// 用于标识和调试服务实例
	ServiceName() string

	// OnStart 在服务器启动时触发
	OnStart() error
	// OnStop 在服务器停止时触发
	OnStop() error
}

IBaseService 服务基类接口 所有 Service 类必须继承此接口并实现 GetServiceName 方法 系统通过此接口判断是否符合标准服务定义

type IMessageListener added in v0.0.7

type IMessageListener interface {
	// ID 获取消息 ID
	ID() string
	// Body 获取消息体
	Body() []byte
	// Headers 获取消息头
	Headers() map[string]any
}

IMessageListener 消息监听器接口 定义消息队列相关的消息和订阅选项 此接口避免了与 manager 包的循环依赖

type ISubscribeOption added in v0.0.7

type ISubscribeOption interface{}

ISubscribeOption 订阅选项接口

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL