diff --git a/api/admin/admin.go b/api/admin/admin.go
index 649b62a..3481f55 100644
--- a/api/admin/admin.go
+++ b/api/admin/admin.go
@@ -37,6 +37,7 @@ type IAdminV1 interface {
ForwardingTraceGet(ctx context.Context, req *v1.ForwardingTraceGetReq) (res *v1.ForwardingTraceGetRes, err error)
OperationLogExport(ctx context.Context, req *v1.OperationLogExportReq) (res *v1.OperationLogExportRes, err error)
ContentFilterLogList(ctx context.Context, req *v1.ContentFilterLogListReq) (res *v1.ContentFilterLogListRes, err error)
+ ContentFilterLogClear(ctx context.Context, req *v1.ContentFilterLogClearReq) (res *v1.ContentFilterLogClearRes, err error)
AdminLogin(ctx context.Context, req *v1.AdminLoginReq) (res *v1.AdminLoginRes, err error)
AdminMe(ctx context.Context, req *v1.AdminMeReq) (res *v1.AdminMeRes, err error)
AdminLogout(ctx context.Context, req *v1.AdminLogoutReq) (res *v1.AdminLogoutRes, err error)
@@ -75,6 +76,7 @@ type IAdminV1 interface {
ChannelErrorTrend(ctx context.Context, req *v1.ChannelErrorTrendReq) (res *v1.ChannelErrorTrendRes, err error)
ChannelErrorTopChannels(ctx context.Context, req *v1.ChannelErrorTopChannelsReq) (res *v1.ChannelErrorTopChannelsRes, err error)
ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (res *v1.ChannelErrorCategoriesRes, err error)
+ ChannelErrorClear(ctx context.Context, req *v1.ChannelErrorClearReq) (res *v1.ChannelErrorClearRes, err error)
ChannelOAuthAuthURL(ctx context.Context, req *v1.ChannelOAuthAuthURLReq) (res *v1.ChannelOAuthAuthURLRes, err error)
ChannelOAuthExchange(ctx context.Context, req *v1.ChannelOAuthExchangeReq) (res *v1.ChannelOAuthExchangeRes, err error)
ChannelOAuthRefresh(ctx context.Context, req *v1.ChannelOAuthRefreshReq) (res *v1.ChannelOAuthRefreshRes, err error)
diff --git a/api/admin/v1/audit.go b/api/admin/v1/audit.go
index 2647980..24b53d9 100644
--- a/api/admin/v1/audit.go
+++ b/api/admin/v1/audit.go
@@ -142,3 +142,13 @@ type ContentFilterLogListRes struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
+
+// ContentFilterLogClearReq 清空全部内容过滤拦截日志(硬删除,用于日志大量堆积时快速释放数据库空间)
+type ContentFilterLogClearReq struct {
+ g.Meta `path:"/audit/content-filter-logs/clear" method:"delete" mime:"json" tags:"管理后台-审计" summary:"清空内容过滤拦截日志(硬删除全部)"`
+}
+
+// ContentFilterLogClearRes 清空内容过滤拦截日志响应
+type ContentFilterLogClearRes struct {
+ Deleted int64 `json:"deleted" dc:"删除的记录数"`
+}
diff --git a/api/admin/v1/auth.go b/api/admin/v1/auth.go
index 9e067dc..0cc4bef 100644
--- a/api/admin/v1/auth.go
+++ b/api/admin/v1/auth.go
@@ -97,11 +97,14 @@ type AdminSessionItem struct {
UserId int64 `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
- IpAddress string `json:"ip_address"`
- DeviceInfo string `json:"device_info"`
- ExpiresAt string `json:"expires_at"`
- CreatedAt string `json:"created_at"`
- IsCurrent bool `json:"is_current"`
+ // Role 是会话所属账号的角色(super_admin / admin),
+ // 前端据此隐藏非本人超管会话的撤销入口(后端另有 assertCanOperateSessions 兜底)
+ Role string `json:"role"`
+ IpAddress string `json:"ip_address"`
+ DeviceInfo string `json:"device_info"`
+ ExpiresAt string `json:"expires_at"`
+ CreatedAt string `json:"created_at"`
+ IsCurrent bool `json:"is_current"`
}
// AdminRevokeSessionReq 踢出指定会话请求
diff --git a/api/admin/v1/channel_error.go b/api/admin/v1/channel_error.go
index dce4a73..66a8292 100644
--- a/api/admin/v1/channel_error.go
+++ b/api/admin/v1/channel_error.go
@@ -64,3 +64,13 @@ type ChannelErrorCategoriesReq struct {
type ChannelErrorCategoriesRes struct {
Data []map[string]string `json:"data"`
}
+
+// ChannelErrorClearReq 清空全部渠道错误事件(硬删除,用于错误大量堆积时快速释放数据库空间)
+type ChannelErrorClearReq struct {
+ g.Meta `path:"/monitor/channel-errors/clear" method:"delete" mime:"json" tags:"管理后台-监控" summary:"清空渠道错误事件(硬删除全部)"`
+}
+
+// ChannelErrorClearRes 清空渠道错误事件响应
+type ChannelErrorClearRes struct {
+ Deleted int64 `json:"deleted" dc:"删除的记录数"`
+}
diff --git a/internal/controller/admin/admin_v1_audit.go b/internal/controller/admin/admin_v1_audit.go
index e2346d9..5517f3c 100644
--- a/internal/controller/admin/admin_v1_audit.go
+++ b/internal/controller/admin/admin_v1_audit.go
@@ -34,3 +34,6 @@ func (c *ControllerV1) ContentFilterLogList(ctx context.Context, req *v1.Content
func (c *ControllerV1) ForwardingTraceGet(ctx context.Context, req *v1.ForwardingTraceGetReq) (res *v1.ForwardingTraceGetRes, err error) {
return service.Admin().ForwardingTraceGet(ctx, req)
}
+func (c *ControllerV1) ContentFilterLogClear(ctx context.Context, req *v1.ContentFilterLogClearReq) (res *v1.ContentFilterLogClearRes, err error) {
+ return service.Admin().ContentFilterLogClear(ctx, req)
+}
diff --git a/internal/controller/admin/admin_v1_channel_error.go b/internal/controller/admin/admin_v1_channel_error.go
index 51f3ed7..697699f 100644
--- a/internal/controller/admin/admin_v1_channel_error.go
+++ b/internal/controller/admin/admin_v1_channel_error.go
@@ -22,3 +22,6 @@ func (c *ControllerV1) ChannelErrorTopChannels(ctx context.Context, req *v1.Chan
func (c *ControllerV1) ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (res *v1.ChannelErrorCategoriesRes, err error) {
return service.Admin().ChannelErrorCategories(ctx, req)
}
+func (c *ControllerV1) ChannelErrorClear(ctx context.Context, req *v1.ChannelErrorClearReq) (res *v1.ChannelErrorClearRes, err error) {
+ return service.Admin().ChannelErrorClear(ctx, req)
+}
diff --git a/internal/logic/admin/audit.go b/internal/logic/admin/audit.go
index 808998c..c6fffb1 100644
--- a/internal/logic/admin/audit.go
+++ b/internal/logic/admin/audit.go
@@ -600,3 +600,18 @@ func (s *sAdmin) ContentFilterLogList(ctx context.Context, req *v1.ContentFilter
PageSize: pageSize,
}, nil
}
+
+// ContentFilterLogClear 硬删除全部内容过滤拦截日志。
+// 拦截日志为追加型审计流水,堆积后用 TRUNCATE 秒级清空并立即归还磁盘空间
+// (DELETE 需等 VACUUM 回收)。TRUNCATE 不返回行数,故先取删除前条数作为反馈。
+// 表无数据库级外键,TRUNCATE 安全。
+func (s *sAdmin) ContentFilterLogClear(ctx context.Context, _ *v1.ContentFilterLogClearReq) (*v1.ContentFilterLogClearRes, error) {
+ count, err := dao.AudContentFilterLogs.Ctx(ctx).Count()
+ if err != nil {
+ return nil, err
+ }
+ if _, err := g.DB().Ctx(ctx).Exec(ctx, "TRUNCATE TABLE aud_content_filter_logs RESTART IDENTITY"); err != nil {
+ return nil, err
+ }
+ return &v1.ContentFilterLogClearRes{Deleted: int64(count)}, nil
+}
diff --git a/internal/logic/admin/auth.go b/internal/logic/admin/auth.go
index aea3726..e74330c 100644
--- a/internal/logic/admin/auth.go
+++ b/internal/logic/admin/auth.go
@@ -344,9 +344,9 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
ids = append(ids, id)
}
var users []entity.SysAdminUsers
- _ = dao.SysAdminUsers.Ctx(ctx).Fields("id, username, display_name").WhereIn("id", ids).Scan(&users)
+ _ = dao.SysAdminUsers.Ctx(ctx).Fields("id, username, display_name, role").WhereIn("id", ids).Scan(&users)
for _, u := range users {
- userMap[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName}
+ userMap[u.Id] = adminUserBrief{Username: u.Username, DisplayName: u.DisplayName, Role: u.Role}
}
}
@@ -357,6 +357,7 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
UserId: sess.UserId,
Username: userMap[sess.UserId].Username,
DisplayName: userMap[sess.UserId].DisplayName,
+ Role: userMap[sess.UserId].Role,
IpAddress: sess.IpAddress,
DeviceInfo: sess.DeviceInfo,
IsCurrent: sess.Id == currentSessionID,
@@ -377,6 +378,31 @@ func (s *sAdmin) ListSessions(ctx context.Context, req *v1.AdminSessionListReq)
}, nil
}
+// assertCanOperateSessions 校验当前调用者能否处置目标管理员账号的会话。
+//
+// 超级管理员的登录态任何人都不能代为操作(包括其他超管):踢掉超管会话等同于
+// 掐断平台最高权限入口,一旦下放就存在「借会话管理打击超管」的滥用路径。
+// 唯一例外是账号本人管理自己的登录设备(等同在其他设备登出)。
+// 查看不受限:会话列表对超管账号仍然展示,只是不可操作。
+func assertCanOperateSessions(ctx context.Context, targetUserID int64) error {
+ if targetUserID == 0 {
+ return nil
+ }
+ var user *entity.SysAdminUsers
+ err := dao.SysAdminUsers.Ctx(ctx).Where("id", targetUserID).Scan(&user)
+ if err = common.IgnoreScanNoRows(err); err != nil {
+ return err
+ }
+ // 目标账号不存在或不是超管:无可保护对象,交由路由层 user:edit 把关
+ if user == nil || user.Role != "super_admin" {
+ return nil
+ }
+ if common.GetCtxUserID(ctx) == targetUserID {
+ return nil
+ }
+ return common.NewBusinessError(consts.CodeForbidden, "超级管理员的会话仅限本人管理,其他人只能查看")
+}
+
// RevokeSession revokes a specific session.
func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionReq) (*v1.AdminRevokeSessionRes, error) {
// Look up session to get jti for Redis revocation
@@ -385,6 +411,9 @@ func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionRe
return nil, err
}
if sess != nil {
+ if err := assertCanOperateSessions(ctx, sess.UserId); err != nil {
+ return nil, err
+ }
common.MarkSessionRevoked(ctx, sess.Jti)
}
err = common.RevokeSession(ctx, "admin", req.Id)
@@ -396,6 +425,9 @@ func (s *sAdmin) RevokeSession(ctx context.Context, req *v1.AdminRevokeSessionRe
// ForceLogout revokes all sessions for a specific user.
func (s *sAdmin) ForceLogout(ctx context.Context, req *v1.AdminForceLogoutReq) (*v1.AdminForceLogoutRes, error) {
+ if err := assertCanOperateSessions(ctx, req.Id); err != nil {
+ return nil, err
+ }
err := common.RevokeAllSessions(ctx, "admin", req.Id)
if err != nil {
return nil, err
diff --git a/internal/logic/admin/channel_error.go b/internal/logic/admin/channel_error.go
index b1fb755..8a831d3 100644
--- a/internal/logic/admin/channel_error.go
+++ b/internal/logic/admin/channel_error.go
@@ -9,6 +9,7 @@ import (
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/admin/v1"
+ "github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
)
@@ -195,3 +196,18 @@ func (s *sAdmin) ChannelErrorCategories(ctx context.Context, req *v1.ChannelErro
},
}, nil
}
+
+// ChannelErrorClear 硬删除全部渠道错误事件。
+// 错误风暴时事件表堆积很快,用 TRUNCATE 而非逐行 DELETE 秒级清空并立即归还磁盘空间。
+// TRUNCATE 不返回行数,故先取删除前条数作为反馈。表无数据库级外键,TRUNCATE 安全。
+// 注意异步写入器(DefaultChannelErrorWriter)清空后继续写入新事件,属预期行为。
+func (s *sAdmin) ChannelErrorClear(ctx context.Context, _ *v1.ChannelErrorClearReq) (*v1.ChannelErrorClearRes, error) {
+ count, err := dao.ChnErrorEvents.Ctx(ctx).Count()
+ if err != nil {
+ return nil, err
+ }
+ if _, err := g.DB().Ctx(ctx).Exec(ctx, "TRUNCATE TABLE chn_error_events RESTART IDENTITY"); err != nil {
+ return nil, err
+ }
+ return &v1.ChannelErrorClearRes{Deleted: int64(count)}, nil
+}
diff --git a/internal/logic/admin/permission.go b/internal/logic/admin/permission.go
index d0e9e06..523cfc5 100644
--- a/internal/logic/admin/permission.go
+++ b/internal/logic/admin/permission.go
@@ -69,7 +69,7 @@ var predefinedPermissionGroups = []v1.PermissionGroup{
Name: "audit",
Label: "审计日志",
Permissions: []string{
- "audit:view", "audit:export", "audit:read_sensitive",
+ "audit:view", "audit:export", "audit:read_sensitive", "audit:clear",
},
},
{
@@ -97,7 +97,8 @@ var predefinedPermissionGroups = []v1.PermissionGroup{
Name: "system",
Label: "系统设置",
Permissions: []string{
- "system:view", "system:edit", "system:update", "system:plugin",
+ // 注:在线自更新无权限点 —— 整个 update 域硬性限定超管(superAdminOnlyRules)
+ "system:view", "system:edit", "system:plugin",
},
},
{
diff --git a/internal/logic/admin/role.go b/internal/logic/admin/role.go
index b5ca125..0ff0424 100644
--- a/internal/logic/admin/role.go
+++ b/internal/logic/admin/role.go
@@ -110,7 +110,7 @@ var tierPermissions = map[string]map[string][]string{
"audit": {
TierRead: {"audit:view"},
TierOperate: {"audit:view", "audit:export"},
- TierFull: {"audit:view", "audit:export", "audit:read_sensitive"},
+ TierFull: {"audit:view", "audit:export", "audit:read_sensitive", "audit:clear"},
},
"user": {
TierRead: {"user:view"},
@@ -120,7 +120,8 @@ var tierPermissions = map[string]map[string][]string{
"system": {
TierRead: {"system:view"},
TierOperate: {"system:view", "system:edit"},
- TierFull: {"system:view", "system:edit", "system:update", "system:plugin"},
+ // 在线自更新硬性限定超管、无权限点,「完全」档与「操作」档的差异只剩插件
+ TierFull: {"system:view", "system:edit", "system:plugin"},
},
}
@@ -130,8 +131,8 @@ var dangerousPermissions = map[string]string{
"billing:refund": "可对任意租户钱包退款/调账",
"order:refund": "可对订单发起退款到支付渠道",
"audit:read_sensitive": "可查看敏感数据访问日志",
+ "audit:clear": "可硬删除全部拦截日志(不可恢复)",
"system:edit": "可修改支付、汇率、存储、邮件等全站配置",
- "system:update": "可触发系统版本更新",
"system:plugin": "可安装/启停插件(等同于部署代码)",
"user:create": "可创建管理员账号",
"user:edit": "可修改管理员账号与其角色(含提权)",
diff --git a/internal/logic/admin/role_crud.go b/internal/logic/admin/role_crud.go
index f2a4423..1a7c202 100644
--- a/internal/logic/admin/role_crud.go
+++ b/internal/logic/admin/role_crud.go
@@ -22,7 +22,8 @@ import (
//
// 注意这些只是初始值而非系统契约:用户可以任意修改,甚至删掉预置角色自建一套。
var builtinRoleDefaults = map[string][]string{
- // 管理员:除 user:*(账号与角色)、system:update/plugin 外的全部权限。
+ // 管理员:除 user:*(账号与角色)与 system:plugin 外的全部权限
+ //(在线自更新为超管专属,不设权限点)。
// 与运营的区别就是「能碰钱(退款)和系统配置」—— 去掉这两项,两个角色就没有分开的必要。
"admin": {
"dashboard:view",
diff --git a/internal/logic/admin/role_test.go b/internal/logic/admin/role_test.go
index 43c4372..f6463e4 100644
--- a/internal/logic/admin/role_test.go
+++ b/internal/logic/admin/role_test.go
@@ -159,9 +159,9 @@ func TestBuiltinRoleBoundaries(t *testing.T) {
}
}
- // 插件是代码执行面、更新是版本变更,仅限超管
+ // 插件是代码执行面、敏感日志是隐私面,仅限超管(在线自更新无权限点,超管专属)
for code := range builtinRoleDefaults {
- for _, perm := range []string{"system:plugin", "system:update", "audit:read_sensitive"} {
+ for _, perm := range []string{"system:plugin", "audit:read_sensitive"} {
if has(code, perm) {
t.Errorf("预置角色 %s 不应拥有 %s(仅限超级管理员)", code, perm)
}
diff --git a/internal/logic/admin/security.go b/internal/logic/admin/security.go
index 65623ad..ed4c998 100644
--- a/internal/logic/admin/security.go
+++ b/internal/logic/admin/security.go
@@ -255,6 +255,8 @@ func (s *sAdmin) LoginHistory(ctx context.Context, req *v1.AdminLoginHistoryReq)
type adminUserBrief struct {
Username string
DisplayName string
+ // Role 用于会话列表标记超管会话(前端隐藏撤销入口),其他场景可为空
+ Role string
}
func adminUserIdsByUsername(ctx context.Context, keyword string) ([]int64, error) {
diff --git a/internal/middleware/rbac.go b/internal/middleware/rbac.go
index 4ca7770..298b1ba 100644
--- a/internal/middleware/rbac.go
+++ b/internal/middleware/rbac.go
@@ -203,6 +203,7 @@ var adminPermissionRules = []permissionRule{
{method: "GET", prefix: "/api/admin/audit/forwarding-trace/", perm: "audit:view"},
{method: "GET", path: "/api/admin/audit/sensitive-logs", perm: "audit:read_sensitive"},
{method: "GET", path: "/api/admin/audit/content-filter-logs", perm: "audit:view"},
+ {method: "DELETE", path: "/api/admin/audit/content-filter-logs/clear", perm: "audit:clear"},
// ── file 文件管理 ──
{method: "GET", path: "/api/admin/files", perm: "file:view"},
@@ -228,6 +229,7 @@ var adminPermissionRules = []permissionRule{
{method: "PUT", prefix: "/api/admin/error-logs/", perm: "monitor:edit"},
{method: "PUT", path: "/api/admin/error-logs/batch-resolve", perm: "monitor:edit"},
{method: "DELETE", path: "/api/admin/error-logs/clear", perm: "monitor:edit"},
+ {method: "DELETE", path: "/api/admin/monitor/channel-errors/clear", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/error-logs/stats", perm: "monitor:view"},
{method: "GET", path: "/api/admin/cron-jobs", perm: "system:view"},
{method: "POST", prefix: "/api/admin/cron-jobs/", suffix: "/trigger", perm: "system:edit"},
@@ -243,8 +245,6 @@ var adminPermissionRules = []permissionRule{
{method: "PUT", prefix: "/api/admin/payment-channels/", perm: "system:edit"},
{method: "GET", path: "/api/admin/payment-settings", perm: "system:view"},
{method: "PUT", path: "/api/admin/payment-settings", perm: "system:edit"},
- {method: "GET", prefix: "/api/admin/update/", perm: "system:update"},
- {method: "POST", prefix: "/api/admin/update/", perm: "system:update"},
{method: "GET", path: "/api/admin/data-governance/settings", perm: "system:view"},
{method: "PUT", path: "/api/admin/data-governance/settings", perm: "system:edit"},
{method: "POST", prefix: "/api/admin/data-governance/", perm: "system:edit"},
@@ -305,9 +305,11 @@ var adminPermissionRules = []permissionRule{
{method: "POST", path: "/api/admin/auth/logout", perm: "self:access"},
// 当前用户信息与有效权限:登录即可访问自己的资料,不需要任何业务权限点
{method: "GET", path: "/api/admin/auth/me", perm: "self:access"},
- {method: "GET", path: "/api/admin/auth/sessions", perm: "self:access"},
- {method: "DELETE", prefix: "/api/admin/auth/sessions/user/", perm: "user:edit"},
- {method: "DELETE", prefix: "/api/admin/auth/sessions/", perm: "self:access"},
+ // 会话管理列出并处置的是【全部管理员】的登录态,属于账号管理(user 域)的操作能力:
+ // 按用户管理模块的操作档位(user:edit)授权,只读档位不可见。
+ // 超管会话的额外保护在 logic 层(assertCanOperateSessions):仅账号本人可操作。
+ {method: "GET", path: "/api/admin/auth/sessions", perm: "user:edit"},
+ {method: "DELETE", prefix: "/api/admin/auth/sessions/", perm: "user:edit"},
{method: "PUT", path: "/api/admin/auth/change-password", perm: "self:access"},
{method: "POST", prefix: "/api/admin/security/2fa/", perm: "self:access"},
{method: "GET", path: "/api/admin/security/login-history", perm: "audit:view"},
@@ -356,6 +358,13 @@ var superAdminOnlyRules = []permissionRule{
// 给账号分配角色 —— 这是最直接的提权入口
{method: "GET", prefix: "/api/admin/users/", suffix: "/roles"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/roles"},
+
+ // 在线自更新(版本状态 / 检查 / 执行 / 回滚):替换平台二进制并重启服务,
+ // 等同于在服务器上执行代码,是比角色管理更根本的平台级元操作,不下放。
+ // 版本检查(GET)看似只读,但前端每次刷新都会请求,若走权限点授权,
+ // 未授权角色会持续收到 403 —— 硬性限定超管后非超管前端不再发起请求。
+ {method: "GET", prefix: "/api/admin/update/"},
+ {method: "POST", prefix: "/api/admin/update/"},
}
// isSuperAdminOnly 判断路由是否仅限超级管理员。
@@ -407,9 +416,9 @@ func AdminPermissionGuard(r *ghttp.Request) {
return
}
- // 角色管理是权限体系的元操作,硬性限定超管,不参与权限点授权
+ // 角色管理与在线自更新是平台级元操作,硬性限定超管,不参与权限点授权
if isSuperAdminOnly(r.Method, r.URL.Path) {
- response.ErrorMsg(r, consts.CodeForbidden, "角色权限管理仅超级管理员可用")
+ response.ErrorMsg(r, consts.CodeForbidden, "该操作仅超级管理员可用")
return
}
diff --git a/internal/middleware/rbac_coverage_test.go b/internal/middleware/rbac_coverage_test.go
index 096e8eb..bc07a9c 100644
--- a/internal/middleware/rbac_coverage_test.go
+++ b/internal/middleware/rbac_coverage_test.go
@@ -170,6 +170,11 @@ func TestEveryAdminRouteHasPermissionRule(t *testing.T) {
if isAdminPublicPath(path) {
continue
}
+ // 超管专属路由(角色管理、在线自更新)由 superAdminOnlyRules 硬性拦截,
+ // 先于权限规则判定,不走权限点授权,因此不要求出现在 adminPermissionRules 里。
+ if isSuperAdminOnly(r.Method, path) {
+ continue
+ }
if perm := matchPermission(r.Method, path); perm == "" {
missing = append(missing, r.Method+" "+r.Path+" ("+r.Source+")")
}
diff --git a/internal/service/admin.go b/internal/service/admin.go
index 8beaeb0..3c8473f 100644
--- a/internal/service/admin.go
+++ b/internal/service/admin.go
@@ -69,6 +69,11 @@ type (
// ContentFilterLogList returns a paginated list of content filter interception logs.
// 审计数据从审计库查询,关联信息从主库批量查询后在应用层合并。
ContentFilterLogList(ctx context.Context, req *v1.ContentFilterLogListReq) (*v1.ContentFilterLogListRes, error)
+ // ContentFilterLogClear 硬删除全部内容过滤拦截日志。
+ // 拦截日志为追加型审计流水,堆积后用 TRUNCATE 秒级清空并立即归还磁盘空间
+ // (DELETE 需等 VACUUM 回收)。TRUNCATE 不返回行数,故先取删除前条数作为反馈。
+ // 表无数据库级外键,TRUNCATE 安全。
+ ContentFilterLogClear(ctx context.Context, _ *v1.ContentFilterLogClearReq) (*v1.ContentFilterLogClearRes, error)
// Login handles admin login.
Login(ctx context.Context, req *v1.AdminLoginReq) (*v1.AdminLoginRes, error)
// Logout handles admin logout.
@@ -151,6 +156,11 @@ type (
ChannelErrorTopChannels(ctx context.Context, req *v1.ChannelErrorTopChannelsReq) (*v1.ChannelErrorTopChannelsRes, error)
// ChannelErrorCategories 错误分类选项
ChannelErrorCategories(ctx context.Context, req *v1.ChannelErrorCategoriesReq) (*v1.ChannelErrorCategoriesRes, error)
+ // ChannelErrorClear 硬删除全部渠道错误事件。
+ // 错误风暴时事件表堆积很快,用 TRUNCATE 而非逐行 DELETE 秒级清空并立即归还磁盘空间。
+ // TRUNCATE 不返回行数,故先取删除前条数作为反馈。表无数据库级外键,TRUNCATE 安全。
+ // 注意异步写入器(DefaultChannelErrorWriter)清空后继续写入新事件,属预期行为。
+ ChannelErrorClear(ctx context.Context, _ *v1.ChannelErrorClearReq) (*v1.ChannelErrorClearRes, error)
// ChannelOAuthAuthURL 生成 OAuth 授权链接
ChannelOAuthAuthURL(ctx context.Context, req *v1.ChannelOAuthAuthURLReq) (*v1.ChannelOAuthAuthURLRes, error)
// ChannelOAuthExchange OAuth 授权码换取令牌
diff --git a/web/admin/src/router/admin.ts b/web/admin/src/router/admin.ts
index f2555ca..27a2b8e 100644
--- a/web/admin/src/router/admin.ts
+++ b/web/admin/src/router/admin.ts
@@ -119,7 +119,7 @@ const adminRoutes: RouteRecordRaw[] = [
path: 'sessions',
name: 'AdminSessions',
component: () => import('@/views/security/SessionsPage.vue'),
- meta: { title: '会话管理' },
+ meta: { title: '会话管理', perm: 'user:edit' },
},
{
path: 'permissions',
diff --git a/web/admin/src/views/AdminLayout.vue b/web/admin/src/views/AdminLayout.vue
index e0e4ceb..cb9278e 100644
--- a/web/admin/src/views/AdminLayout.vue
+++ b/web/admin/src/views/AdminLayout.vue
@@ -128,6 +128,8 @@ async function fetchUpdateStatus() {
}
function openUpdateModal() {
+ // 在线更新是超管专属能力(后端 superAdminOnlyRules 拦截),非超管不打开弹窗也不发请求
+ if (!isSuperAdminUser.value) return
updateModalVisible.value = true
fetchUpdateStatus()
checkUpdate() // 打开弹窗时刷新版本检查,确保当前版本/最新版本为最新
@@ -356,7 +358,7 @@ const ALL_MENU_GROUPS = [
items: [
{ name: 'AdminLoginHistory', label: '登录历史', icon: IconClockCircle, perm: 'audit:view' },
{ name: 'AdminTenantLoginHistory', label: '租户登录历史', icon: IconClockCircle, perm: 'audit:view' },
- { name: 'AdminSessions', label: '会话管理', icon: IconClockCircle },
+ { name: 'AdminSessions', label: '会话管理', icon: IconClockCircle, perm: 'user:edit' },
{ name: 'AdminAudit', label: '操作日志', icon: IconFile, perm: 'audit:view' },
],
},
@@ -515,9 +517,12 @@ onMounted(() => {
updateMobile()
window.addEventListener('resize', updateMobile)
- // Check for updates on mount
- checkUpdate()
- fetchUpdateStatus() // 取后端实时版本,用于准确的"是否可更新"判断
+ // 在线更新是超管专属能力(后端 superAdminOnlyRules 拦截),
+ // 非超管不发请求,避免每次刷新页面都弹「缺少权限」错误提示
+ if (isSuperAdminUser.value) {
+ checkUpdate()
+ fetchUpdateStatus() // 取后端实时版本,用于准确的"是否可更新"判断
+ }
fetchWorkbenchBadges()
badgeTimer = setInterval(fetchWorkbenchBadges, 60000)
diff --git a/web/admin/src/views/monitor/ChannelErrorsPage.vue b/web/admin/src/views/monitor/ChannelErrorsPage.vue
index eb1ab1d..40d54ff 100644
--- a/web/admin/src/views/monitor/ChannelErrorsPage.vue
+++ b/web/admin/src/views/monitor/ChannelErrorsPage.vue
@@ -1,11 +1,12 @@
@@ -442,7 +444,7 @@ onMounted(() => {
手动清理过期文件
-
+
触发数据导出