refactor: separate backend domains and storage adapters

This commit is contained in:
2026-08-14 01:37:56 +08:00
parent e6775f0966
commit 8c4c6ba661
102 changed files with 6644 additions and 5735 deletions
+4 -4
View File
@@ -3,9 +3,9 @@ package middleware
import (
"net/http"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
@@ -22,16 +22,16 @@ func AccessControl(access *service.AccessService, audit *service.AuditService) g
if err != nil || !allowed {
requestID, _ := c.Get("request_id")
requestIDText, _ := requestID.(string)
_ = audit.RecordDataAccess(c.Request.Context(), &biz.DataAccessLog{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
_ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
c.AbortWithStatusJSON(http.StatusForbidden, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "权限不足"})
return
}
scope, err := access.ResolveDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
if err != nil {
httpx.Fail(c, "数据权限加载失败")
return
}
c.Request = c.Request.WithContext(biz.NewDataScopeContext(c.Request.Context(), scope))
c.Request = c.Request.WithContext(requestContext)
c.Next()
}
}
+46
View File
@@ -0,0 +1,46 @@
package middleware
import (
"bytes"
"io"
"strings"
"time"
"kra/internal/service"
"kra/internal/service/dto"
"github.com/gin-gonic/gin"
)
func OperationAudit(service *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
c.Next()
return
}
path := c.Request.URL.Path
if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
c.Next()
return
}
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
}
writer := &captureWriter{ResponseWriter: c.Writer}
c.Writer = writer
started := time.Now()
c.Next()
userID := uint(0)
if claims := Claims(c); claims != nil {
userID = claims.ID
}
requestID, _ := c.Get("request_id")
status, errorMessage := c.Writer.Status(), ""
if status >= 400 {
errorMessage = writer.body.String()
}
_ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: stringValue(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
}
}
+66
View File
@@ -0,0 +1,66 @@
package middleware
import (
"bytes"
"encoding/json"
"strings"
"github.com/gin-gonic/gin"
)
type captureWriter struct {
gin.ResponseWriter
body bytes.Buffer
}
func (w *captureWriter) Write(data []byte) (int, error) {
if w.body.Len() < 32768 {
remaining := 32768 - w.body.Len()
if len(data) > remaining {
_, _ = w.body.Write(data[:remaining])
} else {
_, _ = w.body.Write(data)
}
}
return w.ResponseWriter.Write(data)
}
func redactJSON(raw []byte) string {
if len(raw) == 0 {
return ""
}
if len(raw) > 32768 {
raw = raw[:32768]
}
var value any
if json.Unmarshal(raw, &value) != nil {
return string(raw)
}
var clean func(any)
clean = func(current any) {
switch v := current.(type) {
case map[string]any:
for key, item := range v {
lower := strings.ToLower(key)
if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "secret") {
v[key] = "******"
} else {
clean(item)
}
}
case []any:
for _, item := range v {
clean(item)
}
}
}
clean(value)
encoded, _ := json.Marshal(value)
return string(encoded)
}
func stringValue(value any) string { text, _ := value.(string); return text }
func isBootstrapPath(path string) bool {
return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/")
}
+48
View File
@@ -0,0 +1,48 @@
package middleware
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func EmailErrorAlert(service *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
if strings.Contains(c.Request.URL.Path, "/email/") {
c.Next()
return
}
writer := &captureWriter{ResponseWriter: c.Writer}
c.Writer = writer
started := time.Now()
c.Next()
failed := c.Writer.Status() >= 400
if !failed {
var result struct {
Code int `json:"code"`
}
failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != httpx.CodeSuccess
}
if !failed {
return
}
username := "Unknown"
if claims := Claims(c); claims != nil && claims.Username != "" {
username = claims.Username
}
subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
body := bytes.NewBuffer(nil)
fmt.Fprintf(body, "请求方式:%s<br>请求路径:%s<br>状态码:%d<br>耗时:%s<br>错误响应:<pre>%s</pre>", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes()))
if err := service.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil {
logger.Error("send HTTP error email", "error", err)
}
}
}
+38
View File
@@ -0,0 +1,38 @@
package middleware
import (
"net/http"
"strconv"
"time"
"kra/internal/server/httpx"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
if isBootstrapPath(c.Request.URL.Path) {
c.Next()
return
}
config, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || config == nil || !config.LimitEnable {
c.Next()
return
}
window := config.LimitWindow
if window < 1 {
window = 60
}
bucket := time.Now().Unix() / int64(window)
key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
count, cacheErr := system.CacheIncrement(c.Request.Context(), key, time.Duration(window+1)*time.Second)
if cacheErr == nil && int(count) > config.LimitCount {
c.AbortWithStatusJSON(http.StatusTooManyRequests, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "请求过于频繁"})
return
}
c.Next()
}
}