chore: initial import

This commit is contained in:
phamnazage-jpg
2026-05-12 17:47:32 +08:00
commit fc54ba84b2
104 changed files with 11575 additions and 0 deletions

52
pkg/errors/errors.go Normal file
View File

@@ -0,0 +1,52 @@
package errors
import "fmt"
// AppError 是应用错误结构
type AppError struct {
Code string // OPS_{CATEGORY}_{CODE}
HTTPStatus int
Message string
Detail map[string]any
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// 预定义错误
var (
ErrBadRequest = &AppError{Code: "OPS_GEN_4001", HTTPStatus: 400, Message: "请求参数错误"}
ErrUnauthorized = &AppError{Code: "OPS_GEN_4002", HTTPStatus: 401, Message: "未授权"}
ErrForbidden = &AppError{Code: "OPS_GEN_4003", HTTPStatus: 403, Message: "权限不足"}
ErrNotFound = &AppError{Code: "OPS_GEN_4004", HTTPStatus: 404, Message: "资源不存在"}
ErrConflict = &AppError{Code: "OPS_GEN_4005", HTTPStatus: 409, Message: "资源冲突"}
ErrPayloadTooLarge = &AppError{Code: "OPS_GEN_4006", HTTPStatus: 413, Message: "请求体过大"}
ErrInternal = &AppError{Code: "OPS_GEN_5001", HTTPStatus: 500, Message: "内部服务错误"}
ErrInvalidMetricName = &AppError{Code: "OPS_MET_4001", HTTPStatus: 400, Message: "指标名称无效"}
ErrInvalidTimeRange = &AppError{Code: "OPS_MET_4002", HTTPStatus: 400, Message: "时间范围不合法"}
)
// WithDetail 为错误添加详细信息
func (e *AppError) WithDetail(detail map[string]any) *AppError {
return &AppError{
Code: e.Code,
HTTPStatus: e.HTTPStatus,
Message: e.Message,
Detail: detail,
}
}
// Wrap 包裹原始错误
func Wrap(err error, appErr *AppError) *AppError {
if err == nil {
return nil
}
return &AppError{
Code: appErr.Code,
HTTPStatus: appErr.HTTPStatus,
Message: fmt.Sprintf("%s: %v", appErr.Message, err),
Detail: appErr.Detail,
}
}

45
pkg/errors/errors_test.go Normal file
View File

@@ -0,0 +1,45 @@
package errors
import (
"errors"
"testing"
)
func TestAppErrorErrorIncludesCodeAndMessage(t *testing.T) {
err := &AppError{Code: "OPS_TEST", Message: "failed"}
if got := err.Error(); got != "[OPS_TEST] failed" {
t.Fatalf("unexpected error string: %s", got)
}
}
func TestWithDetailReturnsCopyWithoutMutatingBase(t *testing.T) {
detail := map[string]any{"field": "name"}
err := ErrBadRequest.WithDetail(detail)
if err == ErrBadRequest {
t.Fatal("expected a copy, got original pointer")
}
if err.Code != ErrBadRequest.Code || err.HTTPStatus != ErrBadRequest.HTTPStatus || err.Message != ErrBadRequest.Message {
t.Fatalf("metadata not preserved: %+v", err)
}
if err.Detail["field"] != "name" {
t.Fatalf("detail not attached: %+v", err.Detail)
}
if ErrBadRequest.Detail != nil {
t.Fatalf("base error was mutated: %+v", ErrBadRequest.Detail)
}
}
func TestWrap(t *testing.T) {
if Wrap(nil, ErrInternal) != nil {
t.Fatal("nil input should return nil")
}
wrapped := Wrap(errors.New("boom"), ErrInternal)
if wrapped.Code != ErrInternal.Code || wrapped.HTTPStatus != ErrInternal.HTTPStatus {
t.Fatalf("metadata not preserved: %+v", wrapped)
}
if wrapped.Message != "内部服务错误: boom" {
t.Fatalf("unexpected message: %s", wrapped.Message)
}
}

59
pkg/response/response.go Normal file
View File

@@ -0,0 +1,59 @@
package response
import (
"encoding/json"
"net/http"
"github.com/company/ai-ops/pkg/errors"
)
// Response 是统一响应结构
type Response struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
}
// JSON 返回 JSON 响应
func JSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Success 返回成功响应
func Success(w http.ResponseWriter, data any) {
JSON(w, http.StatusOK, Response{Data: data})
}
// Error 返回错误响应
func Error(w http.ResponseWriter, err *errors.AppError) {
JSON(w, err.HTTPStatus, Response{
Code: err.Code,
Message: err.Message,
})
}
// Paginated 是分页响应结构
type Paginated struct {
Items any `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// PaginatedResponse 返回分页响应
func PaginatedResponse(w http.ResponseWriter, items any, total, page, pageSize int) {
totalPages := total / pageSize
if total%pageSize > 0 {
totalPages++
}
Success(w, Paginated{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
})
}

View File

@@ -0,0 +1,66 @@
package response
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
errorspkg "github.com/company/ai-ops/pkg/errors"
)
func TestJSONWritesStatusContentTypeAndBody(t *testing.T) {
w := httptest.NewRecorder()
JSON(w, http.StatusCreated, map[string]string{"ok": "true"})
if w.Code != http.StatusCreated {
t.Fatalf("status = %d", w.Code)
}
if got := w.Header().Get("Content-Type"); got != "application/json" {
t.Fatalf("content-type = %s", got)
}
if got := w.Body.String(); got != "{\"ok\":\"true\"}\n" {
t.Fatalf("body = %s", got)
}
}
func TestSuccessAndErrorResponses(t *testing.T) {
t.Run("success", func(t *testing.T) {
w := httptest.NewRecorder()
Success(w, map[string]any{"id": "1"})
var out Response
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
data := out.Data.(map[string]any)
if data["id"] != "1" {
t.Fatalf("unexpected data: %+v", out.Data)
}
})
t.Run("error", func(t *testing.T) {
w := httptest.NewRecorder()
Error(w, errorspkg.ErrForbidden)
if w.Code != http.StatusForbidden {
t.Fatalf("status = %d", w.Code)
}
if got := w.Body.String(); got == "" || !json.Valid(w.Body.Bytes()) {
t.Fatalf("invalid json body: %q", got)
}
})
}
func TestPaginatedResponseComputesTotalPages(t *testing.T) {
w := httptest.NewRecorder()
PaginatedResponse(w, []string{"a", "b"}, 21, 2, 10)
var out Response
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatal(err)
}
payload := out.Data.(map[string]any)
if payload["total_pages"].(float64) != 3 {
t.Fatalf("total_pages = %+v", payload["total_pages"])
}
}