diff --git a/README.md b/README.md index 45f225a..f243431 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,13 @@ docker run -p 8080:8080 --env-file .env ez-api ### 本地联合开发(配合 balancer) -- 目录结构建议:`/workspace/` 下并列放置 `ez-api` 与 `balancer`。 -- 初始化/更新 Go 工作区(Go 1.20+):在 `/workspace` 执行 +- 目录结构建议:`/workspace/` 下并列放置 `ez-api`、`balancer`、`foundation`。 +- 初始化本地 Go 工作区(Go 1.20+,不要求提交到任一仓库):在 `/workspace` 执行 ```bash - go work use ./ez-api ./ez-api/test ./balancer + go work init + go work use ./ez-api ./balancer ./foundation ``` - 仓库已附带 `go.work`,若路径一致可直接复用;若放在其他位置请按上面命令重建。 + 如果你只使用已发布的 `github.com/ez-api/foundation vX.Y.Z`,则不需要 `go.work`。 ### 集成测试 @@ -97,12 +98,17 @@ cd ez-api ## 日志 - 业务代码统一使用标准库 `log/slog`(`logger.Info("msg", "k", v)` 风格)。 -- 输出后端仍为 [zerolog](https://github.com/rs/zerolog)(通过 `internal/logging` 的 slog handler bridge),默认 ConsoleWriter。 +- 输出后端仍为 [zerolog](https://github.com/rs/zerolog)(通过 `github.com/ez-api/foundation/logging` 的 slog handler bridge),默认 ConsoleWriter。 - 通过 `EZ_LOG_LEVEL` 控制控制平面的日志等级,配合异步 DB 写入(LogWriter)一起使用。 ## JSON -- 项目内 JSON 编解码统一走 `ez-api/internal/jsoncodec`(内部使用 Sonic)。 +- 项目内 JSON 编解码统一走 `github.com/ez-api/foundation/jsoncodec`(内部使用 Sonic)。 + +## 依赖约定 + +- Control Plane(ez-api)与 Data Plane(balancer)共享一部分“协议约定/基础设施”代码(JSON、日志、provider type 规则),统一沉淀在 `github.com/ez-api/foundation`。 +- 本仓库的 `go.mod` 需要依赖一个可用的 `foundation` 版本:发布后建议锁定到 `vX.Y.Z`;本地联调可使用 `go.work`(见下文)。 ## 设计决策 diff --git a/cmd/server/main.go b/cmd/server/main.go index 78b103e..8457bbb 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -12,10 +12,10 @@ import ( _ "github.com/ez-api/ez-api/docs" "github.com/ez-api/ez-api/internal/api" "github.com/ez-api/ez-api/internal/config" - "github.com/ez-api/ez-api/internal/logging" "github.com/ez-api/ez-api/internal/middleware" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" + "github.com/ez-api/foundation/logging" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" swaggerFiles "github.com/swaggo/files" diff --git a/go.mod b/go.mod index 8e9eae0..d68c9a0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.5 require ( github.com/bytedance/sonic v1.14.0 + github.com/ez-api/foundation v0.0.0 github.com/gin-gonic/gin v1.11.0 github.com/redis/go-redis/v9 v9.17.2 github.com/rs/zerolog v1.34.0 diff --git a/internal/api/handler.go b/internal/api/handler.go index 09d99f3..ddae8ca 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -8,6 +8,7 @@ import ( "github.com/ez-api/ez-api/internal/dto" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" + "github.com/ez-api/foundation/provider" "github.com/gin-gonic/gin" "gorm.io/gorm" ) @@ -43,6 +44,9 @@ func (h *Handler) CreateProvider(c *gin.Context) { return } + providerType := provider.NormalizeType(req.Type) + googleLocation := provider.DefaultGoogleLocation(providerType, req.GoogleLocation) + group := strings.TrimSpace(req.Group) if group == "" { group = "default" @@ -59,11 +63,11 @@ func (h *Handler) CreateProvider(c *gin.Context) { provider := model.Provider{ Name: req.Name, - Type: req.Type, + Type: strings.TrimSpace(req.Type), BaseURL: req.BaseURL, APIKey: req.APIKey, GoogleProject: strings.TrimSpace(req.GoogleProject), - GoogleLocation: strings.TrimSpace(req.GoogleLocation), + GoogleLocation: googleLocation, Group: group, Models: strings.Join(req.Models, ","), Status: status, @@ -122,12 +126,18 @@ func (h *Handler) UpdateProvider(c *gin.Context) { return } + nextType := strings.TrimSpace(existing.Type) + if t := strings.TrimSpace(req.Type); t != "" { + nextType = t + } + nextTypeLower := provider.NormalizeType(nextType) + update := map[string]any{} if strings.TrimSpace(req.Name) != "" { update["name"] = req.Name } if strings.TrimSpace(req.Type) != "" { - update["type"] = req.Type + update["type"] = strings.TrimSpace(req.Type) } if strings.TrimSpace(req.BaseURL) != "" { update["base_url"] = req.BaseURL @@ -140,6 +150,8 @@ func (h *Handler) UpdateProvider(c *gin.Context) { } if strings.TrimSpace(req.GoogleLocation) != "" { update["google_location"] = strings.TrimSpace(req.GoogleLocation) + } else if provider.IsVertexFamily(nextTypeLower) && strings.TrimSpace(existing.GoogleLocation) == "" { + update["google_location"] = provider.DefaultGoogleLocation(nextTypeLower, "") } if req.Models != nil { update["models"] = strings.Join(req.Models, ",") diff --git a/internal/jsoncodec/jsoncodec.go b/internal/jsoncodec/jsoncodec.go deleted file mode 100644 index b160289..0000000 --- a/internal/jsoncodec/jsoncodec.go +++ /dev/null @@ -1,17 +0,0 @@ -package jsoncodec - -import ( - "io" - - "github.com/bytedance/sonic" -) - -func Marshal(v any) ([]byte, error) { return sonic.Marshal(v) } - -func Unmarshal(data []byte, v any) error { return sonic.Unmarshal(data, v) } - -func UnmarshalString(data string, v any) error { return sonic.UnmarshalString(data, v) } - -func NewEncoder(w io.Writer) sonic.Encoder { return sonic.ConfigDefault.NewEncoder(w) } - -func NewDecoder(r io.Reader) sonic.Decoder { return sonic.ConfigDefault.NewDecoder(r) } diff --git a/internal/logging/logging.go b/internal/logging/logging.go deleted file mode 100644 index e43ef5a..0000000 --- a/internal/logging/logging.go +++ /dev/null @@ -1,63 +0,0 @@ -package logging - -import ( - "log/slog" - "os" - "strings" - "time" - - "github.com/rs/zerolog" -) - -type Options struct { - Service string -} - -func New(opts Options) (*slog.Logger, zerolog.Logger) { - level := parseLevel(strings.TrimSpace(os.Getenv("EZ_LOG_LEVEL"))) - zerolog.SetGlobalLevel(toZerologLevel(level)) - - output := zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.RFC3339, - } - - zl := zerolog.New(output). - Level(toZerologLevel(level)). - With(). - Timestamp(). - Str("service", strings.TrimSpace(opts.Service)). - Logger() - - sl := slog.New(NewZerologHandler(zl, level)) - slog.SetDefault(sl) - return sl, zl -} - -func parseLevel(raw string) slog.Level { - switch strings.ToLower(raw) { - case "debug": - return slog.LevelDebug - case "warn", "warning": - return slog.LevelWarn - case "error": - return slog.LevelError - case "info", "": - return slog.LevelInfo - default: - return slog.LevelInfo - } -} - -func toZerologLevel(level slog.Level) zerolog.Level { - switch { - case level <= slog.LevelDebug: - return zerolog.DebugLevel - case level <= slog.LevelInfo: - return zerolog.InfoLevel - case level <= slog.LevelWarn: - return zerolog.WarnLevel - default: - return zerolog.ErrorLevel - } -} diff --git a/internal/logging/zerolog_handler.go b/internal/logging/zerolog_handler.go deleted file mode 100644 index d88d304..0000000 --- a/internal/logging/zerolog_handler.go +++ /dev/null @@ -1,133 +0,0 @@ -package logging - -import ( - "context" - "fmt" - "log/slog" - "strings" - - "github.com/rs/zerolog" -) - -type ZerologHandler struct { - logger zerolog.Logger - level slog.Level - attrs []slog.Attr - groups []string -} - -func NewZerologHandler(logger zerolog.Logger, level slog.Level) *ZerologHandler { - return &ZerologHandler{ - logger: logger, - level: level, - } -} - -func (h *ZerologHandler) Enabled(_ context.Context, level slog.Level) bool { - return level >= h.level -} - -func (h *ZerologHandler) Handle(_ context.Context, record slog.Record) error { - event := h.eventFor(record.Level) - if event == nil { - return nil - } - - for _, attr := range h.attrs { - h.addAttr(event, h.key(attr.Key), attr.Value) - } - record.Attrs(func(attr slog.Attr) bool { - h.addAttr(event, h.key(attr.Key), attr.Value) - return true - }) - - event.Msg(record.Message) - return nil -} - -func (h *ZerologHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - cp := h.clone() - cp.attrs = append(cp.attrs, attrs...) - return cp -} - -func (h *ZerologHandler) WithGroup(name string) slog.Handler { - if strings.TrimSpace(name) == "" { - return h - } - cp := h.clone() - cp.groups = append(cp.groups, name) - return cp -} - -func (h *ZerologHandler) clone() *ZerologHandler { - cp := *h - cp.attrs = append([]slog.Attr(nil), h.attrs...) - cp.groups = append([]string(nil), h.groups...) - return &cp -} - -func (h *ZerologHandler) key(k string) string { - k = strings.TrimSpace(k) - if k == "" { - return "" - } - if len(h.groups) == 0 { - return k - } - return strings.Join(h.groups, ".") + "." + k -} - -func (h *ZerologHandler) eventFor(level slog.Level) *zerolog.Event { - switch { - case level >= slog.LevelError: - return h.logger.Error() - case level >= slog.LevelWarn: - return h.logger.Warn() - case level >= slog.LevelInfo: - return h.logger.Info() - default: - return h.logger.Debug() - } -} - -func (h *ZerologHandler) addAttr(event *zerolog.Event, key string, value slog.Value) { - if event == nil || strings.TrimSpace(key) == "" { - return - } - - value = value.Resolve() - - switch value.Kind() { - case slog.KindGroup: - for _, groupAttr := range value.Group() { - groupKey := h.key(key + "." + groupAttr.Key) - h.addAttr(event, groupKey, groupAttr.Value.Resolve()) - } - case slog.KindString: - event.Str(key, value.String()) - case slog.KindBool: - event.Bool(key, value.Bool()) - case slog.KindInt64: - event.Int64(key, value.Int64()) - case slog.KindUint64: - event.Uint64(key, value.Uint64()) - case slog.KindFloat64: - event.Float64(key, value.Float64()) - case slog.KindDuration: - event.Dur(key, value.Duration()) - case slog.KindTime: - event.Time(key, value.Time()) - default: - anyValue := value.Any() - if err, ok := anyValue.(error); ok { - event.AnErr(key, err) - return - } - if stringer, ok := anyValue.(fmt.Stringer); ok { - event.Str(key, stringer.String()) - return - } - event.Interface(key, anyValue) - } -} diff --git a/internal/service/sync.go b/internal/service/sync.go index fa9a902..c2c9b7f 100644 --- a/internal/service/sync.go +++ b/internal/service/sync.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/ez-api/ez-api/internal/jsoncodec" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/util" + "github.com/ez-api/foundation/jsoncodec" "github.com/redis/go-redis/v9" "gorm.io/gorm" )