mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
refactor(structure): group platform, infra, and shared packages
Reorganize internal packages into platform/infra/shared layers and update imports, docs, and seed-count tests to match current system configs.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package shared 提供跨模块共享的常量、错误定义和通用类型。
|
||||
package shared
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package shared
|
||||
|
||||
// 通用业务错误消息常量
|
||||
const (
|
||||
BannedAccount = "账号已被封禁"
|
||||
UnAuthorized = "未登录"
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AbortBadRequest 以 400 中断请求并将错误挂载到 Gin Error 链,供全局中间件统一记录 Trace 并响应。
|
||||
func AbortBadRequest(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusBadRequest, msg)
|
||||
}
|
||||
|
||||
// AbortUnauthorized 以 401 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortUnauthorized(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusUnauthorized, msg)
|
||||
}
|
||||
|
||||
// AbortForbidden 以 403 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortForbidden(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusForbidden, msg)
|
||||
}
|
||||
|
||||
// AbortNotFound 以 404 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortNotFound(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusNotFound, msg)
|
||||
}
|
||||
|
||||
// AbortInternal 以 500 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortInternal(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusInternalServerError, msg)
|
||||
}
|
||||
|
||||
// AbortTooManyRequests 以 429 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortTooManyRequests(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusTooManyRequests, msg)
|
||||
}
|
||||
|
||||
// AbortConflict 以 409 中断请求并将错误挂载到 Gin Error 链。
|
||||
func AbortConflict(c *gin.Context, msg string) {
|
||||
AbortWithError(c, http.StatusConflict, msg)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// ErrorHandlerMiddleware 捕获 c.Errors 并统一格式化为 JSON 返回给客户端,同时将其记录到 Span 异常中。
|
||||
// 与 AbortWithError / AbortBadRequest 等配合使用,是全局 OTel 友好错误响应的唯一出口。
|
||||
func ErrorHandlerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
if len(c.Errors) == 0 || c.Writer.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
err := c.Errors.Last().Err
|
||||
span := trace.SpanFromContext(c.Request.Context())
|
||||
if span.IsRecording() {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
}
|
||||
|
||||
var apiErr *APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
c.JSON(apiErr.Code, Err(apiErr.Msg))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, Err("内部系统错误"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func TestAbortWithError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
AbortWithError(c, http.StatusBadRequest, "invalid input")
|
||||
|
||||
require.Len(t, c.Errors, 1)
|
||||
|
||||
var apiErr *APIError
|
||||
require.True(t, errors.As(c.Errors.Last().Err, &apiErr))
|
||||
assert.Equal(t, http.StatusBadRequest, apiErr.Code)
|
||||
assert.Equal(t, "invalid input", apiErr.Msg)
|
||||
assert.True(t, c.IsAborted())
|
||||
}
|
||||
|
||||
func TestErrorHandlerMiddleware_APIErrorStatusCodes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
message string
|
||||
abort func(*gin.Context, string)
|
||||
}{
|
||||
{"400 Bad Request", http.StatusBadRequest, "bad request", AbortBadRequest},
|
||||
{"401 Unauthorized", http.StatusUnauthorized, "unauthorized", AbortUnauthorized},
|
||||
{"403 Forbidden", http.StatusForbidden, "forbidden", AbortForbidden},
|
||||
{"404 Not Found", http.StatusNotFound, "not found", AbortNotFound},
|
||||
{"409 Conflict", http.StatusConflict, "conflict", AbortConflict},
|
||||
{"429 Too Many Requests", http.StatusTooManyRequests, "too many requests", AbortTooManyRequests},
|
||||
{"500 Internal Server Error", http.StatusInternalServerError, "internal error", AbortInternal},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandlerMiddleware())
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
tc.abort(c, tc.message)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, tc.statusCode, w.Code)
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
var body Response[any]
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
assert.Equal(t, tc.message, body.ErrorMsg)
|
||||
assert.Nil(t, body.Data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorHandlerMiddleware_SkipsWhenNoErrors(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandlerMiddleware())
|
||||
r.GET("/ok", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, OK("success"))
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/ok", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var body Response[string]
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
assert.Equal(t, "success", body.Data)
|
||||
assert.Empty(t, body.ErrorMsg)
|
||||
}
|
||||
|
||||
func TestErrorHandlerMiddleware_SkipsWhenResponseAlreadyWritten(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandlerMiddleware())
|
||||
r.GET("/written", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, OKNil())
|
||||
_ = c.Error(NewError(http.StatusBadRequest, "should not overwrite"))
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/written", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var body Response[any]
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
assert.Empty(t, body.ErrorMsg)
|
||||
assert.Nil(t, body.Data)
|
||||
}
|
||||
|
||||
func TestErrorHandlerMiddleware_FallbackForNonAPIError(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandlerMiddleware())
|
||||
r.GET("/plain", func(c *gin.Context) {
|
||||
_ = c.Error(errors.New("plain error"))
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/plain", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
var body Response[any]
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
|
||||
assert.Equal(t, "内部系统错误", body.ErrorMsg)
|
||||
assert.Nil(t, body.Data)
|
||||
}
|
||||
|
||||
func TestErrorHandlerMiddleware_RecordsSpanOnAPIError(t *testing.T) {
|
||||
sr := tracetest.NewSpanRecorder()
|
||||
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
|
||||
otel.SetTracerProvider(tp)
|
||||
defer otel.SetTracerProvider(trace.NewNoopTracerProvider())
|
||||
|
||||
tracer := tp.Tracer("test")
|
||||
ctx, span := tracer.Start(context.Background(), "request")
|
||||
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandlerMiddleware())
|
||||
r.GET("/err", func(c *gin.Context) {
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
AbortBadRequest(c, "bad request")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/err", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
span.End()
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
spans := sr.Ended()
|
||||
require.Len(t, spans, 1)
|
||||
assert.Equal(t, codes.Error, spans[0].Status().Code)
|
||||
assert.Equal(t, "bad request", spans[0].Status().Description)
|
||||
require.NotEmpty(t, spans[0].Events())
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2025 linux.do
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package response provides shared HTTP API response structures.
|
||||
package response
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Response 通用响应体
|
||||
type Response[T any] struct {
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
// Any 用于 Swagger 文档的响应类型(非泛型)
|
||||
// swag 不支持泛型,使用此类型替代 Response[T]
|
||||
type Any struct {
|
||||
ErrorMsg string `json:"error_msg" example:""`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// APIError 统一的 API 业务错误类型,可被全局错误处理中间件捕获
|
||||
type APIError struct {
|
||||
Code int
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// NewError 实例化一个 APIError
|
||||
func NewError(code int, msg string) *APIError {
|
||||
return &APIError{Code: code, Msg: msg}
|
||||
}
|
||||
|
||||
// AbortWithError 将 API 错误挂载到 Gin Context 并中断执行流
|
||||
func AbortWithError(c *gin.Context, code int, msg string) {
|
||||
_ = c.Error(NewError(code, msg))
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// OK 构造成功响应
|
||||
func OK[T any](data T) Response[T] {
|
||||
return Response[T]{Data: data}
|
||||
}
|
||||
|
||||
// OKNil 构造成功响应(data 为 null)
|
||||
func OKNil() Response[any] {
|
||||
return Response[any]{Data: nil}
|
||||
}
|
||||
|
||||
// Err 构造错误响应
|
||||
func Err(msg string) Response[any] {
|
||||
return Response[any]{ErrorMsg: msg, Data: nil}
|
||||
}
|
||||
Reference in New Issue
Block a user