feat: update template

This commit is contained in:
2026-08-19 22:05:49 +08:00
parent 0e674e9d56
commit 5a71093b0c
67 changed files with 2070 additions and 585 deletions

View File

@@ -5,6 +5,9 @@ import "fmt"
const (
UserApiPermissionsPattern = "user:api:permissions:*"
UserInfoPattern = "user:info:*"
UserAuthStatePattern = "user:auth_state:*"
AuthRefreshPattern = "auth:refresh:*" // 新增:覆盖 <hash> 和 user:<id> 两种 key
)
func UserApiPermissions(id int32) string {
@@ -23,3 +26,7 @@ func AuthRefresh(hash string) string {
func AuthRefreshUser(userID int32) string {
return fmt.Sprintf("auth:refresh:user:%d", userID)
}
func UserAuthState(id int32) string {
return fmt.Sprintf("user:auth_state:%d", id)
}

View File

@@ -26,7 +26,6 @@ var (
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, "分类数据不存在")
ErrTagNotFound = New(http.StatusNotFound, "标签数据不存在")
@@ -49,5 +48,7 @@ var (
ErrTagCodeAlreadyExists = New(http.StatusBadRequest, "标签编码不允许重复")
ErrMenusPathUniqueIdx = New(http.StatusBadRequest, "菜单路径不允许重复")
ErrApiMethodPathAlreadyExists = New(http.StatusBadRequest, "接口方法(method)路径(path)不允许重复")
ErrInvalidApiPath = New(http.StatusBadRequest, "接口路径格式不正确,必须以/开头,参数段需为{name}格式")
ErrFileTypeNotAllowed = New(http.StatusBadRequest, "该文件类型不允许上传")
ErrBodyTooLarge = New(http.StatusRequestEntityTooLarge, "请求体过大")
ErrInternalServer = New(http.StatusInternalServerError, "内部服务器错误")
)

View File

@@ -6,6 +6,7 @@ import (
"io"
"net"
"net/http"
"net/netip"
"net/url"
"server/internal/config"
"server/internal/model/common"
@@ -58,8 +59,19 @@ func URLParamInt32(r *http.Request, key string) (int32, error) {
return int32(n), nil
}
func BindJson(r *http.Request, dest any) error {
// maxJSONBodySize JSON 请求体上限1MB
const maxJSONBodySize = 1 << 20
func BindJson(w http.ResponseWriter, r *http.Request, dest any) error {
// 超限时 Decode 返回 *http.MaxBytesError → 413
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBodySize)
if err := json.NewDecoder(r.Body).Decode(dest); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
return errs.ErrBodyTooLarge
}
if errors.Is(err, io.EOF) {
return errs.ErrEmptyBody
}
@@ -73,17 +85,28 @@ func BindJson(r *http.Request, dest any) error {
return nil
}
// ClientIP 获取客户端 IP,依次从 X-Forwarded-For、X-Real-IP、RemoteAddr 取值
func ClientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
if len(parts) > 0 {
return strings.TrimSpace(parts[0])
}
}
// ClientIP 获取客户端 IPtrust_proxy=true 时信任代理头,否则用 RemoteAddr(防伪造)
func ClientIP(r *http.Request) netip.Addr {
addr, _ := netip.ParseAddr(clientIPString(r))
return addr
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
func clientIPString(r *http.Request) string {
// 只在 trust_proxy=true 时信任代理头(防伪造)
if config.GetBool("server.trust_proxy") {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
for _, v := range parts {
if ip := strings.TrimSpace(v); ip != "" {
return ip
}
}
}
// 再取 X-Real-IPnginx 单层代理覆盖时最准)
if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
return realIP
}
}
// RemoteAddr: IP:port
@@ -91,7 +114,6 @@ func ClientIP(r *http.Request) string {
if err == nil {
return host
}
return r.RemoteAddr
}

View File

@@ -0,0 +1,18 @@
package safego
import (
"log/slog"
"runtime/debug"
)
// Go 以协程执行 fn并捕获 panic 记录日志,防止协程崩溃杀死整个进程
func Go(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
slog.Error("goroutine panic recovered", "panic", r, "stack", string(debug.Stack()))
}
}()
fn()
}()
}