This commit is contained in:
2026-08-13 21:51:20 +08:00
parent be5362913b
commit 21c264fcd7
60 changed files with 10174 additions and 69 deletions
+44
View File
@@ -0,0 +1,44 @@
package server
import (
"net/http"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerAccessRoutes(group *gin.RouterGroup, engine *gin.Engine, svc *service.AccessService) {
registerAuthorityRoutes(group, svc)
registerMenuRoutes(group, svc)
registerAPIRoutes(group, engine, svc)
registerPolicyRoutes(group, svc)
registerDepartmentRoutes(group, svc)
registerPositionRoutes(group, svc)
}
func accessControl(svc *service.AccessService, audit *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
claims := currentClaims(c)
if claims == nil {
noAuth(c, "未登录或非法访问")
return
}
path := c.Request.URL.Path
allowed, err := svc.Authorize(c.Request.Context(), claims.AuthorityID, path, c.Request.Method)
if err != nil || !allowed {
requestID, _ := c.Get("request_id")
_ = audit.RecordDataAccess(c.Request.Context(), &biz.DataAccessLog{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: toString(requestID), Method: c.Request.Method, Path: c.Request.URL.Path, Detail: "Casbin policy denied the request"})
c.AbortWithStatusJSON(http.StatusForbidden, response{Code: codeError, Data: nil, Msg: "权限不足"})
return
}
scope, err := svc.ResolveDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
if err != nil {
fail(c, "数据权限加载失败")
return
}
c.Request = c.Request.WithContext(biz.NewDataScopeContext(c.Request.Context(), scope))
c.Next()
}
}
+209
View File
@@ -0,0 +1,209 @@
package server
import (
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerAPIRoutes(group *gin.RouterGroup, engine *gin.Engine, svc *service.AccessService) {
router := group.Group("/api")
router.POST("/getApiList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Path string `json:"path"`
Description string `json:"description"`
APIGroup string `json:"apiGroup"`
Method string `json:"method"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, total, err := svc.APIs(c.Request.Context(), req.Page, req.PageSize, &biz.API{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
router.POST("/getAllApis", func(c *gin.Context) {
items, _, err := svc.APIs(c.Request.Context(), 1, 10000, nil)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"apis": items}, "获取成功")
})
type apiRequest struct {
ID uint `json:"ID"`
Path string `json:"path"`
Description string `json:"description"`
APIGroup string `json:"apiGroup"`
Method string `json:"method"`
}
router.POST("/createApi", func(c *gin.Context) {
var req apiRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.CreateAPI(c.Request.Context(), &biz.API{Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method}); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
router.POST("/updateApi", func(c *gin.Context) {
var req apiRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateAPI(c.Request.Context(), &biz.API{ID: req.ID, Path: req.Path, Description: req.Description, APIGroup: req.APIGroup, Method: req.Method}); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
router.POST("/deleteApi", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteAPIs(c.Request.Context(), []uint{req.ID}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.DELETE("/deleteApisByIds", func(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteAPIs(c.Request.Context(), req.IDs); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.POST("/getApiById", func(c *gin.Context) {
var req struct {
ID uint `json:"id"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
item, err := svc.API(c.Request.Context(), req.ID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"api": item}, "获取成功")
})
router.GET("/getApiGroups", func(c *gin.Context) {
items, _, err := svc.APIs(c.Request.Context(), 1, 10000, nil)
if err != nil {
fail(c, "获取失败")
return
}
seen := map[string]bool{}
groups := []string{}
for _, item := range items {
value, _ := item["apiGroup"].(string)
if value != "" && !seen[value] {
seen[value] = true
groups = append(groups, value)
}
}
writeResult(c, codeSuccess, gin.H{"groups": groups}, "获取成功")
})
router.GET("/getApiRoles", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("apiId"), 10, 64)
ids, err := svc.APIRoleIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
router.POST("/setApiRoles", func(c *gin.Context) {
var req struct {
APIID uint `json:"apiId"`
AuthorityIDs []uint `json:"authorityIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetAPIRoles(c.Request.Context(), req.APIID, req.AuthorityIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
router.GET("/syncApi", func(c *gin.Context) {
routes := engine.Routes()
items := make([]*biz.API, 0, len(routes))
for _, route := range routes {
items = append(items, &biz.API{Path: route.Path, Method: route.Method})
}
result, err := svc.SyncAPIs(c.Request.Context(), items)
if err != nil {
fail(c, "同步检查失败")
return
}
writeResult(c, codeSuccess, result, "获取成功")
})
router.POST("/ignoreApi", func(c *gin.Context) {
var req struct {
Path string `json:"path"`
Method string `json:"method"`
Flag bool `json:"flag"`
}
if c.ShouldBindJSON(&req) != nil || req.Path == "" || req.Method == "" {
fail(c, "参数错误")
return
}
if err := svc.SetAPIIgnored(c.Request.Context(), req.Path, req.Method, req.Flag); err != nil {
fail(c, "忽略设置失败")
return
}
ok(c)
})
router.POST("/enterSyncApi", func(c *gin.Context) {
var req struct {
NewAPIs []apiRequest `json:"newApis"`
DeleteAPIs []apiRequest `json:"deleteApis"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
toDomain := func(values []apiRequest) []*biz.API {
items := make([]*biz.API, 0, len(values))
for _, value := range values {
items = append(items, &biz.API{ID: value.ID, Path: value.Path, Description: value.Description, APIGroup: value.APIGroup, Method: value.Method})
}
return items
}
if err := svc.ApplyAPISync(c.Request.Context(), toDomain(req.NewAPIs), toDomain(req.DeleteAPIs)); err != nil {
fail(c, "同步失败")
return
}
ok(c)
})
router.GET("/freshCasbin", func(c *gin.Context) { ok(c) })
}
+101
View File
@@ -0,0 +1,101 @@
package server
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"io"
"kra/internal/biz"
"kra/internal/service"
"strings"
"time"
)
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 operationAudit(svc *service.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
if 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 := currentClaims(c); claims != nil {
userID = claims.ID
}
requestID, _ := c.Get("request_id")
status := c.Writer.Status()
errorMessage := ""
if status >= 400 {
errorMessage = writer.body.String()
}
_ = svc.RecordOperation(c.Request.Context(), &biz.OperationRecord{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: toString(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
}
}
func toString(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
+246
View File
@@ -0,0 +1,246 @@
package server
import (
"github.com/gin-gonic/gin"
"kra/internal/biz"
"kra/internal/service"
"strconv"
)
func idsFromQuery(c *gin.Context) []uint {
values := c.QueryArray("IDs[]")
if len(values) == 0 {
values = c.QueryArray("ids[]")
}
ids := make([]uint, 0, len(values))
for _, v := range values {
id, _ := strconv.ParseUint(v, 10, 64)
if id > 0 {
ids = append(ids, uint(id))
}
}
return ids
}
func registerAuditRoutes(group, public *gin.RouterGroup, svc *service.AuditService) {
operations := group.Group("/sysOperationRecord")
operations.GET("/getSysOperationRecordList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
status, _ := strconv.Atoi(c.Query("status"))
userID, _ := strconv.ParseUint(c.Query("user_id"), 10, 64)
items, total, err := svc.Operations(c.Request.Context(), page, size, &biz.OperationRecord{Path: c.Query("path"), Method: c.Query("method"), Status: status, IP: c.Query("ip"), UserID: uint(userID)})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
operations.GET("/findSysOperationRecord", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Operation(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"resysOperationRecord": item}, "查询成功")
})
operations.DELETE("/deleteSysOperationRecord", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := svc.DeleteOperations(c.Request.Context(), []uint{uint(id)}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
operations.DELETE("/deleteSysOperationRecordByIds", func(c *gin.Context) {
if err := svc.DeleteOperations(c.Request.Context(), idsFromQuery(c)); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
logins := group.Group("/sysLoginLog")
logins.GET("/getLoginLogList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Logins(c.Request.Context(), page, size, &biz.LoginLog{Username: c.Query("username"), IP: c.Query("ip")})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
logins.GET("/findLoginLog", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Login(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, item, "查询成功")
})
logins.DELETE("/deleteLoginLog", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := svc.DeleteLogins(c.Request.Context(), []uint{uint(id)}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
logins.DELETE("/deleteLoginLogByIds", func(c *gin.Context) {
if err := svc.DeleteLogins(c.Request.Context(), idsFromQuery(c)); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
access := group.Group("/dataAccessLog")
access.POST("/getDataAccessLogList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
EventType string `json:"eventType"`
TargetTable string `json:"targetTable"`
UserID uint `json:"userId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, total, err := svc.DataAccess(c.Request.Context(), req.Page, req.PageSize, &biz.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable, UserID: req.UserID})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
access.DELETE("/deleteDataAccessLogByIds", func(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteDataAccess(c.Request.Context(), req.IDs); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
viewer := group.Group("/logViewer")
viewer.GET("/dates", func(c *gin.Context) {
data, err := svc.LogDates(c.Request.Context(), c.Query("month"))
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, data, "获取成功")
})
viewer.GET("/files", func(c *gin.Context) {
data, err := svc.LogFiles(c.Request.Context(), c.Query("date"))
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, data, "获取成功")
})
viewer.GET("/content", func(c *gin.Context) {
var cursor *int64
if raw := c.Query("cursor"); raw != "" {
value, err := strconv.ParseInt(raw, 10, 64)
if err != nil || value < 0 {
fail(c, "参数错误")
return
}
cursor = &value
}
data, err := svc.LogContent(c.Request.Context(), c.Query("date"), c.Query("path"), cursor)
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, data, "获取成功")
})
errorsRoute := group.Group("/sysError")
errorsRoute.DELETE("/deleteSysError", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := svc.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
errorsRoute.DELETE("/deleteSysErrorByIds", func(c *gin.Context) {
if err := svc.DeleteErrors(c.Request.Context(), idsFromQuery(c)); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
errorsRoute.PUT("/updateSysError", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
Form string `json:"form"`
Info string `json:"info"`
Level string `json:"level"`
Solution string `json:"solution"`
Status string `json:"status"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateError(c.Request.Context(), &biz.ErrorRecord{ID: req.ID, Form: req.Form, Info: req.Info, Level: req.Level, Solution: req.Solution, Status: req.Status}); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
errorsRoute.GET("/findSysError", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Error(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"resysError": item}, "查询成功")
})
errorsRoute.GET("/getSysErrorList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Errors(c.Request.Context(), page, size, &biz.ErrorRecord{Form: c.Query("form"), Level: c.Query("level"), Status: c.Query("status")})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
errorsRoute.GET("/getSysErrorSolution", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Error(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"solution": item["solution"]}, "获取成功")
})
public.Group("/sysError").POST("/createSysError", func(c *gin.Context) {
var req struct {
Form string `json:"form"`
Info string `json:"info"`
Level string `json:"level"`
RequestID string `json:"request_id"`
TraceID string `json:"trace_id"`
}
if c.ShouldBindJSON(&req) != nil || req.Form == "" {
fail(c, "参数错误")
return
}
if err := svc.CreateError(c.Request.Context(), &biz.ErrorRecord{Form: req.Form, Info: req.Info, Level: req.Level, RequestID: req.RequestID, TraceID: req.TraceID}); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
}
+134
View File
@@ -0,0 +1,134 @@
package server
import (
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerAuthorityRoutes(group *gin.RouterGroup, svc *service.AccessService) {
router := group.Group("/authority")
router.POST("/getAuthorityList", func(c *gin.Context) {
items, err := svc.Authorities(c.Request.Context())
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, items, "获取成功")
})
type authorityRequest struct {
AuthorityID uint `json:"authorityId"`
AuthorityName string `json:"authorityName"`
ParentID *uint `json:"parentId"`
DataScope int `json:"dataScope"`
DefaultRouter string `json:"defaultRouter"`
}
router.POST("/createAuthority", func(c *gin.Context) {
var req authorityRequest
if c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
fail(c, "参数错误")
return
}
value := &biz.Authority{AuthorityID: req.AuthorityID, AuthorityName: req.AuthorityName, ParentID: req.ParentID, DataScope: req.DataScope, DefaultRouter: req.DefaultRouter}
if err := svc.CreateAuthority(c.Request.Context(), value); err != nil {
fail(c, "创建失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"authority": gin.H{"authorityId": value.AuthorityID, "authorityName": value.AuthorityName, "parentId": value.ParentID, "dataScope": value.DataScope, "defaultRouter": value.DefaultRouter}}, "创建成功")
})
router.POST("/copyAuthority", func(c *gin.Context) {
var req struct {
Authority authorityRequest `json:"authority"`
OldAuthorityID uint `json:"oldAuthorityId"`
}
if c.ShouldBindJSON(&req) != nil || req.Authority.AuthorityID == 0 || req.OldAuthorityID == 0 {
fail(c, "参数错误")
return
}
value := &biz.Authority{AuthorityID: req.Authority.AuthorityID, AuthorityName: req.Authority.AuthorityName, ParentID: req.Authority.ParentID, DataScope: req.Authority.DataScope, DefaultRouter: req.Authority.DefaultRouter}
if err := svc.CopyAuthority(c.Request.Context(), req.OldAuthorityID, value); err != nil {
fail(c, "拷贝失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"authority": gin.H{"authorityId": value.AuthorityID, "authorityName": value.AuthorityName, "parentId": value.ParentID, "dataScope": value.DataScope, "defaultRouter": value.DefaultRouter}}, "拷贝成功")
})
router.PUT("/updateAuthority", func(c *gin.Context) {
var req authorityRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
value := &biz.Authority{AuthorityID: req.AuthorityID, AuthorityName: req.AuthorityName, ParentID: req.ParentID, DataScope: req.DataScope, DefaultRouter: req.DefaultRouter}
if err := svc.UpdateAuthority(c.Request.Context(), value); err != nil {
fail(c, "更新失败")
return
}
writeResult(c, codeSuccess, gin.H{"authority": gin.H{"authorityId": value.AuthorityID, "authorityName": value.AuthorityName, "parentId": value.ParentID, "dataScope": value.DataScope, "defaultRouter": value.DefaultRouter}}, "更新成功")
})
router.POST("/deleteAuthority", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteAuthority(c.Request.Context(), req.AuthorityID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
router.POST("/setRoleUsers", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
UserIDs []uint `json:"userIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetAuthorityUsers(c.Request.Context(), req.AuthorityID, req.UserIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
router.GET("/getUsersByAuthority", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("authorityId"), 10, 64)
ids, err := svc.AuthorityUserIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
router.POST("/setDataScope", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
DataScope int `json:"dataScope"`
DepartmentIDs []uint `json:"deptIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetDataScope(c.Request.Context(), req.AuthorityID, req.DataScope, req.DepartmentIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
router.GET("/getDataScopeDepts", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("authorityId"), 10, 64)
ids, err := svc.DataScopeDepartmentIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
}
+224
View File
@@ -0,0 +1,224 @@
package server
import (
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"kra/internal/biz"
"kra/internal/service"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type exportTemplateRequest struct {
ID uint `json:"ID"`
DBName string `json:"dbName"`
Name string `json:"name"`
TableName string `json:"tableName"`
TemplateID string `json:"templateID"`
TemplateInfo string `json:"templateInfo"`
SQL string `json:"sql"`
ImportSQL string `json:"importSql"`
Limit *int `json:"limit"`
Order string `json:"order"`
Conditions []struct {
From string `json:"from"`
Column string `json:"column"`
Operator string `json:"operator"`
} `json:"conditions"`
Joins []struct {
Join string `json:"joins"`
Table string `json:"table"`
On string `json:"on"`
} `json:"joinTemplate"`
}
func exportTemplateDomain(v exportTemplateRequest) *biz.ExportTemplate {
out := &biz.ExportTemplate{ID: v.ID, DBName: v.DBName, Name: v.Name, TableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
for _, x := range v.Conditions {
out.Conditions = append(out.Conditions, biz.ExportCondition{From: x.From, Column: x.Column, Operator: x.Operator})
}
for _, x := range v.Joins {
out.Joins = append(out.Joins, biz.ExportJoin{Join: x.Join, Table: x.Table, On: x.On})
}
return out
}
func exportParams(values url.Values) map[string]string {
out := map[string]string{}
nested, _ := url.ParseQuery(values.Get("params"))
for key, items := range nested {
if len(items) > 0 {
out[key] = items[0]
}
}
for key, items := range values {
if key != "params" && len(items) > 0 {
out[key] = items[0]
}
}
return out
}
type exportToken struct {
TemplateID string `json:"templateID"`
Params map[string]string `json:"params"`
Blank bool `json:"blank"`
}
func registerExportRoutes(group, public *gin.RouterGroup, system *service.SystemService, svc *service.ExportService) {
router := group.Group("/sysExportTemplate")
router.POST("/createSysExportTemplate", func(c *gin.Context) {
var req exportTemplateRequest
if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.TemplateID == "" {
fail(c, "参数错误")
return
}
if err := svc.Create(c.Request.Context(), exportTemplateDomain(req)); err != nil {
fail(c, "创建失败:"+err.Error())
return
}
ok(c)
})
router.PUT("/updateSysExportTemplate", func(c *gin.Context) {
var req exportTemplateRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.Update(c.Request.Context(), exportTemplateDomain(req)); err != nil {
fail(c, "更新失败:"+err.Error())
return
}
ok(c)
})
router.DELETE("/deleteSysExportTemplate", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Delete(c.Request.Context(), []uint{req.ID}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.DELETE("/deleteSysExportTemplateByIds", func(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Delete(c.Request.Context(), req.IDs); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.GET("/findSysExportTemplate", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Template(c.Request.Context(), uint(id), "")
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"resysExportTemplate": item}, "查询成功")
})
router.GET("/getSysExportTemplateList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Templates(c.Request.Context(), page, size, &biz.ExportTemplate{Name: c.Query("name"), TableName: c.Query("tableName"), TemplateID: c.Query("templateID")})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
router.GET("/previewSQL", func(c *gin.Context) {
sql, err := svc.Preview(c.Request.Context(), c.Query("templateID"), exportParams(c.Request.URL.Query()))
if err != nil {
fail(c, "预览失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"sql": sql}, "获取成功")
})
issue := func(blank bool) gin.HandlerFunc {
return func(c *gin.Context) {
tid := c.Query("templateID")
if tid == "" {
fail(c, "模板ID不能为空")
return
}
token := strings.ReplaceAll(uuid.NewString(), "-", "")
raw, _ := json.Marshal(exportToken{TemplateID: tid, Params: exportParams(c.Request.URL.Query()), Blank: blank})
if err := system.CacheSet(c.Request.Context(), "export:"+token, string(raw), 5*time.Minute); err != nil {
fail(c, "导出令牌创建失败")
return
}
writeResult(c, codeSuccess, gin.H{"token": token}, "获取成功")
}
}
router.GET("/exportExcel", issue(false))
router.GET("/exportTemplate", issue(true))
router.POST("/importExcel", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
fail(c, "请选择导入文件")
return
}
opened, err := file.Open()
if err != nil {
fail(c, "读取文件失败")
return
}
defer opened.Close()
tid := c.PostForm("templateID")
if tid == "" {
tid = c.Query("templateID")
}
if err = svc.Import(c.Request.Context(), tid, opened); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
pub := public.Group("/sysExportTemplate")
download := func(expectBlank bool) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token")
raw, okValue, err := system.CacheGet(c.Request.Context(), "export:"+token)
if err != nil || !okValue {
fail(c, "导出令牌无效或已过期")
return
}
_ = system.CacheDelete(c.Request.Context(), "export:"+token)
var value exportToken
if json.Unmarshal([]byte(raw), &value) != nil || value.Blank != expectBlank {
fail(c, "导出令牌无效")
return
}
var data []byte
var name string
if expectBlank {
data, name, err = svc.ExportBlankTemplate(c.Request.Context(), value.TemplateID)
} else {
data, name, err = svc.Export(c.Request.Context(), value.TemplateID, value.Params)
}
if err != nil {
fail(c, "导出失败:"+err.Error())
return
}
c.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.QueryEscape(name))
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
}
}
pub.GET("/exportExcelByToken", download(false))
pub.GET("/exportTemplateByToken", download(true))
}
+577
View File
@@ -0,0 +1,577 @@
package server
import (
"context"
"errors"
"log/slog"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/service"
"kra/pkg/adminauth"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/mojocn/base64Captcha"
)
// GinServer adapts Gin to the Kratos transport.Server lifecycle.
type GinServer struct {
network string
address string
server *http.Server
listener net.Listener
logger *slog.Logger
}
func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.SystemService, access *service.AccessService, settings *service.SettingsService, versions *service.VersionService, exports *service.ExportService, audit *service.AuditService, tasks *service.TaskService, media *service.MediaService, scheduler *TaskScheduler, logger *slog.Logger) *GinServer {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(ginRequestMeta(), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
if config != nil && config.Local != nil && config.Local.StorePath != "" {
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if pathPrefix != "/" {
engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)})
}
}
prefix := ""
if config != nil {
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
}
public := engine.Group(prefix)
public.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
registerPublicRoutes(public, engine, config, svc, settings, access, audit)
private := engine.Group(prefix)
private.Use(jwtAuth(config, settings), accessControl(access, audit))
registerPrivateRoutes(private, svc)
registerAccessRoutes(private, engine, access)
registerSettingsRoutes(private, public, settings)
registerSystemConfigRoutes(private, config, settings)
registerVersionRoutes(private, versions)
registerExportRoutes(private, public, svc, exports)
registerAuditRoutes(private, public, audit)
registerTaskRoutes(private, tasks, scheduler)
registerMediaRoutes(private, media)
engine.NoRoute(func(c *gin.Context) {
fail(c, "请求的接口不存在")
})
network := "tcp"
address := ":8000"
if c != nil && c.Http != nil {
if c.Http.Network != "" {
network = c.Http.Network
}
if c.Http.Addr != "" {
address = c.Http.Addr
}
}
return &GinServer{
network: network,
address: address,
server: &http.Server{Addr: address, Handler: engine},
logger: logger,
}
}
func jwtAuth(config *conf.AdminBackend, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("x-token")
if token == "" {
token, _ = c.Cookie("x-token")
}
secret := ""
if config != nil && config.Jwt != nil {
secret = config.Jwt.SigningKey
}
claims, err := adminauth.Parse(token, secret)
if err != nil {
noAuth(c, "未登录或非法访问")
return
}
if disabled, checkErr := settings.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled {
noAuth(c, "登录状态已失效")
return
}
c.Set("admin_claims", claims)
c.Next()
}
}
func currentClaims(c *gin.Context) *adminauth.Claims {
claims, _ := c.Get("admin_claims")
result, _ := claims.(*adminauth.Claims)
return result
}
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) {
expiration := 3 * time.Minute
keyLong, width, height := 6, 240, 80
if config != nil && config.Captcha != nil {
if config.Captcha.StoreExpiration != nil {
expiration = config.Captcha.StoreExpiration.AsDuration()
}
if config.Captcha.KeyLong > 0 {
keyLong = int(config.Captcha.KeyLong)
}
if config.Captcha.ImgWidth > 0 {
width = int(config.Captcha.ImgWidth)
}
if config.Captcha.ImgHeight > 0 {
height = int(config.Captcha.ImgHeight)
}
}
store := &captchaStore{service: svc, expiration: expiration}
base := group.Group("/base")
base.POST("/captcha", func(c *gin.Context) {
security, _ := settings.CurrentSecurity(c.Request.Context())
openCaptcha := true
if security != nil {
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
if security.CaptchaOpen > 0 {
failures, _ := cachedInt(c.Request.Context(), svc, "login:fail:"+c.ClientIP())
openCaptcha = failures >= security.CaptchaOpen
}
}
if !openCaptcha {
writeResult(c, codeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
return
}
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
id, picture, _, err := base64Captcha.NewCaptcha(driver, store).Generate()
if err != nil {
fail(c, "验证码获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
})
base.POST("/login", func(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
Captcha string `json:"captcha"`
CaptchaID string `json:"captchaId"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
fail(c, "请输入用户名和密码")
return
}
security, _ := settings.CurrentSecurity(c.Request.Context())
if security != nil && security.LockEnable {
if _, locked, _ := svc.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
fail(c, "账户已锁定,请稍后重试")
return
}
}
requireCaptcha := security == nil || security.CaptchaOpen == 0
if security != nil && security.CaptchaOpen > 0 {
failures, _ := cachedInt(c.Request.Context(), svc, "login:fail:"+c.ClientIP())
requireCaptcha = failures >= security.CaptchaOpen
}
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !store.Verify(req.CaptchaID, req.Captcha, true)) {
fail(c, "验证码错误")
return
}
result, err := svc.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
_ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
ttl := time.Hour
if security != nil && security.CaptchaTimeout > 0 {
ttl = time.Duration(security.CaptchaTimeout) * time.Second
}
failures, _ := incrementCached(c.Request.Context(), svc, "login:fail:"+c.ClientIP(), ttl)
if security != nil && security.LockEnable && failures >= security.LockThreshold {
_ = svc.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
}
fail(c, "用户名不存在或者密码错误")
return
}
userID, _ := result.User["ID"].(uint)
_ = audit.RecordLogin(c.Request.Context(), &biz.LoginLog{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
_ = svc.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
_ = svc.CacheDelete(c.Request.Context(), "login:lock:"+req.Username)
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: result.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
writeResult(c, codeSuccess, result, "登录成功")
})
initGroup := group.Group("/init")
initGroup.POST("/checkdb", func(c *gin.Context) {
initialized, err := svc.IsInitialized(c.Request.Context())
if err != nil {
fail(c, "数据库状态检查失败")
return
}
message := "数据库无需初始化"
if !initialized {
message = "前往初始化数据库"
}
writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
})
initGroup.POST("/initdb", func(c *gin.Context) {
if err := svc.Initialize(c.Request.Context()); err != nil {
fail(c, "自动创建数据库失败")
return
}
routes := engine.Routes()
apis := make([]*biz.API, 0, len(routes))
for _, route := range routes {
apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)})
}
if err := access.ApplyAPISync(c.Request.Context(), apis, nil); err != nil {
fail(c, "数据库已初始化,但 API 权限数据写入失败")
return
}
writeResult(c, codeSuccess, gin.H{}, "自动创建数据库成功")
})
}
func routeGroup(path string) string {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "base"
}
func cachedInt(ctx context.Context, svc *service.SystemService, key string) (int, error) {
value, ok, err := svc.CacheGet(ctx, key)
if err != nil || !ok {
return 0, err
}
result, err := strconv.Atoi(value)
return result, err
}
func incrementCached(ctx context.Context, svc *service.SystemService, key string, ttl time.Duration) (int, error) {
value, _ := cachedInt(ctx, svc, key)
value++
return value, svc.CacheSet(ctx, key, strconv.Itoa(value), ttl)
}
func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
cfg, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || cfg == nil || !cfg.LimitEnable {
c.Next()
return
}
window := cfg.LimitWindow
if window < 1 {
window = 60
}
bucket := time.Now().Unix() / int64(window)
key := "rate:" + c.ClientIP() + ":" + strconv.FormatInt(bucket, 10)
count, cacheErr := incrementCached(c.Request.Context(), svc, key, time.Duration(window+1)*time.Second)
if cacheErr == nil && count > cfg.LimitCount {
c.AbortWithStatusJSON(http.StatusTooManyRequests, response{Code: codeError, Data: nil, Msg: "请求过于频繁"})
return
}
c.Next()
}
}
type captchaStore struct {
service *service.SystemService
expiration time.Duration
}
func (s *captchaStore) key(id string) string { return "captcha:" + id }
func (s *captchaStore) Set(id, value string) error {
return s.service.CacheSet(context.Background(), s.key(id), value, s.expiration)
}
func (s *captchaStore) Get(id string, clear bool) string {
value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
if err != nil || !ok {
return ""
}
if clear {
_ = s.service.CacheDelete(context.Background(), s.key(id))
}
return value
}
func (s *captchaStore) Verify(id, answer string, clear bool) bool {
if id == "" || answer == "" {
return false
}
return strings.EqualFold(s.Get(id, clear), answer)
}
type filesOnly struct{ http.FileSystem }
func (f filesOnly) Open(name string) (http.File, error) {
file, err := f.FileSystem.Open(name)
if err != nil {
return nil, err
}
info, statErr := file.Stat()
if statErr != nil {
_ = file.Close()
return nil, statErr
}
if info.IsDir() {
_ = file.Close()
return nil, os.ErrPermission
}
return file, nil
}
func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService) {
user := group.Group("/user")
user.POST("/getUserList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Username string `json:"username"`
NickName string `json:"nickName"`
Phone string `json:"phone"`
Email string `json:"email"`
OrderKey string `json:"orderKey"`
Desc bool `json:"desc"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
list, total, err := svc.ListUsers(c.Request.Context(), req.Page, req.PageSize, &biz.UserListFilter{Username: req.Username, NickName: req.NickName, Phone: req.Phone, Email: req.Email, OrderKey: req.OrderKey, Desc: req.Desc})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
user.POST("/admin_register", func(c *gin.Context) {
var req struct {
Username string `json:"userName"`
Password string `json:"passWord"`
NickName string `json:"nickName"`
HeaderImg string `json:"headerImg"`
AuthorityID uint `json:"authorityId"`
AuthorityIDs []uint `json:"authorityIds"`
Enable int `json:"enable"`
Phone string `json:"phone"`
Email string `json:"email"`
}
if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
fail(c, "参数错误")
return
}
result, err := svc.CreateUser(c.Request.Context(), service.UserInput{Username: req.Username, Password: req.Password, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email})
if err != nil {
fail(c, "注册失败")
return
}
writeResult(c, codeSuccess, gin.H{"user": result}, "注册成功")
})
user.PUT("/setUserInfo", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
NickName string `json:"nickName"`
HeaderImg string `json:"headerImg"`
AuthorityID uint `json:"authorityId"`
AuthorityIDs []uint `json:"authorityIds"`
Enable int `json:"enable"`
Phone string `json:"phone"`
Email string `json:"email"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: req.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, AuthorityID: req.AuthorityID, AuthorityIDs: req.AuthorityIDs, Enable: req.Enable, Phone: req.Phone, Email: req.Email}); err != nil {
fail(c, "修改失败")
return
}
ok(c)
})
user.PUT("/setSelfInfo", func(c *gin.Context) {
claims := currentClaims(c)
if claims == nil {
noAuth(c, "未登录或非法访问")
return
}
var req struct {
NickName string `json:"nickName"`
HeaderImg string `json:"headerImg"`
Phone string `json:"phone"`
Email string `json:"email"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateUser(c.Request.Context(), service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: 1}); err != nil {
fail(c, "修改失败")
return
}
ok(c)
})
user.DELETE("/deleteUser", func(c *gin.Context) {
var req struct {
ID uint `form:"id" json:"id"`
}
_ = c.ShouldBind(&req)
if req.ID == 0 {
var body struct {
ID uint `json:"ID"`
}
_ = c.ShouldBindJSON(&body)
req.ID = body.ID
}
if err := svc.DeleteUser(c.Request.Context(), req.ID); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
user.POST("/resetPassword", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
Password string `json:"password"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.ResetPassword(c.Request.Context(), req.ID, req.Password); err != nil {
fail(c, "重置失败")
return
}
ok(c)
})
user.POST("/changePassword", func(c *gin.Context) {
claims := currentClaims(c)
var req struct {
Password string `json:"password"`
NewPassword string `json:"newPassword"`
}
if claims == nil || c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.ChangePassword(c.Request.Context(), claims.ID, req.Password, req.NewPassword); err != nil {
fail(c, "修改失败,原密码与当前账户不符")
return
}
ok(c)
})
user.PUT("/setSelfSetting", func(c *gin.Context) {
claims := currentClaims(c)
var setting map[string]any
if claims == nil || c.ShouldBindJSON(&setting) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetUserSetting(c.Request.Context(), claims.ID, setting); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
user.POST("/setUserAuthorities", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
AuthorityIDs []uint `json:"authorityIds"`
}
if c.ShouldBindJSON(&req) != nil || req.ID == 0 || len(req.AuthorityIDs) == 0 {
fail(c, "参数错误")
return
}
if err := svc.SetUserAuthorities(c.Request.Context(), req.ID, req.AuthorityIDs); err != nil {
fail(c, "修改失败")
return
}
ok(c)
})
user.POST("/setUserAuthority", func(c *gin.Context) {
claims := currentClaims(c)
var req struct {
AuthorityID uint `json:"authorityId"`
}
if claims == nil || c.ShouldBindJSON(&req) != nil || req.AuthorityID == 0 {
fail(c, "参数错误")
return
}
login, err := svc.SwitchAuthority(c.Request.Context(), claims.ID, req.AuthorityID)
if err != nil {
fail(c, err.Error())
return
}
c.Header("new-token", login.Token)
c.Header("new-expires-at", strconv.FormatInt(login.ExpiresAt/1000, 10))
maxAge := int(time.Until(time.UnixMilli(login.ExpiresAt)).Seconds())
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: login.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
ok(c)
})
user.GET("/getUserInfo", func(c *gin.Context) {
claims := currentClaims(c)
if claims == nil {
noAuth(c, "未登录或非法访问")
return
}
result, err := svc.User(c.Request.Context(), claims.ID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"userInfo": result}, "获取成功")
})
menu := group.Group("/menu")
menu.POST("/getMenu", func(c *gin.Context) {
claims := currentClaims(c)
if claims == nil {
noAuth(c, "未登录或非法访问")
return
}
menus, err := svc.Menus(c.Request.Context(), claims.AuthorityID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"menus": menus}, "获取成功")
})
jwt := group.Group("/jwt")
jwt.POST("/jsonInBlacklist", func(c *gin.Context) {
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
ok(c)
})
}
func (s *GinServer) Start(ctx context.Context) error {
listener, err := net.Listen(s.network, s.address)
if err != nil {
return err
}
s.listener = listener
s.server.BaseContext = func(net.Listener) context.Context { return ctx }
s.logger.InfoContext(ctx, "Gin HTTP server listening", "addr", listener.Addr().String())
if err := s.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func (s *GinServer) Stop(ctx context.Context) error {
s.logger.InfoContext(ctx, "Gin HTTP server stopping")
return s.server.Shutdown(ctx)
}
func ginRequestMeta() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-Id")
if requestID == "" {
requestID = uuid.NewString()
}
c.Header("X-Request-Id", requestID)
c.Set("request_id", requestID)
c.Next()
}
}
+265
View File
@@ -0,0 +1,265 @@
package server
import (
"github.com/gin-gonic/gin"
"kra/internal/biz"
"kra/internal/service"
"strconv"
)
func registerMediaRoutes(group *gin.RouterGroup, svc *service.MediaService) {
files := group.Group("/fileUploadAndDownload")
files.POST("/upload", func(c *gin.Context) {
claims := currentClaims(c)
header, err := c.FormFile("file")
if claims == nil || err != nil {
fail(c, "接收文件失败")
return
}
opened, err := header.Open()
if err != nil {
fail(c, "读取文件失败")
return
}
defer opened.Close()
category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
save := c.DefaultQuery("noSave", "0") == "0"
item, err := svc.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
if err != nil {
fail(c, "上传失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"file": item}, "上传成功")
})
files.POST("/getFileList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Keyword string `json:"keyword"`
ClassID int `json:"classId"`
Tag string `json:"tag"`
UserID uint `json:"userId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, total, err := svc.MediaList(c.Request.Context(), req.Page, req.PageSize, req.Keyword, req.ClassID, req.Tag, req.UserID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
files.POST("/deleteFile", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Delete(c.Request.Context(), req.ID); err != nil {
fail(c, "删除失败:"+err.Error())
return
}
ok(c)
})
files.POST("/deleteFiles", func(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
failed := []uint{}
for _, id := range req.IDs {
if svc.Delete(c.Request.Context(), id) != nil {
failed = append(failed, id)
}
}
if len(failed) > 0 {
writeResult(c, codeSuccess, gin.H{"failedIds": failed}, "部分文件删除失败")
return
}
ok(c)
})
files.GET("/findFile", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
item, err := svc.Media(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, item, "获取成功")
})
files.POST("/editFileName", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
Name string `json:"name"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Rename(c.Request.Context(), req.ID, req.Name); err != nil {
fail(c, "编辑失败")
return
}
ok(c)
})
files.POST("/importURL", func(c *gin.Context) {
var req []struct {
Name string `json:"name"`
ClassID int `json:"classId"`
URL string `json:"url"`
Tag string `json:"tag"`
Key string `json:"key"`
Size int64 `json:"size"`
Mime string `json:"mime"`
MD5 string `json:"md5"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
claims := currentClaims(c)
items := make([]*biz.MediaFile, 0, len(req))
for _, v := range req {
items = append(items, &biz.MediaFile{Name: v.Name, CategoryID: v.ClassID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: claims.ID})
}
if err := svc.ImportURLs(c.Request.Context(), items); err != nil {
fail(c, "导入失败")
return
}
ok(c)
})
files.POST("/listOssFiles", func(c *gin.Context) {
var req struct {
Prefix string `json:"prefix"`
Cursor string `json:"cursor"`
Limit int `json:"limit"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, next, more, err := svc.Storage(c.Request.Context(), req.Prefix, req.Cursor, req.Limit)
if err != nil {
fail(c, "列举失败")
return
}
writeResult(c, codeSuccess, gin.H{"list": items, "nextCursor": next, "hasMore": more}, "获取成功")
})
categories := group.Group("/attachmentCategory")
categories.GET("/getCategoryList", func(c *gin.Context) {
items, err := svc.Categories(c.Request.Context())
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, items, "获取成功")
})
categories.POST("/addCategory", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
Name string `json:"name"`
PID uint `json:"pid"`
}
if c.ShouldBindJSON(&req) != nil || req.Name == "" {
fail(c, "参数错误")
return
}
if err := svc.SaveCategory(c.Request.Context(), &biz.AttachmentCategory{ID: req.ID, Name: req.Name, ParentID: req.PID}); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
categories.POST("/deleteCategory", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteCategory(c.Request.Context(), req.ID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
upload := group.Group("/mediaUpload")
upload.POST("/init", func(c *gin.Context) {
claims := currentClaims(c)
var req struct {
FileName string `json:"fileName"`
FileHash string `json:"fileHash"`
FileSize int64 `json:"fileSize"`
ChunkSize int64 `json:"chunkSize"`
ChunkTotal int `json:"chunkTotal"`
}
if claims == nil || c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
result, err := svc.InitUpload(c.Request.Context(), claims.ID, req.FileName, req.FileHash, req.FileSize, req.ChunkSize, req.ChunkTotal)
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, result, "获取成功")
})
upload.POST("/chunk", func(c *gin.Context) {
claims := currentClaims(c)
uploadID, _ := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
index, _ := strconv.Atoi(c.PostForm("chunkIndex"))
header, err := c.FormFile("chunk")
if claims == nil || err != nil {
fail(c, "接收分片失败")
return
}
opened, err := header.Open()
if err != nil {
fail(c, "读取分片失败")
return
}
defer opened.Close()
if err = svc.SaveChunk(c.Request.Context(), claims.ID, uint(uploadID), index, c.PostForm("chunkHash"), opened); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
upload.POST("/complete", func(c *gin.Context) {
claims := currentClaims(c)
var req struct {
UploadID uint `json:"uploadId"`
}
if claims == nil || c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
item, err := svc.CompleteUpload(c.Request.Context(), claims.ID, req.UploadID)
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"media": item}, "上传成功")
})
upload.DELETE("/:uploadId", func(c *gin.Context) {
claims := currentClaims(c)
id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
if claims == nil {
fail(c, "未登录")
return
}
if err := svc.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
}
+188
View File
@@ -0,0 +1,188 @@
package server
import (
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type menuRequest struct {
ID uint `json:"ID"`
ParentID uint `json:"parentId"`
Path string `json:"path"`
Name string `json:"name"`
Hidden bool `json:"hidden"`
Component string `json:"component"`
Sort int `json:"sort"`
Meta struct {
ActiveName string `json:"activeName"`
KeepAlive bool `json:"keepAlive"`
DefaultMenu bool `json:"defaultMenu"`
Title string `json:"title"`
Icon string `json:"icon"`
CloseTab bool `json:"closeTab"`
TransitionType string `json:"transitionType"`
} `json:"meta"`
MenuButtons []struct {
ID uint `json:"ID"`
Name string `json:"name"`
Description string `json:"desc"`
} `json:"menuBtn"`
}
func menuDomain(v menuRequest) *biz.Menu {
return &biz.Menu{ID: v.ID, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.Meta.ActiveName, KeepAlive: v.Meta.KeepAlive, DefaultMenu: v.Meta.DefaultMenu, Title: v.Meta.Title, Icon: v.Meta.Icon, CloseTab: v.Meta.CloseTab, TransitionType: v.Meta.TransitionType}
}
func menuButtons(v menuRequest, menuID uint) []*biz.MenuButton {
buttons := make([]*biz.MenuButton, 0, len(v.MenuButtons))
for _, button := range v.MenuButtons {
buttons = append(buttons, &biz.MenuButton{ID: button.ID, Name: button.Name, Description: button.Description, MenuID: menuID})
}
return buttons
}
func registerMenuRoutes(group *gin.RouterGroup, svc *service.AccessService) {
router := group.Group("/menu")
router.POST("/getMenuList", func(c *gin.Context) {
items, err := svc.Menus(c.Request.Context())
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: int64(len(items)), Page: 1, PageSize: 999}, "获取成功")
})
router.POST("/getBaseMenuTree", func(c *gin.Context) {
items, err := svc.MenuTree(c.Request.Context())
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"menus": items}, "获取成功")
})
router.POST("/addBaseMenu", func(c *gin.Context) {
var req menuRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
menu := menuDomain(req)
if err := svc.CreateMenu(c.Request.Context(), menu); err != nil {
fail(c, "新增失败")
return
}
if err := svc.ReplaceMenuButtons(c.Request.Context(), menu.ID, menuButtons(req, menu.ID)); err != nil {
fail(c, "按钮保存失败")
return
}
ok(c)
})
router.POST("/updateBaseMenu", func(c *gin.Context) {
var req menuRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateMenu(c.Request.Context(), menuDomain(req)); err != nil {
fail(c, "更新失败")
return
}
if err := svc.ReplaceMenuButtons(c.Request.Context(), req.ID, menuButtons(req, req.ID)); err != nil {
fail(c, "按钮保存失败")
return
}
ok(c)
})
router.POST("/deleteBaseMenu", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteMenu(c.Request.Context(), req.ID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
router.POST("/getBaseMenuById", func(c *gin.Context) {
var req struct {
ID uint `json:"id"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
item, err := svc.Menu(c.Request.Context(), req.ID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"menu": item}, "获取成功")
})
router.POST("/addMenuAuthority", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
Menus []struct {
ID uint `json:"ID"`
} `json:"menus"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
ids := make([]uint, 0, len(req.Menus))
for _, m := range req.Menus {
ids = append(ids, m.ID)
}
if err := svc.SetAuthorityMenus(c.Request.Context(), req.AuthorityID, ids); err != nil {
fail(c, "添加失败")
return
}
ok(c)
})
router.POST("/getMenuAuthority", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
menus, err := svc.AuthorityMenus(c.Request.Context(), req.AuthorityID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"menus": menus}, "获取成功")
})
router.GET("/getMenuRoles", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("menuId"), 10, 64)
ids, err := svc.MenuRoleIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
router.POST("/setMenuRoles", func(c *gin.Context) {
var req struct {
MenuID uint `json:"menuId"`
AuthorityIDs []uint `json:"authorityIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetMenuRoles(c.Request.Context(), req.MenuID, req.AuthorityIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
}
+237
View File
@@ -0,0 +1,237 @@
package server
import (
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerDepartmentRoutes(group *gin.RouterGroup, svc *service.AccessService) {
router := group.Group("/department")
router.POST("/getDepartmentList", func(c *gin.Context) {
var req struct {
Name string `json:"name"`
}
_ = c.ShouldBindJSON(&req)
items, err := svc.Departments(c.Request.Context(), req.Name)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, items, "获取成功")
})
type departmentRequest struct {
ID uint `json:"ID"`
Name string `json:"name"`
ParentID uint `json:"parentId"`
Sort int `json:"sort"`
LeaderID uint `json:"leaderId"`
Status bool `json:"status"`
}
router.POST("/createDepartment", func(c *gin.Context) {
var req departmentRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.CreateDepartment(c.Request.Context(), &biz.Department{Name: req.Name, ParentID: req.ParentID, Sort: req.Sort, LeaderID: req.LeaderID, Status: req.Status}); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
router.PUT("/updateDepartment", func(c *gin.Context) {
var req departmentRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdateDepartment(c.Request.Context(), &biz.Department{ID: req.ID, Name: req.Name, ParentID: req.ParentID, Sort: req.Sort, LeaderID: req.LeaderID, Status: req.Status}); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
router.DELETE("/deleteDepartment", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteDepartment(c.Request.Context(), req.ID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
router.GET("/findDepartment", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
item, err := svc.Department(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, item, "获取成功")
})
router.GET("/getDepartmentUsers", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("deptId"), 10, 64)
ids, err := svc.DepartmentUserIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
router.POST("/setDepartmentUsers", func(c *gin.Context) {
var req struct {
DepartmentID uint `json:"deptId"`
UserIDs []uint `json:"userIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetDepartmentUsers(c.Request.Context(), req.DepartmentID, req.UserIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
group.POST("/user/setUserDepartments", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
DepartmentIDs []uint `json:"deptIds"`
Primary uint `json:"primaryDeptId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetUserDepartments(c.Request.Context(), req.ID, req.DepartmentIDs, req.Primary); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
}
func registerPositionRoutes(group *gin.RouterGroup, svc *service.AccessService) {
router := group.Group("/position")
router.POST("/getPositionList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Name string `json:"name"`
Code string `json:"code"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, total, err := svc.Positions(c.Request.Context(), req.Page, req.PageSize, &biz.Position{Name: req.Name, Code: req.Code})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
type positionRequest struct {
ID uint `json:"ID"`
Name string `json:"name"`
Code string `json:"code"`
Sort int `json:"sort"`
Status bool `json:"status"`
Remark string `json:"remark"`
}
router.POST("/createPosition", func(c *gin.Context) {
var req positionRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.CreatePosition(c.Request.Context(), &biz.Position{Name: req.Name, Code: req.Code, Sort: req.Sort, Status: req.Status, Remark: req.Remark}); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
router.PUT("/updatePosition", func(c *gin.Context) {
var req positionRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.UpdatePosition(c.Request.Context(), &biz.Position{ID: req.ID, Name: req.Name, Code: req.Code, Sort: req.Sort, Status: req.Status, Remark: req.Remark}); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
router.DELETE("/deletePosition", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeletePosition(c.Request.Context(), req.ID); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.GET("/findPosition", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
item, err := svc.Position(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, item, "获取成功")
})
router.GET("/getPositionUsers", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("positionId"), 10, 64)
ids, err := svc.PositionUserIDs(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, ids, "获取成功")
})
router.POST("/setPositionUsers", func(c *gin.Context) {
var req struct {
PositionID uint `json:"positionId"`
UserIDs []uint `json:"userIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetPositionUsers(c.Request.Context(), req.PositionID, req.UserIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
group.POST("/user/setUserPositions", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
PositionIDs []uint `json:"positionIds"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetUserPositions(c.Request.Context(), req.ID, req.PositionIDs); err != nil {
fail(c, "设置失败")
return
}
ok(c)
})
}
+97
View File
@@ -0,0 +1,97 @@
package server
import (
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerPolicyRoutes(group *gin.RouterGroup, svc *service.AccessService) {
casbin := group.Group("/casbin")
casbin.POST("/updateCasbin", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
Infos []struct {
Path string `json:"path"`
Method string `json:"method"`
} `json:"casbinInfos"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
paths := make([]*biz.API, 0, len(req.Infos))
for _, v := range req.Infos {
paths = append(paths, &biz.API{Path: v.Path, Method: v.Method})
}
if err := svc.SetPolicyPaths(c.Request.Context(), req.AuthorityID, paths); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
casbin.POST("/getPolicyPathByAuthorityId", func(c *gin.Context) {
var req struct {
AuthorityID uint `json:"authorityId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
paths, err := svc.PolicyPaths(c.Request.Context(), req.AuthorityID)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"paths": paths}, "获取成功")
})
buttons := group.Group("/authorityBtn")
buttons.POST("/getAuthorityBtn", func(c *gin.Context) {
var req struct {
MenuID uint `json:"menuID"`
AuthorityID uint `json:"authorityId"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
selected, err := svc.SelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID)
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"selected": selected}, "查询成功")
})
buttons.POST("/setAuthorityBtn", func(c *gin.Context) {
var req struct {
MenuID uint `json:"menuID"`
AuthorityID uint `json:"authorityId"`
Selected []uint `json:"selected"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.SetSelectedButtons(c.Request.Context(), req.AuthorityID, req.MenuID, req.Selected); err != nil {
fail(c, "分配失败")
return
}
ok(c)
})
buttons.POST("/canRemoveAuthorityBtn", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
allowed, err := svc.CanRemoveButton(c.Request.Context(), uint(id))
if err != nil {
fail(c, "检查失败")
return
}
if !allowed {
fail(c, "此按钮正在被使用无法删除")
return
}
ok(c)
})
}
+50
View File
@@ -0,0 +1,50 @@
package server
import (
"net/http"
"github.com/gin-gonic/gin"
)
const (
codeSuccess = 0
codeError = 7
codePasswordChangeRequired = 10001
)
type response struct {
Code int `json:"code"`
Data any `json:"data"`
Msg string `json:"msg"`
}
type pageResult struct {
List any `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
func writeResult(c *gin.Context, code int, data any, message string) {
c.JSON(http.StatusOK, response{Code: code, Data: data, Msg: message})
}
func ok(c *gin.Context) {
writeResult(c, codeSuccess, gin.H{}, "操作成功")
}
func okWithData(c *gin.Context, data any) {
writeResult(c, codeSuccess, data, "成功")
}
func fail(c *gin.Context, message string) {
writeResult(c, codeError, gin.H{}, message)
}
func noAuth(c *gin.Context, message string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, response{
Code: codeError,
Data: nil,
Msg: message,
})
}
+1 -1
View File
@@ -5,4 +5,4 @@ import (
)
// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)
var ProviderSet = wire.NewSet(NewGRPCServer, NewGinServer, NewTaskScheduler)
+437
View File
@@ -0,0 +1,437 @@
package server
import (
"encoding/json"
"strconv"
"kra/internal/biz"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
type dictionaryRequest struct {
ID uint `json:"ID" form:"ID"`
Name string `json:"name" form:"name"`
Type string `json:"type" form:"type"`
Status *bool `json:"status" form:"status"`
Desc string `json:"desc" form:"desc"`
ParentID *uint `json:"parentID" form:"parentID"`
}
func dictionaryDomain(v dictionaryRequest) *biz.Dictionary {
status := true
if v.Status != nil {
status = *v.Status
}
return &biz.Dictionary{ID: v.ID, Name: v.Name, Type: v.Type, Status: status, Desc: v.Desc, ParentID: v.ParentID}
}
type detailRequest struct {
ID uint `json:"ID" form:"ID"`
Label string `json:"label" form:"label"`
Value string `json:"value" form:"value"`
Extend string `json:"extend" form:"extend"`
Status *bool `json:"status" form:"status"`
Sort int `json:"sort" form:"sort"`
DictionaryID uint `json:"sysDictionaryID" form:"sysDictionaryID"`
ParentID *uint `json:"parentID" form:"parentID"`
Level int `json:"level"`
Path string `json:"path"`
}
func detailDomain(v detailRequest) *biz.DictionaryDetail {
status := true
if v.Status != nil {
status = *v.Status
}
return &biz.DictionaryDetail{ID: v.ID, Label: v.Label, Value: v.Value, Extend: v.Extend, Status: status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID, Level: v.Level, Path: v.Path}
}
func registerSettingsRoutes(group *gin.RouterGroup, public *gin.RouterGroup, svc *service.SettingsService) {
registerDictionaryRoutes(group, svc)
registerParameterRoutes(group, svc)
registerAPITokenRoutes(group, svc)
}
func registerDictionaryRoutes(group *gin.RouterGroup, svc *service.SettingsService) {
dictionaries := group.Group("/sysDictionary")
dictionaries.POST("/createSysDictionary", func(c *gin.Context) {
var req dictionaryRequest
if c.ShouldBindJSON(&req) != nil || req.Name == "" || req.Type == "" {
fail(c, "参数错误")
return
}
if err := svc.CreateDictionary(c.Request.Context(), dictionaryDomain(req)); err != nil {
fail(c, "创建失败:"+err.Error())
return
}
ok(c)
})
dictionaries.PUT("/updateSysDictionary", func(c *gin.Context) {
var req dictionaryRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.UpdateDictionary(c.Request.Context(), dictionaryDomain(req)); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
dictionaries.DELETE("/deleteSysDictionary", func(c *gin.Context) {
var req dictionaryRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.DeleteDictionary(c.Request.Context(), req.ID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
dictionaries.GET("/findSysDictionary", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Dictionary(c.Request.Context(), uint(id), c.Query("type"), true)
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"reSysDictionary": item}, "查询成功")
})
listHandler := func(details bool) gin.HandlerFunc {
return func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Dictionaries(c.Request.Context(), page, size, c.Query("name"), c.Query("type"), details)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
}
}
dictionaries.GET("/getSysDictionaryList", listHandler(false))
dictionaries.GET("/getSysDictionaryListWithDetails", listHandler(true))
dictionaries.GET("/exportSysDictionary", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Dictionary(c.Request.Context(), uint(id), "", true)
if err != nil {
fail(c, "导出失败")
return
}
delete(item, "ID")
delete(item, "CreatedAt")
delete(item, "UpdatedAt")
delete(item, "DeletedAt")
writeResult(c, codeSuccess, item, "导出成功")
})
dictionaries.POST("/importSysDictionary", func(c *gin.Context) {
var req struct {
JSON string `json:"json"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
var payload struct {
Name string `json:"name"`
Type string `json:"type"`
Status *bool `json:"status"`
Desc string `json:"desc"`
Details []detailRequest `json:"sysDictionaryDetails"`
}
if json.Unmarshal([]byte(req.JSON), &payload) != nil || payload.Name == "" || payload.Type == "" {
fail(c, "JSON 格式错误")
return
}
dictionary := dictionaryDomain(dictionaryRequest{Name: payload.Name, Type: payload.Type, Status: payload.Status, Desc: payload.Desc})
if err := svc.CreateDictionary(c.Request.Context(), dictionary); err != nil {
fail(c, "导入失败:"+err.Error())
return
}
mapping := map[uint]uint{}
pending := append([]detailRequest(nil), payload.Details...)
for len(pending) > 0 {
progress := false
next := pending[:0]
for _, item := range pending {
oldID := item.ID
if item.ParentID != nil && *item.ParentID != 0 {
parent, exists := mapping[*item.ParentID]
if !exists {
next = append(next, item)
continue
}
item.ParentID = &parent
}
value := detailDomain(item)
value.ID = 0
value.DictionaryID = dictionary.ID
if err := svc.CreateDictionaryDetail(c.Request.Context(), value); err != nil {
fail(c, "导入详情失败")
return
}
if oldID != 0 {
mapping[oldID] = value.ID
}
progress = true
}
if !progress {
fail(c, "字典层级关系无效")
return
}
pending = next
}
ok(c)
})
details := group.Group("/sysDictionaryDetail")
details.POST("/createSysDictionaryDetail", func(c *gin.Context) {
var req detailRequest
if c.ShouldBindJSON(&req) != nil || req.DictionaryID == 0 {
fail(c, "参数错误")
return
}
if err := svc.CreateDictionaryDetail(c.Request.Context(), detailDomain(req)); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
details.PUT("/updateSysDictionaryDetail", func(c *gin.Context) {
var req detailRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.UpdateDictionaryDetail(c.Request.Context(), detailDomain(req)); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
details.DELETE("/deleteSysDictionaryDetail", func(c *gin.Context) {
var req detailRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.DeleteDictionaryDetail(c.Request.Context(), req.ID); err != nil {
fail(c, err.Error())
return
}
ok(c)
})
details.GET("/findSysDictionaryDetail", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.DictionaryDetail(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"reSysDictionaryDetail": item}, "查询成功")
})
details.GET("/getSysDictionaryDetailList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
did, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
items, total, err := svc.DictionaryDetails(c.Request.Context(), page, size, uint(did), c.Query("label"), c.Query("value"))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
treeHandler := func(byType bool) gin.HandlerFunc {
return func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
typ := ""
if byType {
typ = c.Query("type")
}
items, err := svc.DictionaryTree(c.Request.Context(), uint(id), typ)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, items, "获取成功")
}
}
details.GET("/getDictionaryTreeList", treeHandler(false))
details.GET("/getDictionaryTreeListByType", treeHandler(true))
details.GET("/getDictionaryDetailsByParent", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("parentID"), 10, 64)
did, _ := strconv.ParseUint(c.Query("sysDictionaryID"), 10, 64)
items, _, err := svc.DictionaryDetails(c.Request.Context(), 1, 10000, uint(did), "", "")
if err != nil {
fail(c, "获取失败")
return
}
out := []map[string]any{}
for _, item := range items {
parent, _ := item["parentID"].(*uint)
if (id == 0 && parent == nil) || (parent != nil && uint64(*parent) == id) {
out = append(out, item)
}
}
writeResult(c, codeSuccess, out, "获取成功")
})
details.GET("/getDictionaryPath", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.DictionaryDetail(c.Request.Context(), uint(id))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, gin.H{"path": item["path"]}, "获取成功")
})
}
type parameterRequest struct {
ID uint `json:"ID" form:"ID"`
Name string `json:"name" form:"name"`
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
Desc string `json:"desc" form:"desc"`
}
func parameterDomain(v parameterRequest) *biz.SystemParameter {
return &biz.SystemParameter{ID: v.ID, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
}
func registerParameterRoutes(group *gin.RouterGroup, svc *service.SettingsService) {
router := group.Group("/sysParams")
router.POST("/createSysParams", func(c *gin.Context) {
var req parameterRequest
if c.ShouldBindJSON(&req) != nil || req.Key == "" {
fail(c, "参数错误")
return
}
if err := svc.CreateParameter(c.Request.Context(), parameterDomain(req)); err != nil {
fail(c, "创建失败")
return
}
ok(c)
})
router.PUT("/updateSysParams", func(c *gin.Context) {
var req parameterRequest
if c.ShouldBindJSON(&req) != nil || req.ID == 0 {
fail(c, "参数错误")
return
}
if err := svc.UpdateParameter(c.Request.Context(), parameterDomain(req)); err != nil {
fail(c, "更新失败")
return
}
ok(c)
})
router.DELETE("/deleteSysParams", func(c *gin.Context) {
var req parameterRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteParameters(c.Request.Context(), []uint{req.ID}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.DELETE("/deleteSysParamsByIds", func(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DeleteParameters(c.Request.Context(), req.IDs); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.GET("/findSysParams", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Parameter(c.Request.Context(), uint(id), "")
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"resysParams": item}, "查询成功")
})
router.GET("/getSysParam", func(c *gin.Context) {
item, err := svc.Parameter(c.Request.Context(), 0, c.Query("key"))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, gin.H{"data": item}, "查询成功")
})
router.GET("/getSysParamsList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Parameters(c.Request.Context(), page, size, &biz.SystemParameter{Name: c.Query("name"), Key: c.Query("key")})
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
}
func registerAPITokenRoutes(group *gin.RouterGroup, svc *service.SettingsService) {
router := group.Group("/sysApiToken")
router.POST("/createApiToken", func(c *gin.Context) {
var req struct {
UserID uint `json:"userId"`
AuthorityID uint `json:"authorityId"`
Days int `json:"days"`
Remark string `json:"remark"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
token, err := svc.CreateAPIToken(c.Request.Context(), req.UserID, req.AuthorityID, req.Days, req.Remark)
if err != nil {
fail(c, "签发失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{"token": token}, "签发成功")
})
router.POST("/getApiTokenList", func(c *gin.Context) {
var req struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
UserID uint `json:"userId"`
Status *bool `json:"status"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
items, total, err := svc.APITokens(c.Request.Context(), req.Page, req.PageSize, req.UserID, req.Status)
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: req.Page, PageSize: req.PageSize}, "获取成功")
})
router.POST("/deleteApiToken", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.DisableAPIToken(c.Request.Context(), req.ID); err != nil {
fail(c, "作废失败")
return
}
ok(c)
})
}
+115
View File
@@ -0,0 +1,115 @@
package server
import (
"runtime"
"syscall"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/service"
"github.com/gin-gonic/gin"
)
func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, settings *service.SettingsService) {
security := group.Group("/securityConfig")
security.GET("/getSecurityConfig", func(c *gin.Context) {
value, err := settings.Security(c.Request.Context())
if err != nil {
fail(c, "获取安全配置失败")
return
}
writeResult(c, codeSuccess, value, "获取成功")
})
security.POST("/setSecurityConfig", func(c *gin.Context) {
var value biz.SecurityConfig
if c.ShouldBindJSON(&value) != nil {
fail(c, "参数错误")
return
}
result, err := settings.SaveSecurity(c.Request.Context(), &value)
if err != nil {
fail(c, err.Error())
return
}
writeResult(c, codeSuccess, result, "设置成功")
})
system := group.Group("/system")
system.POST("/getSystemConfig", func(c *gin.Context) {
admin := gin.H{"routerPrefix": ""}
if config != nil {
admin["routerPrefix"] = config.RouterPrefix
if config.Jwt != nil {
admin["jwt"] = gin.H{"signingKey": "******", "expiresTime": config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": config.Jwt.BufferTime.AsDuration().String(), "issuer": config.Jwt.Issuer}
}
if config.Captcha != nil {
admin["captcha"] = gin.H{"keyLong": config.Captcha.KeyLong, "imgWidth": config.Captcha.ImgWidth, "imgHeight": config.Captcha.ImgHeight, "storeExpiration": config.Captcha.StoreExpiration.AsDuration().String()}
}
if config.Local != nil {
admin["local"] = gin.H{"storePath": config.Local.StorePath, "pathPrefix": config.Local.PathPrefix}
}
}
writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin}}, "获取成功")
})
system.POST("/setSystemConfig", func(c *gin.Context) {
var req struct {
Config struct {
Admin struct {
RouterPrefix string `json:"routerPrefix"`
JWT struct {
SigningKey string `json:"signingKey"`
Issuer string `json:"issuer"`
} `json:"jwt"`
Local struct {
StorePath string `json:"storePath"`
PathPrefix string `json:"pathPrefix"`
} `json:"local"`
} `json:"admin"`
} `json:"config"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if config != nil {
config.RouterPrefix = req.Config.Admin.RouterPrefix
if config.Jwt != nil {
if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" {
config.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey
}
if req.Config.Admin.JWT.Issuer != "" {
config.Jwt.Issuer = req.Config.Admin.JWT.Issuer
}
}
if config.Local != nil {
if req.Config.Admin.Local.StorePath != "" {
config.Local.StorePath = req.Config.Admin.Local.StorePath
}
if req.Config.Admin.Local.PathPrefix != "" {
config.Local.PathPrefix = req.Config.Admin.Local.PathPrefix
}
}
}
ok(c)
})
system.POST("/reloadSystem", func(c *gin.Context) { writeResult(c, codeSuccess, gin.H{"reload": true}, "配置已热更新") })
system.POST("/getServerInfo", func(c *gin.Context) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
disk := []gin.H{}
var stat syscall.Statfs_t
if syscall.Statfs(".", &stat) == nil {
total := uint64(stat.Blocks) * uint64(stat.Bsize)
free := uint64(stat.Bavail) * uint64(stat.Bsize)
used := total - free
percent := 0
if total > 0 {
percent = int(used * 100 / total)
}
disk = append(disk, gin.H{"mountPoint": ".", "usedMb": used / 1024 / 1024, "usedGb": used / 1024 / 1024 / 1024, "totalMb": total / 1024 / 1024, "totalGb": total / 1024 / 1024 / 1024, "usedPercent": percent})
}
server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": []float64{}, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": mem.Alloc / 1024 / 1024, "totalMb": mem.Sys / 1024 / 1024, "usedPercent": 0}, "disk": disk}
writeResult(c, codeSuccess, gin.H{"server": server}, "获取成功")
})
}
+181
View File
@@ -0,0 +1,181 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"kra/internal/biz"
"kra/internal/service"
)
type taskRequest struct {
ID uint `json:"ID"`
Name string `json:"name"`
Description string `json:"description"`
Spec string `json:"spec"`
WithSeconds bool `json:"withSeconds"`
ExecutorType string `json:"executorType"`
MethodName string `json:"methodName"`
Params json.RawMessage `json:"params"`
HTTPURL string `json:"httpUrl"`
HTTPMethod string `json:"httpMethod"`
HTTPHeader json.RawMessage `json:"httpHeader"`
HTTPBody string `json:"httpBody"`
HTTPAllowPrivate bool `json:"httpAllowPrivate"`
Enabled bool `json:"enabled"`
}
func taskDomain(v taskRequest) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: v.Params, HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: v.HTTPHeader, HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func registerTaskRoutes(group *gin.RouterGroup, svc *service.TaskService, scheduler *TaskScheduler) {
router := group.Group("/timedTask")
router.POST("/createTimedTask", func(c *gin.Context) {
var req taskRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
task := taskDomain(req)
if err := svc.Create(c.Request.Context(), task); err != nil {
fail(c, "创建失败:"+err.Error())
return
}
if err := scheduler.Schedule(task); err != nil {
fail(c, "调度失败:"+err.Error())
return
}
ok(c)
})
router.PUT("/updateTimedTask", func(c *gin.Context) {
var req taskRequest
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
task := taskDomain(req)
if err := svc.Update(c.Request.Context(), task); err != nil {
fail(c, "更新失败:"+err.Error())
return
}
if err := scheduler.Schedule(task); err != nil {
fail(c, "调度失败:"+err.Error())
return
}
ok(c)
})
router.DELETE("/deleteTimedTask", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
scheduler.Remove(uint(id))
if err := svc.Delete(c.Request.Context(), uint(id)); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.POST("/toggleTimedTask", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
Enabled bool `json:"enabled"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
if err := svc.Toggle(c.Request.Context(), req.ID, req.Enabled); err != nil {
fail(c, "设置失败")
return
}
task, err := svc.Task(c.Request.Context(), req.ID)
if err != nil {
fail(c, "任务不存在")
return
}
if err = scheduler.Schedule(task); err != nil {
fail(c, "调度失败")
return
}
ok(c)
})
router.POST("/triggerTimedTask", func(c *gin.Context) {
var req struct {
ID uint `json:"ID"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
task, err := svc.Task(c.Request.Context(), req.ID)
if err != nil {
fail(c, "任务不存在")
return
}
scheduler.Trigger(task)
ok(c)
})
router.GET("/getTimedTaskList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
var enabled *bool
if raw := c.Query("enabled"); raw != "" {
value, _ := strconv.ParseBool(raw)
enabled = &value
}
items, total, err := svc.Tasks(c.Request.Context(), page, size, &biz.TimedTask{Name: c.Query("name"), ExecutorType: c.Query("executorType"), EnabledFilter: enabled}, scheduler.NextRuns())
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
router.GET("/getTimedTaskLogList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
taskID, _ := strconv.ParseUint(c.Query("taskId"), 10, 64)
items, total, err := svc.Logs(c.Request.Context(), page, size, uint(taskID), c.Query("status"))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
router.GET("/getRegisteredMethods", func(c *gin.Context) {
writeResult(c, codeSuccess, gin.H{"methods": svc.RegisteredMethods()}, "获取成功")
})
router.GET("/alertStream", func(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
flusher, okFlush := c.Writer.(http.Flusher)
if !okFlush {
fail(c, "SSE 不受支持")
return
}
events := scheduler.Subscribe()
defer scheduler.Unsubscribe(events)
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
_, _ = fmt.Fprint(c.Writer, "event: connected\ndata: {}\n\n")
flusher.Flush()
for {
select {
case event, open := <-events:
if !open {
return
}
_, _ = fmt.Fprintf(c.Writer, "event: alert\ndata: %s\n\n", event)
flusher.Flush()
case <-ticker.C:
_, _ = fmt.Fprint(c.Writer, ": keepalive\n\n")
flusher.Flush()
case <-c.Request.Context().Done():
return
}
}
})
}
+155
View File
@@ -0,0 +1,155 @@
package server
import (
"context"
"encoding/json"
"fmt"
"github.com/robfig/cron/v3"
"kra/internal/biz"
"kra/internal/service"
"log/slog"
"sync"
"time"
)
type TaskScheduler struct {
service *service.TaskService
logger *slog.Logger
standard *cron.Cron
seconds *cron.Cron
mu sync.Mutex
entries map[uint]scheduledEntry
subMu sync.RWMutex
subscribers map[chan []byte]struct{}
}
type scheduledEntry struct {
seconds bool
entry cron.EntryID
}
func NewTaskScheduler(service *service.TaskService, logger *slog.Logger) *TaskScheduler {
return &TaskScheduler{service: service, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[chan []byte]struct{}{}}
}
func (s *TaskScheduler) Start(ctx context.Context) error {
s.standard.Start()
s.seconds.Start()
items, _, err := s.service.Tasks(ctx, 1, 100000, nil, nil)
if err == nil {
for _, raw := range items {
id, _ := raw["ID"].(uint)
if task, findErr := s.service.Task(ctx, id); findErr == nil && task.Enabled {
if scheduleErr := s.Schedule(task); scheduleErr != nil {
s.logger.ErrorContext(ctx, "restore timed task failed", "id", id, "error", scheduleErr)
}
}
}
} else {
s.logger.WarnContext(ctx, "timed task table is not ready", "error", err)
}
<-ctx.Done()
return nil
}
func (s *TaskScheduler) Stop(ctx context.Context) error {
standardDone := s.standard.Stop().Done()
secondsDone := s.seconds.Stop().Done()
select {
case <-standardDone:
case <-ctx.Done():
return ctx.Err()
}
select {
case <-secondsDone:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (s *TaskScheduler) Remove(id uint) {
s.mu.Lock()
defer s.mu.Unlock()
if old, ok := s.entries[id]; ok {
if old.seconds {
s.seconds.Remove(old.entry)
} else {
s.standard.Remove(old.entry)
}
delete(s.entries, id)
}
}
func (s *TaskScheduler) Schedule(task *biz.TimedTask) error {
s.Remove(task.ID)
if !task.Enabled {
return nil
}
copy := *task
run := func() {
log := s.service.Run(context.Background(), &copy, "auto")
if log.Status != "success" {
s.Broadcast(map[string]any{"taskId": copy.ID, "taskName": copy.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
}
}
var id cron.EntryID
var err error
if task.WithSeconds {
id, err = s.seconds.AddFunc(task.Spec, run)
} else {
id, err = s.standard.AddFunc(task.Spec, run)
}
if err != nil {
return err
}
s.mu.Lock()
s.entries[task.ID] = scheduledEntry{seconds: task.WithSeconds, entry: id}
s.mu.Unlock()
return nil
}
func (s *TaskScheduler) NextRuns() map[uint]time.Time {
s.mu.Lock()
defer s.mu.Unlock()
out := map[uint]time.Time{}
for id, item := range s.entries {
if item.seconds {
out[id] = s.seconds.Entry(item.entry).Next
} else {
out[id] = s.standard.Entry(item.entry).Next
}
}
return out
}
func (s *TaskScheduler) Trigger(task *biz.TimedTask) {
go func() {
log := s.service.Run(context.Background(), task, "manual")
if log.Status != "success" {
s.Broadcast(map[string]any{"taskId": task.ID, "taskName": task.Name, "status": log.Status, "errorMsg": log.ErrorMsg, "time": time.Now()})
}
}()
}
func (s *TaskScheduler) Subscribe() chan []byte {
ch := make(chan []byte, 16)
s.subMu.Lock()
s.subscribers[ch] = struct{}{}
s.subMu.Unlock()
return ch
}
func (s *TaskScheduler) Unsubscribe(ch chan []byte) {
s.subMu.Lock()
if _, ok := s.subscribers[ch]; ok {
delete(s.subscribers, ch)
close(ch)
}
s.subMu.Unlock()
}
func (s *TaskScheduler) Broadcast(value any) {
raw, err := json.Marshal(value)
if err != nil {
raw = []byte(fmt.Sprint(value))
}
s.subMu.RLock()
defer s.subMu.RUnlock()
for ch := range s.subscribers {
select {
case ch <- raw:
default:
}
}
}
+165
View File
@@ -0,0 +1,165 @@
package server
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"kra/internal/biz"
"kra/internal/service"
)
type versionMenuRequest struct {
Path string `json:"path"`
Name string `json:"name"`
Hidden bool `json:"hidden"`
Component string `json:"component"`
Sort int `json:"sort"`
Meta struct {
ActiveName string `json:"activeName"`
KeepAlive bool `json:"keepAlive"`
DefaultMenu bool `json:"defaultMenu"`
Title string `json:"title"`
Icon string `json:"icon"`
CloseTab bool `json:"closeTab"`
TransitionType string `json:"transitionType"`
} `json:"meta"`
MenuButtons []struct {
Name string `json:"name"`
Desc string `json:"desc"`
} `json:"menuBtn"`
Children []versionMenuRequest `json:"children"`
}
func versionMenuDomain(v versionMenuRequest) *biz.Menu {
m := &biz.Menu{Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.Meta.ActiveName, KeepAlive: v.Meta.KeepAlive, DefaultMenu: v.Meta.DefaultMenu, Title: v.Meta.Title, Icon: v.Meta.Icon, CloseTab: v.Meta.CloseTab, TransitionType: v.Meta.TransitionType}
for _, b := range v.MenuButtons {
m.Buttons = append(m.Buttons, &biz.MenuButton{Name: b.Name, Description: b.Desc})
}
for _, child := range v.Children {
m.Children = append(m.Children, versionMenuDomain(child))
}
return m
}
func registerVersionRoutes(group *gin.RouterGroup, svc *service.VersionService) {
router := group.Group("/sysVersion")
router.DELETE("/deleteSysVersion", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
if err := svc.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.DELETE("/deleteSysVersionByIds", func(c *gin.Context) {
raw := c.QueryArray("IDs[]")
ids := make([]uint, 0, len(raw))
for _, v := range raw {
id, _ := strconv.ParseUint(v, 10, 64)
ids = append(ids, uint(id))
}
if err := svc.DeleteVersions(c.Request.Context(), ids); err != nil {
fail(c, "删除失败")
return
}
ok(c)
})
router.GET("/findSysVersion", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
item, err := svc.Version(c.Request.Context(), uint(id))
if err != nil {
fail(c, "查询失败")
return
}
writeResult(c, codeSuccess, item, "查询成功")
})
router.GET("/getSysVersionList", func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
items, total, err := svc.Versions(c.Request.Context(), page, size, c.Query("versionName"), c.Query("versionCode"))
if err != nil {
fail(c, "获取失败")
return
}
writeResult(c, codeSuccess, pageResult{List: items, Total: total, Page: page, PageSize: size}, "获取成功")
})
router.POST("/exportVersion", func(c *gin.Context) {
var req struct {
VersionName string `json:"versionName"`
VersionCode string `json:"versionCode"`
Description string `json:"description"`
MenuIDs []uint `json:"menuIds"`
APIIDs []uint `json:"apiIds"`
DictIDs []uint `json:"dictIds"`
}
if c.ShouldBindJSON(&req) != nil || req.VersionName == "" || req.VersionCode == "" {
fail(c, "参数错误")
return
}
if err := svc.Export(c.Request.Context(), req.VersionName, req.VersionCode, req.Description, req.MenuIDs, req.APIIDs, req.DictIDs); err != nil {
fail(c, "创建发版失败:"+err.Error())
return
}
ok(c)
})
router.GET("/downloadVersionJson", func(c *gin.Context) {
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
raw, code, err := svc.VersionData(c.Request.Context(), uint(id))
if err != nil {
fail(c, "下载失败")
return
}
name := fmt.Sprintf("version_%s_%s.json", code, time.Now().Format("20060102150405"))
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
c.Data(http.StatusOK, "application/json; charset=utf-8", []byte(raw))
})
router.POST("/importVersion", func(c *gin.Context) {
var req struct {
Version struct {
Name string `json:"name"`
Code string `json:"code"`
Description string `json:"description"`
ExportTime string `json:"exportTime"`
} `json:"version"`
Menus []versionMenuRequest `json:"menus"`
APIs []struct {
Path string `json:"path"`
Description string `json:"description"`
APIGroup string `json:"apiGroup"`
Method string `json:"method"`
} `json:"apis"`
Dictionaries []struct {
Name string `json:"name"`
Type string `json:"type"`
Status bool `json:"status"`
Desc string `json:"desc"`
Details []detailRequest `json:"sysDictionaryDetails"`
} `json:"dictionaries"`
}
if c.ShouldBindJSON(&req) != nil {
fail(c, "参数错误")
return
}
bundle := &biz.VersionBundle{Name: req.Version.Name, Code: req.Version.Code, Description: req.Version.Description, ExportTime: req.Version.ExportTime}
for _, m := range req.Menus {
bundle.Menus = append(bundle.Menus, versionMenuDomain(m))
}
for _, a := range req.APIs {
bundle.APIs = append(bundle.APIs, &biz.API{Path: a.Path, Description: a.Description, APIGroup: a.APIGroup, Method: a.Method})
}
for _, d := range req.Dictionaries {
dictionary := &biz.Dictionary{Name: d.Name, Type: d.Type, Status: d.Status, Desc: d.Desc}
for _, detail := range d.Details {
dictionary.Details = append(dictionary.Details, detailDomain(detail))
}
bundle.Dictionaries = append(bundle.Dictionaries, dictionary)
}
if err := svc.Import(c.Request.Context(), bundle); err != nil {
fail(c, "导入失败:"+err.Error())
return
}
ok(c)
})
}