echoswg
Builds Go APIs on the custom echoswg framework. Invoke when adding routes, auth, Swagger docs, or bootstrapping an API module in this repository.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Builds Go APIs on the custom echoswg framework. Invoke when adding routes, auth, Swagger docs, or bootstrapping an API module in this repository.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
| name | echoswg |
| description | Builds Go APIs on the custom echoswg framework. Invoke when adding routes, auth, Swagger docs, or bootstrapping an API module in this repository. |
用于当前仓库及其衍生业务项目中的 Go API 开发。它帮助你基于 echoswg 框架自动完成路由配置、Swagger 文档生成接入、认证授权接入,并遵循本 skill 与 example-skeleton 中定义的代码组织方式。
在以下场景主动调用本 skill:
echoswg 的 HTTP API。controller 中注册路由,并让 Swagger 自动生成接口文档。security.RequireAuth(...) 鉴权。example-skeleton 的模式创建 controller、service、security、main 装配代码。如果任务只是修改纯业务逻辑、与路由和 API 协议无关,不必调用本 skill。
main 中逐条挂载,而是在各个 controller 文件的 init() 中自动注册。main 通过匿名导入 controller 包触发这些 init(),例如 _ "your/module/controller"。echoswg.NewApiGroup(...) 创建分组。WithOperationId(...) 是默认必写项;WithDescription(...)、WithSummary(...) 视需要补充。controller/
service。service/
service.User、service.Payment。security/
security.RequireAuth(...) 接入。main.go
controller 包。每个资源域通常对应一个 controller 文件,并在 init() 中完成分组与路由注册:
package controller
import (
"your/module/security"
"your/module/service"
"your/module/util"
"github.com/yb7/echoswg"
)
type OrderController struct{}
func init() {
c := new(OrderController)
g := echoswg.NewApiGroup(util.EchoInstance, "Order", "/api/orders")
g.POST("", security.RequireAuth(), c.Create, echoswg.WithOperationId("createOrder"))
g.GET("", security.RequireAuth(), c.Query, echoswg.WithOperationId("queryOrders"))
g.GET("/:ID", security.RequireAuth(), c.GetByID, echoswg.WithOperationId("getOrderById"))
g.PUT("/:ID", security.RequireAuth(), c.Update, echoswg.WithOperationId("updateOrder"))
g.DELETE("/:ID", security.RequireAuth(), c.Delete, echoswg.WithOperationId("deleteOrder"))
}
约定如下:
tag 使用资源域名称,供 Swagger tag 分组。prefix 使用资源根路径,例如 /api/orders。WithOperationId(...)。WithDescription(...) 用于补充接口说明;不需要时可省略。g.POST(...)、g.GET(...)、g.PUT(...)、g.DELETE(...) 接收的参数里,函数类型会按顺序组成一条执行链。框架会从左到右依次调用这些函数,并把上一步返回的非 error 结果缓存起来,供下一步按类型自动注入。
例如:
g.POST(
"",
security.RequireAuth(security.RoleAnonymous),
c.Create,
echoswg.WithOperationId("createDemo"),
echoswg.WithDescription("create demo"),
)
它的实际含义是:
security.RequireAuth(security.RoleAnonymous) 返回的函数。security.AuthCtx 后,框架会把这个值缓存到当前调用链中。c.Create 时,如果它的参数列表里声明了 security.AuthCtx,框架就会把上一步产出的 AuthCtx 自动注入进去。WithOperationId(...)、WithDescription(...) 这类 PathSchemaOption 只参与 Swagger 元数据构建,不参与运行时执行链。也就是说,下面这种写法并不是“把 RequireAuth(...) 当成普通中间件挂上去”,而是“把它当作链路中的前置函数,让它产出的类型值继续传给后续 handler”。
再看一个更直观的例子:
g.POST(
"",
security.RequireAuth(),
c.Create,
echoswg.WithOperationId("createOrder"),
)
func (*OrderController) Create(ctx security.AuthCtx, req *struct {
Body *service.CreateOrderVo
}) (*service.OrderVo, error) {
return service.Order.Create(ctx, req.Body)
}
上面的调用链中:
RequireAuth() 负责从请求中解析登录态,并返回 security.AuthCtxc.Create 负责消费 security.AuthCtx 和请求体req 则不是前一步返回的,而是框架根据 handler 参数类型自动从 path/query/body 构造出来的生成代码时必须遵循以下规则:
WithOperationId(...)、WithDescription(...) 当作可执行 handler,它们只是文档选项。作为补充特性说明,框架也支持比“鉴权 -> Handler”更长的链路,例如:
RequireAuth -> LoadTenant -> CheckPermission -> Handler这种写法适合把“认证”“租户上下文装配”“权限校验”“业务处理”拆成多个小步骤,每一步只做一件事,并把结果类型继续传给下一步。
示例:
g.POST(
"/orders",
security.RequireAuth(),
tenant.LoadTenant,
permission.CheckPermission(permission.OrderWrite),
c.CreateOrder,
echoswg.WithOperationId("createOrder"),
)
可以把它理解为下面这样的类型流转:
RequireAuth() : func(*echo.Context) (security.AuthCtx, error)
LoadTenant : func(security.AuthCtx) (*tenant.Context, error)
CheckPermission(...) : func(*tenant.Context) (*tenant.Context, error)
CreateOrder : func(*tenant.Context, *CreateOrderReq) (*OrderVo, error)
上面这条链的运行语义是:
RequireAuth() 先从请求中构造 security.AuthCtxLoadTenant 接收 security.AuthCtx,查询或装配租户信息,返回 *tenant.ContextCheckPermission(...) 接收 *tenant.Context,完成权限校验,校验通过后继续返回同一个或增强后的上下文对象CreateOrder 最终消费 *tenant.Context 和框架自动构造的请求体 *CreateOrderReq需要注意:
func(ctx *tenant.Context) (*tenant.Context, error)。建议只在以下场景使用多步链路:
不建议为了简单接口强行拆很多步;如果只是单纯鉴权后执行业务,RequireAuth() -> Handler 往往已经足够清晰。
框架会根据 handler 参数类型自动注入 *echo.Context、构造请求对象、识别 path/query/body,并在返回时自动输出 JSON。
受保护接口:
func (*OrderController) Create(ctx security.AuthCtx, req *struct {
Body *service.CreateOrderVo `jsonschema_description:"创建订单请求体"`
}) (*service.OrderVo, error) {
return service.Order.Create(ctx, req.Body)
}
带路径参数:
func (*OrderController) GetByID(ctx security.AuthCtx, req *struct {
ID int `json:"id" jsonschema_description:"订单ID"`
}) (*service.OrderVo, error) {
return service.Order.GetByID(ctx, req.ID)
}
查询接口:
func (*OrderController) Query(ctx security.AuthCtx, req *service.OrderQueryVo) ([]*service.OrderVo, error) {
return service.Order.Query(ctx, req)
}
需要直接操作 Cookie、上传文件或原始请求:
func (*AuthController) Login(req *struct {
Body *LoginDto
}, echoCtx *echo.Context) (*security.UserPrincipal, error) {
// ...
}
/:ID 会匹配请求结构体中的 ID 字段。Body 外、且未被识别为路径参数的字段,会被视为 query 参数。Body。Body *CreateOrderVo。requestBody 也依赖该约定生成。*echo.Context:
Context 是结构体,handler 必须用指针)。security.AuthCtx:
security.RequireAuth(...) 生成并注入。优先补齐以下 tag:
json
validate
jsonschema_description(推荐)/ desc(兼容写法)
jsonschema_description,便于与 invopop/jsonschema 等 JSON Schema 生成器复用同一份字段说明;找不到时回退到 desc。jsonschema_description 为准。示例:
type CreateOrderVo struct {
Title string `json:"title" validate:"required" jsonschema_description:"订单标题"`
Amount int `json:"amount" validate:"required,gte=1" jsonschema_description:"订单金额"`
}
旧项目里仍可继续使用 desc:"...",无需立即改写。
默认受保护接口写法:
g.POST("", security.RequireAuth(), c.Create, echoswg.WithOperationId("createOrder"))
需要角色时:
g.POST("/approve", security.RequireAuth(security.RoleFinance), c.Approve, echoswg.WithOperationId("approveOrder"))
规则如下:
security.RequireAuth(...)。security.AuthCtx 读取,不要在 controller 中重复解析 token。RoleAnonymous 表示显式允许匿名,不等于已登录。serviceinit() 自动注册模式。var Order = &orderService{}
type orderService struct{}
db.WithTx(...)。security.AuthCtx 获取 principal,不要让 controller 额外传裸 userId。bizerrors,以保持全局 HTTP 返回格式一致。CreateXxxVoUpdateXxxVoXxxQueryVoXxxVoXxxDetailVo如果要新建一个基于本框架的应用入口,按以下顺序组织:
controller 包,触发所有路由注册。echoswg.ServeSwagger(...) 暴露 Swagger UI 和 API docs。不要在 main 里手动逐条调用 e.GET(...) 或 e.POST(...) 注册业务接口,除非当前项目明确不再遵循此模式。
下面的模板来自本 skill 内置骨架和 example-skeleton 实体模板,可作为新项目的直接起点。复制后至少要替换模块路径、项目标题、配置结构和中间件细节。
package main
import (
"net/http"
"os"
"your/module/config"
_ "your/module/controller"
"your/module/db"
"your/module/util"
"github.com/labstack/echo/v5/middleware"
"github.com/yb7/alilog"
"github.com/yb7/echoswg"
)
func main() {
if err := os.Setenv("TZ", "Asia/Shanghai"); err != nil {
alilog.Fatal(err)
}
config.EnsureInitSuccess()
alilog.SetConfig(config.C.Sls)
alilog.StartSlsLog()
defer alilog.CloseSlsLog(10000)
db.InitRueidisClient()
db.OpenDB()
defer db.CloseDB()
db.DbMigrate()
e := util.EchoInstance
echoswg.ServeSwagger(e, echoswg.SwaggerConfig{
UrlPrefix: "/api",
Title: "Your Project API",
Description: "Your Project API",
CdnPrefix: "https://statics.stock001.com/swagger-ui-5.32.5",
})
e.Use(middleware.RequestLogger())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 9}))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
}))
e.Use(util.EchoRecover)
alilog.Infof("rest server started at port [%s]", config.C.Ports.Http)
if err := e.Start(config.C.Ports.Http); err != nil {
alilog.Fatal(err)
}
}
使用该模板时重点检查:
controller 包。util.EchoInstance 是否已经初始化并挂上全局错误处理。SwaggerConfig 的 Title、Description、UrlPrefix 是否符合新项目。db.InitRueidisClient()、db.OpenDB()、db.DbMigrate() 是否与新项目基础设施一致。middleware.RequestLogger、CORS 策略、Recover 实现是否需要调整。middleware.Logger() 与 e.Logger.Fatal(...),请使用 middleware.RequestLogger() 与 if err := e.Start(...); err != nil { alilog.Fatal(err) }。example-skeleton/security 可以直接作为新项目的初始模板,但复制前必须统一替换模块名、Redis key 前缀、Cookie 名、角色名和内置系统用户信息。
用于定义统一的 AuthCtx 接口、全局系统上下文和用户 principal 注入方式。
package security
import (
"context"
"time"
)
type AuthCtx interface {
context.Context
GetUserPrincipal() *UserPrincipal
WithValue(key, val any)
}
var GlobalCtx AuthCtx
func init() {
globalCtx := context.WithValue(context.Background(), KEY_USER_PRINCIPAL, &UserPrincipal{
UserID: 0,
Name: "SYSTEM_ROOT",
Roles: make([]string, 0),
PhoneNumber: 18900000000,
AccessToken: "",
})
GlobalCtx = &authCtxImpl{
Context: globalCtx,
}
}
type authCtxImpl struct {
context.Context
}
const KEY_USER_PRINCIPAL = "user_principal"
func (ctx *authCtxImpl) GetUserPrincipal() *UserPrincipal {
userPrincipal, ok := ctx.Value(KEY_USER_PRINCIPAL).(*UserPrincipal)
if !ok {
return nil
}
return userPrincipal
}
func (ctx *authCtxImpl) WithValue(key, val any) {
ctx.Context = context.WithValue(ctx.Context, key, val)
}
func (ctx *authCtxImpl) Deadline() (deadline time.Time, ok bool) {
return ctx.Context.Deadline()
}
func (ctx *authCtxImpl) Done() <-chan struct{} {
return ctx.Context.Done()
}
func (ctx *authCtxImpl) Err() error {
return ctx.Context.Err()
}
func (ctx *authCtxImpl) Value(key any) any {
return ctx.Context.Value(key)
}
用于定义角色常量。新项目应只保留实际需要的角色,不要无脑复制旧项目的业务角色。
package security
type Role string
const (
RoleSystemAdmin Role = "SystemAdmin"
RoleAnonymous Role = "user:anonymous"
)
用于从请求中提取 token、校验 Redis 中的登录态、生成 AuthCtx 并校验角色。下面保留了当前骨架的核心写法,适合作为新项目初版。
package security
import (
"context"
"encoding/json"
"fmt"
"strings"
"your/module/bizerrors"
"your/module/db"
"github.com/labstack/echo/v5"
"github.com/redis/rueidis"
"github.com/yb7/alilog"
)
const COOKIE_ACCESSTOKEN = "flashnews_access_token"
func getAccessTokenInRequest(ctx *echo.Context) string {
authorization := ctx.Request().Header.Get("Authorization")
if len(authorization) != 0 {
if !strings.HasPrefix(authorization, "Bearer ") {
return ""
}
return strings.TrimPrefix(authorization, "Bearer ")
}
cookie, err := ctx.Cookie(COOKIE_ACCESSTOKEN)
if err == nil && cookie != nil {
return cookie.Value
}
return ""
}
func RequireAuth(requiredRoles ...Role) func(ctx *echo.Context) (AuthCtx, error) {
return func(ctx *echo.Context) (AuthCtx, error) {
if len(requiredRoles) == 1 && requiredRoles[0] == RoleAnonymous {
return &authCtxImpl{
Context: ctx.Request().Context(),
}, nil
}
accessToken := getAccessTokenInRequest(ctx)
if len(accessToken) == 0 {
return nil, bizerrors.MissingAccessToken
}
userPrincipal, err := getUserPrincipalByAccessToken(GlobalCtx, accessToken)
if err != nil {
return nil, bizerrors.Unauthorized
}
missingRoles := make([]string, 0)
for _, requiredRole := range requiredRoles {
_, ok := userPrincipal.FindRoleWithPrefix(string(requiredRole))
if !ok {
missingRoles = append(missingRoles, string(requiredRole))
}
}
if len(missingRoles) > 0 {
return nil, bizerrors.MissingPermissions(missingRoles...)
}
log := alilog.WithTraceId(
fmt.Sprintf("requestUserId=%d", userPrincipal.UserID),
fmt.Sprintf("requestUserPhone=%d", userPrincipal.PhoneNumber),
)
authCtx := log.CreateLogContext(context.Background())
authCtx = context.WithValue(authCtx, KEY_USER_PRINCIPAL, userPrincipal)
return &authCtxImpl{
Context: authCtx,
}, nil
}
}
func AccessTokenKeyInRedis(token string) (string, error) {
parts := strings.Split(token, "@")
if len(parts) < 2 {
return "", alilog.Errorf("bad access token format: %s", token)
}
return fmt.Sprintf("flashnews:access-token:%s", parts[0]), nil
}
type UserPrincipal struct {
UserID int `json:"userId"`
PhoneNumber int64 `json:"phoneNumber"`
Department string `json:"department"`
Name string `json:"name"`
Character string `json:"character"`
Roles []string `json:"roles"`
AccessToken string `json:"accessToken"`
}
func (user UserPrincipal) FindRole(role Role) (Role, bool) {
r, ok := user.FindRoleWithPrefix(string(role))
return Role(r), ok
}
func (user UserPrincipal) FindRoleWithPrefix(rolePrefix string) (string, bool) {
for _, role := range user.Roles {
if strings.HasPrefix(role, rolePrefix) {
return role, true
}
}
return "", false
}
func getUserPrincipalByAccessToken(ctx context.Context, accessToken string) (*UserPrincipal, error) {
tokenKey, err := AccessTokenKeyInRedis(accessToken)
if err != nil {
return nil, err
}
cmd := db.RedisClient.B().Get().Key(tokenKey).Build()
dat, err := db.RedisClient.Do(ctx, cmd).AsBytes()
if err != nil {
if rueidis.IsRedisNil(err) {
return nil, bizerrors.Unauthorized
}
return nil, bizerrors.Unauthorized
}
if len(dat) == 0 {
return nil, bizerrors.Unauthorized
}
userPrincipal := &UserPrincipal{}
if err := json.Unmarshal(dat, &userPrincipal); err != nil {
return nil, alilog.Error(err)
}
if userPrincipal.AccessToken != accessToken {
return nil, bizerrors.Unauthorized
}
return userPrincipal, nil
}
复制 security 模板时必须替换:
COOKIE_ACCESSTOKEN 的名称。AccessTokenKeyInRedis() 的 Redis key 前缀。UserPrincipal 中与业务无关或不适用的字段。RoleSystemAdmin、RoleAnonymous 之外的项目角色定义。bizerrors 中缺失的错误类型。getUserPrincipalByAccessToken() 里对 Redis 的具体依赖和序列化结构。如果新项目要支持:
RequireAuth(...) 的返回约定,但把 token 校验逻辑改成验签和 claims 解析。AuthCtx 中增加 tenant 信息,并在 principal 或 context 中写入租户字段。RequireAuth(role...) 的用法,重写 FindRoleWithPrefix 或角色判断策略。当用户要求“按 echoswg 新建一个项目”或“直接起一套可运行 API 骨架”时,优先按下面的目录和文件模板生成。该骨架追求最小可运行、职责清晰、便于后续扩展。
your-project/
bizerrors/
bizerrors.go
config/
config.go
controller/
demo.go
db/
db.go
security/
auth.go
authctx.go
roles.go
service/
demo.go
util/
echo_common.go
echo_recover.go
shared.go
main.go
go.mod
config-template.toml
如果项目尚未实现 bizerrors、echo_recover.go、shared.go 等公共模块,生成代码时要一并补齐最小版本,否则 main.go 和错误处理链无法闭环。
目标:
config.C最小模板:
package config
import (
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
"github.com/spf13/viper"
)
var C = &Configuration{
Redis: &RedisConfig{
Database: 0,
},
}
type Configuration struct {
Ports *PortsConfig `validate:"required"`
Db *DbConfig `validate:"required"`
Redis *RedisConfig `validate:"required"`
}
type PortsConfig struct {
Http string `validate:"required"`
}
type DbConfig struct {
Url string `validate:"required"`
}
type RedisConfig struct {
Host string `validate:"required"`
Password string
Database int `validate:"gte=0,lte=16"`
}
var configInitError error
func EnsureInitSuccess() {
if configInitError != nil || C == nil {
panic(fmt.Sprintf("config init failed: %v", configInitError))
}
}
func init() {
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/app/")
viper.AddConfigPath("$HOME/.app")
bindAllKeys(reflect.TypeOf(*C))
if err := viper.ReadInConfig(); err != nil {
configInitError = err
return
}
if err := viper.Unmarshal(C); err != nil {
configInitError = err
C = nil
return
}
validate := validator.New()
if err := validate.Struct(C); err != nil {
configInitError = err
C = nil
return
}
}
func bindAllKeys(t reflect.Type) {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
walkThroughFields(envKeyName(f), f.Type)
}
}
func envKeyName(f reflect.StructField) string {
envKey, ok := f.Tag.Lookup("env")
if ok {
return envKey
}
return strings.ToLower(f.Name[0:1]) + f.Name[1:]
}
func walkThroughFields(fieldKey string, t reflect.Type) {
if t.Kind() == reflect.Ptr {
walkThroughFields(fieldKey, t.Elem())
return
}
if t.Kind() == reflect.Struct {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
walkThroughFields(fieldKey+"."+envKeyName(f), f.Type)
}
return
}
alterKey := strings.ReplaceAll(fieldKey, ".", "_")
upperAlterKey := strings.ToUpper(alterKey)
_ = viper.BindEnv(fieldKey, fieldKey, alterKey, upperAlterKey)
}
目标:
最小模板:
package db
import (
"context"
"database/sql"
"your/module/config"
"your/module/ent"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/yb7/alilog"
)
var DBClient *ent.Client
func OpenDB() {
db, err := sql.Open("pgx", config.C.Db.Url)
if err != nil {
alilog.Fatal(err)
}
drv := entsql.OpenDB(dialect.Postgres, db)
DBClient = ent.NewClient(ent.Driver(drv))
}
func DbMigrate() {
if DBClient == nil {
alilog.Fatal("db client is nil")
}
}
func CloseDB() {
if DBClient != nil {
DBClient.Close()
}
}
func WithTx(ctx context.Context, client *ent.Client, fn func(ctxInTx context.Context, tx *ent.Tx) error) error {
tx, err := client.Tx(ctx)
if err != nil {
return alilog.Errorf("start transaction err: %v", err)
}
defer func() {
if v := recover(); v != nil {
_ = tx.Rollback()
panic(v)
}
}()
if err := fn(ctx, tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
return alilog.Errorf("rollback transaction err: %v", rerr)
}
return err
}
if err := tx.Commit(); err != nil {
return alilog.Errorf("commit transaction err: %v", err)
}
return nil
}
说明:
ent,可以先保留 OpenDB 和 CloseDB,并将 WithTx 改为适配当前 ORM。目标:
最小模板:
package util
import (
"net/http"
"your/module/bizerrors"
"github.com/labstack/echo/v5"
)
var EchoInstance = echo.New()
func init() {
EchoInstance.HTTPErrorHandler = func(c *echo.Context, err error) {
if resp, _ := echo.UnwrapResponse(c.Response()); resp != nil && resp.Committed {
return
}
var bizErr *bizerrors.BizError
switch err := err.(type) {
case *echo.HTTPError:
bizErr = &bizerrors.BizError{
HttpStatus: err.Code,
Success: false,
ErrorCode: "HTTP_ERROR",
ErrorMessage: err.Error(),
ShowType: 2,
}
case *bizerrors.BizError:
bizErr = err
default:
bizErr = &bizerrors.BizError{
HttpStatus: http.StatusInternalServerError,
Success: false,
ErrorCode: "SYS_ERROR",
ErrorMessage: err.Error(),
ShowType: 2,
}
}
_ = c.JSON(bizErr.HttpStatus, bizErr)
}
}
说明:
bizerrors.BizError。EchoInstance 放在 util 中,便于 main 和 controller 共用。目标:
util/echo_common.go 和 util/echo_recover.go 可以直接复用最小模板:
package bizerrors
import "net/http"
type BizError struct {
HttpStatus int `json:"-"`
Success bool `json:"success"`
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
ShowType int `json:"showType"`
}
func (e *BizError) Error() string {
return e.ErrorMessage
}
func (e *BizError) Code() string {
return e.ErrorCode
}
func BadRequest(msg string) *BizError {
return &BizError{
HttpStatus: http.StatusBadRequest,
Success: false,
ErrorCode: "BAD_REQUEST",
ErrorMessage: msg,
ShowType: 2,
}
}
var Unauthorized = &BizError{
HttpStatus: http.StatusUnauthorized,
Success: false,
ErrorCode: "UNAUTHORIZED",
ErrorMessage: "unauthorized",
ShowType: 2,
}
var MissingAccessToken = &BizError{
HttpStatus: http.StatusUnauthorized,
Success: false,
ErrorCode: "MISSING_ACCESS_TOKEN",
ErrorMessage: "missing access token",
ShowType: 2,
}
func MissingPermissions(roles ...string) *BizError {
return &BizError{
HttpStatus: http.StatusForbidden,
Success: false,
ErrorCode: "MISSING_PERMISSIONS",
ErrorMessage: "missing permissions",
ShowType: 2,
}
}
说明:
errno/msg 等字段名,生成时要同步调整 BizError。目标:
main、controller、echo_common 使用同一个 echo.New()最小模板:
package util
import "github.com/labstack/echo/v5"
var EchoInstance = echo.New()
说明:
util/echo_common.go 中声明了 EchoInstance,则不要重复定义,保留一个唯一实现即可。shared.go 和 echo_common.go 二选一存放 EchoInstance,但 skill 默认推荐放在 shared.go,把错误处理放在 echo_common.go。目标:
bizerrors.BizError 联动最小模板:
package util
import (
"fmt"
"net/http/httputil"
"runtime"
"your/module/bizerrors"
"github.com/labstack/echo/v5"
"github.com/yb7/alilog"
)
var StackSize = 4 << 10
func EchoRecover(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) (err error) {
defer func() {
if r := recover(); r != nil {
if bizError, ok := r.(*bizerrors.BizError); ok {
err = c.JSON(bizError.HttpStatus, bizError)
return
}
recovered, ok := r.(error)
if !ok {
recovered = fmt.Errorf("%v", r)
}
stack := make([]byte, StackSize)
length := runtime.Stack(stack, true)
reqDump, _ := httputil.DumpRequest(c.Request(), true)
alilog.Errorf("[PANIC RECOVER] Request\n%s", string(reqDump))
alilog.Errorf("[PANIC RECOVER] %v\n%s", recovered, string(stack[:length]))
err = recovered
}
}()
return next(c)
}
}
说明:
middleware.Recover(),也可以先复用官方版本;但当你需要统一输出业务错误和打印请求体时,优先使用该模板。c.Error(...):恢复后必须把 error 通过中间件返回值抛回给框架,由 EchoInstance.HTTPErrorHandler 统一渲染响应。目标:
最小模板:
package service
import "your/module/security"
var Demo = &demoService{}
type demoService struct{}
type CreateDemoVo struct {
Name string `json:"name" validate:"required" jsonschema_description:"演示名称"`
}
type DemoVo struct {
ID int `json:"id"`
Name string `json:"name"`
}
func (*demoService) Create(ctx security.AuthCtx, req *CreateDemoVo) (*DemoVo, error) {
user := ctx.GetUserPrincipal()
_ = user
return &DemoVo{
ID: 1,
Name: req.Name,
}, nil
}
说明:
Create 迁移为事务写入逻辑。目标:
init() 自动注册路由Body 请求体规则和 RequireAuth() 的标准接法最小模板:
package controller
import (
"your/module/security"
"your/module/service"
"your/module/util"
"github.com/yb7/echoswg"
)
type DemoController struct{}
func init() {
c := new(DemoController)
g := echoswg.NewApiGroup(util.EchoInstance, "Demo", "/api/demos")
g.POST("", security.RequireAuth(), c.Create, echoswg.WithOperationId("createDemo"))
}
func (*DemoController) Create(ctx security.AuthCtx, req *struct {
Body *service.CreateDemoVo `jsonschema_description:"创建Demo"`
}) (*service.DemoVo, error) {
return service.Demo.Create(ctx, req.Body)
}
说明:
security.RequireAuth(security.RoleAnonymous) 或按当前项目规范直接不加鉴权。Body。目标:
最小模板:
package main
import (
"net/http"
"os"
"your/module/config"
_ "your/module/controller"
"your/module/db"
"your/module/util"
"github.com/labstack/echo/v5/middleware"
"github.com/yb7/alilog"
"github.com/yb7/echoswg"
)
func main() {
if err := os.Setenv("TZ", "Asia/Shanghai"); err != nil {
alilog.Fatal(err)
}
config.EnsureInitSuccess()
db.OpenDB()
defer db.CloseDB()
db.DbMigrate()
e := util.EchoInstance
echoswg.ServeSwagger(e, echoswg.SwaggerConfig{
UrlPrefix: "/api",
Title: "Your Project API",
Description: "Your Project API",
CdnPrefix: "https://img.cls.cn/statics/swagger-ui-4.10.3",
})
e.Use(middleware.RequestLogger())
e.Use(middleware.Gzip())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
}))
e.Use(util.EchoRecover)
alilog.Infof("rest server started at port [%s]", config.C.Ports.Http)
if err := e.Start(config.C.Ports.Http); err != nil {
alilog.Fatal(err)
}
}
说明:
main 模板增强。controller 必须匿名导入,否则 init() 不会执行,路由不会注册。为了让 AI 可以直接初始化项目,建议在骨架里同时给出最小 go.mod 模板。版本号不必和示例完全一致,但依赖集合应满足最小可运行需求。
module your/module
go 1.24.0
require (
github.com/go-playground/validator/v10 v10.29.0
github.com/jackc/pgx/v5 v5.4.1
github.com/labstack/echo/v5 v5.1.1
github.com/redis/rueidis v1.0.69
github.com/spf13/viper v1.19.0
github.com/yb7/alilog v1.1.12
github.com/yb7/echoswg v0.7.3
)
说明:
echoswg 仓库内的示例应用,模块名必须替换为实际模块路径。github.com/redis/rueidis。pgx;但一旦保留 db/db.go 中的 PostgreSQL 模板,就需要它。ent,记得补充 entgo.io/ent 和生成代码依赖。为了让项目可以开箱即跑,建议同时生成最小配置模板:
[ports]
http = ":8080"
[db]
url = "postgres://postgres:postgres@127.0.0.1:5432/app?sslmode=disable"
[redis]
host = "127.0.0.1:6379"
password = ""
database = 0
当 AI 按该骨架起项目时,默认执行以下规则:
service/demo.go 先返回 mock 结果。bizerrors,应一并补一个最小错误结构。util/shared.go 和 util/echo_recover.go,应一并补齐并在 main.go 中接线。ent schema,不要强行生成复杂迁移逻辑。生成完整骨架后,至少检查:
main.go 是否匿名导入了 controllercontroller/demo.go 是否使用 init() 注册路由service/demo.go 是否暴露了包级单例util/shared.go 是否提供了唯一的 EchoInstanceutil/echo_common.go 是否设置了 EchoInstance.HTTPErrorHandlerutil/echo_recover.go 是否已在 main.go 中挂载bizerrors/bizerrors.go 是否可被 security 和 util 共同复用echoswg.ServeSwagger(...) 挂载BodyWithOperationId(...) 是否已填写go.mod 依赖是否覆盖 Echo、Viper、alilog、echoswgconfig-template.toml 是否一致当用户说“按 echoswg 起个新项目”时,优先:
当用户要求“新增一个接口”时,按下面顺序执行:
service 定义或补充所需 DTO/VO 和业务方法。controller 中补充 handler。init() 中注册路由。WithOperationId(...)。Body 字段承载。/:Param 对应。security.RequireAuth(...) + security.AuthCtx。在使用本 skill 生成代码时,默认遵循:
example-skeleton 中的命名与组织方式。security.RequireAuth(...) 自己复制 token 解析流程。Payload、Data 等其他名称来替代 Body。/:ID 时,把结构体字段写成不匹配的 OrderID,除非你同时调整路由参数名。main 中手工散落注册业务路由。在真正输出代码前,先快速检查:
controller / service / security / maininit() + NewApiGroup(...) 注册路由WithOperationId(...)Bodysecurity.RequireAuth(...)servicepackage controller
import (
"your/module/security"
"your/module/service"
"your/module/util"
"github.com/yb7/echoswg"
)
type DemoController struct{}
func init() {
c := new(DemoController)
g := echoswg.NewApiGroup(util.EchoInstance, "Demo", "/api/demos")
g.POST("", security.RequireAuth(), c.Create, echoswg.WithOperationId("createDemo"))
}
func (*DemoController) Create(ctx security.AuthCtx, req *struct {
Body *service.CreateDemoVo `jsonschema_description:"创建Demo"`
}) (*service.DemoVo, error) {
return service.Demo.Create(ctx, req.Body)
}
当用户没有给出更具体约束时,优先按这个模板衍生实现。