做双端审批流
This commit is contained in:
1 parent
5b4dbb2b46
commit
ffaadc26ae
69 files changed
+6662
-1071
No files matched your search
+23
-14
@@ -25,9 +25,16 @@ const (
|
||||
)
|
||||
|
||||
// protectedPrefixes 需要鉴权的业务前缀。
|
||||
// 仅保护业务 API:官网(/ /site/ /index/…)、开放 API(/api/…)、
|
||||
// 仅保护业务 API:官网(/ /site/ /index/…)、开放/登录器接口(/api/ 下的公开项)、
|
||||
// 静态资源与 ACME 验证路径均不在其中,避免误伤公开接口。
|
||||
var protectedPrefixes = []string{"/backend/", "/platform/", "/app/"}
|
||||
// 移动端(uniapp)已并入 /api/,故显式列出其中需要鉴权的移动端前缀,
|
||||
// 而登录器公开接口(/api/getcard、/api/cursor/*、/api/softwareupgrade/check 等)
|
||||
// 不在下列前缀内,保持公开。
|
||||
var protectedPrefixes = []string{
|
||||
"/backend/", "/platform/",
|
||||
"/api/oa/", "/api/notebook/", "/api/schedule", "/api/currentUser",
|
||||
"/api/activity/", "/api/home", "/api/quick-menus",
|
||||
}
|
||||
|
||||
// authWhitelist 受保护前缀内无需鉴权的路径(前缀匹配)
|
||||
var authWhitelist = []string{
|
||||
@@ -44,23 +51,25 @@ var authWhitelist = []string{
|
||||
"/backend/verifyAccount",
|
||||
"/backend/logout",
|
||||
|
||||
// platform 登录/找回密码
|
||||
// platform 登录/注册/找回密码
|
||||
"/platform/login",
|
||||
"/platform/sendLoginCode",
|
||||
"/platform/register",
|
||||
"/platform/sendRegisterCode",
|
||||
"/platform/resetPassword",
|
||||
"/platform/logout",
|
||||
// 平台开放接口(客户端更新通知,无需登录)
|
||||
"/platform/api/",
|
||||
|
||||
// app 登录/注册/找回密码
|
||||
"/app/login",
|
||||
"/app/sendLoginCode",
|
||||
"/app/register",
|
||||
"/app/sendRegisterCode",
|
||||
"/app/resetPassword",
|
||||
"/app/sendResetCode",
|
||||
"/app/verifyAccount",
|
||||
"/app/logout",
|
||||
// 移动端(/api)登录/注册/找回密码(/api/login 前缀同时覆盖 loginBySms、极验配置)
|
||||
"/api/login",
|
||||
"/api/sendLoginCode",
|
||||
"/api/register",
|
||||
"/api/sendRegisterCode",
|
||||
"/api/resetPassword",
|
||||
"/api/sendResetCode",
|
||||
"/api/verifyAccount",
|
||||
"/api/logout",
|
||||
// 登录器日程提醒确认(公开回调)
|
||||
"/api/schedule/reminder/ack",
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -60,16 +60,16 @@ func BeginOperationLog(ctx *context.Context) {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // 等待响应完成
|
||||
logRow := &models.SystemOperationLog{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
RequestData: strPtr(reqBody),
|
||||
Status: 1,
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
RequestData: strPtr(reqBody),
|
||||
Status: 1,
|
||||
ExecutionTime: 0,
|
||||
}
|
||||
_, err := models.Orm.Insert(logRow)
|
||||
@@ -88,20 +88,40 @@ func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// isMobileAppPath 判断是否属于移动端(/api/)功能接口前缀集合。
|
||||
// 登录器公开接口(/api/getcard、/api/cursor/*、/api/softwareupgrade/*、
|
||||
// /api/schedule/reminder/ack 等)不在此集合内。
|
||||
func isMobileAppPath(url string) bool {
|
||||
mobilePrefixes := []string{
|
||||
"/api/oa/", "/api/notebook/", "/api/currentUser",
|
||||
"/api/activity/", "/api/home", "/api/quick-menus",
|
||||
}
|
||||
// /api/schedule 需区分:/api/schedule/reminder/ack 属登录器,排除
|
||||
if strings.HasPrefix(url, "/api/schedule") {
|
||||
return !strings.HasPrefix(url, "/api/schedule/reminder/ack")
|
||||
}
|
||||
for _, p := range mobilePrefixes {
|
||||
if strings.HasPrefix(url, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WriteDeleteLog 控制器删除后直接写操作日志(带标题)
|
||||
func WriteDeleteLog(userID uint64, tid int, module, title string) {
|
||||
moduleCN := oplogModuleLabel(module)
|
||||
desc := fmt.Sprintf("您删除了%s:【%s】", moduleCN, truncateString(title, 20))
|
||||
tidVal := uint64(tid)
|
||||
logRow := &models.SystemOperationLog{
|
||||
UserID: userID,
|
||||
Tid: &tidVal,
|
||||
Module: module,
|
||||
Action: "删除",
|
||||
Method: "DELETE",
|
||||
URL: fmt.Sprintf("/app/%s", module),
|
||||
Status: 1,
|
||||
RequestData: &desc,
|
||||
UserID: userID,
|
||||
Tid: &tidVal,
|
||||
Module: module,
|
||||
Action: "删除",
|
||||
Method: "DELETE",
|
||||
URL: fmt.Sprintf("/api/%s", module),
|
||||
Status: 1,
|
||||
RequestData: &desc,
|
||||
ExecutionTime: 0,
|
||||
}
|
||||
models.Orm.Insert(logRow)
|
||||
@@ -291,12 +311,8 @@ func shouldSkipLogging(method, url string) bool {
|
||||
if strings.HasPrefix(url, "/api/softwareupgrade/check") {
|
||||
return true
|
||||
}
|
||||
// App 端读接口跳过(只记录写操作:POST/PUT/DELETE)
|
||||
if strings.HasPrefix(url, "/app/") && method == "GET" {
|
||||
return true
|
||||
}
|
||||
// App 端 DELETE 由控制器自行记录(需要标题信息)
|
||||
if strings.HasPrefix(url, "/app/") && method == "DELETE" {
|
||||
// 移动端(已并入 /api/)读接口跳过(只记录写操作:POST/PUT/DELETE)
|
||||
if isMobileAppPath(url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+42
-50
@@ -13,7 +13,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// 获取当前请求的路径
|
||||
path := ctx.Input.URL()
|
||||
|
||||
|
||||
// 不需要权限验证的路径列表
|
||||
publicPaths := []string{
|
||||
"/api/login",
|
||||
@@ -23,19 +23,19 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
"/api/program-infos/public",
|
||||
"/api/files/public",
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为公开路径
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为公开预览接口
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
@@ -43,7 +43,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
// 这里直接返回,因为JWT中间件已经拦截了
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
@@ -52,15 +52,15 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取当前路由对应的权限标识
|
||||
permission := getPermissionByPath(path, ctx.Input.Method())
|
||||
|
||||
|
||||
// 如果没有权限标识,说明该接口不需要权限控制
|
||||
if permission == "" {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
hasPermission, err := services.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
@@ -71,7 +71,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if !hasPermission {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
@@ -90,26 +90,19 @@ func getPermissionByPath(path, method string) string {
|
||||
// 这里只列举了部分示例,实际应该从数据库中加载
|
||||
permissionMap := map[string]string{
|
||||
// 用户管理
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id":"user:changePassword",
|
||||
|
||||
// 角色管理
|
||||
"GET:/api/roles": "role:list",
|
||||
"POST:/api/roles": "role:create",
|
||||
"GET:/api/roles/:id": "role:detail",
|
||||
"POST:/api/roles/:id": "role:update",
|
||||
"DELETE:/api/roles/:id": "role:delete",
|
||||
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id": "user:changePassword",
|
||||
|
||||
// 菜单管理
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
// 文件管理
|
||||
"GET:/api/files": "file:list",
|
||||
"POST:/api/files": "file:upload",
|
||||
@@ -121,29 +114,29 @@ func getPermissionByPath(path, method string) string {
|
||||
"DELETE:/api/files/:id": "file:delete",
|
||||
"GET:/api/files/search": "file:search",
|
||||
"GET:/api/files/statistics": "file:statistics",
|
||||
|
||||
|
||||
// 租户管理
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
// 知识库
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
}
|
||||
|
||||
|
||||
// 匹配路径(简化版本,不支持动态参数匹配)
|
||||
key := method + ":" + path
|
||||
if perm, ok := permissionMap[key]; ok {
|
||||
return perm
|
||||
}
|
||||
|
||||
|
||||
// 尝试匹配动态路由(简单的ID参数替换)
|
||||
// 例如:/api/user/123 -> /api/user/:id
|
||||
pathParts := strings.Split(path, "/")
|
||||
@@ -152,19 +145,19 @@ func getPermissionByPath(path, method string) string {
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
methodPart := parts[0]
|
||||
pathPattern := parts[1]
|
||||
|
||||
|
||||
if methodPart != method {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
patternParts := strings.Split(pathPattern, "/")
|
||||
if len(patternParts) != len(pathParts) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
match := true
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
@@ -176,13 +169,12 @@ func getPermissionByPath(path, method string) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if match {
|
||||
return perm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制)
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user