Files
team-api/internal/middleware/rbac.go
T

423 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"strings"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/qianfree/team-api/internal/consts"
"github.com/qianfree/team-api/internal/logic/admin"
"github.com/qianfree/team-api/internal/response"
)
// RequirePermission returns middleware that checks if the admin user has the required permission.
func RequirePermission(permission string) func(r *ghttp.Request) {
return func(r *ghttp.Request) {
role := GetUserRole(r.Context())
userID := GetUserID(r.Context())
if role == "super_admin" {
r.Middleware.Next()
return
}
if !admin.HasPermission(r.Context(), userID, role, permission) {
response.ErrorMsg(r, consts.CodeForbidden, "缺少权限:"+permission)
return
}
r.Middleware.Next()
}
}
// RequireTenantRole returns middleware that checks if the tenant user has the required role.
func RequireTenantRole(roles ...string) func(r *ghttp.Request) {
roleSet := make(map[string]bool)
for _, r := range roles {
roleSet[r] = true
}
return func(r *ghttp.Request) {
role := GetUserRole(r.Context())
if !roleSet[role] {
response.ErrorMsg(r, consts.CodeForbidden, consts.MsgForbidden)
return
}
r.Middleware.Next()
}
}
// adminPermissionRules defines the mapping from route patterns to permission points.
// Unmapped admin routes are denied by default in AdminPermissionGuard.
var adminPermissionRules = []permissionRule{
// ── tenant 租户管理 ──
{method: "GET", path: "/api/admin/tenants", perm: "tenant:view"},
{method: "GET", prefix: "/api/admin/tenants/", perm: "tenant:view"},
{method: "POST", path: "/api/admin/tenants", perm: "tenant:create"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/status", perm: "tenant:suspend"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/channel-scope", perm: "tenant:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", perm: "tenant:edit"},
{method: "GET", path: "/api/admin/tenants/export", perm: "tenant:view"},
{method: "GET", path: "/api/admin/tenants/select", perm: "tenant:view"},
{method: "GET", path: "/api/admin/tenants/members/export", perm: "member:view"},
// ── user 用户管理(管理员) ──
{method: "GET", path: "/api/admin/users", perm: "user:view"},
{method: "GET", path: "/api/admin/users/export", perm: "user:view"},
{method: "POST", path: "/api/admin/users", perm: "user:create"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/status", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/reset-password", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/unlock", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", perm: "user:edit"},
{method: "DELETE", prefix: "/api/admin/users/", perm: "user:delete"},
// ── channel 渠道管理 ──
{method: "GET", path: "/api/admin/channels", perm: "channel:view"},
{method: "GET", path: "/api/admin/channels/export", perm: "channel:view"},
{method: "GET", path: "/api/admin/channels/provider-default-urls", perm: "channel:view"},
{method: "GET", prefix: "/api/admin/channels/", perm: "channel:view"},
{method: "POST", path: "/api/admin/channels", perm: "channel:create"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/test", perm: "channel:test"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/clone", perm: "channel:create"},
{method: "POST", prefix: "/api/admin/channels/", suffix: "/keys", perm: "channel:edit"},
{method: "DELETE", prefix: "/api/admin/channels/", suffix: "/keys/", perm: "channel:edit"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/keys", perm: "channel:view"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/abilities", perm: "channel:view"},
{method: "PUT", prefix: "/api/admin/channels/", suffix: "/abilities", perm: "channel:edit"},
{method: "GET", prefix: "/api/admin/channels/", suffix: "/health_trend", perm: "channel:view"},
{method: "PUT", prefix: "/api/admin/channels/", perm: "channel:edit"},
{method: "DELETE", prefix: "/api/admin/channels/", perm: "channel:delete"},
// Channel OAuth helper endpoints
{method: "POST", path: "/api/admin/channels/oauth/auth-url", perm: "channel:edit"},
{method: "POST", path: "/api/admin/channels/oauth/exchange", perm: "channel:edit"},
{method: "POST", path: "/api/admin/channels/oauth/refresh", perm: "channel:edit"},
// ── model 模型管理 ──
{method: "GET", path: "/api/admin/models", perm: "model:view"},
{method: "GET", path: "/api/admin/models/options", perm: "model:view"},
{method: "GET", path: "/api/admin/models/export", perm: "model:view"},
{method: "GET", path: "/api/admin/models/official-info", perm: "model:view"},
{method: "GET", prefix: "/api/admin/models/", suffix: "/pricing", perm: "model:view"},
{method: "PUT", prefix: "/api/admin/models/", suffix: "/pricing", perm: "model:edit"},
{method: "GET", prefix: "/api/admin/models/", suffix: "/official-pricing", perm: "model:view"},
{method: "GET", prefix: "/api/admin/models/", perm: "model:view"},
{method: "POST", path: "/api/admin/models", perm: "model:create"},
{method: "POST", path: "/api/admin/models/export-json", perm: "model:view"},
{method: "POST", path: "/api/admin/models/import-preview", perm: "model:create"},
{method: "POST", path: "/api/admin/models/import", perm: "model:create"},
{method: "PUT", prefix: "/api/admin/models/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/models/", perm: "model:delete"},
// Model groups
{method: "GET", path: "/api/admin/model-groups", perm: "model:view"},
{method: "GET", path: "/api/admin/model-groups/options", perm: "model:view"},
{method: "GET", prefix: "/api/admin/model-groups/", suffix: "/models", perm: "model:view"},
{method: "PUT", prefix: "/api/admin/model-groups/", suffix: "/models", perm: "model:edit"},
{method: "GET", prefix: "/api/admin/model-groups/", perm: "model:view"},
{method: "POST", path: "/api/admin/model-groups", perm: "model:create"},
{method: "PUT", prefix: "/api/admin/model-groups/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/model-groups/", perm: "model:delete"},
// Tenant model assignments
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/models", perm: "model:view"},
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/available-models", perm: "model:view"},
{method: "GET", prefix: "/api/admin/tenants/", suffix: "/groups", perm: "model:view"},
{method: "POST", prefix: "/api/admin/tenants/", suffix: "/models", perm: "model:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/groups", perm: "model:edit"},
{method: "PUT", prefix: "/api/admin/tenants/", suffix: "/models/", perm: "model:edit"},
{method: "DELETE", prefix: "/api/admin/tenants/", suffix: "/models/", perm: "model:delete"},
// ── billing 计费管理 ──
{method: "GET", path: "/api/admin/billing-records", perm: "billing:view"},
{method: "GET", path: "/api/admin/billing-records/export", perm: "billing:export"},
{method: "GET", path: "/api/admin/usage-logs", perm: "billing:view"},
{method: "GET", path: "/api/admin/usage-logs/export", perm: "billing:export"},
{method: "GET", path: "/api/admin/transactions", perm: "billing:view"},
{method: "GET", path: "/api/admin/wallets", perm: "billing:view"},
{method: "GET", prefix: "/api/admin/wallets/", perm: "billing:view"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/adjust", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/offline-recharge", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/frozen-items/release", perm: "billing:refund"},
{method: "POST", prefix: "/api/admin/wallets/", suffix: "/frozen-items/release-all", perm: "billing:refund"},
{method: "PUT", prefix: "/api/admin/wallets/", suffix: "/warning-threshold", perm: "billing:view"},
// ── plan 套餐管理 ──
{method: "GET", path: "/api/admin/plans", perm: "plan:view"},
{method: "GET", path: "/api/admin/plans/export", perm: "plan:view"},
{method: "GET", prefix: "/api/admin/plans/", perm: "plan:view"},
{method: "POST", path: "/api/admin/plans", perm: "plan:create"},
{method: "PUT", prefix: "/api/admin/plans/", perm: "plan:edit"},
{method: "DELETE", prefix: "/api/admin/plans/", perm: "plan:delete"},
// ── order 订单管理 ──
{method: "GET", path: "/api/admin/orders", perm: "order:view"},
{method: "GET", path: "/api/admin/orders/export", perm: "order:view"},
{method: "GET", prefix: "/api/admin/orders/", perm: "order:view"},
{method: "POST", prefix: "/api/admin/orders/", suffix: "/refund", perm: "order:refund"},
{method: "POST", prefix: "/api/admin/orders/", suffix: "/complete", perm: "order:view"},
// ── operation 内容运营 ──
{method: "GET", path: "/api/admin/changelogs", perm: "operation:view"},
{method: "POST", path: "/api/admin/changelogs", perm: "operation:edit"},
{method: "PUT", prefix: "/api/admin/changelogs/", perm: "operation:edit"},
{method: "DELETE", prefix: "/api/admin/changelogs/", perm: "operation:edit"},
{method: "POST", prefix: "/api/admin/changelogs/", suffix: "/publish", perm: "operation:edit"},
{method: "GET", path: "/api/admin/announcements", perm: "operation:view"},
{method: "POST", path: "/api/admin/announcements", perm: "operation:edit"},
{method: "PUT", prefix: "/api/admin/announcements/", perm: "operation:edit"},
{method: "GET", path: "/api/admin/notification/templates", perm: "operation:view"},
{method: "GET", prefix: "/api/admin/notification/templates/", perm: "operation:view"},
{method: "PUT", prefix: "/api/admin/notification/templates/", perm: "operation:edit"},
{method: "POST", prefix: "/api/admin/notification/templates/", suffix: "/test", perm: "operation:edit"},
{method: "GET", path: "/api/admin/notification/messages", perm: "operation:view"},
{method: "POST", path: "/api/admin/notification/messages/send", perm: "operation:edit"},
{method: "POST", path: "/api/admin/notification/messages/broadcast", perm: "operation:edit"},
{method: "GET", path: "/api/admin/feedbacks", perm: "support:view"},
{method: "GET", path: "/api/admin/feedbacks/stats", perm: "support:view"},
{method: "POST", prefix: "/api/admin/feedbacks/", suffix: "/reply", perm: "support:reply"},
{method: "PUT", prefix: "/api/admin/feedbacks/", suffix: "/status", perm: "support:edit"},
{method: "GET", path: "/api/admin/tickets", perm: "support:view"},
{method: "GET", prefix: "/api/admin/tickets/", perm: "support:view"},
{method: "PUT", prefix: "/api/admin/tickets/", suffix: "/assign", perm: "support:edit"},
{method: "POST", prefix: "/api/admin/tickets/", suffix: "/reply", perm: "support:reply"},
{method: "PUT", prefix: "/api/admin/tickets/", suffix: "/status", perm: "support:edit"},
// ── help center 帮助中心 ──
{method: "GET", path: "/api/admin/help-categories", perm: "support:view"},
{method: "POST", path: "/api/admin/help-categories", perm: "support:edit"},
{method: "PUT", prefix: "/api/admin/help-categories/", perm: "support:edit"},
{method: "DELETE", prefix: "/api/admin/help-categories/", perm: "support:edit"},
{method: "GET", path: "/api/admin/help-articles", perm: "support:view"},
{method: "GET", prefix: "/api/admin/help-articles/", perm: "support:view"},
{method: "POST", path: "/api/admin/help-articles", perm: "support:edit"},
{method: "PUT", prefix: "/api/admin/help-articles/", perm: "support:edit"},
{method: "DELETE", prefix: "/api/admin/help-articles/", perm: "support:edit"},
// ── audit 审计日志 ──
{method: "GET", path: "/api/admin/audit/config", perm: "audit:view"},
{method: "PUT", path: "/api/admin/audit/config", perm: "audit:view"},
{method: "GET", path: "/api/admin/audit/operation-logs", perm: "audit:view"},
{method: "GET", path: "/api/admin/audit/operation-logs/export", perm: "audit:export"},
{method: "GET", path: "/api/admin/audit/request-logs", perm: "audit:view"},
{method: "GET", prefix: "/api/admin/audit/request-logs/", 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"},
// ── file 文件管理 ──
{method: "GET", path: "/api/admin/files", perm: "file:view"},
{method: "GET", path: "/api/admin/files/stats", perm: "file:view"},
{method: "POST", path: "/api/admin/files/cleanup", perm: "file:cleanup"},
{method: "GET", prefix: "/api/admin/files/", suffix: "/download", perm: "file:view"},
{method: "GET", prefix: "/api/admin/files/", suffix: "/serve", perm: "file:view"},
{method: "DELETE", prefix: "/api/admin/files/", perm: "file:delete"},
// ── monitor 监控告警 ──
{method: "GET", prefix: "/api/admin/monitor/", perm: "monitor:view"},
{method: "GET", path: "/api/admin/alert/rules", perm: "monitor:view"},
{method: "GET", path: "/api/admin/alert/options", perm: "monitor:view"},
{method: "POST", path: "/api/admin/alert/rules", perm: "monitor:edit"},
{method: "PUT", prefix: "/api/admin/alert/rules/", perm: "monitor:edit"},
{method: "DELETE", prefix: "/api/admin/alert/rules/", perm: "monitor:edit"},
{method: "POST", prefix: "/api/admin/alert/rules/", suffix: "/test", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/alert/events", perm: "monitor:view"},
{method: "PUT", prefix: "/api/admin/alert/events/", perm: "monitor:edit"},
{method: "DELETE", path: "/api/admin/alert/events/clear", perm: "monitor:edit"},
{method: "GET", path: "/api/admin/error-logs", perm: "monitor:view"},
{method: "GET", prefix: "/api/admin/error-logs/", perm: "monitor:view"},
{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: "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"},
// ── system 系统设置 ──
{method: "GET", path: "/api/admin/settings/categories", perm: "system:view"},
{method: "GET", prefix: "/api/admin/settings/", perm: "system:view"},
{method: "PUT", prefix: "/api/admin/settings/", perm: "system:edit"},
// Payment settings & channels (system scope)
{method: "GET", path: "/api/admin/payment-channels", perm: "system:view"},
{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"},
{method: "GET", prefix: "/api/admin/plugins", perm: "system:plugin"},
{method: "POST", prefix: "/api/admin/plugins/", perm: "system:plugin"},
{method: "PUT", prefix: "/api/admin/plugins/", perm: "system:plugin"},
{method: "GET", prefix: "/api/admin/email/", perm: "system:view"},
// ── tenant level config 租户等级配置 ──
{method: "GET", path: "/api/admin/tenant-level-configs", perm: "tenant:view"},
{method: "POST", path: "/api/admin/tenant-level-configs", perm: "tenant:create"},
{method: "PUT", prefix: "/api/admin/tenant-level-configs/", perm: "tenant:edit"},
{method: "DELETE", prefix: "/api/admin/tenant-level-configs/", perm: "tenant:delete"},
// ── promo 优惠码管理 ──
{method: "GET", path: "/api/admin/promo-codes", perm: "promo:view"},
{method: "GET", path: "/api/admin/promo-codes/export", perm: "promo:view"},
{method: "GET", prefix: "/api/admin/promo-codes/", perm: "promo:view"},
{method: "POST", path: "/api/admin/promo-codes", perm: "promo:create"},
{method: "PUT", prefix: "/api/admin/promo-codes/", perm: "promo:edit"},
// ── member 成员管理(管理后台维度) ──
{method: "GET", path: "/api/admin/members", perm: "member:view"},
{method: "POST", path: "/api/admin/members", perm: "member:import"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/disable", perm: "member:view"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/enable", perm: "member:view"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/reset-password", perm: "member:view"},
{method: "PUT", prefix: "/api/admin/members/", suffix: "/unlock", perm: "member:view"},
// ── redemption 兑换码管理 ──
{method: "GET", path: "/api/admin/redemptions", perm: "redemption:view"},
{method: "GET", path: "/api/admin/redemptions/usages", perm: "redemption:view"},
{method: "GET", path: "/api/admin/redemptions/export", perm: "redemption:view"},
{method: "POST", path: "/api/admin/redemptions", perm: "redemption:create"},
{method: "PUT", prefix: "/api/admin/redemptions/", perm: "redemption:edit"},
// ── permission 权限管理(仅 user 组) ──
{method: "GET", path: "/api/admin/permissions", perm: "user:view"},
{method: "GET", prefix: "/api/admin/users/", suffix: "/permissions", perm: "user:view"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/permissions", perm: "user:edit"},
{method: "PUT", prefix: "/api/admin/users/", suffix: "/data-scopes", perm: "user:edit"},
// ── admin session and security ──
{method: "POST", path: "/api/admin/auth/logout", 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"},
{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"},
{method: "GET", path: "/api/admin/security/tenant-login-history", perm: "audit:view"},
{method: "GET", prefix: "/api/admin/agreements/", suffix: "/acceptances", perm: "audit:view"},
{method: "GET", path: "/api/admin/agreements/pending", perm: "self:access"},
{method: "POST", path: "/api/admin/agreements/accept", perm: "self:access"},
{method: "GET", path: "/api/admin/agreements", perm: "system:view"},
{method: "GET", prefix: "/api/admin/agreements/", perm: "system:view"},
{method: "POST", path: "/api/admin/agreements", perm: "system:edit"},
{method: "PUT", prefix: "/api/admin/agreements/", perm: "system:edit"},
{method: "DELETE", prefix: "/api/admin/agreements/", perm: "system:edit"},
{method: "POST", prefix: "/api/admin/agreements/", suffix: "/publish", perm: "system:edit"},
// ── dashboard and async task management ──
{method: "GET", prefix: "/api/admin/dashboard", perm: "dashboard:view"},
{method: "GET", path: "/api/admin/tasks", perm: "task:view"},
{method: "GET", prefix: "/api/admin/tasks/", perm: "task:view"},
{method: "POST", prefix: "/api/admin/tasks/", suffix: "/cancel", perm: "task:edit"},
{method: "POST", path: "/api/admin/usage-logs/cleanup", perm: "system:edit"},
{method: "GET", path: "/api/admin/usage-logs/cleanup/tasks", perm: "system:view"},
{method: "POST", prefix: "/api/admin/usage-logs/cleanup/tasks/", suffix: "/cancel", perm: "system:edit"},
}
type permissionRule struct {
method string // HTTP method
path string // exact path match (mutually exclusive with prefix)
prefix string // path prefix match
suffix string // path suffix match (combined with prefix)
perm string // required permission point
}
// AdminPermissionGuard enforces RBAC permission checks for admin routes.
// 当前策略:未匹配到权限规则的接口默认放行(仅对已认证 admin)。
// 已配置规则的接口仍按权限点鉴权;漏配的接口不至于直接 403,降低上线回归风险。
// 待 adminPermissionRules 全量补齐后,可将下方 perm=="" 分支改回返回 403 以收紧权限。
func AdminPermissionGuard(r *ghttp.Request) {
if isAdminPublicPath(r.URL.Path) {
r.Middleware.Next()
return
}
role := GetUserRole(r.Context())
if role == "super_admin" {
r.Middleware.Next()
return
}
perm := matchPermission(r.Method, r.URL.Path)
if perm == "" {
// 默认放行:未配置权限规则的接口,对已认证 admin 放行
r.Middleware.Next()
return
}
if perm == "self:access" {
r.Middleware.Next()
return
}
userID := GetUserID(r.Context())
if !admin.HasPermission(r.Context(), userID, role, perm) {
response.ErrorMsg(r, consts.CodeForbidden, "缺少权限:"+perm)
return
}
r.Middleware.Next()
}
// matchPermission finds the permission required for the given method and path.
// Matching priority: exact path > prefix+suffix > prefix-only (catch-all).
// This three-phase approach eliminates rule ordering dependencies.
func matchPermission(method, path string) string {
// Phase 1: exact path match (highest priority)
for _, rule := range adminPermissionRules {
if rule.method != method || rule.path == "" {
continue
}
if path == rule.path {
return rule.perm
}
}
// Phase 2: prefix+suffix match (more specific)
var bestPrefixSuffix *permissionRule
for _, rule := range adminPermissionRules {
if rule.method != method || rule.prefix == "" || rule.suffix == "" {
continue
}
if strings.HasPrefix(path, rule.prefix) && suffixMatches(path, rule.suffix) {
if bestPrefixSuffix == nil || len(rule.prefix)+len(rule.suffix) > len(bestPrefixSuffix.prefix)+len(bestPrefixSuffix.suffix) {
r := rule
bestPrefixSuffix = &r
}
}
}
if bestPrefixSuffix != nil {
return bestPrefixSuffix.perm
}
// Phase 3: prefix-only match (catch-all, lowest priority)
var bestPrefixOnly *permissionRule
for _, rule := range adminPermissionRules {
if rule.method != method || rule.prefix == "" || rule.suffix != "" {
continue
}
if strings.HasPrefix(path, rule.prefix) {
if bestPrefixOnly == nil || len(rule.prefix) > len(bestPrefixOnly.prefix) {
r := rule
bestPrefixOnly = &r
}
}
}
if bestPrefixOnly != nil {
return bestPrefixOnly.perm
}
return ""
}
// suffixMatches 判断 path 是否按【路径分段边界】匹配规则后缀 suffix,避免子串误匹配。
// 旧实现用 strings.Contains(remainder, "test") 会把 /.../latest 误判为命中 /test 规则
// ("latest" 里含 "test"),从而套用错误的权限点。
// - suffix 以 "/" 结尾(如 "/keys/",用于匹配 /.../keys/{id} 这类带尾段的路由):
// 要求 path 中出现完整的 "/keys/" 片段。
// - suffix 不以 "/" 结尾(如 "/status"、"/test"):要求 path 以该后缀结尾,
// 或该后缀后紧跟一个 "/"(即作为完整分段出现,如 /.../test/xxx)。
func suffixMatches(path, suffix string) bool {
if strings.HasSuffix(suffix, "/") {
return strings.Contains(path, suffix)
}
return strings.HasSuffix(path, suffix) || strings.Contains(path, suffix+"/")
}