diff --git a/docs/system-pkg-audit.md b/docs/system-pkg-audit.md index e10c6fd..9dd2f38 100644 --- a/docs/system-pkg-audit.md +++ b/docs/system-pkg-audit.md @@ -30,7 +30,8 @@ - `initialize`:首次安装、配置迁移、种子编排和运行时重载。 - `integration`:Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。 - `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 适配按子包维护。 - `worker`:任务调度、执行器、SSE 订阅及其并发状态。 diff --git a/internal/README.md b/internal/README.md index 3c1d4c1..9e64b76 100644 --- a/internal/README.md +++ b/internal/README.md @@ -13,10 +13,11 @@ - `data`:数据库生命周期、系统仓储、系统表和支付持久化 - `initialize`:数据库首次初始化和系统种子数据编排 - `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器 +- `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据 - `security`:后台 JWT 等安全实现 - `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护: `server/handler`、`server/middleware`、`server/router`、`server/httpx` -- `service`:应用服务、DTO 与领域对象转换和路由元数据;DTO 集中在 +- `service`:应用服务、DTO 与领域对象转换;DTO 集中在 `service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方 - `worker`:定时任务执行与调度 diff --git a/internal/modules/payment/definition.go b/internal/modules/payment/definition.go index 89528e0..825579f 100644 --- a/internal/modules/payment/definition.go +++ b/internal/modules/payment/definition.go @@ -3,7 +3,10 @@ package payment import ( + "strings" + datapayment "kra/internal/data/payment" + "kra/internal/routecatalog" "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: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7}, }, - APIs: []module.API{ - {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: "测试支付渠道"}, - }, + APIs: paymentAPIs(), }, } } + +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 +} diff --git a/internal/modules/system/definition.go b/internal/modules/system/definition.go index f24dd08..456c3eb 100644 --- a/internal/modules/system/definition.go +++ b/internal/modules/system/definition.go @@ -3,7 +3,10 @@ package system import ( + "strings" + datasystem "kra/internal/data/repository" + "kra/internal/routecatalog" "kra/pkg/module" ) @@ -12,13 +15,7 @@ import ( func Definition() module.Definition { communication := module.Surface{ Menus: []module.Menu{{Name: "integrationConfig", Path: "integrationConfig", ParentName: "extensions", Component: "view/systemTools/integration/config.vue", Title: "通信集成", Icon: "connection", Sort: 8}}, - APIs: []module.API{ - {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: "删除集成配置"}, - }, + APIs: integrationAPIs(), } return module.Definition{ 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 +} diff --git a/internal/service/route_metadata.go b/internal/routecatalog/catalog.go similarity index 55% rename from internal/service/route_metadata.go rename to internal/routecatalog/catalog.go index 9da0e40..c39cab1 100644 --- a/internal/service/route_metadata.go +++ b/internal/routecatalog/catalog.go @@ -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{ - "DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api"}, - "DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志"}, - "DELETE /department/deleteDepartment": {group: "部门", description: "删除部门"}, - "DELETE /info/deleteInfo": {group: "公告", description: "删除公告"}, - "DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告"}, +const ( + BodyPolicyDefault RouteBodyPolicy = "default" + BodyPolicyIntegrationConfig RouteBodyPolicy = "integration_config" + BodyPolicyPaymentConfig RouteBodyPolicy = "payment_config" + BodyPolicyPaymentCallback RouteBodyPolicy = "payment_callback" +) + +// 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 /position/deletePosition": {group: "岗位", description: "删除岗位"}, - "DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典"}, - "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容"}, - "DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志"}, - "DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志"}, - "DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板"}, - "DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板"}, - "DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志"}, - "DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志"}, + "DELETE /position/deletePosition": {group: "岗位", description: "删除岗位", audit: true}, + "DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典", audit: true}, + "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容", audit: true}, + "DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志", audit: true}, + "DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志", audit: true}, + "DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板", audit: true}, + "DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板", audit: true}, + "DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志", audit: true}, + "DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志", audit: true}, "DELETE /sysOperationRecord/deleteSysOperationRecord": {group: "操作记录", description: "删除操作记录"}, "DELETE /sysOperationRecord/deleteSysOperationRecordByIds": {group: "操作记录", description: "批量删除操作历史"}, - "DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数"}, - "DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数"}, - "DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本"}, - "DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本"}, - "DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务"}, - "DELETE /user/deleteUser": {group: "系统用户", description: "删除用户"}, - "GET /api/getApiGroups": {group: "api", description: "获取路由组"}, + "DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数", audit: true}, + "DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数", audit: true}, + "DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本", audit: true}, + "DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本", audit: true}, + "DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务", audit: true}, + "DELETE /user/deleteUser": {group: "系统用户", description: "删除用户", audit: true}, + "GET /api/getApiGroups": {group: "api", description: "获取路由组", audit: true}, "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 /authority/getDataScopeDepts": {group: "角色", description: "获取角色自定义部门集"}, "GET /authority/getUsersByAuthority": {group: "角色", description: "获取角色关联用户ID列表"}, "GET /department/findDepartment": {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/getInfoDataSource": {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/dates": {group: "文件日志", description: "获取存在日志的日期"}, "GET /logViewer/files": {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/:provider/:tradeNo": {group: "支付", description: "按路径查询支付订单"}, - "POST /payment/create": {group: "支付", description: "创建支付订单"}, - "POST /payment/query": {group: "支付", description: "同步支付订单状态"}, - "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 /position/findPosition": {group: "岗位", description: "根据ID获取岗位"}, + "GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"}, "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/getSysDictionaryList": {group: "系统字典", description: "获取字典列表"}, "GET /sysDictionary/getSysDictionaryListWithDetails": {group: "系统字典", description: "获取字典列表(含明细)"}, @@ -69,7 +103,9 @@ var apiMetadata = map[string]apiMetadataValue{ "GET /sysError/findSysError": {group: "错误日志", description: "根据ID获取错误日志"}, "GET /sysError/getSysErrorList": {group: "错误日志", description: "获取错误日志列表"}, "GET /sysExportTemplate/exportExcel": {group: "导出模板", description: "导出Excel"}, + "GET /sysExportTemplate/exportExcelByToken": {group: "导出模板", description: "按令牌导出Excel", public: true}, "GET /sysExportTemplate/exportTemplate": {group: "导出模板", description: "下载模板"}, + "GET /sysExportTemplate/exportTemplateByToken": {group: "导出模板", description: "按令牌下载模板", public: true}, "GET /sysExportTemplate/findSysExportTemplate": {group: "导出模板", description: "根据ID获取导出模板"}, "GET /sysExportTemplate/getSysExportTemplateList": {group: "导出模板", description: "获取导出模板列表"}, "GET /sysExportTemplate/previewSQL": {group: "导出模板", description: "预览SQL"}, @@ -88,114 +124,251 @@ var apiMetadata = map[string]apiMetadataValue{ "GET /timedTask/getTimedTaskList": {group: "定时任务", description: "获取定时任务列表"}, "GET /timedTask/getTimedTaskLogList": {group: "定时任务", description: "获取定时任务执行日志"}, "GET /user/getUserInfo": {group: "系统用户", description: "获取自身信息(必选)"}, - "POST /api/createApi": {group: "api", description: "创建api"}, - "POST /api/deleteApi": {group: "api", description: "删除Api"}, - "POST /api/enterSyncApi": {group: "api", description: "确认同步API"}, + "POST /api/createApi": {group: "api", description: "创建api", audit: true}, + "POST /api/deleteApi": {group: "api", description: "删除Api", audit: true}, + "POST /api/enterSyncApi": {group: "api", description: "确认同步API", audit: true}, "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/ignoreApi": {group: "api", description: "忽略API"}, - "POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表"}, - "POST /api/updateApi": {group: "api", description: "更新Api"}, + "POST /api/ignoreApi": {group: "api", description: "忽略API", audit: true}, + "POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表", audit: true}, + "POST /api/updateApi": {group: "api", description: "更新Api", audit: true}, "POST /attachmentCategory/addCategory": {group: "媒体库分类", description: "添加/编辑分类"}, "POST /attachmentCategory/deleteCategory": {group: "媒体库分类", description: "删除分类"}, - "POST /authority/copyAuthority": {group: "角色", description: "拷贝角色"}, - "POST /authority/createAuthority": {group: "角色", description: "创建角色"}, - "POST /authority/deleteAuthority": {group: "角色", description: "删除角色"}, + "POST /authority/copyAuthority": {group: "角色", description: "拷贝角色", audit: true}, + "POST /authority/createAuthority": {group: "角色", description: "创建角色", audit: true}, + "POST /authority/deleteAuthority": {group: "角色", description: "删除角色", audit: true}, "POST /authority/getAuthorityList": {group: "角色", description: "获取角色列表"}, - "POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限"}, - "POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户"}, + "POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限", audit: true}, + "POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户", audit: true}, "POST /authorityBtn/canRemoveAuthorityBtn": {group: "按钮权限", description: "删除按钮"}, "POST /authorityBtn/getAuthorityBtn": {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/updateCasbin": {group: "casbin", description: "更改角色api权限"}, + "POST /casbin/updateCasbin": {group: "casbin", description: "更改角色api权限", audit: true}, "POST /dataAccessLog/getDataAccessLogList": {group: "数据权限审计", description: "获取数据权限审计日志"}, - "POST /department/createDepartment": {group: "部门", description: "创建部门"}, + "POST /department/createDepartment": {group: "部门", description: "创建部门", audit: true}, "POST /department/getDepartmentList": {group: "部门", description: "获取部门树"}, - "POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)"}, - "POST /email/emailTest": {group: "email", description: "发送测试邮件"}, - "POST /email/sendEmail": {group: "email", description: "发送邮件"}, + "POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)", audit: true}, + "POST /email/emailTest": {group: "email", description: "发送测试邮件", audit: true}, + "POST /email/sendEmail": {group: "email", description: "发送邮件", audit: true}, "POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"}, + "POST /fileUploadAndDownload/deleteFiles": {group: "文件上传与下载", description: "批量删除文件"}, "POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"}, "POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"}, "POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"}, + "POST /fileUploadAndDownload/listOssFiles": {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 /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"}, "POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"}, "POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"}, - "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单"}, - "POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系"}, - "POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单"}, + "POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单", audit: true}, + "POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系", audit: true}, + "POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单", audit: true}, "POST /menu/getBaseMenuById": {group: "菜单", description: "根据id获取菜单"}, "POST /menu/getBaseMenuTree": {group: "菜单", description: "获取用户动态路由"}, "POST /menu/getMenu": {group: "菜单", description: "获取菜单树(必选)"}, "POST /menu/getMenuAuthority": {group: "菜单", description: "获取指定角色menu"}, "POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"}, - "POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"}, - "POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"}, - "POST /integration/configs/:kind/:provider/test": {group: "集成配置", description: "测试通信集成连接"}, - "PUT /integration/configs/:kind/:provider": {group: "集成配置", description: "保存集成配置"}, - "DELETE /integration/configs/:kind/:provider": {group: "集成配置", description: "删除集成配置"}, + "POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表", audit: true}, + "POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单", audit: true}, + "POST /payment/callback/:provider": {group: "支付", description: "处理支付回调", public: true, bodyPolicy: BodyPolicyPaymentCallback}, + "POST /payment/create": {group: "支付", description: "创建支付订单", audit: true}, + "POST /payment/fulfill": {group: "支付", description: "重试支付订单发货", audit: true}, "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/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"}, - "POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置"}, - "POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token"}, - "POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token"}, - "POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表"}, - "POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典"}, - "POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON"}, - "POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容"}, - "POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志"}, - "POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板"}, - "POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel"}, - "POST /sysOperationRecord/createSysOperationRecord": {group: "操作记录", description: "新增操作记录"}, - "POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数"}, + "POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)", audit: true}, + "POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置", audit: true}, + "POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token", audit: true}, + "POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token", audit: true}, + "POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表", audit: true}, + "POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典", audit: true}, + "POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON", audit: true}, + "POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容", audit: true}, + "POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志", public: true}, + "POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板", audit: true}, + "POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel", audit: true}, + "POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数", audit: true}, "POST /system/getServerInfo": {group: "系统服务", description: "获取服务器信息"}, "POST /system/getSystemConfig": {group: "系统服务", description: "获取配置文件内容"}, - "POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容"}, - "POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本"}, - "POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本"}, - "POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务"}, - "POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务"}, - "POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务"}, - "POST /user/admin_register": {group: "系统用户", description: "用户注册"}, - "POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)"}, + "POST /system/reloadSystem": {group: "系统服务", description: "重载系统配置", audit: true}, + "POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容", audit: true}, + "POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本", audit: true}, + "POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本", audit: true}, + "POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务", audit: true}, + "POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务", audit: true}, + "POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务", audit: true}, + "POST /user/admin_register": {group: "系统用户", description: "用户注册", audit: true}, + "POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)", audit: true}, "POST /user/getUserList": {group: "系统用户", description: "获取用户列表"}, - "POST /user/resetPassword": {group: "系统用户", description: "重置用户密码"}, - "POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组"}, - "POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)"}, - "POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门"}, - "POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位"}, - "PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息"}, - "PUT /department/updateDepartment": {group: "部门", description: "更新部门"}, - "PUT /info/updateInfo": {group: "公告", description: "更新公告"}, - "PUT /position/updatePosition": {group: "岗位", description: "更新岗位"}, - "PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典"}, - "PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容"}, - "PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志"}, - "PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板"}, - "PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数"}, - "PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务"}, - "PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)"}, - "PUT /user/setSelfSetting": {group: "系统用户", description: "用户界面配置"}, - "PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息"}, + "POST /user/resetPassword": {group: "系统用户", description: "重置用户密码", audit: true}, + "POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组", audit: true}, + "POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)", audit: true}, + "POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门", audit: true}, + "POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位", audit: true}, + "PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息", audit: true}, + "PUT /department/updateDepartment": {group: "部门", description: "更新部门", audit: true}, + "PUT /info/updateInfo": {group: "公告", description: "更新公告", audit: true}, + "PUT /integration/configs/:kind/:provider": {group: "集成配置", description: "保存集成配置", audit: true, bodyPolicy: BodyPolicyIntegrationConfig}, + "PUT /position/updatePosition": {group: "岗位", description: "更新岗位", audit: true}, + "PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典", audit: true}, + "PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容", audit: true}, + "PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志", audit: true}, + "PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板", audit: true}, + "PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数", audit: true}, + "PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务", audit: true}, + "PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)", audit: true}, + "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 RouteMetadata(method, path string) (string, string) { - if value, ok := apiMetadata[strings.ToUpper(method)+" "+path]; ok { - return value.group, value.description +func splitKey(key string) (string, string) { + parts := strings.SplitN(key, " ", 2) + if len(parts) != 2 { + return "", key } - return routeGroup(path), "" + return strings.ToUpper(parts[0]), normalizePath(parts[1]) } -func routeGroup(path string) string { - parts := strings.Split(strings.Trim(path, "/"), "/") +func descriptorKey(method, path string) string { + 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] != "" { return parts[0] } diff --git a/internal/routecatalog/catalog_test.go b/internal/routecatalog/catalog_test.go new file mode 100644 index 0000000..f651a3b --- /dev/null +++ b/internal/routecatalog/catalog_test.go @@ -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) + } + } +} diff --git a/internal/server/README.md b/internal/server/README.md index 51fc608..0a03045 100644 --- a/internal/server/README.md +++ b/internal/server/README.md @@ -8,5 +8,10 @@ grouped by role: - `router/`: resource route registration and the system route registrar - `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 to the root package. diff --git a/internal/server/gin_test.go b/internal/server/gin_test.go index f0b5c50..2df519e 100644 --- a/internal/server/gin_test.go +++ b/internal/server/gin_test.go @@ -12,6 +12,7 @@ import ( "testing" "kra/internal/conf" + "kra/internal/routecatalog" "kra/internal/server/handler" "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) { var output bytes.Buffer 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` diff --git a/internal/server/middleware/access_log.go b/internal/server/middleware/access_log.go index 4b9eca9..a59cbb8 100644 --- a/internal/server/middleware/access_log.go +++ b/internal/server/middleware/access_log.go @@ -16,6 +16,7 @@ import ( "time" "kra/internal/conf" + "kra/internal/routecatalog" "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 { logLimit = int(config.Zap.AccessLogMaxBytes) } - paymentCallback := isPaymentCallbackPath(c.Request.URL.Path) - paymentConfigWrite := isPaymentIntegrationConfigWrite(c.Request.Method, c.Request.URL.Path) + bodyPolicy := routecatalog.BodyPolicyFor(c.Request.Method, c.Request.URL.Path) + paymentCallback := bodyPolicy == routecatalog.BodyPolicyPaymentCallback + paymentConfigWrite := bodyPolicy == routecatalog.BodyPolicyPaymentConfig requestText := "" if paymentCallback { requestText = paymentCallbackSummary(requestBody, c.GetHeader("Content-Type")) @@ -159,16 +161,6 @@ func isMediaUploadRoute(route string) bool { 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 { parts := strings.Split(strings.Trim(path, "/"), "/") for index := 0; index+2 < len(parts); index++ { @@ -179,6 +171,12 @@ func paymentCallbackProvider(path string) string { 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 { mediaType, _, err := mime.ParseMediaType(contentType) 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 + "]" } -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 { digest := sha256.Sum256(body) return "[支付配置正文已省略 body_bytes=" + strconv.Itoa(len(body)) + " body_sha256=" + hex.EncodeToString(digest[:]) + "]" diff --git a/internal/server/middleware/audit.go b/internal/server/middleware/audit.go index 1ad9d40..918178b 100644 --- a/internal/server/middleware/audit.go +++ b/internal/server/middleware/audit.go @@ -13,6 +13,7 @@ import ( "time" "kra/internal/conf" + "kra/internal/routecatalog" "kra/internal/service" "kra/internal/service/dto" @@ -27,8 +28,12 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin. c.Next() return } + routePath := c.FullPath() + if routePath == "" { + routePath = c.Request.URL.Path + } path := c.Request.URL.Path - if !recordsOperation(c.Request.Method, path) { + if !routecatalog.ShouldAudit(c.Request.Method, routePath) { c.Next() return } @@ -86,7 +91,7 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin. } errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() 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) } 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") } -// recordsOperation mirrors the routes on which operation records are enabled. -// Matching by suffix keeps the behavior stable when router-prefix is configured. +// recordsOperation remains as a small compatibility helper for tests and +// custom middleware chains that do not have a Gin context. func recordsOperation(method, path string) bool { - for route := range operationRoutes { - parts := strings.SplitN(route, " ", 2) - if len(parts) != 2 || parts[0] != method || !operationPathMatches(parts[1], path) { - continue - } - return true - } - return false + return routecatalog.ShouldAudit(method, path) } - -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 -}() diff --git a/internal/server/swagger.go b/internal/server/swagger.go index 3dd0fe8..70fd5e9 100644 --- a/internal/server/swagger.go +++ b/internal/server/swagger.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - "kra/internal/service" + "kra/internal/routecatalog" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -87,7 +87,8 @@ func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string apiPath = "/" } 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 == "" { description = route.Method + " " + apiPath } @@ -103,7 +104,7 @@ func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string if parameters := swaggerPathParameters(apiPath); len(parameters) > 0 { operation["parameters"] = parameters } - if !swaggerPublicPath(apiPath) { + if !descriptor.Public { operation["security"] = []map[string][]string{{"ApiKeyAuth": {}}} } if paths[documentPath] == nil { @@ -156,16 +157,3 @@ func swaggerPathParameters(path string) []map[string]any { } 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 -} diff --git a/internal/server/swagger_test.go b/internal/server/swagger_test.go index 61fc060..6ad1570 100644 --- a/internal/server/swagger_test.go +++ b/internal/server/swagger_test.go @@ -12,6 +12,7 @@ func TestSwaggerMarksOnlyPublicRoutesWithoutAuth(t *testing.T) { {Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/info/getInfoDataSource"}, {Method: "GET", Path: "/info/getInfoPublic"}, + {Method: "POST", Path: "/payment/callback/:provider"}, }, "", "test") var payload struct { 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) } } - if _, ok := payload.Paths["/info/getInfoPublic"]["get"]["security"]; ok { - t.Fatal("public route /info/getInfoPublic requires authentication") + for path, method := range map[string]string{"/info/getInfoPublic": "get", "/payment/callback/{provider}": "post"} { + if _, ok := payload.Paths[path][method]["security"]; ok { + t.Fatalf("public route %s requires authentication", path) + } } } diff --git a/internal/service/README.md b/internal/service/README.md index 18b94b2..fa3ed2d 100644 --- a/internal/service/README.md +++ b/internal/service/README.md @@ -2,7 +2,7 @@ `service` adapts HTTP-facing DTOs to business usecases and owns application 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. `dto_alias.go` is a compatibility bridge for existing service callers. New diff --git a/internal/service/system_init.go b/internal/service/system_init.go index 88cc550..58006e8 100644 --- a/internal/service/system_init.go +++ b/internal/service/system_init.go @@ -2,8 +2,9 @@ package service import ( "context" - "kra/internal/biz/system" + "kra/internal/biz/system" + "kra/internal/routecatalog" "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)) for _, route := range routes { 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}) } return s.Initialize(ctx, input, apis)