chore: initial commit
This commit is contained in:
31
internal/pkg/cache/cache.go
vendored
Normal file
31
internal/pkg/cache/cache.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"server/internal/db/sqlc"
|
||||
|
||||
"github.com/maypok86/otter/v2"
|
||||
)
|
||||
|
||||
type Caches struct {
|
||||
SysUserApisCache *otter.Cache[int32, []db.GetSysUserApisRow]
|
||||
}
|
||||
|
||||
func NewCaches() *Caches {
|
||||
sysUserApisCache := otter.Must(&otter.Options[int32, []db.GetSysUserApisRow]{
|
||||
MaximumSize: 1_000,
|
||||
})
|
||||
|
||||
return &Caches{
|
||||
SysUserApisCache: sysUserApisCache,
|
||||
}
|
||||
}
|
||||
|
||||
// ClearSysUserCache 清理单个用户缓存
|
||||
func (c *Caches) ClearSysUserCache(userID int32) {
|
||||
c.SysUserApisCache.Invalidate(userID)
|
||||
}
|
||||
|
||||
// ClearAllSysUserCache 清理所有用户缓存
|
||||
func (c *Caches) ClearAllSysUserCache() {
|
||||
c.SysUserApisCache.InvalidateAll()
|
||||
}
|
||||
49
internal/pkg/dberr/postgres.go
Normal file
49
internal/pkg/dberr/postgres.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package dberr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
const (
|
||||
SysUserAccountKey = "sys_users_account_key"
|
||||
SysRoleCodeKey = "sys_roles_code_key"
|
||||
PostSlugKey = "posts_slug_key"
|
||||
SysPermissionsCodeKey = "sys_permissions_code_key"
|
||||
SysApisMethodPathKey = "sys_apis_method_path_key"
|
||||
CategoryCodeKey = "categories_code_key"
|
||||
)
|
||||
|
||||
func MapRowsAffected(rows int64, err error, notFoundErr error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return notFoundErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MapNoRows(err error, notFoundErr error) error {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return notFoundErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func MapUniqueViolation(err error, constraint string, uniqueErr error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) &&
|
||||
pgErr.Code == "23505" &&
|
||||
pgErr.ConstraintName == constraint {
|
||||
return uniqueErr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
48
internal/pkg/errs/errs.go
Normal file
48
internal/pkg/errs/errs.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package errs
|
||||
|
||||
import "net/http"
|
||||
|
||||
type AppError struct {
|
||||
HTTPCode int
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
func New(httpCode int, msg string) *AppError {
|
||||
return &AppError{
|
||||
HTTPCode: httpCode,
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = New(http.StatusUnauthorized, "用户名或密码错误")
|
||||
ErrUnauthenticated = New(http.StatusUnauthorized, "用户未登录或登录已失效")
|
||||
ErrUnauthorized = New(http.StatusUnauthorized, "认证失败:无法获取当前用户信息")
|
||||
ErrInvalidToken = New(http.StatusUnauthorized, "登录凭证无效")
|
||||
ErrInvalidTokenClaims = New(http.StatusUnauthorized, "登录凭证解析失败")
|
||||
ErrInvalidRefreshToken = New(http.StatusBadRequest, "invalid_grant")
|
||||
ErrExpiredRefreshToken = New(http.StatusBadRequest, "invalid_grant")
|
||||
ErrUserNotFound = New(http.StatusNotFound, "用户数据不存在")
|
||||
ErrCategoryNotFound = New(http.StatusNotFound, "分类数据不存在")
|
||||
ErrSysApiNotFound = New(http.StatusNotFound, "接口数据不存在")
|
||||
ErrPostNotFound = New(http.StatusNotFound, "文章数据不存在")
|
||||
ErrSysMenuNotFound = New(http.StatusNotFound, "菜单数据不存在")
|
||||
ErrSysRoleNotFound = New(http.StatusNotFound, "角色数据不存在")
|
||||
ErrCannotDeleteSuperAdmin = New(http.StatusForbidden, "超级管理员账号无法被删除")
|
||||
ErrSlugRequired = New(http.StatusBadRequest, "slug不能为空")
|
||||
ErrIDRequired = New(http.StatusBadRequest, "id不能为空")
|
||||
ErrPermissionDenied = New(http.StatusForbidden, "没有权限访问该资源")
|
||||
ErrInvalidID = New(http.StatusBadRequest, "id非法请检查传入的id")
|
||||
ErrEmptyBody = New(http.StatusBadRequest, "请求内容(body)不能为空")
|
||||
ErrInvalidJSON = New(http.StatusBadRequest, "请求数据格式错误")
|
||||
ErrAccountAlreadyExists = New(http.StatusBadRequest, "账号已存在")
|
||||
ErrCodeAlreadyExists = New(http.StatusBadRequest, "角色编码已存在")
|
||||
ErrSlugAlreadyExists = New(http.StatusBadRequest, "slug已存在")
|
||||
ErrPermissionCodeAlreadyExists = New(http.StatusBadRequest, "权限编码已存在")
|
||||
ErrCategoryCodeAlreadyExists = New(http.StatusBadRequest, "分类编码已存在")
|
||||
ErrSysApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)已存在")
|
||||
)
|
||||
90
internal/pkg/httputil/request.go
Normal file
90
internal/pkg/httputil/request.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"server/internal/config"
|
||||
"server/internal/model/common"
|
||||
"server/internal/pkg/errs"
|
||||
"server/internal/pkg/validator"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// Pagination 分页请求参数
|
||||
func Pagination(r *http.Request) *common.Pagination {
|
||||
q := r.URL.Query()
|
||||
|
||||
// page 默认 1
|
||||
page := int32(1)
|
||||
if p := q.Get("page"); p != "" {
|
||||
if v, err := strconv.Atoi(p); err == nil && v >= 1 {
|
||||
page = int32(v)
|
||||
}
|
||||
}
|
||||
|
||||
// pageSize 默认 10
|
||||
pageSize := int32(10)
|
||||
if ps := q.Get("page_size"); ps != "" {
|
||||
if v, err := strconv.Atoi(ps); err == nil && v >= 1 && v <= 100 {
|
||||
pageSize = int32(v)
|
||||
}
|
||||
}
|
||||
|
||||
return &common.Pagination{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
func URLParamInt32(r *http.Request, key string) (int32, error) {
|
||||
v := chi.URLParam(r, key)
|
||||
|
||||
if v == "" {
|
||||
return 0, errs.ErrIDRequired
|
||||
}
|
||||
|
||||
n, err := strconv.ParseInt(v, 10, 32)
|
||||
if err != nil {
|
||||
return 0, errs.ErrInvalidID
|
||||
}
|
||||
|
||||
return int32(n), nil
|
||||
}
|
||||
|
||||
func BindJson(r *http.Request, dest any) error {
|
||||
if err := json.NewDecoder(r.Body).Decode(dest); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return errs.ErrEmptyBody
|
||||
}
|
||||
return errs.ErrInvalidJSON
|
||||
}
|
||||
|
||||
if err := validator.Struct(dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildFileUrl(path *string) string {
|
||||
if path == nil || *path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
baseURL := config.GetString("file.base_url")
|
||||
if baseURL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
result, err := url.JoinPath(baseURL, *path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
65
internal/pkg/httputil/response.go
Normal file
65
internal/pkg/httputil/response.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"server/internal/model/common"
|
||||
"server/internal/pkg/errs"
|
||||
validatorI18 "server/internal/pkg/validator"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
func respondWithJSON(w http.ResponseWriter, code int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
slog.Error("respondWithJSON: failed to encode response", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Ok(w http.ResponseWriter, data ...any) {
|
||||
resp := common.Response{
|
||||
Message: "ok",
|
||||
}
|
||||
|
||||
if len(data) > 0 && data[0] != nil {
|
||||
resp.Data = data[0]
|
||||
}
|
||||
|
||||
respondWithJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func OkWithPage(w http.ResponseWriter, pageResp *common.PageResponse) {
|
||||
pageResp.Message = "ok"
|
||||
respondWithJSON(w, http.StatusOK, pageResp)
|
||||
}
|
||||
|
||||
// Fail 响应失败
|
||||
func Fail(w http.ResponseWriter, err error) {
|
||||
var msg string
|
||||
// 默认code 500
|
||||
httpStatusCode := http.StatusInternalServerError
|
||||
|
||||
var validationErrs validator.ValidationErrors
|
||||
var appErr *errs.AppError
|
||||
if errors.As(err, &validationErrs) {
|
||||
httpStatusCode = http.StatusBadRequest
|
||||
msgs := validatorI18.Translate(err)
|
||||
msg = strings.Join(msgs, "; ")
|
||||
} else if errors.As(err, &appErr) {
|
||||
httpStatusCode = appErr.HTTPCode
|
||||
msg = appErr.Msg
|
||||
} else {
|
||||
msg = err.Error()
|
||||
}
|
||||
|
||||
resp := common.Response{
|
||||
Message: msg,
|
||||
}
|
||||
|
||||
respondWithJSON(w, httpStatusCode, resp)
|
||||
}
|
||||
76
internal/pkg/logger/logger.go
Normal file
76
internal/pkg/logger/logger.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"server/internal/config"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// New 根据配置创建 slog.Logger,并将其设置为默认 logger。
|
||||
// 日志会写入文件(自动按大小/时间滚动),并可选同时输出到控制台。
|
||||
func New(cfg *config.Config) *slog.Logger {
|
||||
logCfg := cfg.Log
|
||||
|
||||
// 文件滚动写入
|
||||
fileWriter := &lumberjack.Logger{
|
||||
Filename: defaultStr(logCfg.Filename, "logs/app.log"),
|
||||
MaxSize: defaultInt(logCfg.MaxSize, 100), // MB
|
||||
MaxBackups: defaultInt(logCfg.MaxBackups, 30),
|
||||
MaxAge: defaultInt(logCfg.MaxAge, 30), // days
|
||||
Compress: logCfg.Compress,
|
||||
}
|
||||
|
||||
var writer io.Writer = fileWriter
|
||||
if logCfg.Console {
|
||||
writer = io.MultiWriter(os.Stdout, fileWriter)
|
||||
}
|
||||
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: parseLevel(logCfg.Level),
|
||||
AddSource: false,
|
||||
}
|
||||
|
||||
// 生产环境用 JSON,便于日志采集/检索;开发环境用文本更易读
|
||||
var handler slog.Handler
|
||||
if config.IsDev() {
|
||||
handler = slog.NewTextHandler(writer, opts)
|
||||
} else {
|
||||
handler = slog.NewJSONHandler(writer, opts)
|
||||
}
|
||||
|
||||
l := slog.New(handler)
|
||||
slog.SetDefault(l) // 让全局 slog.Info/Error 也走同一套配置
|
||||
return l
|
||||
}
|
||||
|
||||
func parseLevel(level string) slog.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return slog.LevelDebug
|
||||
case "warn", "warning":
|
||||
return slog.LevelWarn
|
||||
case "error":
|
||||
return slog.LevelError
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
func defaultStr(v, def string) string {
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func defaultInt(v, def int) int {
|
||||
if v <= 0 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
28
internal/pkg/validator/null_int32.go
Normal file
28
internal/pkg/validator/null_int32.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package validator
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// NullInt32 Valid(false) + Set(true) = null | Valid(false) + Set(false) = nil
|
||||
type NullInt32 struct {
|
||||
Value int32
|
||||
Valid bool
|
||||
Set bool
|
||||
}
|
||||
|
||||
func (i *NullInt32) UnmarshalJSON(data []byte) error {
|
||||
i.Set = true
|
||||
|
||||
if string(data) == "null" {
|
||||
i.Valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
var temp int32
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.Value = temp
|
||||
i.Valid = true
|
||||
return nil
|
||||
}
|
||||
66
internal/pkg/validator/validator.go
Normal file
66
internal/pkg/validator/validator.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-playground/locales/zh"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
zhtranslations "github.com/go-playground/validator/v10/translations/zh"
|
||||
)
|
||||
|
||||
var (
|
||||
trans ut.Translator
|
||||
validate *validator.Validate
|
||||
)
|
||||
|
||||
func InitValidatorZh() {
|
||||
langZh := zh.New()
|
||||
uni := ut.New(langZh, langZh)
|
||||
var found bool
|
||||
trans, found = uni.GetTranslator("zh")
|
||||
|
||||
if !found {
|
||||
panic("translator 'zh' not found")
|
||||
}
|
||||
|
||||
err := zhtranslations.RegisterDefaultTranslations(validate, trans)
|
||||
if err != nil {
|
||||
panic("failed to register zh translations: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
validate = validator.New()
|
||||
|
||||
// 注册自定义标签名称
|
||||
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
|
||||
name := field.Tag.Get("json")
|
||||
if name == "-" {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
})
|
||||
|
||||
// 初始化中文翻译器
|
||||
InitValidatorZh()
|
||||
}
|
||||
|
||||
func Translate(err error) []string {
|
||||
var validateErrs validator.ValidationErrors
|
||||
if errors.As(err, &validateErrs) {
|
||||
messages := make([]string, 0, len(validateErrs))
|
||||
for _, e := range validateErrs {
|
||||
messages = append(messages, e.Translate(trans))
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
// 非校验错误 直接返回
|
||||
return []string{err.Error()}
|
||||
}
|
||||
|
||||
func Struct(s any) error {
|
||||
return validate.Struct(s)
|
||||
}
|
||||
Reference in New Issue
Block a user