做双端审批流

This commit is contained in:
hero920103 committed 2026-10-07 13:46:32 +08:00
1 parent 5b4dbb2b46
commit ffaadc26ae
69 files changed
+6662 -1071

No files matched your search

+23 -14
View File
@@ -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 (
+40 -24
View File
@@ -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
View File
@@ -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 ""
}