优化结构

This commit is contained in:
Yvan 2026-08-22 17:43:14 +08:00
parent 491ed21d48
commit 44ab14448a
14 changed files with 413 additions and 430 deletions

View File

@ -30,7 +30,8 @@
- `initialize`:首次安装、配置迁移、种子编排和运行时重载。 - `initialize`:首次安装、配置迁移、种子编排和运行时重载。
- `integration`Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。 - `integration`Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。
- `security`JWT claims、签发/解析和后台安全实现。 - `security`JWT claims、签发/解析和后台安全实现。
- `service`HTTP DTO`service/dto`、DTO 与 DO 转换、应用服务和路由元数据。 - `routecatalog`HTTP 公开性、操作审计、请求体策略和 API 分组/说明的统一目录。
- `service`HTTP DTO`service/dto`、DTO 与 DO 转换和应用服务。
- `server`Gin 生命周期handler、middleware、router、HTTP 适配按子包维护。 - `server`Gin 生命周期handler、middleware、router、HTTP 适配按子包维护。
- `worker`任务调度、执行器、SSE 订阅及其并发状态。 - `worker`任务调度、执行器、SSE 订阅及其并发状态。

View File

@ -13,10 +13,11 @@
- `data`:数据库生命周期、系统仓储、系统表和支付持久化 - `data`:数据库生命周期、系统仓储、系统表和支付持久化
- `initialize`:数据库首次初始化和系统种子数据编排 - `initialize`:数据库首次初始化和系统种子数据编排
- `integration`Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器 - `integration`Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器
- `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据
- `security`:后台 JWT 等安全实现 - `security`:后台 JWT 等安全实现
- `server`Gin server 组合与生命周期;横切 HTTP 代码按子包维护: - `server`Gin server 组合与生命周期;横切 HTTP 代码按子包维护:
`server/handler`、`server/middleware`、`server/router`、`server/httpx` `server/handler`、`server/middleware`、`server/router`、`server/httpx`
- `service`应用服务、DTO 与领域对象转换和路由元数据DTO 集中在 - `service`应用服务、DTO 与领域对象转换DTO 集中在
`service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方 `service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方
- `worker`:定时任务执行与调度 - `worker`:定时任务执行与调度

View File

@ -3,7 +3,10 @@
package payment package payment
import ( import (
"strings"
datapayment "kra/internal/data/payment" datapayment "kra/internal/data/payment"
"kra/internal/routecatalog"
"kra/pkg/module" "kra/pkg/module"
) )
@ -17,18 +20,18 @@ func Definition() module.Definition {
{Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/systemTools/payment/orders.vue", Title: "支付订单", Icon: "wallet", Sort: 6}, {Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/systemTools/payment/orders.vue", Title: "支付订单", Icon: "wallet", Sort: 6},
{Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7}, {Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7},
}, },
APIs: []module.API{ APIs: paymentAPIs(),
{Path: "/payment/orders", Method: "GET", Group: "支付", Description: "分页查询支付订单"},
{Path: "/payment/order", Method: "POST", Group: "支付", Description: "查询支付订单"},
{Path: "/payment/orders/:provider/:tradeNo", Method: "GET", Group: "支付", Description: "按路径查询支付订单"},
{Path: "/payment/create", Method: "POST", Group: "支付", Description: "创建支付订单"},
{Path: "/payment/query", Method: "POST", Group: "支付", Description: "同步支付订单状态"},
{Path: "/payment/refund", Method: "POST", Group: "支付", Description: "申请支付订单退款"},
{Path: "/payment/orders/:provider/:tradeNo/refund", Method: "POST", Group: "支付", Description: "按路径申请支付订单退款"},
{Path: "/payment/fulfill", Method: "POST", Group: "支付", Description: "重试支付订单发货"},
{Path: "/payment/orders/:provider/:tradeNo/fulfill", Method: "POST", Group: "支付", Description: "按路径重试支付订单发货"},
{Path: "/payment/providers/:provider/test", Method: "POST", Group: "支付", Description: "测试支付渠道"},
},
}, },
} }
} }
func paymentAPIs() []module.API {
items := make([]module.API, 0, 10)
for _, descriptor := range routecatalog.Descriptors() {
if descriptor.Public || !strings.HasPrefix(descriptor.Path, "/payment") || descriptor.Description == "" {
continue
}
items = append(items, module.API{Path: descriptor.Path, Method: descriptor.Method, Group: descriptor.Group, Description: descriptor.Description})
}
return items
}

View File

@ -3,7 +3,10 @@
package system package system
import ( import (
"strings"
datasystem "kra/internal/data/repository" datasystem "kra/internal/data/repository"
"kra/internal/routecatalog"
"kra/pkg/module" "kra/pkg/module"
) )
@ -12,13 +15,7 @@ import (
func Definition() module.Definition { func Definition() module.Definition {
communication := module.Surface{ communication := module.Surface{
Menus: []module.Menu{{Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}}, Menus: []module.Menu{{Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}},
APIs: []module.API{ APIs: integrationAPIs(),
{Path: "/integration/configs/:kind", Method: "GET", Group: "集成配置", Description: "按类型获取集成配置"},
{Path: "/integration/configs/:kind/:provider", Method: "GET", Group: "集成配置", Description: "获取指定集成配置"},
{Path: "/integration/configs/:kind/:provider", Method: "PUT", Group: "集成配置", Description: "保存集成配置"},
{Path: "/integration/configs/:kind/:provider/test", Method: "POST", Group: "集成配置", Description: "测试通信集成连接"},
{Path: "/integration/configs/:kind/:provider", Method: "DELETE", Group: "集成配置", Description: "删除集成配置"},
},
} }
return module.Definition{ return module.Definition{
Name: "system", Name: "system",
@ -30,3 +27,14 @@ func Definition() module.Definition {
}, },
} }
} }
func integrationAPIs() []module.API {
items := make([]module.API, 0, 5)
for _, descriptor := range routecatalog.Descriptors() {
if descriptor.Public || !strings.HasPrefix(descriptor.Path, "/integration/configs") || descriptor.Description == "" {
continue
}
items = append(items, module.API{Path: descriptor.Path, Method: descriptor.Method, Group: descriptor.Group, Description: descriptor.Description})
}
return items
}

View File

@ -1,62 +1,96 @@
package service package routecatalog
import "strings" import (
"sort"
"strings"
)
type apiMetadataValue struct{ group, description string } // RouteBodyPolicy controls request-body handling for sensitive routes.
type RouteBodyPolicy string
var apiMetadata = map[string]apiMetadataValue{ const (
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api"}, BodyPolicyDefault RouteBodyPolicy = "default"
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志"}, BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config"
"DELETE /department/deleteDepartment": {group: "部门", description: "删除部门"}, BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config"
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告"}, BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback"
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告"}, )
// Descriptor is the shared HTTP contract consumed by service bootstrap,
// Swagger generation and audit middleware.
type Descriptor struct {
Method string
Path string
Public bool
Audit bool
Group string
Description string
BodyPolicy RouteBodyPolicy
}
type routeValue struct {
group string
description string
public bool
audit bool
bodyPolicy RouteBodyPolicy
}
// routes is the single declaration point for cross-cutting route policy.
// Gin router files still bind handlers; tests keep those registrations aligned
// with this catalog while public, audit and API metadata live only here.
var routes = map[string]routeValue{
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api", audit: true},
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志", audit: true},
"DELETE /department/deleteDepartment": {group: "部门", description: "删除部门", audit: true},
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告", audit: true},
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告", audit: true},
"DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置", audit: true},
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"}, "DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"},
"DELETE /position/deletePosition": {group: "岗位", description: "删除岗位"}, "DELETE /position/deletePosition": {group: "岗位", description: "删除岗位", audit: true},
"DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典"}, "DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典", audit: true},
"DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容"}, "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容", audit: true},
"DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志"}, "DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志", audit: true},
"DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志"}, "DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志", audit: true},
"DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板"}, "DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板", audit: true},
"DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板"}, "DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板", audit: true},
"DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志"}, "DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志", audit: true},
"DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志"}, "DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志", audit: true},
"DELETE /sysOperationRecord/deleteSysOperationRecord": {group: "操作记录", description: "删除操作记录"}, "DELETE /sysOperationRecord/deleteSysOperationRecord": {group: "操作记录", description: "删除操作记录"},
"DELETE /sysOperationRecord/deleteSysOperationRecordByIds": {group: "操作记录", description: "批量删除操作历史"}, "DELETE /sysOperationRecord/deleteSysOperationRecordByIds": {group: "操作记录", description: "批量删除操作历史"},
"DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数"}, "DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数", audit: true},
"DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数"}, "DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数", audit: true},
"DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本"}, "DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本", audit: true},
"DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本"}, "DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本", audit: true},
"DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务"}, "DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务", audit: true},
"DELETE /user/deleteUser": {group: "系统用户", description: "删除用户"}, "DELETE /user/deleteUser": {group: "系统用户", description: "删除用户", audit: true},
"GET /api/getApiGroups": {group: "api", description: "获取路由组"}, "GET /api/getApiGroups": {group: "api", description: "获取路由组", audit: true},
"GET /api/getApiRoles": {group: "api", description: "获取指定API关联角色列表"}, "GET /api/getApiRoles": {group: "api", description: "获取指定API关联角色列表"},
"GET /api/syncApi": {group: "api", description: "获取待同步API"}, "GET /api/syncApi": {group: "api", description: "获取待同步API", audit: true},
"GET /api/freshCasbin": {group: "api", description: "刷新 Casbin 缓存"},
"GET /attachmentCategory/getCategoryList": {group: "媒体库分类", description: "分类列表"}, "GET /attachmentCategory/getCategoryList": {group: "媒体库分类", description: "分类列表"},
"GET /authority/getDataScopeDepts": {group: "角色", description: "获取角色自定义部门集"}, "GET /authority/getDataScopeDepts": {group: "角色", description: "获取角色自定义部门集"},
"GET /authority/getUsersByAuthority": {group: "角色", description: "获取角色关联用户ID列表"}, "GET /authority/getUsersByAuthority": {group: "角色", description: "获取角色关联用户ID列表"},
"GET /department/findDepartment": {group: "部门", description: "根据ID获取部门"}, "GET /department/findDepartment": {group: "部门", description: "根据ID获取部门"},
"GET /department/getDepartmentUsers": {group: "部门", description: "获取部门成员ID列表"}, "GET /department/getDepartmentUsers": {group: "部门", description: "获取部门成员ID列表"},
"GET /fileUploadAndDownload/findFile": {group: "文件上传与下载", description: "获取文件详情"},
"GET /health": {group: "base", description: "健康检查", public: true},
"GET /info/findInfo": {group: "公告", description: "根据ID获取公告"}, "GET /info/findInfo": {group: "公告", description: "根据ID获取公告"},
"GET /info/getInfoDataSource": {group: "公告", description: "获取公告数据源"},
"GET /info/getInfoList": {group: "公告", description: "获取公告列表"}, "GET /info/getInfoList": {group: "公告", description: "获取公告列表"},
"GET /info/getInfoPublic": {group: "公告", description: "获取公开公告", public: true},
"GET /integration/configs/:kind": {group: "集成配置", description: "按类型获取集成配置"},
"GET /integration/configs/:kind/:provider": {group: "集成配置", description: "获取指定集成配置"},
"GET /logViewer/content": {group: "文件日志", description: "分块读取日志文件内容"}, "GET /logViewer/content": {group: "文件日志", description: "分块读取日志文件内容"},
"GET /logViewer/dates": {group: "文件日志", description: "获取存在日志的日期"}, "GET /logViewer/dates": {group: "文件日志", description: "获取存在日志的日期"},
"GET /logViewer/files": {group: "文件日志", description: "获取日期下的日志文件"}, "GET /logViewer/files": {group: "文件日志", description: "获取日期下的日志文件"},
"GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"}, "GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"},
"GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"},
"GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"},
"GET /integration/configs/:kind": {group: "集成配置", description: "按类型获取集成配置"},
"GET /integration/configs/:kind/:provider": {group: "集成配置", description: "获取指定集成配置"},
"GET /payment/orders": {group: "支付", description: "分页查询支付订单"}, "GET /payment/orders": {group: "支付", description: "分页查询支付订单"},
"GET /payment/orders/:provider/:tradeNo": {group: "支付", description: "按路径查询支付订单"}, "GET /payment/orders/:provider/:tradeNo": {group: "支付", description: "按路径查询支付订单"},
"POST /payment/create": {group: "支付", description: "创建支付订单"}, "GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"},
"POST /payment/query": {group: "支付", description: "同步支付订单状态"}, "GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"},
"POST /payment/refund": {group: "支付", description: "申请支付订单退款"},
"POST /payment/orders/:provider/:tradeNo/refund": {group: "支付", description: "按路径申请支付订单退款"},
"POST /payment/fulfill": {group: "支付", description: "重试支付订单发货"},
"POST /payment/orders/:provider/:tradeNo/fulfill": {group: "支付", description: "按路径重试支付订单发货"},
"POST /payment/providers/:provider/test": {group: "支付", description: "测试支付渠道配置与沙箱交易链路"},
"GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"}, "GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"},
"GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON"}, "GET /swagger/*any": {group: "base", description: "Swagger 文档", public: true},
"GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON", audit: true},
"GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典建议选择"}, "GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典建议选择"},
"GET /sysDictionary/getSysDictionaryList": {group: "系统字典", description: "获取字典列表"}, "GET /sysDictionary/getSysDictionaryList": {group: "系统字典", description: "获取字典列表"},
"GET /sysDictionary/getSysDictionaryListWithDetails": {group: "系统字典", description: "获取字典列表(含明细)"}, "GET /sysDictionary/getSysDictionaryListWithDetails": {group: "系统字典", description: "获取字典列表(含明细)"},
@ -69,7 +103,9 @@ var apiMetadata = map[string]apiMetadataValue{
"GET /sysError/findSysError": {group: "错误日志", description: "根据ID获取错误日志"}, "GET /sysError/findSysError": {group: "错误日志", description: "根据ID获取错误日志"},
"GET /sysError/getSysErrorList": {group: "错误日志", description: "获取错误日志列表"}, "GET /sysError/getSysErrorList": {group: "错误日志", description: "获取错误日志列表"},
"GET /sysExportTemplate/exportExcel": {group: "导出模板", description: "导出Excel"}, "GET /sysExportTemplate/exportExcel": {group: "导出模板", description: "导出Excel"},
"GET /sysExportTemplate/exportExcelByToken": {group: "导出模板", description: "按令牌导出Excel", public: true},
"GET /sysExportTemplate/exportTemplate": {group: "导出模板", description: "下载模板"}, "GET /sysExportTemplate/exportTemplate": {group: "导出模板", description: "下载模板"},
"GET /sysExportTemplate/exportTemplateByToken": {group: "导出模板", description: "按令牌下载模板", public: true},
"GET /sysExportTemplate/findSysExportTemplate": {group: "导出模板", description: "根据ID获取导出模板"}, "GET /sysExportTemplate/findSysExportTemplate": {group: "导出模板", description: "根据ID获取导出模板"},
"GET /sysExportTemplate/getSysExportTemplateList": {group: "导出模板", description: "获取导出模板列表"}, "GET /sysExportTemplate/getSysExportTemplateList": {group: "导出模板", description: "获取导出模板列表"},
"GET /sysExportTemplate/previewSQL": {group: "导出模板", description: "预览SQL"}, "GET /sysExportTemplate/previewSQL": {group: "导出模板", description: "预览SQL"},
@ -88,114 +124,251 @@ var apiMetadata = map[string]apiMetadataValue{
"GET /timedTask/getTimedTaskList": {group: "定时任务", description: "获取定时任务列表"}, "GET /timedTask/getTimedTaskList": {group: "定时任务", description: "获取定时任务列表"},
"GET /timedTask/getTimedTaskLogList": {group: "定时任务", description: "获取定时任务执行日志"}, "GET /timedTask/getTimedTaskLogList": {group: "定时任务", description: "获取定时任务执行日志"},
"GET /user/getUserInfo": {group: "系统用户", description: "获取自身信息(必选)"}, "GET /user/getUserInfo": {group: "系统用户", description: "获取自身信息(必选)"},
"POST /api/createApi": {group: "api", description: "创建api"}, "POST /api/createApi": {group: "api", description: "创建api", audit: true},
"POST /api/deleteApi": {group: "api", description: "删除Api"}, "POST /api/deleteApi": {group: "api", description: "删除Api", audit: true},
"POST /api/enterSyncApi": {group: "api", description: "确认同步API"}, "POST /api/enterSyncApi": {group: "api", description: "确认同步API", audit: true},
"POST /api/getAllApis": {group: "api", description: "获取所有api"}, "POST /api/getAllApis": {group: "api", description: "获取所有api"},
"POST /api/getApiById": {group: "api", description: "获取api详细信息"}, "POST /api/getApiById": {group: "api", description: "获取api详细信息", audit: true},
"POST /api/getApiList": {group: "api", description: "获取api列表"}, "POST /api/getApiList": {group: "api", description: "获取api列表"},
"POST /api/ignoreApi": {group: "api", description: "忽略API"}, "POST /api/ignoreApi": {group: "api", description: "忽略API", audit: true},
"POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表"}, "POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表", audit: true},
"POST /api/updateApi": {group: "api", description: "更新Api"}, "POST /api/updateApi": {group: "api", description: "更新Api", audit: true},
"POST /attachmentCategory/addCategory": {group: "媒体库分类", description: "添加/编辑分类"}, "POST /attachmentCategory/addCategory": {group: "媒体库分类", description: "添加/编辑分类"},
"POST /attachmentCategory/deleteCategory": {group: "媒体库分类", description: "删除分类"}, "POST /attachmentCategory/deleteCategory": {group: "媒体库分类", description: "删除分类"},
"POST /authority/copyAuthority": {group: "角色", description: "拷贝角色"}, "POST /authority/copyAuthority": {group: "角色", description: "拷贝角色", audit: true},
"POST /authority/createAuthority": {group: "角色", description: "创建角色"}, "POST /authority/createAuthority": {group: "角色", description: "创建角色", audit: true},
"POST /authority/deleteAuthority": {group: "角色", description: "删除角色"}, "POST /authority/deleteAuthority": {group: "角色", description: "删除角色", audit: true},
"POST /authority/getAuthorityList": {group: "角色", description: "获取角色列表"}, "POST /authority/getAuthorityList": {group: "角色", description: "获取角色列表"},
"POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限"}, "POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限", audit: true},
"POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户"}, "POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户", audit: true},
"POST /authorityBtn/canRemoveAuthorityBtn": {group: "按钮权限", description: "删除按钮"}, "POST /authorityBtn/canRemoveAuthorityBtn": {group: "按钮权限", description: "删除按钮"},
"POST /authorityBtn/getAuthorityBtn": {group: "按钮权限", description: "获取已有按钮权限"}, "POST /authorityBtn/getAuthorityBtn": {group: "按钮权限", description: "获取已有按钮权限"},
"POST /authorityBtn/setAuthorityBtn": {group: "按钮权限", description: "设置按钮权限"}, "POST /authorityBtn/setAuthorityBtn": {group: "按钮权限", description: "设置按钮权限"},
"POST /base/captcha": {group: "base", description: "获取验证码", public: true},
"POST /base/login": {group: "base", description: "登录", public: true},
"POST /casbin/getPolicyPathByAuthorityId": {group: "casbin", description: "获取权限列表"}, "POST /casbin/getPolicyPathByAuthorityId": {group: "casbin", description: "获取权限列表"},
"POST /casbin/updateCasbin": {group: "casbin", description: "更改角色api权限"}, "POST /casbin/updateCasbin": {group: "casbin", description: "更改角色api权限", audit: true},
"POST /dataAccessLog/getDataAccessLogList": {group: "数据权限审计", description: "获取数据权限审计日志"}, "POST /dataAccessLog/getDataAccessLogList": {group: "数据权限审计", description: "获取数据权限审计日志"},
"POST /department/createDepartment": {group: "部门", description: "创建部门"}, "POST /department/createDepartment": {group: "部门", description: "创建部门", audit: true},
"POST /department/getDepartmentList": {group: "部门", description: "获取部门树"}, "POST /department/getDepartmentList": {group: "部门", description: "获取部门树"},
"POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)"}, "POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)", audit: true},
"POST /email/emailTest": {group: "email", description: "发送测试邮件"}, "POST /email/emailTest": {group: "email", description: "发送测试邮件", audit: true},
"POST /email/sendEmail": {group: "email", description: "发送邮件"}, "POST /email/sendEmail": {group: "email", description: "发送邮件", audit: true},
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"}, "POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"},
"POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件"},
"POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"}, "POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"},
"POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"}, "POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"}, "POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
"POST /fileUploadAndDownload/listOssFiles": {group: "文件上传与下载", description: "获取对象存储文件列表"},
"POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)"}, "POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)"},
"POST /info/createInfo": {group: "公告", description: "新建公告"}, "POST /info/createInfo": {group: "公告", description: "新建公告", audit: true},
"POST /init/checkdb": {group: "初始化", description: "检查数据库", public: true},
"POST /init/initdb": {group: "初始化", description: "初始化数据库", public: true},
"POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接", audit: true},
"POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"}, "POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"},
"POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"}, "POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"},
"POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"},
"POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"}, "POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"},
"POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单"}, "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单", audit: true},
"POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系"}, "POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系", audit: true},
"POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单"}, "POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单", audit: true},
"POST /menu/getBaseMenuById": {group: "菜单", description: "根据id获取菜单"}, "POST /menu/getBaseMenuById": {group: "菜单", description: "根据id获取菜单"},
"POST /menu/getBaseMenuTree": {group: "菜单", description: "获取用户动态路由"}, "POST /menu/getBaseMenuTree": {group: "菜单", description: "获取用户动态路由"},
"POST /menu/getMenu": {group: "菜单", description: "获取菜单树(必选)"}, "POST /menu/getMenu": {group: "菜单", description: "获取菜单树(必选)"},
"POST /menu/getMenuAuthority": {group: "菜单", description: "获取指定角色menu"}, "POST /menu/getMenuAuthority": {group: "菜单", description: "获取指定角色menu"},
"POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"}, "POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"},
"POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"}, "POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表", audit: true},
"POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"}, "POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单", audit: true},
"POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接"}, "POST /payment/callback/:provider": {group: "支付", description: "处理支付回调", public: true, bodyPolicy: BodyPolicyPaymentCallback},
"PUT /integration/configs/:kind/:provider": {group: "集成配置", description: "保存集成配置"}, "POST /payment/create": {group: "支付", description: "创建支付订单", audit: true},
"DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置"}, "POST /payment/fulfill": {group: "支付", description: "重试支付订单发货", audit: true},
"POST /payment/order": {group: "支付", description: "查询支付订单"}, "POST /payment/order": {group: "支付", description: "查询支付订单"},
"POST /position/createPosition": {group: "岗位", description: "创建岗位"}, "POST /payment/orders/:provider/:tradeNo/fulfill": {group: "支付", description: "按路径重试支付订单发货", audit: true},
"POST /payment/orders/:provider/:tradeNo/refund": {group: "支付", description: "按路径申请支付订单退款", audit: true},
"POST /payment/providers/:provider/test": {group: "支付", description: "测试支付渠道配置与沙箱交易链路", audit: true},
"POST /payment/query": {group: "支付", description: "同步支付订单状态", audit: true},
"POST /payment/refund": {group: "支付", description: "申请支付订单退款", audit: true},
"POST /position/createPosition": {group: "岗位", description: "创建岗位", audit: true},
"POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"}, "POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"},
"POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"}, "POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)", audit: true},
"POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置"}, "POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置", audit: true},
"POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token"}, "POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token", audit: true},
"POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token"}, "POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token", audit: true},
"POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表"}, "POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表", audit: true},
"POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典"}, "POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典", audit: true},
"POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON"}, "POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON", audit: true},
"POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容"}, "POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容", audit: true},
"POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志"}, "POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志", public: true},
"POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板"}, "POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板", audit: true},
"POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel"}, "POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel", audit: true},
"POST /sysOperationRecord/createSysOperationRecord": {group: "操作记录", description: "新增操作记录"}, "POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数", audit: true},
"POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数"},
"POST /system/getServerInfo": {group: "系统服务", description: "获取服务器信息"}, "POST /system/getServerInfo": {group: "系统服务", description: "获取服务器信息"},
"POST /system/getSystemConfig": {group: "系统服务", description: "获取配置文件内容"}, "POST /system/getSystemConfig": {group: "系统服务", description: "获取配置文件内容"},
"POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容"}, "POST /system/reloadSystem": {group: "系统服务", description: "重载系统配置", audit: true},
"POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本"}, "POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容", audit: true},
"POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本"}, "POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本", audit: true},
"POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务"}, "POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本", audit: true},
"POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务"}, "POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务", audit: true},
"POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务"}, "POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务", audit: true},
"POST /user/admin_register": {group: "系统用户", description: "用户注册"}, "POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务", audit: true},
"POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)"}, "POST /user/admin_register": {group: "系统用户", description: "用户注册", audit: true},
"POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)", audit: true},
"POST /user/getUserList": {group: "系统用户", description: "获取用户列表"}, "POST /user/getUserList": {group: "系统用户", description: "获取用户列表"},
"POST /user/resetPassword": {group: "系统用户", description: "重置用户密码"}, "POST /user/resetPassword": {group: "系统用户", description: "重置用户密码", audit: true},
"POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组"}, "POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组", audit: true},
"POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)"}, "POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)", audit: true},
"POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门"}, "POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门", audit: true},
"POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位"}, "POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位", audit: true},
"PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息"}, "PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息", audit: true},
"PUT /department/updateDepartment": {group: "部门", description: "更新部门"}, "PUT /department/updateDepartment": {group: "部门", description: "更新部门", audit: true},
"PUT /info/updateInfo": {group: "公告", description: "更新公告"}, "PUT /info/updateInfo": {group: "公告", description: "更新公告", audit: true},
"PUT /position/updatePosition": {group: "岗位", description: "更新岗位"}, "PUT /integration/configs/:kind/:provider": {group: "集成配置", description: "保存集成配置", audit: true, bodyPolicy: BodyPolicyIntegrationConfig},
"PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典"}, "PUT /position/updatePosition": {group: "岗位", description: "更新岗位", audit: true},
"PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容"}, "PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典", audit: true},
"PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志"}, "PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容", audit: true},
"PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板"}, "PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志", audit: true},
"PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数"}, "PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板", audit: true},
"PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务"}, "PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数", audit: true},
"PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)"}, "PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务", audit: true},
"PUT /user/setSelfSetting": {group: "系统用户", description: "用户界面配置"}, "PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)", audit: true},
"PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息"}, "PUT /user/setSelfSetting": {group: "系统用户", description: "用户界面配置", audit: true},
"PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息", audit: true},
} }
// Metadata returns the administration group and description for a route. func splitKey(key string) (string, string) {
func RouteMetadata(method, path string) (string, string) { parts := strings.SplitN(key, " ", 2)
if value, ok := apiMetadata[strings.ToUpper(method)+" "+path]; ok { if len(parts) != 2 {
return value.group, value.description return "", key
} }
return routeGroup(path), "" return strings.ToUpper(parts[0]), normalizePath(parts[1])
} }
func routeGroup(path string) string { func descriptorKey(method, path string) string {
parts := strings.Split(strings.Trim(path, "/"), "/") return strings.ToUpper(strings.TrimSpace(method)) + " " + normalizePath(path)
}
func normalizePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return "/"
}
return "/" + strings.Trim(path, "/")
}
func descriptorFrom(key string, value routeValue) Descriptor {
method, path := splitKey(key)
group := value.group
if group == "" {
group = RouteGroup(path)
}
return Descriptor{
Method: method, Path: path, Public: value.public, Audit: value.audit,
Group: group, Description: value.description, BodyPolicy: value.bodyPolicy,
}
}
func pathMatches(pattern, path string) bool {
patternParts := strings.Split(strings.Trim(normalizePath(pattern), "/"), "/")
pathParts := strings.Split(strings.Trim(normalizePath(path), "/"), "/")
if len(patternParts) == 1 && patternParts[0] == "" {
return len(pathParts) == 1 && pathParts[0] == ""
}
if len(pathParts) < len(patternParts) {
return false
}
offset := len(pathParts) - len(patternParts)
for index, patternPart := range patternParts {
if strings.HasPrefix(patternPart, "*") {
return true
}
actual := pathParts[offset+index]
if strings.HasPrefix(patternPart, ":") {
if actual == "" {
return false
}
continue
}
if patternPart != actual {
return false
}
}
return true
}
// Lookup accepts either a canonical Gin route template or an actual URL. A
// configured router prefix is tolerated by matching canonical paths by suffix.
func Lookup(method, path string) (Descriptor, bool) {
key := descriptorKey(method, path)
if value, ok := routes[key]; ok {
return descriptorFrom(key, value), true
}
for key, value := range routes {
descriptor := descriptorFrom(key, value)
if strings.EqualFold(descriptor.Method, method) && pathMatches(descriptor.Path, path) {
return descriptor, true
}
}
return Descriptor{}, false
}
func Describe(method, path string) Descriptor {
if descriptor, ok := Lookup(method, path); ok {
return descriptor
}
return Descriptor{
Method: strings.ToUpper(strings.TrimSpace(method)),
Path: normalizePath(path),
Group: RouteGroup(path),
}
}
func ShouldAudit(method, path string) bool {
descriptor, ok := Lookup(method, path)
return ok && descriptor.Audit
}
func IsPublic(method, path string) bool {
descriptor, ok := Lookup(method, path)
return ok && descriptor.Public
}
// Descriptors returns an immutable, stable-order snapshot for startup and
// contract tests.
func Descriptors() []Descriptor {
values := make([]Descriptor, 0, len(routes))
for key, value := range routes {
values = append(values, descriptorFrom(key, value))
}
sort.Slice(values, func(i, j int) bool {
if values[i].Path == values[j].Path {
return values[i].Method < values[j].Method
}
return values[i].Path < values[j].Path
})
return values
}
// BodyPolicyFor resolves generic integration configuration writes against the
// actual URL so only kind=payment receives the payment-config summary policy.
func BodyPolicyFor(method, path string) RouteBodyPolicy {
descriptor, ok := Lookup(method, path)
if !ok {
return BodyPolicyDefault
}
if descriptor.BodyPolicy != BodyPolicyIntegrationConfig {
return descriptor.BodyPolicy
}
parts := strings.Split(strings.Trim(normalizePath(path), "/"), "/")
for index := 0; index+2 < len(parts); index++ {
if parts[index] == "integration" && parts[index+1] == "configs" && parts[index+2] == "payment" {
return BodyPolicyPaymentConfig
}
}
return BodyPolicyDefault
}
func RouteGroup(path string) string {
parts := strings.Split(strings.Trim(normalizePath(path), "/"), "/")
if len(parts) > 0 && parts[0] != "" { if len(parts) > 0 && parts[0] != "" {
return parts[0] return parts[0]
} }

View File

@ -0,0 +1,37 @@
package routecatalog
import "testing"
func TestLookupMatchesRouteTemplatesAndPrefixes(t *testing.T) {
descriptor, ok := Lookup("POST", "/admin/payment/orders/alipay/order-1/refund")
if !ok || !descriptor.Audit || descriptor.Path != "/payment/orders/:provider/:tradeNo/refund" {
t.Fatalf("descriptor = %#v, ok=%v", descriptor, ok)
}
if descriptor, ok = Lookup("GET", "/admin/payment/orders/alipay/order-1"); !ok || descriptor.Audit || descriptor.Public {
t.Fatalf("private query descriptor = %#v, ok=%v", descriptor, ok)
}
}
func TestRoutePoliciesShareOneDescriptor(t *testing.T) {
callback := Describe("POST", "/payment/callback/:provider")
if !callback.Public || callback.Audit || callback.BodyPolicy != BodyPolicyPaymentCallback {
t.Fatalf("callback descriptor = %#v", callback)
}
if policy := BodyPolicyFor("PUT", "/admin/integration/configs/payment/saobei"); policy != BodyPolicyPaymentConfig {
t.Fatalf("payment policy = %q", policy)
}
if policy := BodyPolicyFor("PUT", "/admin/integration/configs/mq/emqx"); policy != BodyPolicyDefault {
t.Fatalf("mq policy = %q", policy)
}
if descriptor := Describe("POST", "/system/reloadSystem"); !descriptor.Audit || descriptor.Group != "系统服务" {
t.Fatalf("reload descriptor = %#v", descriptor)
}
}
func TestEveryRouteHasAPIMetadata(t *testing.T) {
for _, descriptor := range Descriptors() {
if descriptor.Group == "" || descriptor.Description == "" {
t.Errorf("route metadata is incomplete: %#v", descriptor)
}
}
}

View File

@ -8,5 +8,10 @@ grouped by role:
- `router/`: resource route registration and the system route registrar - `router/`: resource route registration and the system route registrar
- `httpx/`: system adapter for shared response and cookie helpers - `httpx/`: system adapter for shared response and cookie helpers
Cross-cutting route policy lives in `internal/routecatalog`: public/private
Swagger security, operation-audit flags, sensitive request-body handling and
API group/description metadata must be declared there. Router contract tests
keep the catalog aligned with the Gin registrations.
Keep new files in the matching role directory instead of adding transport files Keep new files in the matching role directory instead of adding transport files
to the root package. to the root package.

View File

@ -12,6 +12,7 @@ import (
"testing" "testing"
"kra/internal/conf" "kra/internal/conf"
"kra/internal/routecatalog"
"kra/internal/server/handler" "kra/internal/server/handler"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -64,6 +65,24 @@ func TestGinRouteContract(t *testing.T) {
} }
} }
func TestGinRoutesMatchSharedCatalog(t *testing.T) {
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
registered := make(map[string]struct{}, len(engine.Routes()))
for _, route := range engine.Routes() {
key := route.Method + " " + route.Path
registered[key] = struct{}{}
if _, ok := routecatalog.Lookup(route.Method, route.Path); !ok {
t.Errorf("registered route has no descriptor: %s", key)
}
}
for _, descriptor := range routecatalog.Descriptors() {
key := descriptor.Method + " " + descriptor.Path
if _, ok := registered[key]; !ok {
t.Errorf("route descriptor is not registered: %s", key)
}
}
}
func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) { func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&output, nil)) logger := slog.New(slog.NewJSONHandler(&output, nil))
@ -229,196 +248,3 @@ func TestLocalStorageRouteIsNotRegisteredForRemoteStorage(t *testing.T) {
} }
} }
} }
const expectedGinRouteContract = `DELETE /api/deleteApisByIds
DELETE /dataAccessLog/deleteDataAccessLogByIds
DELETE /department/deleteDepartment
DELETE /info/deleteInfo
DELETE /info/deleteInfoByIds
DELETE /mediaUpload/:uploadId
DELETE /position/deletePosition
DELETE /sysDictionary/deleteSysDictionary
DELETE /sysDictionaryDetail/deleteSysDictionaryDetail
DELETE /sysError/deleteSysError
DELETE /sysError/deleteSysErrorByIds
DELETE /sysExportTemplate/deleteSysExportTemplate
DELETE /sysExportTemplate/deleteSysExportTemplateByIds
DELETE /sysLoginLog/deleteLoginLog
DELETE /sysLoginLog/deleteLoginLogByIds
DELETE /sysOperationRecord/deleteSysOperationRecord
DELETE /sysOperationRecord/deleteSysOperationRecordByIds
DELETE /sysParams/deleteSysParams
DELETE /sysParams/deleteSysParamsByIds
DELETE /sysVersion/deleteSysVersion
DELETE /sysVersion/deleteSysVersionByIds
DELETE /timedTask/deleteTimedTask
DELETE /user/deleteUser
GET /api/freshCasbin
GET /api/getApiGroups
GET /api/getApiRoles
GET /api/syncApi
GET /attachmentCategory/getCategoryList
GET /authority/getDataScopeDepts
GET /authority/getUsersByAuthority
GET /department/findDepartment
GET /department/getDepartmentUsers
GET /fileUploadAndDownload/findFile
GET /health
GET /info/findInfo
GET /info/getInfoDataSource
GET /info/getInfoList
GET /info/getInfoPublic
GET /logViewer/content
GET /logViewer/dates
GET /logViewer/files
GET /menu/getMenuRoles
GET /integration/configs/:kind
GET /integration/configs/:kind/:provider
GET /payment/orders
GET /payment/orders/:provider/:tradeNo
GET /position/findPosition
GET /position/getPositionUsers
GET /securityConfig/getSecurityConfig
GET /swagger/*any
GET /sysDictionary/exportSysDictionary
GET /sysDictionary/findSysDictionary
GET /sysDictionary/getSysDictionaryList
GET /sysDictionary/getSysDictionaryListWithDetails
GET /sysDictionaryDetail/findSysDictionaryDetail
GET /sysDictionaryDetail/getDictionaryDetailsByParent
GET /sysDictionaryDetail/getDictionaryPath
GET /sysDictionaryDetail/getDictionaryTreeList
GET /sysDictionaryDetail/getDictionaryTreeListByType
GET /sysDictionaryDetail/getSysDictionaryDetailList
GET /sysError/findSysError
GET /sysError/getSysErrorList
GET /sysExportTemplate/exportExcel
GET /sysExportTemplate/exportExcelByToken
GET /sysExportTemplate/exportTemplate
GET /sysExportTemplate/exportTemplateByToken
GET /sysExportTemplate/findSysExportTemplate
GET /sysExportTemplate/getSysExportTemplateList
GET /sysExportTemplate/previewSQL
GET /sysLoginLog/findLoginLog
GET /sysLoginLog/getLoginLogList
GET /sysOperationRecord/findSysOperationRecord
GET /sysOperationRecord/getSysOperationRecordList
GET /sysParams/findSysParams
GET /sysParams/getSysParam
GET /sysParams/getSysParamsList
GET /sysVersion/downloadVersionJson
GET /sysVersion/findSysVersion
GET /sysVersion/getSysVersionList
GET /timedTask/alertStream
GET /timedTask/getRegisteredMethods
GET /timedTask/getTimedTaskList
GET /timedTask/getTimedTaskLogList
GET /user/getUserInfo
POST /api/createApi
POST /api/deleteApi
POST /api/enterSyncApi
POST /api/getAllApis
POST /api/getApiById
POST /api/getApiList
POST /api/ignoreApi
POST /api/setApiRoles
POST /api/updateApi
POST /attachmentCategory/addCategory
POST /attachmentCategory/deleteCategory
POST /authority/copyAuthority
POST /authority/createAuthority
POST /authority/deleteAuthority
POST /authority/getAuthorityList
POST /authority/setDataScope
POST /authority/setRoleUsers
POST /authorityBtn/canRemoveAuthorityBtn
POST /authorityBtn/getAuthorityBtn
POST /authorityBtn/setAuthorityBtn
POST /base/captcha
POST /base/login
POST /casbin/getPolicyPathByAuthorityId
POST /casbin/updateCasbin
POST /dataAccessLog/getDataAccessLogList
POST /department/createDepartment
POST /department/getDepartmentList
POST /department/setDepartmentUsers
POST /email/emailTest
POST /email/sendEmail
POST /fileUploadAndDownload/deleteFile
POST /fileUploadAndDownload/deleteFiles
POST /fileUploadAndDownload/editFileName
POST /fileUploadAndDownload/getFileList
POST /fileUploadAndDownload/importURL
POST /fileUploadAndDownload/listOssFiles
POST /fileUploadAndDownload/upload
POST /info/createInfo
POST /init/checkdb
POST /init/initdb
POST /integration/configs/:kind/:provider/test
POST /jwt/jsonInBlacklist
POST /mediaUpload/chunk
POST /mediaUpload/complete
POST /mediaUpload/init
POST /menu/addBaseMenu
POST /menu/addMenuAuthority
POST /menu/deleteBaseMenu
POST /menu/getBaseMenuById
POST /menu/getBaseMenuTree
POST /menu/getMenu
POST /menu/getMenuAuthority
POST /menu/getMenuList
POST /menu/setMenuRoles
POST /menu/updateBaseMenu
POST /payment/callback/:provider
POST /payment/create
POST /payment/fulfill
POST /payment/order
POST /payment/orders/:provider/:tradeNo/fulfill
POST /payment/orders/:provider/:tradeNo/refund
POST /payment/query
POST /payment/refund
POST /position/createPosition
POST /position/getPositionList
POST /position/setPositionUsers
POST /securityConfig/setSecurityConfig
POST /sysApiToken/createApiToken
POST /sysApiToken/deleteApiToken
POST /sysApiToken/getApiTokenList
POST /sysDictionary/createSysDictionary
POST /sysDictionary/importSysDictionary
POST /sysDictionaryDetail/createSysDictionaryDetail
POST /sysError/createSysError
POST /sysExportTemplate/createSysExportTemplate
POST /sysExportTemplate/importExcel
POST /sysParams/createSysParams
POST /sysVersion/exportVersion
POST /sysVersion/importVersion
POST /system/getServerInfo
POST /system/getSystemConfig
POST /system/reloadSystem
POST /system/setSystemConfig
POST /timedTask/createTimedTask
POST /timedTask/toggleTimedTask
POST /timedTask/triggerTimedTask
POST /user/admin_register
POST /user/changePassword
POST /user/getUserList
POST /user/resetPassword
POST /user/setUserAuthorities
POST /user/setUserAuthority
POST /user/setUserDepartments
POST /user/setUserPositions
PUT /authority/updateAuthority
PUT /department/updateDepartment
PUT /info/updateInfo
PUT /integration/configs/:kind/:provider
PUT /position/updatePosition
PUT /sysDictionary/updateSysDictionary
PUT /sysDictionaryDetail/updateSysDictionaryDetail
PUT /sysError/updateSysError
PUT /sysExportTemplate/updateSysExportTemplate
PUT /sysParams/updateSysParams
PUT /timedTask/updateTimedTask
PUT /user/setSelfInfo
PUT /user/setSelfSetting
PUT /user/setUserInfo`

View File

@ -16,6 +16,7 @@ import (
"time" "time"
"kra/internal/conf" "kra/internal/conf"
"kra/internal/routecatalog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -75,8 +76,9 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 { if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
logLimit = int(config.Zap.AccessLogMaxBytes) logLimit = int(config.Zap.AccessLogMaxBytes)
} }
paymentCallback := isPaymentCallbackPath(c.Request.URL.Path) bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path)
paymentConfigWrite := isPaymentIntegrationConfigWrite(c.Request.Method, c.Request.URL.Path) paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback
paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig
requestText := "" requestText := ""
if paymentCallback { if paymentCallback {
requestText = paymentCallbackSummary(requestBody, c.GetHeader("Content-Type")) requestText = paymentCallbackSummary(requestBody, c.GetHeader("Content-Type"))
@ -159,16 +161,6 @@ func isMediaUploadRoute(route string) bool {
strings.HasSuffix(route, "/mediaUpload/chunk") strings.HasSuffix(route, "/mediaUpload/chunk")
} }
func isPaymentCallbackPath(path string) bool {
parts := strings.Split(strings.Trim(path, "/"), "/")
for index := 0; index+1 < len(parts); index++ {
if parts[index] == "payment" && parts[index+1] == "callback" {
return true
}
}
return false
}
func paymentCallbackProvider(path string) string { func paymentCallbackProvider(path string) string {
parts := strings.Split(strings.Trim(path, "/"), "/") parts := strings.Split(strings.Trim(path, "/"), "/")
for index := 0; index+2 < len(parts); index++ { for index := 0; index+2 < len(parts); index++ {
@ -179,6 +171,12 @@ func paymentCallbackProvider(path string) string {
return "unknown" return "unknown"
} }
// isPaymentIntegrationConfigWrite is kept as a compatibility seam for
// middleware tests; policy ownership lives in routecatalog.
func isPaymentIntegrationConfigWrite(method, path string) bool {
return routecatalog.BodyPolicyFor(method, path) == routecatalog.BodyPolicyPaymentConfig
}
func paymentCallbackSummary(body []byte, contentType string) string { func paymentCallbackSummary(body []byte, contentType string) string {
mediaType, _, err := mime.ParseMediaType(contentType) mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil || mediaType == "" { if err != nil || mediaType == "" {
@ -191,19 +189,6 @@ func paymentCallbackSummary(body []byte, contentType string) string {
return "[支付回调正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + " content_type=" + mediaType + "]" return "[支付回调正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + " content_type=" + mediaType + "]"
} }
func isPaymentIntegrationConfigWrite(method, path string) bool {
if method != http.MethodPut {
return false
}
parts := strings.Split(strings.Trim(path, "/"), "/")
for index := 0; index+3 < len(parts); index++ {
if parts[index] == "integration" && parts[index+1] == "configs" && parts[index+2] == "payment" && parts[index+3] != "" {
return true
}
}
return false
}
func paymentConfigSummary(body []byte) string { func paymentConfigSummary(body []byte) string {
digest := sha256.Sum256(body) digest := sha256.Sum256(body)
return "[支付配置正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + "]" return "[支付配置正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + "]"

View File

@ -13,6 +13,7 @@ import (
"time" "time"
"kra/internal/conf" "kra/internal/conf"
"kra/internal/routecatalog"
"kra/internal/service" "kra/internal/service"
"kra/internal/service/dto" "kra/internal/service/dto"
@ -27,8 +28,12 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.
c.Next() c.Next()
return return
} }
routePath := c.FullPath()
if routePath == "" {
routePath = c.Request.URL.Path
}
path := c.Request.URL.Path path := c.Request.URL.Path
if !recordsOperation(c.Request.Method, path) { if !routecatalog.ShouldAudit(c.Request.Method, routePath) {
c.Next() c.Next()
return return
} }
@ -86,7 +91,7 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.
} }
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
operationBody := operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes) operationBody := operationRequestBody(requestBody, c.GetHeader("Content-Type"), maxBytes)
if isPaymentIntegrationConfigWrite(c.Request.Method, path) { if routecatalog.BodyPolicyFor(c.Request.Method, path) == routecatalog.BodyPolicyPaymentConfig {
operationBody = paymentConfigSummary(requestBody) operationBody = paymentConfigSummary(requestBody)
} }
if err := recorder.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationBody, Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil { if err := recorder.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: operationBody, Response: responseBody, UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")}); err != nil {
@ -161,62 +166,8 @@ func isDownloadResponse(c *gin.Context) bool {
strings.Contains(header.Get("Content-Transfer-Encoding"), "binary") strings.Contains(header.Get("Content-Transfer-Encoding"), "binary")
} }
// recordsOperation mirrors the routes on which operation records are enabled. // recordsOperation remains as a small compatibility helper for tests and
// Matching by suffix keeps the behavior stable when router-prefix is configured. // custom middleware chains that do not have a Gin context.
func recordsOperation(method, path string) bool { func recordsOperation(method, path string) bool {
for route := range operationRoutes { return routecatalog.ShouldAudit(method, path)
parts := strings.SplitN(route, " ", 2)
if len(parts) != 2 || parts[0] != method || !operationPathMatches(parts[1], path) {
continue
} }
return true
}
return false
}
func operationPathMatches(pattern, path string) bool {
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
pathParts := strings.Split(strings.Trim(path, "/"), "/")
if len(pathParts) < len(patternParts) {
return false
}
pathParts = pathParts[len(pathParts)-len(patternParts):]
for index, patternPart := range patternParts {
if strings.HasPrefix(patternPart, "*") {
return index <= len(pathParts)
}
if index >= len(pathParts) || (strings.HasPrefix(patternPart, ":") == false && patternPart != pathParts[index]) {
return false
}
}
return len(patternParts) == len(pathParts)
}
var operationRoutes = func() map[string]struct{} {
values := []string{
"POST /user/admin_register", "POST /user/changePassword", "POST /user/setUserAuthority", "DELETE /user/deleteUser", "PUT /user/setUserInfo", "PUT /user/setSelfInfo", "POST /user/setUserAuthorities", "POST /user/setUserDepartments", "POST /user/setUserPositions", "POST /user/resetPassword", "PUT /user/setSelfSetting",
"GET /api/getApiGroups", "GET /api/syncApi", "POST /api/ignoreApi", "POST /api/enterSyncApi", "POST /api/createApi", "POST /api/deleteApi", "POST /api/getApiById", "POST /api/updateApi", "DELETE /api/deleteApisByIds", "POST /api/setApiRoles", "POST /casbin/updateCasbin",
"POST /authority/createAuthority", "POST /authority/deleteAuthority", "PUT /authority/updateAuthority", "POST /authority/copyAuthority", "POST /authority/setDataScope", "POST /authority/setRoleUsers",
"POST /menu/addBaseMenu", "POST /menu/addMenuAuthority", "POST /menu/deleteBaseMenu", "POST /menu/updateBaseMenu", "POST /menu/setMenuRoles",
"POST /department/createDepartment", "PUT /department/updateDepartment", "DELETE /department/deleteDepartment", "POST /department/setDepartmentUsers",
"POST /position/createPosition", "PUT /position/updatePosition", "DELETE /position/deletePosition", "POST /position/setPositionUsers",
"POST /sysDictionary/createSysDictionary", "DELETE /sysDictionary/deleteSysDictionary", "PUT /sysDictionary/updateSysDictionary", "POST /sysDictionary/importSysDictionary", "GET /sysDictionary/exportSysDictionary",
"POST /sysDictionaryDetail/createSysDictionaryDetail", "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail", "PUT /sysDictionaryDetail/updateSysDictionaryDetail",
"POST /sysParams/createSysParams", "DELETE /sysParams/deleteSysParams", "DELETE /sysParams/deleteSysParamsByIds", "PUT /sysParams/updateSysParams",
"POST /securityConfig/setSecurityConfig", "POST /system/setSystemConfig", "POST /system/reloadSystem",
"POST /sysApiToken/createApiToken", "POST /sysApiToken/getApiTokenList", "POST /sysApiToken/deleteApiToken",
"DELETE /sysVersion/deleteSysVersion", "DELETE /sysVersion/deleteSysVersionByIds", "POST /sysVersion/exportVersion", "POST /sysVersion/importVersion",
"POST /sysExportTemplate/createSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplateByIds", "PUT /sysExportTemplate/updateSysExportTemplate", "POST /sysExportTemplate/importExcel",
"DELETE /sysError/deleteSysError", "DELETE /sysError/deleteSysErrorByIds", "PUT /sysError/updateSysError",
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
"POST /timedTask/createTimedTask", "PUT /timedTask/updateTimedTask", "DELETE /timedTask/deleteTimedTask", "POST /timedTask/toggleTimedTask", "POST /timedTask/triggerTimedTask",
"POST /info/createInfo", "DELETE /info/deleteInfo", "DELETE /info/deleteInfoByIds", "PUT /info/updateInfo", "POST /email/emailTest", "POST /email/sendEmail",
"PUT /integration/configs/:kind/:provider", "POST /integration/configs/:kind/:provider/test", "DELETE /integration/configs/:kind/:provider",
"POST /payment/create", "POST /payment/query", "POST /payment/refund", "POST /payment/orders/:provider/:tradeNo/refund", "POST /payment/fulfill", "POST /payment/orders/:provider/:tradeNo/fulfill", "POST /payment/providers/:provider/test",
}
out := make(map[string]struct{}, len(values))
for _, value := range values {
out[value] = struct{}{}
}
return out
}()

View File

@ -8,7 +8,7 @@ import (
"strings" "strings"
"sync" "sync"
"kra/internal/service" "kra/internal/routecatalog"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
@ -87,7 +87,8 @@ func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string
apiPath = "/" apiPath = "/"
} }
documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`) documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`)
group, description := service.RouteMetadata(route.Method, apiPath) descriptor := routecatalog.Describe(route.Method, apiPath)
group, description := descriptor.Group, descriptor.Description
if description == "" { if description == "" {
description = route.Method + " " + apiPath description = route.Method + " " + apiPath
} }
@ -103,7 +104,7 @@ func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string
if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 { if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 {
operation["parameters"] = parameters operation["parameters"] = parameters
} }
if !swaggerPublicPath(apiPath) { if !descriptor.Public {
operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}} operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}}
} }
if paths[documentPath] == nil { if paths[documentPath] == nil {
@ -156,16 +157,3 @@ func swaggerPathParameters(path string) []map[string]any {
} }
return parameters return parameters
} }
func swaggerPublicPath(path string) bool {
for _, marker := range []string{
"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
"/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
"/sysError/createSysError", "/info/getInfoPublic",
} {
if path == marker {
return true
}
}
return false
}

View File

@ -12,6 +12,7 @@ func TestSwaggerMarksOnlyPublicRoutesWithoutAuth(t *testing.T) {
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/api/freshCasbin"},
{Method: "GET", Path: "/info/getInfoDataSource"}, {Method: "GET", Path: "/info/getInfoDataSource"},
{Method: "GET", Path: "/info/getInfoPublic"}, {Method: "GET", Path: "/info/getInfoPublic"},
{Method: "POST", Path: "/payment/callback/:provider"},
}, "", "test") }, "", "test")
var payload struct { var payload struct {
Paths map[string]map[string]map[string]any `json:"paths"` Paths map[string]map[string]map[string]any `json:"paths"`
@ -24,7 +25,9 @@ func TestSwaggerMarksOnlyPublicRoutesWithoutAuth(t *testing.T) {
t.Fatalf("private route %s was marked public", path) t.Fatalf("private route %s was marked public", path)
} }
} }
if _, ok := payload.Paths["/info/getInfoPublic"]["get"]["security"]; ok { for path, method := range map[string]string{"/info/getInfoPublic": "get", "/payment/callback/{provider}": "post"} {
t.Fatal("public route /info/getInfoPublic requires authentication") if _, ok := payload.Paths[path][method]["security"]; ok {
t.Fatalf("public route %s requires authentication", path)
}
} }
} }

View File

@ -2,7 +2,7 @@
`service` adapts HTTP-facing DTOs to business usecases and owns application `service` adapts HTTP-facing DTOs to business usecases and owns application
orchestration. Request/response/filter contracts live in `dto/`; the root orchestration. Request/response/filter contracts live in `dto/`; the root
package contains resource services, conversions, route metadata, and security package contains resource services, conversions, and security
coordination. coordination.
`dto_alias.go` is a compatibility bridge for existing service callers. New `dto_alias.go` is a compatibility bridge for existing service callers. New

View File

@ -2,8 +2,9 @@ package service
import ( import (
"context" "context"
"kra/internal/biz/system"
"kra/internal/biz/system"
"kra/internal/routecatalog"
"kra/internal/service/dto" "kra/internal/service/dto"
) )
@ -28,7 +29,8 @@ func (s *SystemConfigService) InitializeRoutes(ctx context.Context, input *dto.D
apis := make([]*system.API, 0, len(routes)) apis := make([]*system.API, 0, len(routes))
for _, route := range routes { for _, route := range routes {
path := NormalizeRoutePath(route.Path, s.settings.RouterPrefix()) path := NormalizeRoutePath(route.Path, s.settings.RouterPrefix())
group, description := RouteMetadata(route.Method, path) descriptor := routecatalog.Describe(route.Method, path)
group, description := descriptor.Group, descriptor.Description
apis = append(apis, &system.API{Path: path, Method: route.Method, APIGroup: group, Description: description}) apis = append(apis, &system.API{Path: path, Method: route.Method, APIGroup: group, Description: description})
} }
return s.Initialize(ctx, input, apis) return s.Initialize(ctx, input, apis)