fix(permissions):修复权限相关问题

This commit is contained in:
qian committed 2026-09-03 00:46:57 +08:00
1 parent 8d4e8744d5
commit 9d61abda86
23 files changed
+225 -33

No files matched your search

+2
View File
@@ -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)
+10
View File
@@ -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:"删除的记录数"`
}
+8 -5
View File
@@ -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 踢出指定会话请求
+10
View File
@@ -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:"删除的记录数"`
}
@@ -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)
}
@@ -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)
}
+15
View File
@@ -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
}
+34 -2
View File
@@ -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
+16
View File
@@ -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
}
+3 -2
View File
@@ -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",
},
},
{
+4 -3
View File
@@ -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": "可修改管理员账号与其角色(含提权)",
+2 -1
View File
@@ -22,7 +22,8 @@ import (
//
// 注意这些只是初始值而非系统契约:用户可以任意修改,甚至删掉预置角色自建一套。
var builtinRoleDefaults = map[string][]string{
// 管理员:除 user:*(账号与角色)、system:update/plugin 外的全部权限。
// 管理员:除 user:*(账号与角色)与 system:plugin 外的全部权限
//(在线自更新为超管专属,不设权限点)。
// 与运营的区别就是「能碰钱(退款)和系统配置」—— 去掉这两项,两个角色就没有分开的必要。
"admin": {
"dashboard:view",
+2 -2
View File
@@ -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)
}
+2
View File
@@ -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) {
+16 -7
View File
@@ -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
}
@@ -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+")")
}
+10
View File
@@ -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 授权码换取令牌
+1 -1
View File
@@ -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',
+9 -4
View File
@@ -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)
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { ref, reactive, onMounted, h, computed } from 'vue'
import { Tag, Button, Select, Input, DatePicker } from '@arco-design/web-vue'
import { Message, Modal, Tag, Button, Select, Input, DatePicker } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import TableStats from '@/components/TableStats.vue'
import ResponsiveTable from '@/components/ResponsiveTable.vue'
import request from '@/utils/request'
import { hasPermission } from '@/utils/permission'
const loading = ref(false)
const data = ref<any[]>([])
@@ -202,6 +203,28 @@ function handlePageSizeChange(pageSize: number) {
fetchData()
}
// 清空全部渠道错误事件(硬删除):错误风暴时事件表堆积很快,需二次确认。
function clearErrors() {
Modal.confirm({
title: '清空全部渠道错误事件',
content: '将硬删除全部渠道错误事件记录(含全部统计依据的历史数据),此操作不可恢复,确定继续?',
okText: '清空',
okButtonProps: { status: 'danger' },
cancelText: '取消',
onOk: async () => {
try {
await request.delete('/admin/monitor/channel-errors/clear')
Message.success('渠道错误事件已清空')
pagination.current = 1
fetchData()
fetchStats()
} catch {
// error auto-shown by Axios interceptor
}
},
})
}
const statsCards = computed(() => {
if (!stats.value) return []
const rateLimitCount = stats.value.by_category?.find((c: any) => c.error_category === 'rate_limit')?.count || 0
@@ -224,7 +247,13 @@ onMounted(() => {
<template>
<div class="page-table">
<PageHeader title="渠道错误监控" description="上游渠道错误事件记录与趋势分析" />
<PageHeader title="渠道错误监控" description="上游渠道错误事件记录与趋势分析">
<template #actions>
<a-button v-if="hasPermission('monitor:edit')" status="danger" @click="clearErrors">
清空日志
</a-button>
</template>
</PageHeader>
<!-- Stats Cards -->
<div class="stats-row">
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import { Tag } from '@arco-design/web-vue'
import { Message, Modal, Tag } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import TableStats from '@/components/TableStats.vue'
import ResponsiveTable from '@/components/ResponsiveTable.vue'
import request from '@/utils/request'
import { hasPermission } from '@/utils/permission'
const loading = ref(false)
const data = ref<any[]>([])
@@ -223,6 +224,27 @@ function handleReset() {
fetchData()
}
// 清空全部拦截日志(硬删除):日志大量堆积时快速释放数据库空间,需二次确认。
function clearLogs() {
Modal.confirm({
title: '清空全部拦截日志',
content: '将硬删除全部内容过滤拦截日志记录,此操作不可恢复,确定继续?',
okText: '清空',
okButtonProps: { status: 'danger' },
cancelText: '取消',
onOk: async () => {
try {
await request.delete('/admin/audit/content-filter-logs/clear')
Message.success('拦截日志已清空')
pagination.current = 1
fetchData()
} catch {
// error auto-shown by Axios interceptor
}
},
})
}
onMounted(() => {
fetchData()
})
@@ -230,7 +252,13 @@ onMounted(() => {
<template>
<div class="page-table">
<PageHeader title="过滤拦截日志" description="查看内容过滤拦截记录,分析敏感词触发趋势" />
<PageHeader title="过滤拦截日志" description="查看内容过滤拦截记录,分析敏感词触发趋势">
<template #actions>
<a-button v-if="hasPermission('audit:clear')" status="danger" @click="clearLogs">
清空日志
</a-button>
</template>
</PageHeader>
<a-card :bordered="false" class="mb-4">
<a-space wrap>
@@ -8,6 +8,9 @@ import PageHeader from '@/components/PageHeader.vue'
import TableStats from '@/components/TableStats.vue'
import ResponsiveTable from '@/components/ResponsiveTable.vue'
import request from '@/utils/request'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const loading = ref(false)
const data = ref<any[]>([])
@@ -71,6 +74,8 @@ const columns: TableColumnData[] = [
fixed: 'right',
render({ record }) {
if (record.is_current) return null
// 超管会话仅账号本人可撤销(后端 assertCanOperateSessions 同规则兜底),其他人只能查看
if (record.role === 'super_admin' && record.user_id !== authStore.user?.id) return null
return h(Popconfirm, {
content: '确定撤销该会话?',
onOk: () => revokeSession(record.id),
+4 -2
View File
@@ -415,7 +415,9 @@ async function submitExport() {
onMounted(() => {
fetchStats()
fetchList()
fetchRetention()
// 保留策略读的是系统设置接口(system:view/system:edit 权限域),
// 页面本身只要求 file:view —— 无权限时不请求,避免进页面就弹 403
if (hasPermission('system:edit')) fetchRetention()
})
</script>
@@ -442,7 +444,7 @@ onMounted(() => {
</template>
</APopover>
<AButton v-if="hasPermission('file:cleanup')" :loading="cleanupLoading" @click="manualCleanup">手动清理过期文件</AButton>
<AButton type="primary" @click="openExportDialog">
<AButton v-if="hasPermission('system:edit')" type="primary" @click="openExportDialog">
<template #icon><IconExport /></template>
触发数据导出
</AButton>