This commit is contained in:
parent
d09e6b42bd
commit
9715b2ef1a
|
|
@ -0,0 +1,314 @@
|
|||
# Kra 与官方 GVA 接口一致性审查与整改计划
|
||||
|
||||
## 1. 审查结论
|
||||
|
||||
本计划以官方 GVA 快照
|
||||
`C:\Users\Yvan\AppData\Local\Temp\gva-parity-02f3783`、commit
|
||||
`02f37833255e0e339c3d69199cb5a468f17de9fc`(2026-08-06)为唯一行为基准,
|
||||
逐项比较当前 Kra 的方法/路径、请求绑定与默认值、边界校验、鉴权与数据
|
||||
范围、事务与副作用、响应 envelope,以及前端调用覆盖。
|
||||
|
||||
当前本地路由契约测试固定了 **178 条已注册 Gin 路由**。这 178 条包含
|
||||
`/health` 与 Swagger;本地上传文件的 `GET`/`HEAD` 通过
|
||||
`NoRoute -> serveLocalStorage` 动态处理,不计入 `engine.Routes()`。GVA 则
|
||||
用 `StaticFS` 注册静态 wildcard,因此“178”只能表示本地注册表大小,不能
|
||||
作为两端完整路由集合相等的证明。
|
||||
|
||||
审查结果可以概括为:
|
||||
|
||||
- 纳入范围的普通业务接口,在正常成功/失败路径上的绑定、权限、主要数据
|
||||
变更、事务和 JSON envelope 基本与 GVA 一致;逐接口证据见下方三份矩阵。
|
||||
- 没有发现需要判为 P0 的已确认缺陷。
|
||||
- 有 4 项 P1 路由/管理契约差异、3 项 P2 部署或故障语义差异、2 项 P3
|
||||
文档/输入兼容差异,详见第 4 节。
|
||||
- “操作审计额外脱敏/trace 元数据”不是缺口:复核后确认 GVA 与 Kra 都
|
||||
具备同一组敏感字段脱敏、1 MiB 响应捕获上限以及 request/trace/device
|
||||
元数据;只需保留回归测试。
|
||||
|
||||
## 2. 范围与排除项
|
||||
|
||||
### 纳入范围
|
||||
|
||||
纳入所有已经迁移到 Kra 的系统、媒体、公告和邮件接口。模块级路由计数
|
||||
如下,合计 178 条:
|
||||
|
||||
| 模块 | 路由数 | 逐接口矩阵 |
|
||||
| --- | ---: | --- |
|
||||
| Bootstrap/public(health、base、init、Swagger) | 6 | [identity/access](./parity-identity-access.md)、[crosscut](./parity-crosscut.md) |
|
||||
| API registry + Casbin | 16 | [identity/access](./parity-identity-access.md) |
|
||||
| User + JWT session | 14 | [identity/access](./parity-identity-access.md) |
|
||||
| Authority + menu + button permission | 23 | [identity/access](./parity-identity-access.md) |
|
||||
| Department + position | 14 | [identity/access](./parity-identity-access.md) |
|
||||
| Dictionary + dictionary detail | 17 | [ops/content](./parity-ops-content.md) |
|
||||
| Parameters + security + system + API token | 16 | [ops/content](./parity-ops-content.md) |
|
||||
| Operation/login/data-access/log viewer/error audit | 19 | [ops/content](./parity-ops-content.md) |
|
||||
| Export template + version | 19 | [ops/content](./parity-ops-content.md) |
|
||||
| Timed task + SSE | 9 | [ops/content](./parity-ops-content.md) |
|
||||
| Media files + categories + chunk upload | 15 | [ops/content](./parity-ops-content.md)、[crosscut](./parity-crosscut.md) |
|
||||
| Announcement + email | 10 | [ops/content](./parity-ops-content.md) |
|
||||
|
||||
完整的本地调用链、前端覆盖、未被前端调用的后端接口和 178 条逐行路由
|
||||
清单见 [local-api-inventory.md](./local-api-inventory.md)。其中除
|
||||
`GET /health`、`GET /swagger/*any` 两条平台路由外,其余 176 条已迁移业务
|
||||
路由都在 identity/access 或 ops/content 表中逐行出现;ops 表额外保留一行
|
||||
GVA 的 AI `getSysErrorSolution` 作为 `EXCLUDED` 对照,因此不能用该表的
|
||||
原始行数直接当作纳入计数。
|
||||
|
||||
覆盖核对结果:两个逐接口矩阵合计 177 行、去重后仍为 177 行;与本地
|
||||
178 条注册路由比较时,仅缺少上面两条平台路由,且唯一多出的行正是被标记
|
||||
为 `EXCLUDED` 的 GVA AI action。平台路由由 crosscut 审查,未被遗漏。
|
||||
|
||||
### 明确排除
|
||||
|
||||
按用户要求,不把下列未迁移或明确不需要迁移的模块计入一致性缺口:
|
||||
|
||||
- `system/sys_auto_code.go`、`sys_auto_code_history.go`、`sys_skills.go`;
|
||||
- GVA plugin 下的 AI/LLM/MCP/CLI/skills、auto/plugin-management 路由;
|
||||
- `example/exa_customer.go` 及 customer/example 功能;
|
||||
- `GET /sysError/getSysErrorSolution`:它调用 `AutoCodeService.LLMAuto`,
|
||||
仅在 ops 矩阵中标记为 `EXCLUDED`,不要求补迁移、Casbin seed 或前端动作。
|
||||
|
||||
公告和邮件虽然在 GVA 中以 plugin 目录组织,但 Kra 已迁移且存在对应路由,
|
||||
因此纳入审查。
|
||||
|
||||
## 3. 逐接口审查口径
|
||||
|
||||
每一行接口都按以下顺序对照:
|
||||
|
||||
1. HTTP method/path、router prefix、公共/私有分组和 operation-record 选择;
|
||||
2. JSON/query/multipart/SSE 绑定、字段名、默认值及必填校验;
|
||||
3. JWT、强制改密、Casbin、DataScope 和错误码/HTTP 状态;
|
||||
4. usecase/repository 的数据变更、事务边界、缓存/黑名单/调度器/文件/邮件
|
||||
等副作用;
|
||||
5. 成功与失败的 `{code,data,msg}` envelope、特殊下载/流式响应和前端调用。
|
||||
|
||||
`OK` 只表示在该接口的正常行为和已检查的边界行为上未发现材料差异,
|
||||
不表示两端内部实现必须相同。持久化黑名单取代 GVA 内存缓存、每请求读取
|
||||
Casbin 数据库、动态配置热加载等属于可接受实现差异,但必须覆盖故障测试。
|
||||
|
||||
## 4. 已确认差异与整改顺序
|
||||
|
||||
### P1-01:静态上传 URL 没有进入本地路由表
|
||||
|
||||
**接口/范围**:GVA `GET /uploads/file/*filepath`、`HEAD /uploads/file/*filepath`;
|
||||
以及所有依赖 `engine.Routes()` 的 `/api/syncApi`、`/init/initdb`、启动路由
|
||||
日志和 API registry。
|
||||
|
||||
- GVA 在 `server/initialize/router.go:60-61` 用
|
||||
`StaticFS(global.GVA_CONFIG.Local.StorePath, ...)` 注册 GET/HEAD wildcard,
|
||||
并把它们放进 `global.GVA_ROUTERS`。
|
||||
- Kra 在 `internal/server/gin.go:63-69,117-141` 只在 `NoRoute` 中按
|
||||
`Local.PathPrefix` 动态服务文件,所以文件能下载,但不出现在
|
||||
`engine.Routes()` 或 `/api/syncApi` 输入;访问日志的 route 也会是
|
||||
`unmatched`。
|
||||
- 默认配置中 `store-path` 与 `path-prefix` 都是 `uploads/file`,但配置
|
||||
分离时两端暴露的 URL 集合不同。基准 GVA 的预生成 Swagger 同样没有
|
||||
静态 wildcard,故 Swagger 不单独计为已确认差异。
|
||||
|
||||
**整改选择**:
|
||||
|
||||
- 严格兼容方案:注册与 GVA 同形的 GET/HEAD wildcard,以 `store-path` 为
|
||||
URL 前缀,同时保留 `nosniff`、非安全媒体 attachment 和目录拒绝;或
|
||||
- 保留 NoRoute 方案,但把静态 URL 明确列为“非 API registry 路由”,在
|
||||
sync/init/日志契约中固定排除并补充迁移说明。
|
||||
|
||||
**验收测试**:临时目录、`store-path != path-prefix`、GET/HEAD、目录、
|
||||
不安全扩展名、router prefix、`syncApi` route diff、初始化 API seed。
|
||||
|
||||
### P1-02:`GET /api/freshCasbin` 的失败语义不一致
|
||||
|
||||
- GVA `server/api/v1/system/sys_api.go:314-329` 调用
|
||||
`CasbinService.FreshCasbin()`;`LoadPolicy`/enforcer 不可用时返回 HTTP
|
||||
200、`code=7`、`刷新失败`。
|
||||
- Kra `internal/server/handler/api.go:201-206` 把该接口实现为恒定成功的
|
||||
DB-policy no-op,返回 `code=0`、`刷新成功`,不会暴露策略存储故障。
|
||||
|
||||
**计划**:决定该兼容接口是执行实际策略加载/可用性检查,还是正式声明
|
||||
“无内存 enforcer 的 no-op”。若目标是 GVA parity,应在失败时返回同样的
|
||||
code/message;至少增加数据库故障注入测试,保证行为不是静默成功。
|
||||
|
||||
**验收测试**:正常刷新、策略表不可读、事务中断、无 enforcer/空连接,
|
||||
比较 status/code/msg 和后续授权是否生效。
|
||||
|
||||
### P1-03:非空 `routerPrefix` 时 API sync/init 的路径规范不同
|
||||
|
||||
**接口**:`GET /api/syncApi`、`POST /api/enterSyncApi`、`POST /init/initdb`,
|
||||
以及由 API path 进入 Casbin policy 的授权链路。
|
||||
|
||||
- Kra `internal/service/api.go:20-27,136-145` 和
|
||||
`internal/service/system_init.go:40-48` 会去掉 `routerPrefix`,把路径
|
||||
存成 `/api/...`;授权中间件也按无前缀路径查策略。
|
||||
- GVA 直接比较带前缀的 `global.GVA_ROUTERS`,因此配置 `/admin` 时
|
||||
`syncApi`/初始化看到的是 `/admin/api/...`;默认的无前缀 ignore seed
|
||||
可能无法命中。
|
||||
|
||||
**计划**:先确定数据库 API path 的唯一规范(带前缀或不带前缀),然后
|
||||
同时修改 sync、init seed、ignore、enterSync 和 Casbin authorize;禁止只
|
||||
修一个接口导致数据库与运行时策略混用两种 path。保留旧数据迁移/回滚
|
||||
策略。
|
||||
|
||||
**验收测试**:空前缀、`/admin` 前缀、前缀尾斜杠、Swagger、sync diff、
|
||||
initdb seed、ignoreApi、setApiRoles/updateCasbin 和真实授权请求。
|
||||
|
||||
### P1-04:CORS 中间件的启用状态与基准不同
|
||||
|
||||
**接口/范围**:所有 HTTP 接口,尤其 `OPTIONS` 预检、strict-whitelist 和
|
||||
`allow-all` 配置。
|
||||
|
||||
- GVA 基准 `server/initialize/router.go:64-65` 的 `Cors/CorsByRules`
|
||||
都是注释状态,当前快照默认不安装 CORS 中间件;配置文件要求手动启用。
|
||||
- Kra `internal/server/gin.go:27` 无条件安装 `CORS(runtime)`。因此默认
|
||||
`OPTIONS`、以及配置 `allow-all`/`strict-whitelist` 时的 header、204/403
|
||||
可达性都可能与 GVA 不同;strict 模式会直接拒绝 GVA 基准会继续处理的
|
||||
非白名单请求。
|
||||
|
||||
**计划**:决定 Kra 是否要保留这一增强。如果目标是严格 parity,应让启用
|
||||
由与 GVA 相同的部署开关控制;如果产品明确需要默认 CORS,则把它作为
|
||||
Kra 专有契约并从“GVA 一致”声明中剥离。
|
||||
|
||||
**验收测试**:无 Origin、allow-all、whitelist 命中/未命中、strict 未命中、
|
||||
OPTIONS、`/health` 例外、带 router prefix 的请求。
|
||||
|
||||
### P2-01:JWT 黑名单存储故障的 fail-open/fail-closed 语义不同
|
||||
|
||||
**接口/范围**:所有私有接口,以及 `POST /jwt/jsonInBlacklist`、API token
|
||||
撤销后的访问。
|
||||
|
||||
- GVA `server/middleware/jwt.go:25-29,81-88` 只检查缓存命中;缓存读取
|
||||
没有错误返回,故障时可能继续解析普通 JWT。
|
||||
- Kra `internal/data/api_token.go:128-140` 的黑名单 DB `Count` 出错会被
|
||||
`AuthenticateToken` 映射为 `ErrTokenDisabled`,把所有私有请求返回为
|
||||
401“异地登陆或令牌失效”。
|
||||
|
||||
**计划**:明确安全策略是 fail-closed 还是可用性优先,并在迁移说明中固定;
|
||||
若要求 GVA 可用性语义,需区分“明确命中黑名单”和“存储不可用”,不要
|
||||
把后者伪装成 token 已失效。
|
||||
|
||||
**验收测试**:黑名单命中/未命中、DB/Redis 超时、服务重启后的黑名单加载、
|
||||
token 刷新与多点登录。
|
||||
|
||||
### P2-02:安全限流 key 与作用面不同
|
||||
|
||||
**接口**:`POST /base/login`、`POST /base/captcha`,以及错误 method/未知
|
||||
路径的边界请求。
|
||||
|
||||
- GVA 只在 `sys_base.go:13-14` 的两个 POST route 上安装
|
||||
`SecurityLimit()`,key 为 `GVA_SecLimit<ip><fullPath>`。
|
||||
- Kra `internal/server/middleware/rate_limit.go:13-36` 通过全局 URL
|
||||
suffix 判断,key 为 `KRA_SecLimit<ip><fullPath>`,不检查 HTTP method;
|
||||
因此未知路径或错误 method 也可能递增限流计数。
|
||||
- 共享 Redis/滚动部署时,前缀不同会使两端计数不连续。
|
||||
|
||||
**计划**:将限流收窄到精确的 POST route,并在需要滚动兼容时使用
|
||||
`GVA_SecLimit` 或双读迁移窗口;保留缓存异常 fail-open 的既有语义。
|
||||
|
||||
**验收测试**:正确 POST、GET/未知 path、router prefix、窗口边界、共享
|
||||
Redis key、缓存故障。
|
||||
|
||||
### P2-03:版本响应头名称不同
|
||||
|
||||
**接口/范围**:所有成功、业务失败、鉴权失败和未匹配路由响应。
|
||||
|
||||
- Kra `internal/server/middleware/access_log.go:35` 写 `X-Kra-Version`。
|
||||
- GVA `server/middleware/access_log.go:69-73` 写 `X-Gva-Version`。
|
||||
|
||||
**计划**:兼容期双写两个 header,或将 GVA header 作为唯一公共契约;增加
|
||||
`/health`、`/base/captcha`、一个私有接口和 404 的 header contract test。
|
||||
|
||||
### 横切回归说明(不新增 parity 缺口)
|
||||
|
||||
以下实现差异目前未判为正常路径缺口,但应纳入同一套回归:Kra 每请求从
|
||||
DB 读取 Casbin、持久化 JWT 黑名单、动态 runtime/CORS reload、`ErrorAudit`
|
||||
对已知 code 7 的过滤、SSE/下载响应捕获。它们都可能在存储或中间件故障时
|
||||
改变可观察结果。
|
||||
|
||||
### P3-01:空 `authorityIds` 的安全输入处理不同
|
||||
|
||||
**接口**:`POST /user/setUserAuthorities`。
|
||||
|
||||
GVA 在删除旧关联后直接访问 `authorityIds[0]`,空数组会变成 panic/500;
|
||||
Kra 在 `internal/biz/user.go:165-170` 返回确定性的业务错误且不丢关联。
|
||||
这是更安全的本地行为,不应复制 GVA 的 panic;应统一成“至少一个角色”的
|
||||
显式校验,并把差异写入客户端契约。
|
||||
|
||||
### P3-02:Swagger 空前缀 `basePath` 字面值不同
|
||||
|
||||
GVA 生成文档的 `docs.SwaggerInfo.BasePath` 默认是空字符串;Kra
|
||||
`internal/server/swagger.go:59-63` 在无前缀时输出 `basePath:"/"`。通常 URL
|
||||
语义等价,但严格 JSON snapshot 或生成客户端可能可见。除非需要字面兼容,
|
||||
可在 P3 阶段修正文档生成器或明确接受差异。
|
||||
|
||||
## 5. 模块级状态摘要
|
||||
|
||||
| 模块 | 逐接口结论 | 需要跟踪的横切项 |
|
||||
| --- | --- | --- |
|
||||
| Bootstrap/base/init/Swagger | 基础绑定、响应和登录流程对齐 | P1-03、P1-04、P2-03、P3-02 |
|
||||
| API/Casbin | API CRUD、策略替换、同步正常路径对齐 | P1-01、P1-02、P1-03 |
|
||||
| User/JWT/session | 用户和 token 正常流程对齐 | P2-01、P2-02、P2-03、P3-01 |
|
||||
| Authority/menu/button | 角色、菜单、按钮和数据范围正常路径对齐 | P1-03(path 规范) |
|
||||
| Department/position | 组织树、主部门、岗位关系正常路径对齐 | P1-03(path 规范) |
|
||||
| Dictionary/parameters | CRUD、树、导入导出和分页对齐 | P2 审计/故障回归 |
|
||||
| System/security/config/token | 配置、reload、API token 和黑名单正常路径对齐 | P1-04、P2-01、P2-02 |
|
||||
| Audit/log viewer/error | 非 AI 错误/日志接口逐项对齐 | `getSysErrorSolution` 排除;P2 回归 |
|
||||
| Export/version | token 下载、模板和版本包流程对齐 | P1-01(静态下载 route table) |
|
||||
| Timed task/SSE | 调度、副作用、SSE heartbeat/event 对齐 | 中间件超时/捕获回归 |
|
||||
| Media/chunk upload | 文件引用、哈希、分片和权限对齐 | P1-01、存储故障回归 |
|
||||
| Announcement/email | CRUD、公开数据源、邮件发送对齐 | CORS/header contract |
|
||||
|
||||
## 6. 实施顺序
|
||||
|
||||
### 阶段 A:先定兼容契约(不改业务数据)
|
||||
|
||||
1. 决定 API path 的规范(是否保存 router prefix)。形成一份迁移说明,
|
||||
明确旧数据库记录、Casbin policy、ignore API 和 `syncApi` 的转换规则。
|
||||
2. 决定静态文件是严格注册为 GVA route,还是正式保留 NoRoute 动态路由;
|
||||
同时决定 CORS 是部署开关还是 Kra 默认能力。
|
||||
3. 决定 JWT 黑名单存储故障的 fail-open/fail-closed 策略,并记录安全评审
|
||||
结论。
|
||||
|
||||
### 阶段 B:处理 P1
|
||||
|
||||
按 P1-01 -> P1-02 -> P1-03 -> P1-04 顺序实现,每项都先补失败测试,再改
|
||||
实现,最后运行对应模块测试。P1-03 必须和 P1-01 一起验证,因为静态
|
||||
route table 会直接进入 sync/init seed。
|
||||
|
||||
### 阶段 C:处理 P2/P3 兼容项
|
||||
|
||||
- P2:双写/统一版本 header;统一或双读限流 key;明确黑名单存储故障响应;
|
||||
收窄限流 method/path。
|
||||
- P3:空角色数组显式校验;Swagger `basePath` 字面值;将所有“安全增强但
|
||||
有差异”的行为写入迁移文档。
|
||||
|
||||
### 阶段 D:回归与灰度
|
||||
|
||||
先在单实例 SQLite/内存依赖跑合同测试,再用共享 Redis/数据库做双版本滚动
|
||||
测试;最后执行前端登录、切换角色、菜单/API/按钮授权、文件上传下载、版本
|
||||
导入导出、任务 SSE 和公告/邮件 smoke 流程。
|
||||
|
||||
## 7. 回归测试清单
|
||||
|
||||
| 测试层 | 必测内容 |
|
||||
| --- | --- |
|
||||
| Route contract | 178 条注册路由;静态 GET/HEAD;空前缀与 `/admin`;重复 method/path;startup route log |
|
||||
| Binding/envelope | 每个矩阵接口的缺字段、类型错误、空数组、分页边界;HTTP 200 + code 7、401、409、403、500 |
|
||||
| API sync/init | `syncApi`、`enterSyncApi`、`initdb`、ignore seed、静态 route、router prefix |
|
||||
| Auth/session | 登录/验证码限流、JWT 过期刷新、黑名单、API token 撤销、多点登录、MustChangePwd allow-list |
|
||||
| Authorization | Casbin deny/allow、freshCasbin 失败、DataScope 1/2/4/5、角色/菜单/按钮/部门关系事务 |
|
||||
| Cross-cutting | CORS modes/OPTIONS、版本 header、request/trace metadata、operation/error audit、panic/broken-pipe |
|
||||
| Storage/media | GET/HEAD upload headers、对象引用计数、URL/分片 MD5、owner check、原子 complete、token 下载一次性消费 |
|
||||
| Task/content | scheduler create/update/toggle/trigger、SSE heartbeat/event、字典层级、版本 staged errors、公告公开端点、邮件发送 |
|
||||
|
||||
## 8. 验收门槛与当前限制
|
||||
|
||||
完成标准:
|
||||
|
||||
- P1 项要么修复并有集成测试,要么由产品/运维明确签字接受为 Kra 专有
|
||||
契约;
|
||||
- 每个纳入模块的逐接口矩阵仍保持一行一接口,不能用“模块整体 OK”替代;
|
||||
- 共享 Redis、非空 router prefix、存储故障和数据库不可用均有可重复测试;
|
||||
- 前端使用的接口和当前无前端调用但已注册的后端接口都完成契约确认。
|
||||
|
||||
本轮只新增审计文档,没有修改业务代码。已通过 `git diff --check`;全量
|
||||
`go test ./...` 未能在当前受限环境中执行,原因是用户级 Go cache 需要沙箱外
|
||||
权限且审批服务返回 503,不应把它误写成测试失败。实现阶段必须在可用的构建
|
||||
环境补跑全量测试。
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
# Local Administration API Inventory
|
||||
|
||||
## Scope and method
|
||||
|
||||
This is the local baseline for the GVA parity audit. It inventories every
|
||||
route registered by `internal/server/gin.go`, excluding no local route from the
|
||||
inventory. The current route-contract test asserts **178 registered Gin
|
||||
routes**, including `GET /health` and the Swagger wildcard. A configured
|
||||
`routerPrefix` is prepended to every path below at runtime. Local-file
|
||||
downloads are handled by the `NoRoute -> serveLocalStorage` fallback, so their
|
||||
`GET`/`HEAD` URL is functional but intentionally does **not** appear in this
|
||||
registered-route count, `engine.Routes()`, or `/api/syncApi` input. The local
|
||||
dynamic Swagger is likewise built only from registered routes; the baseline
|
||||
GVA's pre-generated Swagger also omits the static wildcard. The GVA reference
|
||||
does register that wildcard in `Routes()`, so the count is not proof that the
|
||||
two route tables are identical; see `parity-crosscut.md`.
|
||||
|
||||
Source of truth:
|
||||
|
||||
- registration: `internal/server/router/*.go` and `internal/server/gin.go`;
|
||||
- handler-to-service calls: `internal/server/handler/*.go`;
|
||||
- dependency assembly: `cmd/kratos-admin/wire_gen.go`;
|
||||
- frontend comparison: `web/src/api/**/*.js`, `web/src/modules/**/api/*.js`,
|
||||
and the direct SSE use in `web/src/view/systemTools/timedTask/useAlertStream.js`.
|
||||
|
||||
Unless marked `public`, routes are behind JWT authentication, forced-password
|
||||
change protection, Casbin/data-scope access control, and the normal error/audit
|
||||
middleware. `OK` means the standard JSON success envelope; `Page<T>` means
|
||||
`{ list, total, page, pageSize }`. Download and SSE rows state their special
|
||||
wire result. The `Call` column gives the exact handler and service entrypoint;
|
||||
the module pipeline directly below it completes the service/biz/data chain.
|
||||
|
||||
`FE` means a matching frontend request exists. `FE-direct` means the browser
|
||||
calls it directly rather than via an `api/*.js` function. `--` is a registered
|
||||
backend route without a current frontend call site.
|
||||
|
||||
## Architecture map
|
||||
|
||||
| Local area | Handler service | Biz usecase | Repository/data implementation |
|
||||
| --- | --- | --- | --- |
|
||||
| API, Casbin | `APIService` | `APIUsecase` | `APIRepo` -> `data.apiRepo` |
|
||||
| User/navigation | `UserService`, `AuthService`, `TokenService` | `UserUsecase`, `AuthenticationUsecase`, `TokenUsecase` | `UserRepo`, `APITokenRepo` -> `data.userRepo`, `data.apiTokenRepo` |
|
||||
| Authority | `AuthorityService`, `PermissionService` | `AuthorityUsecase`, `PermissionUsecase` | `AuthorityAccessRepo`, `PermissionRepo` -> `data.authorityAccessRepo`, `data.permissionRepo` |
|
||||
| Menu | `MenuService` | `MenuUsecase` | `MenuRepo` -> `data.menuRepo` |
|
||||
| Organisation | `DepartmentService`, `PositionService` | `DepartmentUsecase`, `PositionUsecase` | `DepartmentRepo`, `PositionRepo` -> `data.departmentRepo`, `data.positionRepo` |
|
||||
| Dictionary | `DictionaryService` | `DictionaryUsecase` | `DictionaryRepo` -> `data.dictionaryRepo` |
|
||||
| Parameters | `ParameterService` | `ParameterUsecase` | `ParameterRepo` -> `data.parameterRepo` |
|
||||
| Runtime/system/security | `SystemConfigService`, `SecurityService` | `SystemConfigUsecase`, `SecurityUsecase` | `InitializationRepo`, `SecurityRepo` -> `data.initializationRepo`, `data.securityRepo`; runtime settings/config store |
|
||||
| Audit/logs/errors | `AuditService`, `LogViewerService` | `AuditUsecase`, `LogViewerUsecase` | `AuditQueryRepo`, `LogFileRepo` -> `data.auditRepo`, `data.logFileRepo` |
|
||||
| Export/version | `ExportService`, `VersionService` | `ExportUsecase`, `VersionUsecase` | `ExportRepo`, `VersionRepo` -> `data.exportRepo`, `data.versionRepo`; export token cache |
|
||||
| Tasks | `TaskService` | `TaskApplicationUsecase` -> `TaskUsecase` | `TaskRepo` -> `data.taskRepo`; worker scheduler/runtime |
|
||||
| Media | `MediaService` | `MediaUsecase` | `MediaRepo` -> `data.mediaRepo`; `FileStorage`, upload sessions |
|
||||
| Announcement/email | `AnnouncementService`, `EmailService` | `AnnouncementUsecase`, `EmailUsecase` | `AnnouncementRepo` -> `data.announcementRepo`; `EmailRepo` -> `data.NewEmailRepo(runtime)` |
|
||||
|
||||
## 1. Bootstrap and public endpoints
|
||||
|
||||
| Method/path | Visibility | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `GET /health` | public | inline Gin handler | none -> `"ok"` | liveness only | -- |
|
||||
| `GET /swagger/*any` | public | `registerSwagger` | Swagger UI/document -> HTML/JSON | generated from registered Gin routes | -- |
|
||||
| `POST /base/captcha` | public | `Public.Captcha -> SecurityService.Captcha` | captcha config -> captcha payload | cache-backed captcha creation when enabled | FE |
|
||||
| `POST /base/login` | public | `Public.Login -> AuthService.Login -> AuthenticationUsecase -> User/Security/Audit repos` | `LoginRequest` -> token/user result | authenticate, issue token, write login audit; apply lockout policy | FE |
|
||||
| `POST /init/checkdb` | public | `Public.CheckDatabase -> SystemConfigService.IsInitialized -> SystemConfigUsecase -> InitializationRepo` | none -> initialization status | read DB/init state | FE |
|
||||
| `POST /init/initdb` | public | `Public.InitializeDatabase -> SystemConfigService.InitializeRoutes -> SystemConfigUsecase -> InitializationRepo` | `dto.DatabaseInitRequest` + current routes -> OK | initialize schema/seed data and register current routes | FE |
|
||||
|
||||
## 2. API registry and Casbin policy
|
||||
|
||||
Pipeline: `API handler -> APIService -> APIUsecase -> APIRepo -> data.apiRepo`.
|
||||
The Casbin endpoints share that pipeline because policy-path associations are
|
||||
stored and queried through the API repository.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /api/getApiList` | `API.List -> ListAPI` | `APIListRequest(page,pageSize,filters)` -> `Page<API>` | read | FE |
|
||||
| `POST /api/getAllApis` | `API.All -> AllAPI` | optional body ignored -> `{apis}` | read non-paginated, strict filtering | FE |
|
||||
| `POST /api/createApi` | `API.Create -> CreateAPIRequest` | `APIRequest(path,method,group,description)` -> `{api}` | create API registry row | FE |
|
||||
| `POST /api/updateApi` | `API.Update -> UpdateAPIRequest` | `APIRequest` -> OK | update registry row | FE |
|
||||
| `POST /api/deleteApi` | `API.Delete -> DeleteAPI` | `{id}` -> OK | delete one API and policy association as repo defines | FE |
|
||||
| `DELETE /api/deleteApisByIds` | `API.DeleteByIDs -> DeleteAPIs` | `{ids}` -> OK | batch delete APIs | FE |
|
||||
| `POST /api/getApiById` | `API.Find -> FindAPIResponse` | `{id}` -> `{api}` | read | FE |
|
||||
| `GET /api/getApiGroups` | `API.Groups -> Groups` | none -> `{groups,apiGroupMap}` | read group metadata | FE |
|
||||
| `GET /api/getApiRoles` | `API.Roles -> APIRoleIDs` | `path,method` -> `authorityIds[]` | read role association | FE |
|
||||
| `POST /api/setApiRoles` | `API.SetRoles -> SetAPIRoles` | `path,method,authorityIds[]` -> OK | replace API-role association | FE |
|
||||
| `GET /api/syncApi` | `API.Sync -> SyncAPIResponses` | live `engine.Routes()` -> sync diff | calculate registry/runtime route diff | FE |
|
||||
| `POST /api/ignoreApi` | `API.Ignore -> SetAPIIgnored` | `path,method,flag` -> OK | mark route ignored for sync | FE |
|
||||
| `POST /api/enterSyncApi` | `API.ApplySync -> ApplyAPISyncRequest` | selected sync actions -> OK | apply API registry synchronization | FE |
|
||||
| `GET /api/freshCasbin` | `API.FreshCasbin` | none -> OK | **public** compatibility no-op; policy is read per authorization decision | FE |
|
||||
| `POST /casbin/updateCasbin` | `API.SetPolicyPaths -> SetPolicyPathsRequest` | `authorityId,paths[]` -> OK | replace policy paths for authority | FE |
|
||||
| `POST /casbin/getPolicyPathByAuthorityId` | `API.PolicyPaths -> PolicyPathResponses` | `{authorityId}` -> `{paths}` | read policy paths | FE |
|
||||
|
||||
## 3. Users, session, and navigation
|
||||
|
||||
User pipeline: `User/Navigation handler -> UserService -> UserUsecase -> UserRepo -> data.userRepo`.
|
||||
Authentication paths add `AuthenticationUsecase` with `SecurityUsecase` and
|
||||
the audit recorder; logout uses `TokenUsecase -> APITokenRepo`.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /user/getUserList` | `User.List -> ListUsersRequest` | `UserListRequest` -> `Page<User>` | read, data-scope aware | FE |
|
||||
| `POST /user/admin_register` | `User.Create -> CreateUserRequest` | `UserRequest` -> `{user}` | create user; password-policy validation | FE |
|
||||
| `PUT /user/setUserInfo` | `User.Update -> UpdateUserRequest` | `UserRequest(id,profile)` -> OK | update selected user | FE |
|
||||
| `PUT /user/setSelfInfo` | `User.UpdateSelf -> UpdateSelfUser` | authenticated `SelfUserRequest` -> OK | update current user's profile | FE |
|
||||
| `DELETE /user/deleteUser` | `User.Delete -> DeleteUser` | `{id}` -> OK | delete user | FE |
|
||||
| `POST /user/resetPassword` | `User.ResetPassword -> ResetPassword` | `id,password` -> OK | set another user's password | FE |
|
||||
| `POST /user/changePassword` | `User.ChangePassword -> ChangePassword` | current and new passwords -> OK | verify and replace current password | FE |
|
||||
| `PUT /user/setSelfSetting` | `User.SetSelfSetting -> SetUserSetting` | current-user UI settings -> OK | persist user settings | FE |
|
||||
| `POST /user/setUserAuthorities` | `User.SetAuthorities -> SetUserAuthorities` | `id,authorityIds[]` -> OK | replace user authority groups | FE |
|
||||
| `POST /user/setUserAuthority` | `User.SwitchAuthority -> AuthService.SwitchAuthority` | current claims + `authorityId` -> new token/user | switch active authority and issue new token | FE |
|
||||
| `GET /user/getUserInfo` | `User.Get -> UserByUUID` | current claims -> `{user}` | read self/authority info | FE |
|
||||
| `POST /menu/getMenu` | `Navigation.Menu -> UserService.Menus` | current authority -> menu tree | read authorized navigation | FE |
|
||||
| `POST /jwt/jsonInBlacklist` | `Session.Logout -> TokenService.BlacklistToken` | bearer token -> OK | blacklist/revoke token | FE |
|
||||
|
||||
## 4. Authorities, menus, and button permissions
|
||||
|
||||
Authority and permissions pipeline: `handler -> AuthorityService/PermissionService ->
|
||||
AuthorityUsecase/PermissionUsecase -> AuthorityAccessRepo/PermissionRepo -> data`.
|
||||
Menu pipeline: `Menu handler -> MenuService -> MenuUsecase -> MenuRepo -> data.menuRepo`.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /authority/getAuthorityList` | `Authority.List -> Authorities` | none -> authority tree/list | read | FE |
|
||||
| `POST /authority/createAuthority` | `Authority.Create -> CreateAuthorityRequest` | authority id/name -> `{authority}` | create authority | FE |
|
||||
| `POST /authority/copyAuthority` | `Authority.Copy -> CopyAuthorityRequest` | source + new authority -> `{authority}` | duplicate authority permissions/menu relations | FE |
|
||||
| `PUT /authority/updateAuthority` | `Authority.Update -> UpdateAuthorityRequest` | authority fields -> `{authority}` | update authority | FE |
|
||||
| `POST /authority/deleteAuthority` | `Authority.Delete -> DeleteAuthority` | `{authorityId}` -> OK | delete authority subject to repo rules | FE |
|
||||
| `POST /authority/setRoleUsers` | `Authority.SetUsers -> SetAuthorityUsers` | authority/user ids -> OK | replace authority-user relation | FE |
|
||||
| `GET /authority/getUsersByAuthority` | `Authority.Users -> AuthorityUserIDs` | `authorityId` -> `userIds[]` | read relation | FE |
|
||||
| `POST /authority/setDataScope` | `Authority.SetDataScope -> SetDataScope` | scope and department ids -> OK | update data-scope and departments | FE |
|
||||
| `GET /authority/getDataScopeDepts` | `Authority.DataScopeDepartments -> DataScopeDepartmentIDs` | `authorityId` -> `departmentIds[]` | read custom scope departments | FE |
|
||||
| `POST /menu/getMenuList` | `Menu.List -> Tree` | none -> menu tree | read | FE |
|
||||
| `POST /menu/getBaseMenuTree` | `Menu.Tree -> Tree` | none -> `{menus}` | read | FE |
|
||||
| `POST /menu/addBaseMenu` | `Menu.Create -> Create` | `MenuRequest` -> OK | create menu, parameters/buttons | FE |
|
||||
| `POST /menu/updateBaseMenu` | `Menu.Update -> Update` | `MenuRequest` -> OK | update menu | FE |
|
||||
| `POST /menu/deleteBaseMenu` | `Menu.Delete -> Delete` | `{id}` -> OK | delete menu and dependent mappings per repo | FE |
|
||||
| `POST /menu/getBaseMenuById` | `Menu.Find -> Find` | `{id}` -> `{menu}` | read | FE |
|
||||
| `POST /menu/addMenuAuthority` | `Menu.SetAuthorityMenus -> SetAuthorityMenus` | authority + menu ids -> OK | replace authority-menu mapping | FE |
|
||||
| `POST /menu/getMenuAuthority` | `Menu.AuthorityMenus -> AuthorityMenus` | `{authorityId}` -> `{menus}` | read mapping | FE |
|
||||
| `GET /menu/getMenuRoles` | `Menu.RoleIDs + DefaultRouterRoleIDs` | `menuId` -> `{authorityIds,defaultRouterAuthorityIds}` | read role mapping | FE |
|
||||
| `POST /menu/setMenuRoles` | `Menu.SetRoles -> SetRoles` | menu + authority ids -> OK | replace menu-role mapping | FE |
|
||||
| `POST /authorityBtn/getAuthorityBtn` | `Permission.Buttons -> SelectedButtons` | authority/menu ids -> selection | read button permission selection | FE |
|
||||
| `POST /authorityBtn/setAuthorityBtn` | `Permission.SetButtons -> SetSelectedButtons` | authority/menu/selected -> OK | replace button permission selection | FE |
|
||||
| `POST /authorityBtn/canRemoveAuthorityBtn` | `Permission.CanRemoveButton -> CanRemoveButton` | menu button id -> boolean | dependency/usage check | FE |
|
||||
|
||||
## 5. Departments and positions
|
||||
|
||||
Department pipeline: `Organization handler -> DepartmentService -> DepartmentUsecase ->
|
||||
DepartmentRepo -> data.departmentRepo`. Position rows substitute the matching
|
||||
`Position*` components and `data.positionRepo`.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /department/getDepartmentList` | `Organization.ListDepartments -> Departments` | filter body -> department tree/list | read | FE |
|
||||
| `POST /department/createDepartment` | `CreateDepartment -> DepartmentService.Create` | `DepartmentRequest` -> OK | create department | FE |
|
||||
| `PUT /department/updateDepartment` | `UpdateDepartment -> Update` | `DepartmentRequest` -> OK | update department | FE |
|
||||
| `DELETE /department/deleteDepartment` | `DeleteDepartment -> Delete` | `{id}` -> OK | delete department | FE |
|
||||
| `GET /department/findDepartment` | `FindDepartment -> Department` | `id` -> department | read | FE |
|
||||
| `GET /department/getDepartmentUsers` | `DepartmentUsers -> UserIDs` | `departmentId` -> `userIds[]` | read membership | FE |
|
||||
| `POST /department/setDepartmentUsers` | `SetDepartmentUsers -> SetUsers` | department/user ids -> OK | replace department membership | FE |
|
||||
| `POST /user/setUserDepartments` | `SetUserDepartments -> DepartmentService.SetUserDepartments` | user + department ids -> OK | replace user's departments | FE |
|
||||
| `POST /position/getPositionList` | `ListPositions -> Positions` | filter body -> position list | read | FE |
|
||||
| `POST /position/createPosition` | `CreatePosition -> PositionService.Create` | `PositionRequest` -> OK | create position | FE |
|
||||
| `PUT /position/updatePosition` | `UpdatePosition -> Update` | `PositionRequest` -> OK | update position | FE |
|
||||
| `DELETE /position/deletePosition` | `DeletePosition -> Delete` | `{id}` -> OK | delete position | FE |
|
||||
| `GET /position/findPosition` | `FindPosition -> Position` | `id` -> position | read | FE |
|
||||
| `GET /position/getPositionUsers` | `PositionUsers -> UserIDs` | `positionId` -> `userIds[]` | read membership | FE |
|
||||
| `POST /position/setPositionUsers` | `SetPositionUsers -> SetUsers` | position/user ids -> OK | replace position membership | FE |
|
||||
| `POST /user/setUserPositions` | `SetUserPositions -> PositionService.SetUserPositions` | user + position ids -> OK | replace user's positions | FE |
|
||||
|
||||
## 6. Dictionaries
|
||||
|
||||
Pipeline: `Dictionary handler -> DictionaryService -> DictionaryUsecase -> DictionaryRepo -> data.dictionaryRepo`.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /sysDictionary/createSysDictionary` | `Create -> CreateDictionaryRequest` | dictionary DTO -> `{dictionary}` | create dictionary | FE |
|
||||
| `PUT /sysDictionary/updateSysDictionary` | `Update -> UpdateDictionaryRequest` | dictionary DTO -> OK | update dictionary | FE |
|
||||
| `DELETE /sysDictionary/deleteSysDictionary` | `Delete -> DeleteDictionary` | `id` -> OK | delete dictionary | FE |
|
||||
| `GET /sysDictionary/findSysDictionary` | `Find -> Dictionary` | id/type/status -> `{sysDictionary}` | read dictionary | FE |
|
||||
| `GET /sysDictionary/getSysDictionaryList` | `List(false) -> Dictionaries` | page/name/type -> `Page<Dictionary>` | read without details | FE |
|
||||
| `GET /sysDictionary/getSysDictionaryListWithDetails` | `List(true) -> Dictionaries` | page/name/type -> `Page<Dictionary+details>` | read with details | -- |
|
||||
| `GET /sysDictionary/exportSysDictionary` | `Export -> ExportDictionary` | `id` -> dictionary export payload | read/export JSON structure | FE |
|
||||
| `POST /sysDictionary/importSysDictionary` | `Import -> ImportDictionaryJSON` | JSON document -> OK | validate and import dictionary | FE |
|
||||
| `POST /sysDictionaryDetail/createSysDictionaryDetail` | `CreateDetail -> CreateDictionaryDetailRequest` | detail DTO -> OK | create detail | FE |
|
||||
| `PUT /sysDictionaryDetail/updateSysDictionaryDetail` | `UpdateDetail -> UpdateDictionaryDetailRequest` | detail DTO -> OK | update detail | FE |
|
||||
| `DELETE /sysDictionaryDetail/deleteSysDictionaryDetail` | `DeleteDetail -> DeleteDictionaryDetail` | `id` -> OK | delete detail | FE |
|
||||
| `GET /sysDictionaryDetail/findSysDictionaryDetail` | `FindDetail -> DictionaryDetail` | `id` -> `{sysDictionaryDetail}` | read detail | FE |
|
||||
| `GET /sysDictionaryDetail/getSysDictionaryDetailList` | `Details -> DictionaryDetails` | page/filter -> `Page<Detail>` | read | FE |
|
||||
| `GET /sysDictionaryDetail/getDictionaryTreeList` | `Tree(false) -> DictionaryTree` | `id` -> tree | read tree by id | FE |
|
||||
| `GET /sysDictionaryDetail/getDictionaryTreeListByType` | `Tree(true) -> DictionaryTree` | `type` -> tree | read tree by type | FE |
|
||||
| `GET /sysDictionaryDetail/getDictionaryDetailsByParent` | `DetailsByParent -> DictionaryDetailsByParent` | dictionary/parent/includeChildren -> details | read descendants | FE |
|
||||
| `GET /sysDictionaryDetail/getDictionaryPath` | `Path -> DictionaryPath` | `id` -> ancestor path | read ancestry | FE |
|
||||
|
||||
## 7. Parameters, API tokens, security, and system configuration
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /sysParams/createSysParams` | `Parameter.Create -> CreateParameterRequest -> ParameterUsecase -> ParameterRepo` | parameter DTO -> OK | create parameter | FE |
|
||||
| `PUT /sysParams/updateSysParams` | `Parameter.Update -> UpdateParameterRequest -> ParameterUsecase -> ParameterRepo` | parameter DTO -> OK | update parameter | FE |
|
||||
| `DELETE /sysParams/deleteSysParams` | `Parameter.Delete -> DeleteParameters -> ParameterUsecase -> ParameterRepo` | `ID` query -> OK | delete parameter | FE |
|
||||
| `DELETE /sysParams/deleteSysParamsByIds` | `Parameter.DeleteMany -> DeleteParameters -> ParameterUsecase -> ParameterRepo` | `IDs[]` -> OK | batch delete | FE |
|
||||
| `GET /sysParams/findSysParams` | `Parameter.Find -> ParameterByID -> ParameterUsecase -> ParameterRepo` | `ID` -> parameter | read | FE |
|
||||
| `GET /sysParams/getSysParam` | `Parameter.Get -> ParameterByKey -> ParameterUsecase -> ParameterRepo` | `key` -> parameter | read by key | FE |
|
||||
| `GET /sysParams/getSysParamsList` | `Parameter.List -> ParametersFilter -> ParameterUsecase -> ParameterRepo` | page/name/key/date -> `Page<Parameter>` | read | FE |
|
||||
| `POST /sysApiToken/createApiToken` | `APIToken.Create -> TokenService.CreateAPIToken -> TokenUsecase -> APITokenRepo` | user/authority/days/remark -> token | issue persisted API token | FE |
|
||||
| `POST /sysApiToken/getApiTokenList` | `APIToken.List -> APITokens -> TokenUsecase -> APITokenRepo` | page/user/status -> `Page<Token>` | read | FE |
|
||||
| `POST /sysApiToken/deleteApiToken` | `APIToken.Delete -> DisableAPIToken -> TokenUsecase -> APITokenRepo` | `id` -> OK | disable token | FE |
|
||||
| `GET /securityConfig/getSecurityConfig` | `SystemConfig.GetSecurity -> SecurityService.Security -> SecurityUsecase -> SecurityRepo` | none -> security config | read | FE |
|
||||
| `POST /securityConfig/setSecurityConfig` | `SystemConfig.SetSecurity -> SaveSecurityRequest -> SecurityUsecase -> SecurityRepo` | security DTO -> config | update security/rate/password policies | FE |
|
||||
| `POST /system/getSystemConfig` | `SystemConfig.Get -> SystemConfigService.Get -> SystemConfigUsecase -> InitializationRepo` | none -> config document | read runtime config | FE |
|
||||
| `POST /system/setSystemConfig` | `SystemConfig.Set -> Set -> SystemConfigUsecase -> InitializationRepo` | config document -> OK | persist runtime config | FE |
|
||||
| `POST /system/reloadSystem` | `SystemConfig.Reload -> Reload -> SystemConfigUsecase + TaskRuntime` | none -> OK | reload runtime config/task schedule | FE |
|
||||
| `POST /system/getServerInfo` | `SystemConfig.ServerInfo -> SystemInfo` | none -> host/process metrics | read system state | FE |
|
||||
|
||||
## 8. Operation, login, data-access, file-log, and error audit
|
||||
|
||||
Audit pipeline: `Audit handler -> AuditService -> AuditUsecase -> AuditQueryRepo -> data.auditRepo`.
|
||||
Log viewer uses `LogViewerService -> LogViewerUsecase -> LogFileRepo -> data.logFileRepo`.
|
||||
Error-record rows use the audit repository's error-record implementation. The
|
||||
public error creation route is intentionally callable by the frontend error handler.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET /sysOperationRecord/getSysOperationRecordList` | `Audit.Operations -> OperationsFilter` | page/path/method/status -> `Page<Operation>` | read | FE |
|
||||
| `GET /sysOperationRecord/findSysOperationRecord` | `Audit.Operation -> Operation` | `id` -> operation | read | -- |
|
||||
| `DELETE /sysOperationRecord/deleteSysOperationRecord` | `Audit.DeleteOperation -> DeleteOperations` | `id` -> OK | delete record | FE |
|
||||
| `DELETE /sysOperationRecord/deleteSysOperationRecordByIds` | `Audit.DeleteOperations -> DeleteOperations` | `ids[]` -> OK | batch delete | FE |
|
||||
| `GET /sysLoginLog/getLoginLogList` | `Audit.Logins -> LoginsFilter` | page/username/status -> `Page<LoginLog>` | read | FE |
|
||||
| `GET /sysLoginLog/findLoginLog` | `Audit.Login -> Login` | `id` -> login log | read | FE |
|
||||
| `DELETE /sysLoginLog/deleteLoginLog` | `Audit.DeleteLogin -> DeleteLogins` | `id` -> OK | delete login log | FE |
|
||||
| `DELETE /sysLoginLog/deleteLoginLogByIds` | `Audit.DeleteLogins -> DeleteLogins` | `ids[]` -> OK | batch delete | FE |
|
||||
| `POST /dataAccessLog/getDataAccessLogList` | `Audit.DataAccess -> DataAccessRequest` | filter/page -> `Page<DataAccessLog>` | read | FE |
|
||||
| `DELETE /dataAccessLog/deleteDataAccessLogByIds` | `Audit.DeleteDataAccess -> DeleteDataAccess` | `ids[]` -> OK | batch delete | FE |
|
||||
| `GET /logViewer/dates` | `Audit.LogDates -> LogViewerService.LogDates` | `month` -> dates | read filesystem log index | FE |
|
||||
| `GET /logViewer/files` | `Audit.LogFiles -> LogViewerService.LogFiles` | `date` -> files | read filesystem log index | FE |
|
||||
| `GET /logViewer/content` | `Audit.LogContent -> LogViewerService.LogContent` | date/path/cursor -> chunk | read log content, cursor pagination | FE |
|
||||
| `POST /sysError/createSysError` | `Audit.CreateError -> AuditRecorder.RecordError` | frontend error payload -> OK | **public** append error record | FE |
|
||||
| `GET /sysError/getSysErrorList` | `Audit.Errors -> ErrorsFilter` | page/form/info/date -> `Page<Error>` | read | FE |
|
||||
| `GET /sysError/findSysError` | `Audit.Error -> Error` | `id` -> error record | read | FE |
|
||||
| `PUT /sysError/updateSysError` | `Audit.UpdateError -> UpdateErrorRequest` | error mutation -> OK | update error record | FE |
|
||||
| `DELETE /sysError/deleteSysError` | `Audit.DeleteError -> DeleteErrors` | `id` -> OK | delete error record | FE |
|
||||
| `DELETE /sysError/deleteSysErrorByIds` | `Audit.DeleteErrors -> DeleteErrors` | `ids[]` -> OK | batch delete | FE |
|
||||
|
||||
## 9. Export templates and versions
|
||||
|
||||
Export pipeline: `Export handler -> ExportService -> ExportUsecase -> ExportRepo -> data.exportRepo`,
|
||||
with short-lived download tokens in the cache. Version pipeline: `Version handler
|
||||
-> VersionService -> VersionUsecase -> VersionRepo -> data.versionRepo`; export and
|
||||
import also read/write menus, APIs, and dictionaries through `VersionUsecase`.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /sysExportTemplate/createSysExportTemplate` | `Export.Create -> CreateRequest` | template DTO -> OK | create template | FE |
|
||||
| `PUT /sysExportTemplate/updateSysExportTemplate` | `Export.Update -> UpdateRequest` | template DTO -> OK | update template | FE |
|
||||
| `DELETE /sysExportTemplate/deleteSysExportTemplate` | `Export.Delete -> Delete` | `id` -> OK | delete template | FE |
|
||||
| `DELETE /sysExportTemplate/deleteSysExportTemplateByIds` | `Export.DeleteMany -> Delete` | `ids[]` -> OK | batch delete | FE |
|
||||
| `GET /sysExportTemplate/findSysExportTemplate` | `Export.Find -> Template` | id/templateID -> template | read | FE |
|
||||
| `GET /sysExportTemplate/getSysExportTemplateList` | `Export.List -> TemplatesFilter` | page/name/table/template/date -> `Page<Template>` | read | FE |
|
||||
| `GET /sysExportTemplate/previewSQL` | `Export.Preview -> Preview` | templateID + query params -> SQL | render query only | FE |
|
||||
| `GET /sysExportTemplate/exportExcel` | `Export.Issue(false) -> IssueToken` | templateID + params -> download token | cache token; no file yet | FE |
|
||||
| `GET /sysExportTemplate/exportTemplate` | `Export.Issue(true) -> IssueToken` | templateID -> download token | cache blank-template token | FE |
|
||||
| `POST /sysExportTemplate/importExcel` | `Export.Import -> Import` | multipart file + templateID -> OK | parse/import spreadsheet | -- |
|
||||
| `GET /sysExportTemplate/exportExcelByToken` | `Export.Download(false) -> ConsumeToken + Export` | token -> XLSX stream | **public**, one-time token consumption and file generation | -- |
|
||||
| `GET /sysExportTemplate/exportTemplateByToken` | `Export.Download(true) -> ConsumeToken + ExportBlankTemplate` | token -> XLSX stream | **public**, one-time token consumption and blank template | -- |
|
||||
| `DELETE /sysVersion/deleteSysVersion` | `Version.Delete -> DeleteVersions` | `ID` -> OK | delete version record | FE |
|
||||
| `DELETE /sysVersion/deleteSysVersionByIds` | `Version.DeleteMany -> DeleteVersions` | `IDs[]` -> OK | batch delete | FE |
|
||||
| `GET /sysVersion/findSysVersion` | `Version.Find -> Version` | `ID` -> version | read | FE |
|
||||
| `GET /sysVersion/getSysVersionList` | `Version.List -> Versions` | page/name/code/date -> `Page<Version>` | read | FE |
|
||||
| `POST /sysVersion/exportVersion` | `Version.Export -> BuildVersionBundle/CreateVersion` | version metadata + menu/API/dict ids -> OK | compose and persist version JSON | FE |
|
||||
| `GET /sysVersion/downloadVersionJson` | `Version.Download -> VersionData` | `ID` -> JSON attachment | create/download serialized version | FE |
|
||||
| `POST /sysVersion/importVersion` | `Version.Import -> ImportRequest/ImportVersionBundle` | version JSON DTO -> OK | import menu/API/dictionary bundle and record import | FE |
|
||||
|
||||
## 10. Scheduled tasks and media
|
||||
|
||||
Task pipeline: `Task handler -> TaskService -> TaskApplicationUsecase -> TaskUsecase ->
|
||||
TaskRepo -> data.taskRepo`, plus the worker scheduler/runtime. Media pipeline:
|
||||
`Media handler -> MediaService -> MediaUsecase -> MediaRepo -> data.mediaRepo`,
|
||||
using configured object/local storage and upload-session persistence.
|
||||
|
||||
| Method/path | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `POST /timedTask/createTimedTask` | `Task.Create -> CreateRequest` | task cron/executor DTO -> OK | persist task and schedule it | FE |
|
||||
| `PUT /timedTask/updateTimedTask` | `Task.Update -> UpdateRequest` | task DTO -> OK | update and reschedule | FE |
|
||||
| `DELETE /timedTask/deleteTimedTask` | `Task.Delete -> Delete` | `{id}` -> OK | unschedule and delete task | FE |
|
||||
| `POST /timedTask/toggleTimedTask` | `Task.Toggle -> Toggle` | `id,enabled` -> OK | enable/disable schedule | FE |
|
||||
| `POST /timedTask/triggerTimedTask` | `Task.Trigger -> Trigger` | `{id}` -> OK | enqueue/manual execute; log result asynchronously | FE |
|
||||
| `GET /timedTask/getTimedTaskList` | `Task.List -> ListRequest` | page/name/type/enabled -> `Page<Task>` | read | FE |
|
||||
| `GET /timedTask/getTimedTaskLogList` | `Task.Logs -> Logs` | page/task/status -> `Page<TaskLog>` | read | FE |
|
||||
| `GET /timedTask/getRegisteredMethods` | `Task.Methods -> RegisteredMethods` | none -> `{methods}` | read task method registry | FE |
|
||||
| `GET /timedTask/alertStream` | `Task.AlertStream -> Subscribe` | authenticated SSE -> event stream | subscribe/unsubscribe user failure alerts | FE-direct |
|
||||
| `POST /fileUploadAndDownload/upload` | `Media.Upload -> Upload` | multipart file/category/save -> media item | write storage; optionally persist metadata | FE |
|
||||
| `POST /fileUploadAndDownload/getFileList` | `Media.List -> MediaList` | media filter/page -> `Page<Media>` | read | FE |
|
||||
| `POST /fileUploadAndDownload/deleteFile` | `Media.Delete -> Delete` | `{id}` -> OK | delete metadata and storage object | FE |
|
||||
| `POST /fileUploadAndDownload/deleteFiles` | `Media.DeleteMany -> Delete` | `ids[]` -> OK | batch delete files | -- |
|
||||
| `GET /fileUploadAndDownload/findFile` | `Media.Find -> Media` | `id` -> media item | read | -- |
|
||||
| `POST /fileUploadAndDownload/editFileName` | `Media.Rename -> Rename` | `id,name` -> OK | rename metadata/storage object per backend | FE |
|
||||
| `POST /fileUploadAndDownload/importURL` | `Media.ImportURLs -> ImportURLRequests` | URL items -> OK | import remote media references | FE |
|
||||
| `POST /fileUploadAndDownload/listOssFiles` | `Media.Storage -> Storage` | prefix/cursor/limit -> object page | list configured object storage | -- |
|
||||
| `GET /attachmentCategory/getCategoryList` | `Media.Categories -> Categories` | none -> categories | read | FE |
|
||||
| `POST /attachmentCategory/addCategory` | `Media.SaveCategory -> SaveCategoryRequest` | category DTO -> OK | create/update category | FE |
|
||||
| `POST /attachmentCategory/deleteCategory` | `Media.DeleteCategory -> DeleteCategory` | `{id}` -> OK | delete category | FE |
|
||||
| `POST /mediaUpload/init` | `Media.InitUpload -> InitUpload` | name/hash/size/chunk metadata -> session/status | create/resume multipart upload session | FE |
|
||||
| `POST /mediaUpload/chunk` | `Media.SaveChunk -> SaveChunk` | multipart chunk + upload/index/hash -> OK | persist/verify upload chunk | FE |
|
||||
| `POST /mediaUpload/complete` | `Media.CompleteUpload -> CompleteUpload` | `uploadId` -> media item | assemble/persist completed media | FE |
|
||||
| `DELETE /mediaUpload/:uploadId` | `Media.CancelUpload -> CancelUpload` | path upload id -> OK | remove pending upload session/chunks | FE |
|
||||
|
||||
## 11. Announcements and email
|
||||
|
||||
Announcement pipeline: `Announcement handler -> AnnouncementService -> AnnouncementUsecase
|
||||
-> AnnouncementRepo -> data.announcementRepo`. Email pipeline: `Email handler ->
|
||||
EmailService -> EmailUsecase -> EmailRepo`, where `NewEmailRepo(runtime)` uses the
|
||||
configured runtime mail client rather than the database.
|
||||
|
||||
| Method/path | Visibility | Call | Key input -> output | Primary effect | FE |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `POST /info/createInfo` | private | `Announcement.Create -> Create` | announcement DTO -> OK | create announcement | FE |
|
||||
| `DELETE /info/deleteInfo` | private | `Announcement.Delete -> Delete` | `ID` -> OK | delete announcement | FE |
|
||||
| `DELETE /info/deleteInfoByIds` | private | `Announcement.DeleteByIDs -> DeleteByIDs` | `IDs[]` -> OK | batch delete | FE |
|
||||
| `PUT /info/updateInfo` | private | `Announcement.Update -> Update` | announcement DTO -> OK | update announcement | FE |
|
||||
| `GET /info/findInfo` | private | `Announcement.Find -> Find` | `ID` -> announcement | read | FE |
|
||||
| `GET /info/getInfoList` | private | `Announcement.List -> List` | page/date range -> `Page<Announcement>` | read | FE |
|
||||
| `GET /info/getInfoDataSource` | public | `Announcement.DataSource -> UserOptions` | none -> `{userID: options}` | read selectable users | FE |
|
||||
| `GET /info/getInfoPublic` | public | `Announcement.Public` | none -> compatibility text | no business read/write | -- |
|
||||
| `POST /email/emailTest` | private | `Email.Test -> Test` | none -> OK | send configured test email | FE |
|
||||
| `POST /email/sendEmail` | private | `Email.Send -> Send` | `to,subject,body` -> OK | send mail | FE |
|
||||
|
||||
## Frontend coverage difference
|
||||
|
||||
The web client has **no unmatched static API URL**: all inspected `service({url})`
|
||||
calls resolve to a registered local route, including the dynamic
|
||||
`DELETE /mediaUpload/:uploadId`. One direct browser request,
|
||||
`GET /timedTask/alertStream`, is used by `EventSource` and is therefore counted
|
||||
as covered even though it has no `web/src/api/timedTask.js` wrapper.
|
||||
|
||||
Registered routes without a current frontend API call are:
|
||||
|
||||
- platform/operational: `GET /health`, `GET /swagger/*any`;
|
||||
- dictionary: `GET /sysDictionary/getSysDictionaryListWithDetails`;
|
||||
- audit: `GET /sysOperationRecord/findSysOperationRecord`;
|
||||
- export: `POST /sysExportTemplate/importExcel`, both public
|
||||
`...ByToken` download routes;
|
||||
- media: `POST /fileUploadAndDownload/deleteFiles`, `GET .../findFile`,
|
||||
`POST .../listOssFiles`;
|
||||
- announcement compatibility endpoint: `GET /info/getInfoPublic`.
|
||||
|
||||
The first group is operational or token-follow-up behavior. The others are
|
||||
backend capabilities not represented by a client function today; they should be
|
||||
explicitly marked as deliberately backend-only or added to the frontend when a
|
||||
GVA behavioral comparison shows they are user-facing.
|
||||
|
||||
## Clearly excluded from the migration parity target
|
||||
|
||||
The supplied GVA checkout has router modules with no corresponding local router
|
||||
registration: `system/sys_auto_code.go`, `system/sys_auto_code_history.go`,
|
||||
`system/sys_skills.go`, and `example/exa_customer.go`. Its plugin tree also
|
||||
contains AI/MCP/CLI/skills routes and auto/plugin-management routes. These
|
||||
code-generation/history, AI/LLM/MCP/skills, plugin-management, and example
|
||||
modules are explicitly outside this audit. The single GVA
|
||||
`GET /sysError/getSysErrorSolution` action is likewise excluded because it
|
||||
delegates to `AutoCodeService.LLMAuto`; the surrounding non-AI error CRUD is
|
||||
included. No parity failure should be opened for excluded routes. The local
|
||||
router inventory does include the GVA system/media modules that were migrated,
|
||||
plus the local `announcement` and `email` modules.
|
||||
|
||||
## Audit use
|
||||
|
||||
For the next phase, compare each row to the same GVA route by method and path,
|
||||
then verify: request binding location, validation/defaulting, response envelope,
|
||||
authorization/data-scope behavior, database/storage transaction effects, and
|
||||
side effects such as cache invalidation, token consumption, scheduler reload, or
|
||||
mail/file output. Keep the exclusions above out of the parity score.
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# GVA 横切接口对照审计
|
||||
|
||||
基准:`C:/Users/Yvan/AppData/Local/Temp/gva-parity-02f3783`(commit `02f37833255e0e339c3d69199cb5a468f17de9fc`,2026-08-06)。
|
||||
范围:只审查已迁移后台接口共用的 HTTP/router/middleware 行为;未迁移的代码生成、AI 等模块不计入缺口。
|
||||
`GET /sysError/getSysErrorSolution` 调用 LLMAuto,按要求排除。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
| 项 | 结论 | 严重度 |
|
||||
|---|---|---|
|
||||
| 路由实际集合 | 业务路由方法和路径基本对齐;本地 `engine.Routes()` 不含动态本地静态文件,而 GVA `StaticFS` 会注册静态 `GET`/`HEAD` wildcard | P1(API sync/route-table 可见集合) |
|
||||
| 静态文件 URL | GVA 以 `local.store-path` 注册静态路由;本地 NoRoute 以 `local.path-prefix` 动态处理。默认值相同,配置分离时行为不同 | P1(配置不一致时) |
|
||||
| Swagger `basePath` | GVA 生成文档的 `docs.SwaggerInfo.BasePath` 在无前缀时为空字符串;本地动态文档把空前缀规范化为 `/`。带 `/admin` 前缀时两端都输出 `/admin` | P3(严格文档/客户端比较) |
|
||||
| 统一响应/状态码 | 成功/业务失败均 HTTP 200 + `{code,data,msg}`;未登录 401;强制改密 409;panic 500;限流 200,和 GVA 一致 | 对齐 |
|
||||
| 请求绑定错误 | `ShouldBindJSON/Query` 错误均转换为 HTTP 200、code 7、data `{}`;与 GVA 一致 | 对齐 |
|
||||
| JWT/刷新/多点登录 | 黑名单、临近过期刷新、cookie/`new-token` 头、多点登录旧 token 作废逻辑对齐;本地用持久化黑名单并 singleflight,属于实现差异 | P2(实现/故障语义需回归) |
|
||||
| MustChangePwd | 允许 `/user/changePassword`、`/user/getUserInfo`、`/jwt/jsonInBlacklist`,其余 409/code 10001;对齐 | 对齐 |
|
||||
| Casbin | 本地每次从 `casbin_rule` 读取并直接 Enforce;GVA 使用内存 enforcer。`freshCasbin` 本地恒成功,GVA `LoadPolicy` 失败会返回 code 7 | P1 |
|
||||
| DataScope | 顺序均为 JWT -> MustChangePwd -> Casbin -> DataScope;本地额外注入 actor/request metadata,不改变已迁移接口授权规则 | 对齐(增强) |
|
||||
| CORS | GVA 基准的 `Cors/CorsByRules` 在 `router.go` 中均为注释、默认未安装;本地无条件安装并按 runtime 执行 allow-all/whitelist/strict。默认 `whitelist` 空配置时差异不显著,但 strict/allow-all 会改变可达性与响应头 | P1(启用配置时行为不兼容) |
|
||||
| 登录/验证码限流 | GVA 仅在 `POST /base/login`、`POST /base/captcha` 路由挂限流;本地是全局 middleware,按 URL 后缀匹配,未匹配路由或其他方法也可能计数。超限 HTTP 200/code 7、缓存异常 fail-open;另有 key 前缀 `KRA_SecLimit` vs `GVA_SecLimit` | P2(作用面与跨版本缓存兼容) |
|
||||
| request/trace metadata | RequestMeta 最先执行,`X-Request-Id`、`X-Trace-Id`、W3C `traceparent` 的校验/回写顺序一致 | 对齐 |
|
||||
| access log | 单一 body/response capture、脱敏、1 MiB response cap、route/status/bytes/identity/trace 字段一致;版本头不同 | P2 |
|
||||
| operation audit | 已迁移的 GVA `OperationRecord` 分组路由均被本地 `operationRoutes` 覆盖;AI `getSysErrorSolution` 排除 | 对齐(需防漏项测试) |
|
||||
| error audit | panic/错误级日志走单一 error sink;本地响应 envelope 的未预期业务失败由 `ErrorAudit` 转为 error log;GVA 由 logger core 捕捉 error level | 对齐(实现不同) |
|
||||
| panic recovery | broken-pipe 仅记录并 abort,普通 panic 返回 500;对齐 | 对齐 |
|
||||
| API sync/init seed | 本地初始化/同步使用 `engine.Routes()`;本地会去掉 router-prefix,GVA 基准直接使用带 prefix 的 `GVA_ROUTERS`。静态 routes 缺失叠加影响 sync/seed | P1(自定义 prefix) |
|
||||
|
||||
## 逐项核验
|
||||
|
||||
### 1. Router 与静态文件
|
||||
|
||||
GVA `server/initialize/router.go` 的顺序为 `RequestMeta -> GinRecovery -> AccessLog -> UploadResponseHeaders -> StaticFS -> Swagger -> Public/Private`;Private 依次安装 `JWTAuth -> MustChangePwdGuard -> CasbinHandler -> DataScope`。本地 `internal/server/gin.go` 为 `RequestMeta -> Recovery -> AccessLog -> CORS -> ErrorAudit -> SecurityRateLimit`,再建立 Public/Private;Private 依次为 `Auth -> MustChangePassword -> AccessControl -> OperationAudit`。已迁移业务 handler 的路径/HTTP method 与路由契约测试一致。
|
||||
|
||||
本地没有调用 `StaticFS`,而是在 `NoRoute` 中用 `serveLocalStorage` 按 `local.path-prefix` 动态匹配;因此动态静态文件不会出现在 `engine.Routes()` 或 `/api/syncApi` 输入中。GVA `StaticFS(global.GVA_CONFIG.Local.StorePath, ...)` 会注册静态 wildcard 的 GET 和 HEAD,并进入 `GVA_ROUTERS`。默认配置两者都为 `uploads/file`,但如果 `store-path != path-prefix`,两端暴露的 URL 集合不同。基准 GVA 的预生成 Swagger 文档也没有 `/uploads/file/*filepath`,而 Kra 的动态 Swagger 同样不会把 NoRoute fallback 当作文档路由,因此 Swagger 本身不单独计为已确认差异。静态响应头(`nosniff`,非安全媒体 `Content-Disposition: attachment`)的效果一致;本地额外拒绝目录。
|
||||
|
||||
Swagger 文档的根路径还有一个低等级差异:基准 `server/docs/docs.go` 的 `BasePath` 默认值为空字符串,`server/initialize/router.go` 仅在配置有前缀时写入;本地 `internal/server/swagger.go:59-63` 将空前缀强制写成 `"/"`。这通常不会改变浏览器请求,但会影响严格 JSON 快照或按 `basePath` 拼接 URL 的客户端。
|
||||
|
||||
**测试建议**:构造临时目录和 `store-path != path-prefix`;断言 GVA 期望的 `GET`/`HEAD /<store-path>/*filepath` 是否进入 route table 与 sync diff,且确认两端 Swagger 文档的静态路由口径;断言本地动态静态响应仍支持 GET/HEAD、目录 404/403、头部一致。
|
||||
|
||||
### 2. 统一响应与绑定错误
|
||||
|
||||
本地 `internal/server/httpx/response.go` 与 GVA `server/model/common/response/response.go` 均定义 `CodeSuccess=0`、`CodeError=7`、`CodePasswordChangeRequired=10001`,正常/业务失败均 `HTTP 200` envelope。`NoAuth` 是 `401` 且 `data:null`;MustChangePwd 是 `409`;Recovery 普通 panic 是 `500` 空响应;CORS 拒绝 `403`;限流是 `200` + code 7。handler 统一在 `ShouldBindJSON/ShouldBindQuery` 失败时写 code 7,未引入 Gin 默认 400。
|
||||
|
||||
**测试建议**:每类 endpoint 发送 malformed JSON、类型错误 query、缺字段 JSON,比较 status、code、data 和 msg;覆盖 401/409/403/429-like(实际 200)/500。
|
||||
|
||||
### 3. JWT、刷新和多点登录
|
||||
|
||||
GVA `middleware/jwt.go` 先检查 blacklist,再解析 token;临近过期时签发新 token,并回写 `new-token`、`new-expires-at` 和 cookie。登录在多点模式下将旧 Redis JWT 加入 blacklist,再写入新 active token。Kra `middleware/auth.go` 通过 `AuthService.AuthenticateToken` 完成同一顺序,并在刷新时回写相同 headers/cookie;登录 usecase 的 `RotateActiveToken` 在多点模式作废旧 token,刷新则传空 old token 保持旧 token 不被提前作废。Kra 用 `jwt_blacklists` 持久化查询并对同一 token 使用 singleflight,GVA 使用无错误返回的缓存 blacklist;因此数据库故障时 Kra 会把所有 token 映射为“异地登陆或令牌失效”并拒绝请求,而 GVA 仍可能继续 JWT 解析。这是故障语义差异,需在部署策略和回归测试中明确。
|
||||
|
||||
### 4. MustChangePwd、Casbin、DataScope
|
||||
|
||||
两端允许列表完全一致:`/user/changePassword`、`/user/getUserInfo`、`/jwt/jsonInBlacklist`。Kra `AccessControl` 会去掉 router-prefix 后按 `authority_id + path + method` 授权,并在同一 request context 注入 DataScope/Actor;GVA `CasbinHandler` 做相同 prefix trim,随后 `DataScope` 注入数据域身份。Kra 的授权实现每请求读取数据库并构造 Casbin model,GVA 使用全局 enforcer 缓存;所以 Kra 写策略后即时可见,而 GVA 依赖 `FreshCasbin` 或写路径主动 reload。
|
||||
|
||||
`GET /api/freshCasbin` 是明确缺口:GVA 调 `e.LoadPolicy()`,失败返回 HTTP 200/code 7/“刷新失败”;Kra handler 直接 HTTP 200/code 0/“刷新成功”,且不会暴露加载失败。即便 Kra 当前无内存 enforcer,该接口的可观察错误语义仍不一致。
|
||||
|
||||
### 5. CORS、限流与 metadata
|
||||
|
||||
CORS 代码实现本身的 allow-all、strict-whitelist、匹配 origin、OPTIONS 204、`GET /health` 例外和头部名称与 GVA helper 相近,但基准 `server/initialize/router.go` 将 `Cors/CorsByRules` 两行都保留为注释,默认并未安装该 middleware;Kra 在 `NewGinEngine` 中无条件安装并每次读取 runtime。因此在 `strict-whitelist` 或 `allow-all` 配置下,Kra 会拒绝/放行并添加 CORS 头,而基准仍按普通路由处理。若部署契约要求启用 CORS,应在基准与本地明确同样的安装开关;否则应把该差异视为 P1。
|
||||
|
||||
登录/验证码限流的窗口、超限响应、缓存异常 fail-open 一致,但作用面不完全相同:GVA 只在 `POST /base/login`、`POST /base/captcha` 路由挂 `SecurityLimit()`,Kra 则在全局 middleware 中按 URL 后缀匹配,未匹配路由或其他方法也可能读取/递增计数。两端 key 前缀仍不同:Kra `KRA_SecLimit`,GVA `GVA_SecLimit`。滚动部署共享缓存时会导致计数不共享,建议统一前缀或明确迁移策略。
|
||||
|
||||
RequestMeta 两端都位于最前,生成/透传 request id、trace id、span id,并按合法 `traceparent` 优先、`X-Trace-Id` 回退;响应头和 context 字段一致。AccessLog 都只读一次 body、包装一次 writer、脱敏敏感 JSON/header、响应捕获上限 1 MiB,并记录 route/status/latency/bytes/identity/request/trace metadata。
|
||||
|
||||
版本响应头是第二个明确可见差异:GVA 写 `X-Gva-Version`,Kra 写 `X-Kra-Version`。如果前端、网关或探针按 GVA header 读取版本,这会造成兼容性缺口;建议双写过渡或在契约中明确新 header。
|
||||
|
||||
### 6. Operation/Error audit 与 recovery
|
||||
|
||||
GVA 通过各资源 router group 的 `Use(middleware.OperationRecord())` 选择写操作;Kra 在全局 Private 后通过 `recordsOperation` 白名单按 method+route suffix 选择,已迁移的 user/api/authority/menu/department/position/dictionary/params/security/system/token/version/export/error/login-log/data-access/timed-task/info/email 路径均有覆盖。`sysError/getSysErrorSolution` 属 LLMAuto,按要求不纳入。
|
||||
|
||||
两端 operation audit 均复用 AccessLog 的 request/response capture,GET 将 query 转为 JSON,multipart 写 `[文件]`,下载响应超限写 `[超出记录长度]`,并记录 request/trace/device metadata。GVA logger core 和 Kra logging error sink 都会将 Error 级别日志落入 `sys_error`;Kra `ErrorAudit` 额外把未预期的 code 7 envelope 提升为 error log,并排除已知客户端失败、sysError/logViewer 自记录,避免重复。
|
||||
|
||||
Recovery 对 broken pipe 只记录并 abort,普通 panic 记录请求/stack 并返回 500;行为一致。
|
||||
|
||||
## 修复/验证计划(按优先级)
|
||||
|
||||
1. **P1 路由集合与 API sync/seed**:决定静态文件是否应纳入 API/route-table 同步。若要严格 GVA parity,应注册与 GVA 同形的 `GET`/`HEAD` static wildcard,并以 `store-path` 作为 URL;若保留动态 NoRoute,应在 sync/seed 明确排除并补文档/测试。额外覆盖非空 router-prefix,并保留两端 Swagger 对静态路由的已验证口径。
|
||||
2. **P1 FreshCasbin**:让 `freshCasbin` 调用实际策略刷新或至少执行数据库/缓存可用性检查;失败返回 GVA 相同的 code 7 envelope。增加故障注入测试。
|
||||
3. **P1 router-prefix sync**:明确数据库中的 API path 是带前缀还是无前缀;当前 Kra `NormalizeRoutePath` 去前缀,而 GVA `GVA_ROUTERS` 直接保留前缀。选择兼容策略并覆盖初始化、`syncApi`、Casbin authorize 三条链路。
|
||||
4. **P1 CORS 启用策略**:让 CORS 是否安装由与 GVA 相同的部署开关决定,或把 Kra 的无条件 CORS 明确记录为专有契约;覆盖 strict/allow-all/OPTIONS。
|
||||
5. **P2 版本头**:兼容期双写 `X-Gva-Version` 与 `X-Kra-Version`,并增加 header contract test。
|
||||
6. **P2 限流与黑名单故障**:收窄限流到两个 POST route,统一/双读 `GVA_SecLimit` key,并明确黑名单存储故障的 fail-open/fail-closed 行为。
|
||||
7. **P2 横切回归**:运行 malformed binding、JWT expired/refresh/multipoint/**blacklist-store failure**、MustChangePwd allow-list、Casbin deny/fresh failure、DataScope ownership、CORS strict/OPTIONS、rate-limit fail-open、GET/HEAD static、operation/error audit、panic/broken-pipe 的 HTTP 集成测试。
|
||||
8. **P3 文档兼容**:决定是否把无前缀 Swagger `basePath` 从 `"/"` 调整为空字符串,并固定 snapshot 规则。
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
# GVA Parity Audit: Identity and Access
|
||||
|
||||
## Scope and method
|
||||
|
||||
- **Reference**: `C:\Users\Yvan\AppData\Local\Temp\gva-parity-02f3783`, commit `02f37833255e0e339c3d69199cb5a468f17de9fc` (the agreed GVA snapshot from 2026-08-06).
|
||||
- **Audited local surface**: `/base`, `/jwt`, `/user`, `/authority`, `/menu`, `/api`, `/casbin`, `/authorityBtn`, `/department`, `/position`, plus request data-scope construction and policy enforcement.
|
||||
- **Excluded by request**: code generation/AutoCode, AI/MCP/skills, plugins, and endpoints that are not migrated into this repository.
|
||||
- **Method**: for every migrated endpoint, compared method/path, binding and defaults, explicit validation, private-route authorization/operation recording, service/repository side effects and transactions, and error/success response envelope.
|
||||
|
||||
`OK` means the six dimensions above match the reference for normal operation. The
|
||||
priority labels follow the consolidated plan: `P1` can change route/management
|
||||
behavior, `P2` affects deployed clients or failure/rolling-deployment semantics,
|
||||
and `P3` is a lower-impact observable difference.
|
||||
|
||||
## Shared routing and authorization evidence
|
||||
|
||||
| Concern | Local evidence | GVA evidence | Assessment |
|
||||
|---|---|---|---|
|
||||
| Public/private route split | `internal/server/gin.go:34-61` | `server/initialize/router.go:71-121` | Both expose base routes publicly and run protected routes through JWT, password-expiry guard, Casbin authorization, and data-scope context. |
|
||||
| Authorization path normalization | `internal/server/middleware/access.go:12-48` | `server/middleware/casbin_rbac.go:12-31` | Both remove `router-prefix` before enforcing `authorityId + path + method`; local reads policies from DB rather than retaining an enforcer cache. |
|
||||
| Data-scope identity | `internal/server/middleware/access.go:35-47`, `internal/data/authority.go:415-480` | `server/middleware/data_scope.go:12-31`, `server/service/system/data_scope.go:17-101` | Scope, direct/all/subtree/custom department resolution, and request-context injection match. |
|
||||
| Operation records | `internal/server/middleware/audit.go:137-170` | route-level `middleware.OperationRecord()` in `server/router/system/*.go` | The local exact route allowlist mirrors the reference's recording groups; rejected auth requests are not recorded in either flow. |
|
||||
|
||||
## Findings
|
||||
|
||||
### P2: security-rate-limit cache namespace differs
|
||||
|
||||
`/base/login` and `/base/captcha` count requests under `KRA_SecLimit<ip><route>` locally, but GVA uses `GVA_SecLimit<ip><route>`. With a shared cache during a rolling migration, rate-limit counters do not carry over, so a caller can obtain a fresh counter simply by reaching the other implementation.
|
||||
|
||||
- Local: `internal/server/middleware/rate_limit.go:13-36`, especially `:29`.
|
||||
- GVA: `server/middleware/limit_ip.go:101-104`.
|
||||
- Recommendation: use `GVA_SecLimit` for the compatible administrative API, or read both prefixes during a documented migration window.
|
||||
- Regression test: seed `GVA_SecLimit127.0.0.1/base/login` at the configured limit and assert the local `POST /base/login` returns code `7` without invoking the handler.
|
||||
|
||||
### P2: version response header name differs on every endpoint
|
||||
|
||||
The local global access logger writes `X-Kra-Version`; GVA writes `X-Gva-Version`. This is an externally observable HTTP contract difference for public and protected responses, including failures generated before handlers run.
|
||||
|
||||
- Local: `internal/server/middleware/access_log.go:20-36`, especially `:35`.
|
||||
- GVA: `server/middleware/access_log.go:69-73`, especially `:71`.
|
||||
- Recommendation: emit `X-Gva-Version` (optionally retain `X-Kra-Version` temporarily as an additive header).
|
||||
- Regression test: request `/health`, `/base/captcha`, and one authenticated endpoint; assert the GVA header name and value are present.
|
||||
|
||||
### P1: `/api/freshCasbin` cannot report the reference refresh failure
|
||||
|
||||
GVA reloads its in-memory Casbin enforcer and returns `刷新失败` if the enforcer is unavailable or policy loading fails. The local authorization implementation intentionally reads `casbin_rule` directly and turns the endpoint into an unconditional success no-op. Normal permission changes take effect correctly, but operators cannot receive the GVA failure response.
|
||||
|
||||
- Local: `internal/server/handler/api.go:201-206`.
|
||||
- GVA: `server/api/v1/system/sys_api.go:314-329`, `server/service/system/sys_casbin.go:159-166`.
|
||||
- Recommendation: document the no-cache implementation in the API migration notes, or provide a compatibility health check that can fail when policy storage is unavailable.
|
||||
- Regression test: inject a failing policy-store read and verify the chosen compatibility behavior is intentional and stable.
|
||||
|
||||
### P3: empty `authorityIds` handling is intentionally safer locally
|
||||
|
||||
`POST /user/setUserAuthorities` accepts an empty array at the HTTP boundary in both implementations. The GVA service then indexes `authorityIds[0]` after deleting the old links (`server/service/system/sys_user.go:199-231`), which produces a panic/500 for an empty array. Kra returns a normal business error from `internal/biz/user.go:165-170` instead. This is safer behavior, but it is still a contract difference that clients/tests should make explicit.
|
||||
|
||||
- Recommendation: keep the safe local response and add a shared validation rule requiring at least one authority ID; do not reproduce the GVA panic.
|
||||
- Regression test: submit `{"id": <user>, "authorityIds": []}` and assert a deterministic non-500 error with no role links lost.
|
||||
|
||||
## Endpoint-by-endpoint matrix
|
||||
|
||||
### Base, session, and token lifecycle
|
||||
|
||||
Route evidence: local `internal/server/router/public.go:9-16`, `internal/server/router/user.go:28-30`; GVA `server/router/system/sys_base.go:10-17`, `server/router/system/sys_jwt.go:9-14`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /base/captcha` | No body. Public; security limiter applies. Reads configured dimensions and IP failure count; starts count at 1 and enables captcha when `open==0` or failures exceed threshold. | Creates base64 digit captcha, persists it with configured expiry, returns `captchaId`, `picPath`, `captchaLength`, `openCaptcha`; same messages. | **P2** only for shared limiter key. Local `internal/server/handler/public.go:35-52`; GVA `server/api/v1/system/sys_captcha.go:29-69`. |
|
||||
| `POST /base/login` | JSON `username/password/captcha/captchaId`; username and password required. Public; security limiter applies. | Same lock check, conditional captcha verification and counter increment, credential/disabled handling, login audit, password-expiry flag, token issue, multipoint old-token revocation/active-token replacement, cookie and response fields. | **P2** only for shared limiter key. Local `internal/server/handler/public.go:54-100`, `internal/biz/authentication.go:63-132`; GVA `server/api/v1/system/sys_user.go:26-167`. |
|
||||
| `POST /jwt/jsonInBlacklist` | Private JWT/Casbin route; token read from `x-token`, then cookie fallback. | Persists revoked JWT and clears cookie; local checks blacklist directly on future authentication while GVA also fills cache. Client-visible invalidation and response match. | **OK**. Local `internal/server/handler/session.go:13-27`, `internal/data/api_token.go:128-130`; GVA `server/api/v1/system/sys_jwt_blacklist.go:20-35`, `server/service/system/jwt_black_list.go:24-35`. |
|
||||
|
||||
### User identity and profile
|
||||
|
||||
Route evidence: local `internal/server/router/user.go:9-30`, `internal/server/router/organization.go:18,28`; GVA `server/router/system/sys_user.go:10-30`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /user/getUserList` | JSON page/search; page and pageSize must be nonzero; private/Casbin. | Filters username/nickname/phone/email; allowed sort fields and default `id desc`; paginated `PageResult`. | **OK**. Local `internal/server/handler/user.go:24-44`, `internal/data/user.go:280-322`; GVA `server/api/v1/system/sys_user.go:185-219`, `server/service/system/sys_user.go:100-144`. |
|
||||
| `POST /user/admin_register` | JSON username/password/nickname/authority; required fields and configured password policy; private/Casbin + operation record. | Hashes password; duplicate-username check and user/role link creation in a transaction; response envelope contains `user`. | **OK**. Local `internal/server/handler/user.go:46-81`, `internal/data/user.go:333-405`; GVA `server/api/v1/system/sys_user.go:170-183`, `server/service/system/sys_user.go:30-69`. |
|
||||
| `PUT /user/setUserInfo` | JSON user fields; nonzero `ID`; private/Casbin + record. | If `authorityIds` supplied, replaces role links first; then updates mutable profile fields, preserving GVA's separate-write partial-failure behavior. | **OK**. Local `internal/server/handler/user.go:83-98`, `internal/biz/user.go:121-133`; GVA `server/api/v1/system/sys_user.go:321-339`. |
|
||||
| `PUT /user/setSelfInfo` | JSON profile; identity comes from JWT; private/Casbin + record. | Updates caller-owned profile fields and returns `设置成功`. | **OK**. Local `internal/server/handler/user.go:100-117`, `internal/data/user.go:406-429`; GVA `server/api/v1/system/sys_user.go:341-362`, `server/service/system/sys_user.go:345-348`. |
|
||||
| `DELETE /user/deleteUser` | JSON `id`, nonzero; blocks self-delete; private/Casbin + record. | Transaction soft-deletes user and deletes role/department/position links. | **OK**. Local `internal/server/handler/user.go:119-139`, `internal/data/user.go:493-510`; GVA `server/api/v1/system/sys_user.go:297-319`, `server/service/system/sys_user.go:301-318`. |
|
||||
| `POST /user/resetPassword` | JSON ID/password; password policy; private/Casbin + record. | Rehashes, stamps `password_updated_at`, preserves `mustChangePassword` state as GVA does. | **OK**. Local `internal/server/handler/user.go:141-156`, `internal/biz/user.go:140-149`; GVA `server/api/v1/system/sys_user.go:410-431`, `server/service/system/sys_user.go:414-421`. |
|
||||
| `POST /user/changePassword` | JSON old/new password; both required; JWT identity; password-expiry guard allows this route. | Verifies old hash, validates new password, rehashes, updates timestamp and clears forced-change flag. | **OK**. Local `internal/server/handler/user.go:158-186`, `internal/biz/user.go:150-164`; GVA `server/api/v1/system/sys_user.go:71-94`, `server/service/system/sys_user.go:75-94`. |
|
||||
| `PUT /user/setSelfSetting` | JSON map; JWT identity; private/Casbin + record. | Marshals and stores `origin_setting`; same success envelope. | **OK**. Local `internal/server/handler/user.go:188-204`, `internal/data/user.go:563-569`; GVA `server/api/v1/system/sys_user.go:345-362`. |
|
||||
| `POST /user/setUserAuthorities` | JSON target ID and authority IDs; private/Casbin + record. | For non-empty IDs, transaction replaces role links and sets first supplied role as primary; strict-role guard is enforced for submitted role IDs. Empty IDs return a deterministic local business error, while GVA panics after deleting links. | **P3** safer-input divergence. Local `internal/server/handler/user.go:206-217`, `internal/biz/user.go:165-170`, `internal/data/user.go:512-544`; GVA `server/api/v1/system/sys_user.go:195-230`, `server/service/system/sys_user.go:199-232`. |
|
||||
| `POST /user/setUserAuthority` | JSON authority ID required; caller identity from JWT; private/Casbin + record. | Verifies membership and default router, updates active authority, signs a same-expiry replacement token, writes `new-token`/`new-expires-at` and cookie. | **OK**. Local `internal/server/handler/user.go:219-244`, `internal/biz/authentication.go:138-153`; GVA `server/api/v1/system/sys_user.go:146-193`, `server/service/system/sys_user.go:150-193`. |
|
||||
| `GET /user/getUserInfo` | No body; JWT identity; private/Casbin and password-expiry guard allows it. | Loads by JWT UUID with authorities, primary/multiple departments, positions, default-router fallback, and name paths. | **OK**. Local `internal/server/handler/user.go:246-258`, `internal/data/user.go:44-97`; GVA `server/api/v1/system/sys_user.go:364-408`, `server/service/system/sys_user.go:368-409`. |
|
||||
| `POST /user/setUserDepartments` | JSON user ID, `deptIds`, `primaryDeptId`; nonzero user ID; private/Casbin + record. | Transaction replaces department links; selects first department as missing primary and rejects a primary outside the submitted set. | **OK**. Local `internal/server/handler/organization.go:119-134`, `internal/data/department.go:242-274`; GVA `server/api/v1/system/sys_user.go:234-274`, `server/service/system/sys_user.go:236-274`. |
|
||||
| `POST /user/setUserPositions` | JSON user ID and position IDs; nonzero user ID; private/Casbin + record. | Transaction replaces user-position links. | **OK**. Local `internal/server/handler/organization.go:238-253`, `internal/data/position.go:133-147`; GVA `server/api/v1/system/sys_user.go:276-295`, `server/service/system/sys_user.go:277-295`. |
|
||||
|
||||
### Authorities and data scope
|
||||
|
||||
Route evidence: local `internal/server/router/authority.go:9-20`; GVA `server/router/system/sys_authority.go:10-26`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /authority/getAuthorityList` | No body; private/Casbin. | Builds tree under current authority; applies strict-tree visibility, including non-root direct children behavior. | **OK**. Local `internal/server/handler/authority.go:15-21`, `internal/data/authority.go:300-342`; GVA `server/api/v1/system/sys_authority.go:163-181`, `server/service/system/sys_authority.go:151-186`. |
|
||||
| `POST /authority/createAuthority` | JSON authority ID/name required; strict mode reparents a root request to actor; private/Casbin + record. | Transaction checks duplicate, applies defaults, seeds dashboard menu and the nine default policies; success contains `authority`. | **OK**. Local `internal/server/handler/authority.go:23-42`, `internal/data/authority.go:72-114`; GVA `server/api/v1/system/sys_authority.go:26-65`, `server/service/system/sys_authority.go:32-74`, `server/model/system/request/sys_casbin.go:15-26`. |
|
||||
| `POST /authority/copyAuthority` | JSON old/new authority; both IDs and new name required; private/Casbin + record. | Transaction copies assigned menus, buttons and deduplicated policies; strict mode prevents copying out-of-scope policy paths. | **OK**. Local `internal/server/handler/authority.go:44-67`, `internal/data/authority.go:115-241`; GVA `server/api/v1/system/sys_authority.go:67-101`, `server/service/system/sys_authority.go:76-123`. |
|
||||
| `PUT /authority/updateAuthority` | JSON authority ID/name required; private/Casbin + record. | Looks up target then updates authority metadata and returns it under `authority`. | **OK**. Local `internal/server/handler/authority.go:69-88`, `internal/data/authority.go:242-250`; GVA `server/api/v1/system/sys_authority.go:133-161`, `server/service/system/sys_authority.go:125-145`. |
|
||||
| `POST /authority/deleteAuthority` | JSON authority ID required; private/Casbin + record. | Transaction blocks users/primary users/children, clears user/menu/button/policy links, permanently removes authority row. | **OK**. Local `internal/server/handler/authority.go:90-104`, `internal/data/authority.go:251-299`; GVA `server/api/v1/system/sys_authority.go:103-131`, `server/service/system/sys_authority.go:127-149`. |
|
||||
| `POST /authority/setRoleUsers` | JSON authority ID required; private/Casbin + record. | Transaction replaces role-user links; changes primary role of removed users only when another role remains. | **OK**. Local `internal/server/handler/authority.go:106-120`, `internal/data/authority.go:343-385`; GVA `server/api/v1/system/sys_authority.go:263-286`, `server/service/system/sys_authority.go:303-367`. |
|
||||
| `GET /authority/getUsersByAuthority` | Query `authorityId`; private/Casbin. | Returns empty array rather than null. | **OK**. Local `internal/server/handler/authority.go:122-136`; GVA `server/api/v1/system/sys_authority.go:236-261`. |
|
||||
| `POST /authority/setDataScope` | JSON authority ID required; private/Casbin + record. | Transaction updates `data_scope`, clears old custom departments, inserts submitted departments only for scope 5. | **OK**. Local `internal/server/handler/authority.go:138-152`, `internal/data/authority.go:390-409`; GVA `server/api/v1/system/sys_authority.go:183-207`, `server/service/system/sys_authority.go:238-262`. |
|
||||
| `GET /authority/getDataScopeDepts` | Query `authorityId`; private/Casbin. | Returns custom department IDs, normalizing nil to `[]`. | **OK**. Local `internal/server/handler/authority.go:154-168`; GVA `server/api/v1/system/sys_authority.go:209-234`, `server/service/system/sys_authority.go:264-270`. |
|
||||
|
||||
### Menu and navigation
|
||||
|
||||
Route evidence: local `internal/server/router/menu.go:9-21`, `internal/server/router/user.go:24-26`; GVA `server/router/system/sys_menu.go:10-29`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /menu/getMenu` | Empty body; JWT authority; private/Casbin. | Reads role-assigned menus, ordered tree, parameters and selected button map; returns `{menus}`. | **OK**. Local `internal/server/handler/navigation.go:14-28`, `internal/data/user.go:197-278`; GVA `server/api/v1/system/sys_menu.go:25-44`, `server/service/system/sys_menu.go:30-87`. |
|
||||
| `POST /menu/getMenuList` | No effective body fields; private/Casbin. | Produces strict-aware base-menu tree with parameters/buttons. | **OK**. Local `internal/server/handler/menu.go:15-22`, `internal/data/menu.go:181-243`; GVA `server/api/v1/system/sys_menu.go:325-342`, `server/service/system/sys_menu.go:89-121`. |
|
||||
| `POST /menu/getBaseMenuTree` | Empty body; private/Casbin. | Same base tree, wrapped as `{menus}`. | **OK**. Local `internal/server/handler/menu.go:24-31`; GVA `server/api/v1/system/sys_menu.go:46-64`. |
|
||||
| `POST /menu/addBaseMenu` | JSON path/name/component/meta.title required; nonnegative sort; private/Casbin + record. | Transaction rejects duplicate name, validates parent and default-router constraints, inserts menu then parameters/buttons. | **OK**. Local `internal/server/handler/menu.go:33-64`, `internal/data/menu.go:60-99`; GVA `server/api/v1/system/sys_menu.go:125-158`, `server/service/system/sys_menu.go:123-171`. |
|
||||
| `POST /menu/updateBaseMenu` | Same validation as create; private/Casbin + record. | Transaction finds menu, checks renamed duplicate, replaces parameters/buttons, updates scalar fields. | **OK**. Local `internal/server/handler/menu.go:66-97`, `internal/data/menu.go:101-122`; GVA `server/api/v1/system/sys_menu.go:190-223`, `server/service/system/sys_base_menu.go`. |
|
||||
| `POST /menu/deleteBaseMenu` | JSON nonzero ID; private/Casbin + record. | Transaction blocks children/default-router use, then deletes menu, relations and permission buttons. | **OK**. Local `internal/server/handler/menu.go:99-114`, `internal/data/menu.go:124-155`; GVA `server/api/v1/system/sys_menu.go:160-188`. |
|
||||
| `POST /menu/getBaseMenuById` | JSON nonzero ID; private/Casbin + record. | Loads menu with parameters/buttons and returns `{menu}`. | **OK**. Local `internal/server/handler/menu.go:116-132`, `internal/data/menu.go:158-180`; GVA `server/api/v1/system/sys_menu.go:225-253`. |
|
||||
| `POST /menu/addMenuAuthority` | JSON authority ID required; private/Casbin + record. | Strict-mode target/menu scope checks, then transaction replaces role-menu links. | **OK**. Local `internal/server/handler/menu.go:134-149`, `internal/data/menu.go:259-309`; GVA `server/api/v1/system/sys_menu.go:66-93`, `server/service/system/sys_menu.go:202-241`. |
|
||||
| `POST /menu/getMenuAuthority` | JSON authority ID required; private/Casbin. | Reads target role's own assigned base menus without applying caller's listing filter; response `{menus}`. | **OK**. Local `internal/server/handler/menu.go:151-167`, `internal/data/menu.go:313-331`; GVA `server/api/v1/system/sys_menu.go:95-123`. |
|
||||
| `GET /menu/getMenuRoles` | Query nonzero `menuId`; private/Casbin. | Returns role IDs and `defaultRouterAuthorityIds`, normalizing nil arrays. | **OK**. Local `internal/server/handler/menu.go:169-196`, `internal/data/menu.go:333-346`; GVA `server/api/v1/system/sys_menu.go:255-296`. |
|
||||
| `POST /menu/setMenuRoles` | JSON nonzero `menuId`; private/Casbin + record. | Transaction replaces all role-menu rows. | **OK**. Local `internal/server/handler/menu.go:198-212`, `internal/data/menu.go:348-364`; GVA `server/api/v1/system/sys_menu.go:298-323`, `server/service/system/sys_menu.go`. |
|
||||
|
||||
### API registry and Casbin policy management
|
||||
|
||||
Route evidence: local `internal/server/router/api.go:9-28`; GVA `server/router/system/sys_api.go:10-35`, `server/router/system/sys_casbin.go:10-19`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /api/getApiList` | JSON page/pageSize nonzero plus filters/order; private/Casbin. | Same filters, allowed sort whitelist and paged `PageResult`. | **OK**. Local `internal/server/handler/api.go:15-34`, `internal/data/api.go:121-181`; GVA `server/api/v1/system/sys_api.go:177-210`, `server/service/system/sys_api.go:185-232`. |
|
||||
| `POST /api/getAllApis` | No body; private/Casbin. | Returns all APIs ordered `id desc`, but strict non-root caller only sees APIs permitted by own policies. | **OK**. Local `internal/server/handler/api.go:36-42`, `internal/data/api.go:121-152`; GVA `server/api/v1/system/sys_api.go:271-289`, `server/service/system/sys_api.go:239-270`. |
|
||||
| `POST /api/createApi` | JSON path/description/apiGroup/method all required; private/Casbin + record. | Duplicate path+method rejection; creates and returns populated API row. | **OK**. Local `internal/server/handler/api.go:44-59`, `internal/data/api.go:55-69`; GVA `server/api/v1/system/sys_api.go:26-53`, `server/service/system/sys_api.go:26-33`. |
|
||||
| `POST /api/updateApi` | Same required fields; private/Casbin + record. | Finds target, rejects duplicate changed path/method, updates matching policies first, then API metadata. | **OK**. Local `internal/server/handler/api.go:61-75`, `internal/data/api.go:70-94`; GVA `server/api/v1/system/sys_api.go:242-269`, `server/service/system/sys_api.go:278-304`. |
|
||||
| `POST /api/deleteApi` | JSON nonzero ID; private/Casbin + record. | Requires existing row then deletes it and matching policy/legacy relation rows. | **OK**. Local `internal/server/handler/api.go:77-91`, `internal/biz/api.go:39-47`, `internal/data/api.go:95-120`; GVA `server/api/v1/system/sys_api.go:147-175`, `server/service/system/sys_api.go:165-177`. |
|
||||
| `DELETE /api/deleteApisByIds` | JSON ID slice; private/Casbin + record. | Transaction deletes selected rows and matching policies; empty/missing ID behavior follows GVA bulk semantics. | **OK**. Local `internal/server/handler/api.go:93-103`, `internal/data/api.go:95-120`; GVA `server/api/v1/system/sys_api.go:291-312`, `server/service/system/sys_api.go:312-332`. |
|
||||
| `POST /api/getApiById` | JSON nonzero ID; private/Casbin + record. | Finds one API and wraps it as `{api}`. | **OK**. Local `internal/server/handler/api.go:105-120`; GVA `server/api/v1/system/sys_api.go:212-240`. |
|
||||
| `GET /api/getApiGroups` | No body; private/Casbin + record. | Natural-ID scan produces first-seen group list and path-segment map. | **OK**. Local `internal/server/handler/api.go:122-128`, `internal/service/api.go:64-86`; GVA `server/api/v1/system/sys_api.go:77-96`, `server/service/system/sys_api.go:35-56`. |
|
||||
| `GET /api/getApiRoles` | Query path and method required; private/Casbin. | Reads role IDs from `p` policies; nil becomes `[]`. | **OK**. Local `internal/server/handler/api.go:130-144`, `internal/data/api.go:182-201`; GVA `server/api/v1/system/sys_api.go:334-360`, `server/service/system/sys_casbin.go:169-184`. |
|
||||
| `POST /api/setApiRoles` | JSON path/method required; private/Casbin + record. | Transaction removes all matching policy rows then inserts the requested roles; GVA cache reload is unnecessary locally because policy is DB-read. | **OK**. Local `internal/server/handler/api.go:146-161`, `internal/data/api.go:202-222`; GVA `server/api/v1/system/sys_api.go:362-389`, `server/service/system/sys_casbin.go:186-206`. |
|
||||
| `GET /api/syncApi` | No body; private/Casbin + record. | Compares engine routes to API rows and ignore rows; returns all arrays as `newApis/deleteApis/ignoreApis`. | **OK**. Local `internal/server/handler/api.go:163-178`, `internal/biz/api.go:50-95`; GVA `server/api/v1/system/sys_api.go:55-75`, `server/service/system/sys_api.go:58-130`. |
|
||||
| `POST /api/ignoreApi` | JSON path/method/flag; private/Casbin + record. | Inserts duplicate ignore row when flag true; permanently deletes all matching ignore rows when false. | **OK**. Local `internal/server/handler/api.go:180-191`, `internal/data/api.go:335-350`; GVA `server/api/v1/system/sys_api.go:98-120`, `server/service/system/sys_api.go:132-137`. |
|
||||
| `POST /api/enterSyncApi` | JSON submitted added/deleted arrays; private/Casbin + record. | Transaction inserts submitted additions and deletes matching APIs/policies. | **OK**. Local `internal/server/handler/api.go:193-199`, `internal/data/api.go:351-384`; GVA `server/api/v1/system/sys_api.go:122-145`, `server/service/system/sys_api.go:139-157`. |
|
||||
| `GET /api/freshCasbin` | Public, no body. | GVA reloads cache; local is a DB-policy no-op. | **P1**; see finding. Local `internal/server/handler/api.go:201-206`; GVA `server/api/v1/system/sys_api.go:314-329`. |
|
||||
| `POST /casbin/updateCasbin` | JSON nonzero authority ID; private/Casbin + record. | Enforces strict target and policy scope, clears old authority policies, deduplicates and adds replacement paths. | **OK**. Local `internal/server/handler/api.go:208-222`, `internal/data/api.go:261-334`; GVA `server/api/v1/system/sys_casbin.go:23-52`, `server/service/system/sys_casbin.go:29-89`. |
|
||||
| `POST /casbin/getPolicyPathByAuthorityId` | JSON nonzero authority ID; private/Casbin. | Returns `{paths}` with path/method policy pairs. Local DB query avoids GVA cache staleness in normal endpoint flows. | **OK**. Local `internal/server/handler/api.go:224-239`, `internal/data/api.go:238-260`; GVA `server/api/v1/system/sys_casbin.go:54-79`, `server/service/system/sys_casbin.go:121-140`. |
|
||||
|
||||
### Authority button permissions
|
||||
|
||||
Route evidence: local `internal/server/router/permission.go:9-14`; GVA `server/router/system/sys_authority_btn.go:11-19`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /authorityBtn/getAuthorityBtn` | JSON authority/menu IDs; private/Casbin. | Loads selected base-button IDs and returns `{selected}`. | **OK**. Local `internal/server/handler/permission.go:17-29`, `internal/data/permission.go:58-75`; GVA `server/api/v1/system/sys_authority_btn.go:21-44`, `server/service/system/sys_authority_btn.go:18-34`. |
|
||||
| `POST /authorityBtn/setAuthorityBtn` | JSON authority/menu IDs and selection; private/Casbin. | Transaction deletes current selection for that authority/menu and inserts submitted IDs. | **OK**. Local `internal/server/handler/permission.go:31-42`, `internal/data/permission.go:76-90`; GVA `server/api/v1/system/sys_authority_btn.go:46-68`, `server/service/system/sys_authority_btn.go:36-63`. |
|
||||
| `POST /authorityBtn/canRemoveAuthorityBtn` | Query `id`; private/Casbin. | Succeeds only when no authority-button reference exists; same failure message. | **OK**. Local `internal/server/handler/permission.go:44-55`, `internal/data/permission.go:91-100`; GVA `server/api/v1/system/sys_authority_btn.go:70-85`, `server/service/system/sys_authority_btn.go:65-74`. |
|
||||
|
||||
### Departments and positions
|
||||
|
||||
Route evidence: local `internal/server/router/organization.go:9-29`; GVA `server/router/system/sys_department.go:10-24`, `server/router/system/sys_position.go:10-24`.
|
||||
|
||||
| Endpoint | Binding, validation, authorization | Core logic / transaction / response | Result and handler evidence |
|
||||
|---|---|---|---|
|
||||
| `POST /department/getDepartmentList` | JSON search is deliberately best-effort; private/Casbin. | Name search returns flat list; no search builds sorted recursive tree with leaders. | **OK**. Local `internal/server/handler/organization.go:20-28`, `internal/data/department.go:173-219`; GVA `server/api/v1/system/sys_department.go:100-119`, `server/service/system/sys_department.go:151-187`. |
|
||||
| `POST /department/createDepartment` | JSON name required; private/Casbin + record. | Resolves parent, derives ancestors (`0` root), and inserts department. | **OK**. Local `internal/server/handler/organization.go:30-44`, `internal/data/department.go:85-100`; GVA `server/api/v1/system/sys_department.go:23-48`, `server/service/system/sys_department.go:88-100`. |
|
||||
| `PUT /department/updateDepartment` | JSON nonzero ID; private/Casbin + record. | Rejects self-parent, validates parent, recomputes this node's ancestors only. | **OK**. Local `internal/server/handler/organization.go:46-60`, `internal/data/department.go:101-120`; GVA `server/api/v1/system/sys_department.go:50-75`, `server/service/system/sys_department.go:102-121`. |
|
||||
| `DELETE /department/deleteDepartment` | JSON ID; private/Casbin + record. | Blocks children, primary-department users and join-table users before deleting. | **OK**. Local `internal/server/handler/organization.go:62-72`, `internal/data/department.go:121-149`; GVA `server/api/v1/system/sys_department.go:77-98`, `server/service/system/sys_department.go:123-149`. |
|
||||
| `GET /department/findDepartment` | Query ID; private/Casbin. | Loads one department with leader. | **OK**. Local `internal/server/handler/organization.go:74-85`, `internal/data/department.go:150-172`; GVA `server/api/v1/system/sys_department.go:121-142`, `server/service/system/sys_department.go:151-153`. |
|
||||
| `GET /department/getDepartmentUsers` | Query department ID; private/Casbin. | Reads join-table user IDs; nil normalizes to `[]`. | **OK**. Local `internal/server/handler/organization.go:87-101`, `internal/data/department.go:220-225`; GVA `server/api/v1/system/sys_department.go:144-169`, `server/service/system/sys_department.go:190-195`. |
|
||||
| `POST /department/setDepartmentUsers` | JSON nonzero department ID; private/Casbin + record. | Transaction replaces membership; clears removed primary department and sets the department for additions without one. | **OK**. Local `internal/server/handler/organization.go:103-117`, `internal/data/department.go:226-241`; GVA `server/api/v1/system/sys_department.go:171-194`, `server/service/system/sys_department.go:197-243`. |
|
||||
| `POST /position/getPositionList` | JSON search; no page validation by either implementation; private/Casbin. | Same name/code/status filters, `sort` ordering, raw supplied limit/offset and `PageResult`. | **OK**. Local `internal/server/handler/organization.go:136-147`, `internal/data/position.go:77-111`; GVA `server/api/v1/system/sys_position.go:100-127`, `server/service/system/sys_position.go:76-101`. |
|
||||
| `POST /position/createPosition` | JSON name required; private/Casbin + record. | Inserts name/code/sort/status/remark. | **OK**. Local `internal/server/handler/organization.go:149-163`, `internal/data/position.go:51-53`; GVA `server/api/v1/system/sys_position.go:23-48`, `server/service/system/sys_position.go:19-22`. |
|
||||
| `PUT /position/updatePosition` | JSON nonzero ID; private/Casbin + record. | Updates name/code/sort/status/remark. | **OK**. Local `internal/server/handler/organization.go:165-179`, `internal/data/position.go:54-56`; GVA `server/api/v1/system/sys_position.go:50-75`, `server/service/system/sys_position.go:24-36`. |
|
||||
| `DELETE /position/deletePosition` | JSON ID; private/Casbin + record. | Blocks existing user-position links, then soft-deletes. | **OK**. Local `internal/server/handler/organization.go:181-191`, `internal/data/position.go:57-70`; GVA `server/api/v1/system/sys_position.go:77-98`, `server/service/system/sys_position.go:38-51`. |
|
||||
| `GET /position/findPosition` | Query ID; private/Casbin. | Loads one position. | **OK**. Local `internal/server/handler/organization.go:193-204`, `internal/data/position.go:71-76`; GVA `server/api/v1/system/sys_position.go:129-150`, `server/service/system/sys_position.go:53-57`. |
|
||||
| `GET /position/getPositionUsers` | Query position ID; private/Casbin. | Reads user IDs and normalizes nil to `[]`. | **OK**. Local `internal/server/handler/organization.go:206-220`, `internal/data/position.go:112-117`; GVA `server/api/v1/system/sys_position.go:152-177`, `server/service/system/sys_position.go:103-108`. |
|
||||
| `POST /position/setPositionUsers` | JSON nonzero position ID; private/Casbin + record. | Transaction replaces all links for target position. | **OK**. Local `internal/server/handler/organization.go:222-236`, `internal/data/position.go:118-132`; GVA `server/api/v1/system/sys_position.go:179-202`, `server/service/system/sys_position.go:110-130`. |
|
||||
|
||||
## Recommended validation plan
|
||||
|
||||
1. **Contract tests**: table-drive every route above for method/path, envelope, success message, and required-field failure. Add the two response-header cases and public `freshCasbin` behavior.
|
||||
2. **Shared-cache migration tests**: run both binaries against one Redis/cache instance and verify captcha/login rate limits, captcha values, multipoint session keys, and JWT blacklist behavior across the boundary.
|
||||
3. **Authority/Casbin transaction tests**: duplicate policies, strict non-root role assignment, copy authority, update API path/method, sync deletion, and role/menu/button replacement with injected write failures.
|
||||
4. **Organization/data-scope tests**: multi-department primary reassignment, department move ancestors, scope 1/2/4/5 read/write filtering, and blocked-write audit events.
|
||||
5. **Manual smoke suite**: login -> switch role -> load menus -> assign API/menu/button policies -> refresh/verify authorization -> logout -> assert old token rejection.
|
||||
|
||||
## Conclusion
|
||||
|
||||
All audited migrated identity/access endpoints match GVA's route, validation, permission, data mutation, transaction, and JSON-envelope behavior under normal operation. The scoped identity/access findings are: the P2 rate-limit cache prefix, the P2 version response-header name, the P1 `freshCasbin` failure-semantics difference, and the P3 safer handling of an empty `authorityIds` array. Cross-cutting route-table/static-file, CORS, JWT-store, and router-prefix synchronization findings are tracked in `parity-crosscut.md` rather than duplicated here.
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
# GVA Operations and Content API Parity Audit
|
||||
|
||||
## Scope and method
|
||||
|
||||
- **Reference:** official GVA working tree `C:\Users\Yvan\AppData\Local\Temp\gva-parity-02f3783`, commit `02f37833255e0e339c3d69199cb5a468f17de9fc`.
|
||||
- **Local target:** this repository's Gin compatibility surface. The global private/public placement and the compatibility operation-record selection are established in `internal/server/gin.go:33-48` and `internal/server/middleware/audit.go:137-178`.
|
||||
- **Checked for each endpoint:** method/path, public vs private placement, operation-record placement, request binding/defaults, business side effects, and response/error envelope. Both applications use a `200` JSON response envelope for business failures (`internal/server/httpx/response.go:28-36`); this audit treats message-only wording differences as compatible unless the control flow or payload differs.
|
||||
- **Deliberately excluded:** GVA auto-code, AI, skills, example, plugin-management and other modules that have not been migrated. This includes the GVA error-log action which delegates to `AutoCodeService.LLMAuto`; it is listed below only so the route inventory is complete, not as a required migration gap.
|
||||
|
||||
`OK` means no material mismatch was found in this read-only audit. `EXCLUDED` is a GVA route intentionally outside the migration scope. The route evidence in the tables is the local router shown first, then the GVA router.
|
||||
|
||||
## Findings
|
||||
|
||||
### Excluded - Error-solution endpoint is intentionally out of scope
|
||||
|
||||
`GET /sysError/getSysErrorSolution?id=<id>` is registered as a private, operation-recorded GVA route in `server/router/system/sys_error.go:15-24`, seeded in both its API and Casbin sources (`server/source/system/api.go:221-225`, `server/source/system/casbin.go:294-298`), and consumed by the GVA UI (`web/src/api/system/sysError.js:120-126`, `web/src/view/systemTools/sysError/sysError.vue:290-293`). It validates `id`, changes the error to `处理中`, starts an asynchronous LLM solution task through `AutoCodeService.LLMAuto`, and returns `已提交至AI处理` (`server/api/v1/system/sys_error.go:179-197`; delegation and state transitions at `server/service/system/sys_error.go:83-129`).
|
||||
|
||||
The local error router exposes only delete/update/find/list/create (`internal/server/router/audit.go:27-33`) and neither `internal/server/handler/audit.go` nor `internal/server/middleware/audit.go:167-171` defines the action/audit selection. The local client module likewise ends after `getSysErrorList` (`web/src/api/system/sysError.js:82-96`), while GVA exports the action at `web/src/api/system/sysError.js:120-126`. This is an expected absence under the user's explicit exclusion of AI/code-generation functionality. It does not count as a parity defect for the scoped migration; do not add its route, Casbin seed, client action, asynchronous state transition, or LLM integration unless that exclusion changes.
|
||||
|
||||
### Cross-cutting audit behavior is aligned
|
||||
|
||||
The selected mutation set matches GVA's per-router `OperationRecord()` use: local `operationRoutes` includes the dictionary, parameter, config, token, export, version, error, login, data-access, timed-task, announcement and email mutations (`internal/server/middleware/audit.go:161-171`). Both implementations reuse the access-log capture, mask the same sensitive JSON fields, preserve the multipart/download truncation markers, and record request/trace/device metadata (`internal/server/middleware/audit.go:27-103`; GVA `server/middleware/access_log.go:43-112`, `server/middleware/operation.go:20-102`). No parity difference was found at this boundary; keep it as a regression-test seam rather than a finding.
|
||||
|
||||
## Per-module endpoint review
|
||||
|
||||
### Dictionary and dictionary detail
|
||||
|
||||
Route sets match GVA (`internal/server/router/dictionary.go:9-29`; GVA `server/router/system/sys_dictionary.go:14-23`, `sys_dictionary_detail.go:14-24`). Local handlers bind the same JSON/query shapes and return the same compatibility wrapper keys, including `resysDictionary`, `reSysDictionaryDetail`, `list`, and `path` (`internal/server/handler/dictionary.go:20-266`). The data layer keeps GVA's `name OR type` search, unpaged dictionary-list behavior, detail filters and `sort,id` ordering (`internal/data/dictionary.go:218-282`, `370-400`); GVA equivalents are `server/service/system/sys_dictionary.go:122-149` and `sys_dictionary_detail.go:180-209`.
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /sysDictionary/createSysDictionary` | OK | JSON dictionary; uniqueness/create; detailed created response. |
|
||||
| `PUT /sysDictionary/updateSysDictionary` | OK | JSON dictionary; update response. |
|
||||
| `DELETE /sysDictionary/deleteSysDictionary` | OK | JSON dictionary ID; deletion side effects retained. |
|
||||
| `GET /sysDictionary/findSysDictionary` | OK | Query dictionary; enabled lookup; `{resysDictionary: ...}`. |
|
||||
| `GET /sysDictionary/getSysDictionaryList` | OK | `name` searches name/type; unpaged list. |
|
||||
| `GET /sysDictionary/getSysDictionaryListWithDetails` | OK | `name` searches name/type; details preloaded and sorted. |
|
||||
| `GET /sysDictionary/exportSysDictionary` | OK | Query `ID` required; export shape retained. |
|
||||
| `POST /sysDictionary/importSysDictionary` | OK | JSON `{json}` import and failure propagation. |
|
||||
| `POST /sysDictionaryDetail/createSysDictionaryDetail` | OK | JSON detail; hierarchy metadata calculated. |
|
||||
| `PUT /sysDictionaryDetail/updateSysDictionaryDetail` | OK | JSON detail; hierarchy update/cycle validation retained. |
|
||||
| `DELETE /sysDictionaryDetail/deleteSysDictionaryDetail` | OK | JSON detail ID; parent-with-children rejection retained. |
|
||||
| `GET /sysDictionaryDetail/findSysDictionaryDetail` | OK | Query `ID`; `{reSysDictionaryDetail: ...}`. |
|
||||
| `GET /sysDictionaryDetail/getSysDictionaryDetailList` | OK | Query page/filter fields `label,value,status,sysDictionaryID,parentID,level`; `sort,id` ordering. |
|
||||
| `GET /sysDictionaryDetail/getDictionaryTreeList` | OK | Query `sysDictionaryID` required; root tree with children. |
|
||||
| `GET /sysDictionaryDetail/getDictionaryTreeListByType` | OK | Query `type` required; type-resolved root tree. |
|
||||
| `GET /sysDictionaryDetail/getDictionaryDetailsByParent` | OK | `sysDictionaryID` required, optional `parentID`/`includeChildren`. |
|
||||
| `GET /sysDictionaryDetail/getDictionaryPath` | OK | Query `id` required; ancestor path. |
|
||||
|
||||
### System parameters
|
||||
|
||||
Route, binding and result shapes match (`internal/server/router/parameter.go:9-19`; GVA `server/router/system/sys_params.go:15-23`; local handler `internal/server/handler/parameter.go:19-92`; GVA API `server/api/v1/system/sys_params.go:22-169`). `name`, `key`, created-time window and page fields reach the local repo in the same way (`internal/service/parameter.go:20-64`, `internal/data/parameter.go:63-90`).
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /sysParams/createSysParams` | OK | Required JSON `name,key,value`; create. |
|
||||
| `PUT /sysParams/updateSysParams` | OK | Required JSON `name,key,value`; struct-update behavior retained. |
|
||||
| `DELETE /sysParams/deleteSysParams` | OK | Query `ID`; delete one. |
|
||||
| `DELETE /sysParams/deleteSysParamsByIds` | OK | Query `IDs[]`; batch delete. |
|
||||
| `GET /sysParams/findSysParams` | OK | Query `ID`; one parameter. |
|
||||
| `GET /sysParams/getSysParam` | OK | Query `key`; one parameter. |
|
||||
| `GET /sysParams/getSysParamsList` | OK | Query page/name/key/created-time window; page result. |
|
||||
|
||||
### Security config, system config/info/reload, and initialization
|
||||
|
||||
The route split and mutation audit placement match GVA: local `internal/server/router/system_config.go:9-21` and `router/public.go:9-16`; GVA `server/router/system/sys_security_config.go:15-18`, `sys_system.go:15-20`, `sys_initdb.go:12-13`. The system config read already returns the required `{config: ...}` payload because `ConfigurationJSON()` constructs that envelope (`internal/data/config_management.go:30-117`), matching GVA `SysConfigResponse` (`server/model/system/response/sys_system.go:5-7`). Local reload also reloads the task runtime (`internal/biz/system_init.go:66-73`), preserving GVA-visible reload behavior while avoiding a process restart.
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `GET /securityConfig/getSecurityConfig` | OK | Private read; security config response fields. |
|
||||
| `POST /securityConfig/setSecurityConfig` | OK | Private JSON update; saved config returned. |
|
||||
| `POST /system/getSystemConfig` | OK | Private read; `{config: ...}` wrapper and secret masking. |
|
||||
| `POST /system/setSystemConfig` | OK | Private JSON `config`; persistent config update. |
|
||||
| `POST /system/reloadSystem` | OK | Private mutation; reloads runtime/task config. |
|
||||
| `POST /system/getServerInfo` | OK | Private server information under `server`. |
|
||||
| `POST /init/checkdb` | OK | Public `needInit` response and compatible message. |
|
||||
| `POST /init/initdb` | OK | Public only while uninitialized; validates DB input and seeds active Gin routes. |
|
||||
|
||||
### API token, export templates, and versions
|
||||
|
||||
Paths and methods match (`internal/server/router/api_token.go:9-15`, `export.go:9-23`, `version.go:9-18`; GVA `sys_api_token.go:15-17`, `sys_export_template.go:18-33`, `sys_version.go:15-23`). API-token revocation also blacklists the persisted JWT before disabling it (`internal/data/api_token.go:123-129`), matching GVA `server/service/system/sys_api_token.go:93-106`. Export token issuance/consumption and public downloads are present in the local handler (`internal/server/handler/export.go:144-225`), and version import/export preserves staged errors (`internal/server/handler/version.go:85-151`, `internal/service/version.go:185-267`).
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /sysApiToken/createApiToken` | OK | JSON `userId,authorityId,days,remark`; authority check/JWT issuance. |
|
||||
| `POST /sysApiToken/getApiTokenList` | OK | JSON page/user/status filters; newest-first page result. |
|
||||
| `POST /sysApiToken/deleteApiToken` | OK | JSON ID; blacklist then mark disabled. |
|
||||
| `POST /sysExportTemplate/createSysExportTemplate` | OK | JSON template; name required/create. |
|
||||
| `PUT /sysExportTemplate/updateSysExportTemplate` | OK | JSON template; name required/update. |
|
||||
| `DELETE /sysExportTemplate/deleteSysExportTemplate` | OK | JSON ID; empty-ID failure preserved. |
|
||||
| `DELETE /sysExportTemplate/deleteSysExportTemplateByIds` | OK | JSON IDs; batch delete. |
|
||||
| `GET /sysExportTemplate/findSysExportTemplate` | OK | Query ID; `resysExportTemplate` key. |
|
||||
| `GET /sysExportTemplate/getSysExportTemplateList` | OK | Query filters/page; page result. |
|
||||
| `GET /sysExportTemplate/previewSQL` | OK | `templateID` required plus encoded `params`; SQL preview. |
|
||||
| `GET /sysExportTemplate/exportExcel` | OK | `templateID` plus params; one-time download-token URL. |
|
||||
| `GET /sysExportTemplate/exportTemplate` | OK | Blank-template token URL. |
|
||||
| `POST /sysExportTemplate/importExcel` | OK | Multipart file plus query `templateID`; import. |
|
||||
| `GET /sysExportTemplate/exportExcelByToken` | OK | Public token consumption; XLSX download. |
|
||||
| `GET /sysExportTemplate/exportTemplateByToken` | OK | Public blank-template token consumption; XLSX download. |
|
||||
| `DELETE /sysVersion/deleteSysVersion` | OK | Query `ID`; delete. |
|
||||
| `DELETE /sysVersion/deleteSysVersionByIds` | OK | Query `IDs[]`; batch delete. |
|
||||
| `GET /sysVersion/findSysVersion` | OK | Query `ID`; version record. |
|
||||
| `GET /sysVersion/getSysVersionList` | OK | Query page/name/code/date window; page result. |
|
||||
| `POST /sysVersion/exportVersion` | OK | Required JSON version name/code and selected menu/API/dictionary data. |
|
||||
| `GET /sysVersion/downloadVersionJson` | OK | Query ID; attachment JSON stream. |
|
||||
| `POST /sysVersion/importVersion` | OK | JSON bundle; staged menu/API/dictionary import errors. |
|
||||
|
||||
### Operation/login/data-access/error/file logs
|
||||
|
||||
The non-AI endpoints and their public/private placement match local `internal/server/router/audit.go:9-33` with GVA `sys_operation_record.go:12-15`, `sys_login_log.go:16-21`, `sys_data_access_log.go:14-17`, `sys_error.go:16-26`, and `sys_log_viewer.go:10-12`. Local log-viewer validation rejects malformed month/date/path before storage access (`internal/server/handler/audit.go:199-271`) and passes the expected errors. Error, operation and login handler bindings are in `internal/server/handler/audit.go:67-343`.
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `GET /sysOperationRecord/getSysOperationRecordList` | OK | Query page/path/method/status; newest-first page result. |
|
||||
| `GET /sysOperationRecord/findSysOperationRecord` | OK | Query ID; `reSysOperationRecord` wrapper. |
|
||||
| `DELETE /sysOperationRecord/deleteSysOperationRecord` | OK | JSON ID; delete one. |
|
||||
| `DELETE /sysOperationRecord/deleteSysOperationRecordByIds` | OK | JSON IDs; batch delete. |
|
||||
| `GET /sysLoginLog/getLoginLogList` | OK | Query page/username/status; newest-first page result. |
|
||||
| `GET /sysLoginLog/findLoginLog` | OK | Query ID; login record. |
|
||||
| `DELETE /sysLoginLog/deleteLoginLog` | OK | JSON ID; delete one. |
|
||||
| `DELETE /sysLoginLog/deleteLoginLogByIds` | OK | JSON IDs; batch delete. |
|
||||
| `POST /dataAccessLog/getDataAccessLogList` | OK | JSON page/eventType/targetTable; page result. |
|
||||
| `DELETE /dataAccessLog/deleteDataAccessLogByIds` | OK | JSON IDs; batch delete. |
|
||||
| `GET /logViewer/dates` | OK | Required query `month`; log-date listing. |
|
||||
| `GET /logViewer/files` | OK | Required query `date`; file listing. |
|
||||
| `GET /logViewer/content` | OK | Required `date,path`, optional cursor; safe content page. |
|
||||
| `POST /sysError/createSysError` | OK | Public JSON error record creation. |
|
||||
| `DELETE /sysError/deleteSysError` | OK | Query `ID`; delete one. |
|
||||
| `DELETE /sysError/deleteSysErrorByIds` | OK | Query `IDs[]`; batch delete. |
|
||||
| `PUT /sysError/updateSysError` | OK | JSON error record update. |
|
||||
| `GET /sysError/findSysError` | OK | Query `ID`; error record. |
|
||||
| `GET /sysError/getSysErrorList` | OK | Query page/form/info/date range; page result. |
|
||||
| `GET /sysError/getSysErrorSolution` | EXCLUDED | GVA private async AI solution submit through `AutoCodeService.LLMAuto`; outside the requested migration scope. |
|
||||
|
||||
### Timed task and SSE
|
||||
|
||||
All nine routes match (`internal/server/router/task.go:9-20`; GVA `server/router/system/sys_timed_task.go:15-26`). Local mutation handlers bind the same task/ID payloads (`internal/server/handler/task.go:24-120`) and drive durable task changes plus scheduler schedule/remove actions (`internal/biz/task.go:157-193`). The local SSE handler has the GVA-required long-lived headers, initial comment, task alert event, heartbeat and deadline removal (`internal/server/handler/task.go:122-161`); GVA calls out the timeout exception at `server/router/system/sys_timed_task.go:24-26`.
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /timedTask/createTimedTask` | OK | JSON task; validate, persist and schedule. |
|
||||
| `PUT /timedTask/updateTimedTask` | OK | JSON task; validate, persist and reschedule. |
|
||||
| `DELETE /timedTask/deleteTimedTask` | OK | JSON required ID; remove schedule then delete. |
|
||||
| `POST /timedTask/toggleTimedTask` | OK | JSON ID/enabled; persist toggle then reschedule. |
|
||||
| `POST /timedTask/triggerTimedTask` | OK | JSON ID; manual asynchronous trigger. |
|
||||
| `GET /timedTask/getTimedTaskList` | OK | Query page/name/executorType/enabled; next-run metadata. |
|
||||
| `GET /timedTask/getTimedTaskLogList` | OK | Query page/taskId/status; page result. |
|
||||
| `GET /timedTask/getRegisteredMethods` | OK | Registered method catalog under `methods`. |
|
||||
| `GET /timedTask/alertStream` | OK | Authenticated SSE `timedTask:alert` stream with heartbeat. |
|
||||
|
||||
### Media uploads, attachment categories, and chunked upload
|
||||
|
||||
Route sets match local `internal/server/router/media.go:9-30` and GVA `server/router/media/media_file_upload_and_download.go:12-19`, `media_attachment_category.go:12-14`, `media_upload.go:10-13`. Local handler bindings follow the GVA APIs (`internal/server/handler/media.go:19-244`; GVA `server/api/v1/media/media_file_upload_download.go:25-216`, `media_upload.go:26-116`). The storage/repository paths retain extension validation, key reference counting on delete, partial batch-delete reporting, attachment-category tree behavior, ownership checks, chunk MD5/full-file MD5, atomic merge claim and cleanup (`internal/biz/media.go:51-170`, `internal/biz/media_upload.go:36-184`, `internal/data/media.go:128-203`).
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /fileUploadAndDownload/upload` | OK | Multipart `file`, `classId`, query `noSave`; upload/create response `file`. |
|
||||
| `POST /fileUploadAndDownload/getFileList` | OK | JSON page/keyword/classId/tag/user/time/order filters. |
|
||||
| `POST /fileUploadAndDownload/deleteFile` | OK | JSON ID; storage delete only for final key reference. |
|
||||
| `POST /fileUploadAndDownload/deleteFiles` | OK | JSON IDs; partial success reports `failedIds`. |
|
||||
| `GET /fileUploadAndDownload/findFile` | OK | Query `id`; file response. |
|
||||
| `POST /fileUploadAndDownload/editFileName` | OK | JSON ID/name; existence check then rename. |
|
||||
| `POST /fileUploadAndDownload/importURL` | OK | JSON file array; batch insert. |
|
||||
| `POST /fileUploadAndDownload/listOssFiles` | OK | JSON prefix/cursor/limit; list/nextCursor/hasMore. |
|
||||
| `GET /attachmentCategory/getCategoryList` | OK | Category tree rooted at PID 0. |
|
||||
| `POST /attachmentCategory/addCategory` | OK | JSON category; sibling uniqueness plus create/update. |
|
||||
| `POST /attachmentCategory/deleteCategory` | OK | JSON ID required; reject non-leaf category. |
|
||||
| `POST /mediaUpload/init` | OK | JSON init; max size, instant/resume probing. |
|
||||
| `POST /mediaUpload/chunk` | OK | Multipart `uploadId,chunkIndex,chunkHash,chunk`; owner/status/MD5 checks. |
|
||||
| `POST /mediaUpload/complete` | OK | JSON `uploadId`; atomic merge, full hash check, media registration. |
|
||||
| `DELETE /mediaUpload/:uploadId` | OK | Path ID; owner-only session/chunk cleanup. |
|
||||
|
||||
### Announcement/info and email
|
||||
|
||||
Announcement route placement matches GVA: private mutations/reads and public datasource/public message (`internal/server/router/announcement.go:9-24`; GVA `server/plugin/announcement/router/info.go:16-29`). The local public response intentionally mirrors GVA's fixed compatibility message (`internal/server/handler/announcement.go:97-99`; GVA `server/plugin/announcement/api/info.go:171-181`). Announcement CRUD/time-range/data-source semantics are retained (`internal/server/handler/announcement.go:23-95`, `internal/data/announcement.go:38-98`; GVA `server/plugin/announcement/service/info.go:15-76`). Email paths, JSON binding and send/test behavior match (`internal/server/router/email.go:9-13`, `internal/server/handler/email.go:15-34`; GVA `server/plugin/email/router/sys_email.go:16-17`, `api/sys_email.go:20-51`).
|
||||
|
||||
| Endpoint | Status | Binding / effect check |
|
||||
| --- | --- | --- |
|
||||
| `POST /info/createInfo` | OK | Private JSON title/content/user/attachments; create. |
|
||||
| `DELETE /info/deleteInfo` | OK | Private query `ID`; delete one. |
|
||||
| `DELETE /info/deleteInfoByIds` | OK | Private query `IDs[]`; batch delete. |
|
||||
| `PUT /info/updateInfo` | OK | Private JSON record; update. |
|
||||
| `GET /info/findInfo` | OK | Private query `ID`; record response. |
|
||||
| `GET /info/getInfoList` | OK | Private query page/created-time window; page result. |
|
||||
| `GET /info/getInfoDataSource` | OK | Public `userID` label/value datasource. |
|
||||
| `GET /info/getInfoPublic` | OK | Public fixed compatibility message, not a listing API. |
|
||||
| `POST /email/emailTest` | OK | Private configured-recipient test mail. |
|
||||
| `POST /email/sendEmail` | OK | Private JSON `to,subject,body`; sends mail. |
|
||||
|
||||
## Suggested regression tests
|
||||
|
||||
1. Add a route contract test that enumerates every in-scope table entry above under both no-prefix and a configured `routerPrefix`; omit the documented AI-excluded `GET /sysError/getSysErrorSolution`.
|
||||
2. Add cross-implementation request fixtures for the high-risk shapes: dictionary-detail hierarchy, export token single use/type mismatch, API-token blacklist revoke, media partial delete/chunk ownership/hash, and timed-task SSE headers plus heartbeat/event format.
|
||||
3. Snapshot the response envelope keys for special cases: `resysDictionary`, `reSysDictionaryDetail`, `reSysOperationRecord`, `resysExportTemplate`, system `{config: ...}`, and page `{list,total,page,pageSize}`.
|
||||
4. Test audit persistence separately from API behavior: mutation-route selection, sensitive-field masking, multipart/download truncation, and the shared `requestId/traceId/deviceId` metadata.
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# Kra 分层与结构优化计划
|
||||
|
||||
## 1. 目标与约束
|
||||
|
||||
本轮优化以现有 Kratos 分层契约为准,不改变 GVA 兼容接口的请求、响应、
|
||||
事务和错误语义。目标是收紧 DTO/DO/PO 边界、降低单文件职责密度,并删除
|
||||
确认未接入的代码;不按业务资源重建整套子包,也不创建泛化 `utils`。
|
||||
|
||||
结构决策遵循以下规则:
|
||||
|
||||
1. DTO 及 JSON/form/binding tag 只放在 `internal/service/dto`。
|
||||
2. DO、usecase、repo interface 和领域上下文保留在 `internal/biz`。
|
||||
3. PO、GORM/Casbin/缓存/文件存储等实现保留在 `internal/data`。
|
||||
4. HTTP 绑定、路由和中间件保留在 `internal/server`;DTO -> DO 转换交给 service。
|
||||
5. 只有无业务状态、至少有两个稳定消费者的能力才建立专用 `internal/<name>` 包。
|
||||
6. 同包拆文件优先于新建 Go 子包,避免扩大导出面和破坏 Wire/事务协作。
|
||||
|
||||
## 2. 总体结论
|
||||
|
||||
静态依赖扫描未发现四层之间的硬性反向 import:`biz` 不依赖 data/service/server,
|
||||
`data` 不依赖 service/server/DTO,`service` 不依赖 data/server,`server` 不依赖
|
||||
data。当前架构无需推倒重建,重点应放在文件职责和边界收紧。
|
||||
|
||||
不建议新增 `pkg/utils`。现有 `pkg/adminauth`、`pkg/logging` 已有清晰复用价值;
|
||||
其他应用专属能力应留在 `internal`,防止把内部实现误扩成公共 API。
|
||||
|
||||
## 3. 按模块审查与执行计划
|
||||
|
||||
| 模块 | 当前判断 | 本轮动作 | 后续动作 |
|
||||
| --- | --- | --- | --- |
|
||||
| 登录与会话 | `LoginResult` 是传输响应,却位于 service 根包 | 移入 `service/dto` | `TokenAuthentication/AuthClaims` 仍跨到 middleware,待专门设计 service 视图后再收口 |
|
||||
| 用户 | handler 构造导出的 `service.UserInput`,边界不够明确 | 将输入模型私有化,新增 DTO request 方法 | `data/user.go` 按 read/write/relations 同包拆分 |
|
||||
| API/Casbin | `data/api.go` 混合目录 CRUD、策略和同步 | 同包拆为 `api.go`、`api_policy.go`、`api_sync.go` | 继续保留 `casbin.go` 作为共享持久化辅助,不建 data 子包 |
|
||||
| 初始化 | `DatabaseInit` 带 binding tag,却位于 service 根包 | 移入 `service/dto` | 初始化事务和 seed 保持在 data,不拆独立 package |
|
||||
| 路由目录/Swagger | 路由分组表由 service 和 server 共同使用 | 抽到无状态的 `internal/routeinfo` | 后续评估与 operation-audit 路由目录合并,避免一次性大改 |
|
||||
| 字典 | handler 直接构造 `biz.DictionaryDetailFilter` | 增加 service request 转换入口 | DTO 文件可在自然增长时再按资源拆分 |
|
||||
| 媒体 | handler 直接构造 `biz.MediaFilter` | 增加 service request 转换入口 | `MediaConfig` 的 biz 类型暴露可在配置 DTO 整理时一并处理 |
|
||||
| 审计/导出 | `page`、`IDsFromQuery` 放在 `audit.go`,但被多个 handler 使用 | 移到 `handler/query.go`,删除未引用 `queryUintValue` | 审计错误映射保留在 handler,它属于 HTTP 展示语义 |
|
||||
| 权限/组织 | data 文件较大,但 PO、事务和权限检查高度协作 | 本轮保持行为不动 | `authority.go` 同包拆 CRUD/relations/data-scope;`system.go` 改为更明确的 identity model/constructor 文件 |
|
||||
| 配置/热更新 | compat、merge、store、watch、bootstrap 已形成职责组 | 保持现状 | 仅在单文件继续增长时同包拆分,不创建 `internal/data/config` 子包 |
|
||||
| 系统指标 | service 直接调用 gopsutil,属于基础设施实现 | 本轮不动,避免扩大改动面 | 建立 biz provider interface,由 data 实现指标采集,service 只转 DTO |
|
||||
| 定时任务 | biz 中存在全局可变 registry,worker 负责具体注册 | 本轮不动 | 中期改为向 `TaskUsecase` 注入方法目录;仍不放入 pkg |
|
||||
| 公共包 | `pkg/validate` 无任何调用,未接入 Gin/Kratos 链路 | 删除 `pkg/validate` | `biz/pagination.go` 按仓库分层契约保留,不因当前调用少而删除 |
|
||||
|
||||
## 4. data 层拆分边界
|
||||
|
||||
### 已执行:API/Casbin
|
||||
|
||||
- `api.go`:repo 构造、PO、转换、API CRUD 与列表。
|
||||
- `api_policy.go`:角色关联、授权判断、policy path 查询与替换。
|
||||
- `api_sync.go`:ignore API 与 API registry 同步。
|
||||
- `casbin.go`:继续保存跨 repo 使用的 Casbin rule 和数据库辅助函数。
|
||||
|
||||
这些文件仍为同一个 `package data`,PO 和事务辅助无需导出。
|
||||
|
||||
### 后续:身份与权限
|
||||
|
||||
建议在后续单独变更中完成,避免与接口兼容整改混在一起:
|
||||
|
||||
- `user.go` 拆为读取/装载、写入、用户关系三个文件;
|
||||
- `authority.go` 拆为角色 CRUD/用户关系和 strict-auth/data-scope;
|
||||
- `system.go` 中共享 PO 与 repo 构造器改用能表达 identity 职责的文件名;
|
||||
- 不把 user/authority/menu PO 移到公共 model 包。
|
||||
|
||||
## 5. 测试文件处理
|
||||
|
||||
本轮不按文件数量删除测试。现有测试主要保护 GVA 兼容语义、事务原子性、
|
||||
Casbin/数据权限、安全、迁移、路由契约、日志路径和调度器行为,删除收益小于
|
||||
回归风险。
|
||||
|
||||
可以在后续纯测试整理中做两类无损合并:
|
||||
|
||||
- 将少量 service 转换测试合并为按资源命名的 conversion 测试文件;
|
||||
- 先用覆盖率确认部门树重复断言,再删除真正被更完整用例覆盖的函数。
|
||||
|
||||
`transactions_test.go`、`gin_test.go`、认证/安全、策略/数据权限、日志文件、
|
||||
任务执行器和初始化 ignore 测试必须保留。
|
||||
|
||||
## 6. 验收
|
||||
|
||||
1. `gofmt` 覆盖全部修改的 Go 文件。
|
||||
2. `go test ./...` 使用仓库内可写 `GOCACHE` 执行。
|
||||
3. 结构调整后重新扫描层间 import 和已删除符号引用。
|
||||
4. 任何全量测试失败都区分为本轮回归或已有环境/资源清理问题。
|
||||
5. 不修改生成文件,不改变 Wire provider、路由数量或外部 JSON 字段。
|
||||
|
|
@ -8,10 +8,7 @@ import (
|
|||
|
||||
"kra/internal/biz"
|
||||
|
||||
"github.com/casbin/casbin/v3"
|
||||
casbinmodel "github.com/casbin/casbin/v3/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type apiRepo struct{ data *Data }
|
||||
|
|
@ -181,204 +178,3 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
|
|||
}
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
|
||||
rows := make([]casbinRulePO, 0)
|
||||
err := policyScope(r.data.gormDB.WithContext(ctx)).
|
||||
Where("v1 = ? AND v2 = ?", path, method).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id, err := strconv.ParseUint(row.V0, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// A path/method need not be present in sys_apis when replacing
|
||||
// all matching policy rows. Do not validate or deduplicate authority IDs.
|
||||
if err := deletePoliciesForPath(tx, path, method); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
rules := make([]casbinRulePO, 0, len(ids))
|
||||
for _, aid := range ids {
|
||||
rules = append(rules, newPolicyRule(aid, path, method))
|
||||
}
|
||||
return tx.Create(&rules).Error
|
||||
})
|
||||
}
|
||||
func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
model, err := casbinmodel.NewModelFromString(`[request_definition]
|
||||
r = sub, obj, act
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
[matchers]
|
||||
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
enforcer, err := casbin.NewEnforcer(model)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
subject := strconv.FormatUint(uint64(aid), 10)
|
||||
for _, policy := range rows {
|
||||
if _, err := enforcer.AddPolicy(subject, policy.V1, policy.V2); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return enforcer.Enforce(subject, path, method)
|
||||
}
|
||||
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []*biz.API
|
||||
if len(rows) > 0 {
|
||||
out = make([]*biz.API, 0, len(rows))
|
||||
}
|
||||
for _, row := range rows {
|
||||
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
config := r.data.runtime.Admin()
|
||||
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var registered []apiPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(®istered).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowedSet := make(map[string]bool, len(registered))
|
||||
if authority.ParentID == nil || *authority.ParentID == 0 {
|
||||
for _, item := range registered {
|
||||
allowedSet[item.Path+"\x00"+item.Method] = true
|
||||
}
|
||||
} else {
|
||||
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policySet := make(map[string]bool, len(policies))
|
||||
for _, item := range policies {
|
||||
policySet[item.V1+"\x00"+item.V2] = true
|
||||
}
|
||||
for _, item := range registered {
|
||||
key := item.Path + "\x00" + item.Method
|
||||
if policySet[key] {
|
||||
allowedSet[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range paths {
|
||||
if !allowedSet[item.Path+"\x00"+item.Method] {
|
||||
return errors.New("存在api不在权限列表中")
|
||||
}
|
||||
}
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
// The reference enforcer removes the old authority policies before it
|
||||
// attempts to add the replacement set. Keep that ordering visible even
|
||||
// though Kra reads policies directly from the database rather than through
|
||||
// an in-memory enforcer.
|
||||
if err := deletePoliciesForAuthority(db, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
// Keep the legacy relation table clean for upgraded installations. It is
|
||||
// not consulted for authorization anymore.
|
||||
if err := db.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rules := make([]casbinRulePO, 0, len(paths))
|
||||
seen := make(map[string]struct{})
|
||||
for _, path := range paths {
|
||||
key := strconv.FormatUint(uint64(aid), 10) + path.Path + path.Method
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
rules = append(rules, newPolicyRule(aid, path.Path, path.Method))
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
return db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rules).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
|
||||
var pos []ignoredAPIPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*biz.API, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, &biz.API{Path: po.Path, Method: po.Method})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
|
||||
po := ignoredAPIPO{Path: path, Method: method}
|
||||
if ignored {
|
||||
// The compatible endpoint creates an ignore row on every request (the table has no
|
||||
// path/method uniqueness constraint); retain that observable behavior.
|
||||
return r.data.gormDB.WithContext(ctx).Create(&po).Error
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
|
||||
}
|
||||
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if len(added) > 0 {
|
||||
pos := make([]apiPO, 0, len(added))
|
||||
for _, item := range added {
|
||||
pos = append(pos, apiPO{Path: item.Path, Method: item.Method, Description: item.Description, APIGroup: item.APIGroup})
|
||||
}
|
||||
// Insert the submitted newApis slice as-is. Do not silently
|
||||
// collapse repeated/existing path+method pairs here; sys_apis itself
|
||||
// intentionally has no uniqueness constraint.
|
||||
if err := tx.Create(&pos).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range deleted {
|
||||
var ids []uint
|
||||
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, item.Method).Pluck("id", &ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err := deletePoliciesForPath(tx, item.Path, item.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id IN ?", ids).Delete(&apiPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"github.com/casbin/casbin/v3"
|
||||
casbinmodel "github.com/casbin/casbin/v3/model"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) {
|
||||
rows := make([]casbinRulePO, 0)
|
||||
err := policyScope(r.data.gormDB.WithContext(ctx)).
|
||||
Where("v1 = ? AND v2 = ?", path, method).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
id, err := strconv.ParseUint(row.V0, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// A path/method need not be present in sys_apis when replacing
|
||||
// all matching policy rows. Do not validate or deduplicate authority IDs.
|
||||
if err := deletePoliciesForPath(tx, path, method); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
rules := make([]casbinRulePO, 0, len(ids))
|
||||
for _, aid := range ids {
|
||||
rules = append(rules, newPolicyRule(aid, path, method))
|
||||
}
|
||||
return tx.Create(&rules).Error
|
||||
})
|
||||
}
|
||||
func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
model, err := casbinmodel.NewModelFromString(`[request_definition]
|
||||
r = sub, obj, act
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
[matchers]
|
||||
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
enforcer, err := casbin.NewEnforcer(model)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
subject := strconv.FormatUint(uint64(aid), 10)
|
||||
for _, policy := range rows {
|
||||
if _, err := enforcer.AddPolicy(subject, policy.V1, policy.V2); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return enforcer.Enforce(subject, path, method)
|
||||
}
|
||||
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
|
||||
rows, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), aid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []*biz.API
|
||||
if len(rows) > 0 {
|
||||
out = make([]*biz.API, 0, len(rows))
|
||||
}
|
||||
for _, row := range rows {
|
||||
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
|
||||
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
config := r.data.runtime.Admin()
|
||||
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
|
||||
var authority authorityPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var registered []apiPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(®istered).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
allowedSet := make(map[string]bool, len(registered))
|
||||
if authority.ParentID == nil || *authority.ParentID == 0 {
|
||||
for _, item := range registered {
|
||||
allowedSet[item.Path+"\x00"+item.Method] = true
|
||||
}
|
||||
} else {
|
||||
policies, err := policyRowsForAuthority(r.data.gormDB.WithContext(ctx), actor.AuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policySet := make(map[string]bool, len(policies))
|
||||
for _, item := range policies {
|
||||
policySet[item.V1+"\x00"+item.V2] = true
|
||||
}
|
||||
for _, item := range registered {
|
||||
key := item.Path + "\x00" + item.Method
|
||||
if policySet[key] {
|
||||
allowedSet[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range paths {
|
||||
if !allowedSet[item.Path+"\x00"+item.Method] {
|
||||
return errors.New("存在api不在权限列表中")
|
||||
}
|
||||
}
|
||||
}
|
||||
db := r.data.gormDB.WithContext(ctx)
|
||||
// The reference enforcer removes the old authority policies before it
|
||||
// attempts to add the replacement set. Keep that ordering visible even
|
||||
// though Kra reads policies directly from the database rather than through
|
||||
// an in-memory enforcer.
|
||||
if err := deletePoliciesForAuthority(db, aid); err != nil {
|
||||
return err
|
||||
}
|
||||
// Keep the legacy relation table clean for upgraded installations. It is
|
||||
// not consulted for authorization anymore.
|
||||
if err := db.Where("authority_id = ?", aid).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rules := make([]casbinRulePO, 0, len(paths))
|
||||
seen := make(map[string]struct{})
|
||||
for _, path := range paths {
|
||||
key := strconv.FormatUint(uint64(aid), 10) + path.Path + path.Method
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
rules = append(rules, newPolicyRule(aid, path.Path, path.Method))
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
return db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rules).Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
|
||||
var pos []ignoredAPIPO
|
||||
if err := r.data.gormDB.WithContext(ctx).Find(&pos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*biz.API, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, &biz.API{Path: po.Path, Method: po.Method})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error {
|
||||
po := ignoredAPIPO{Path: path, Method: method}
|
||||
if ignored {
|
||||
// The compatible endpoint creates an ignore row on every request (the table has no
|
||||
// path/method uniqueness constraint); retain that observable behavior.
|
||||
return r.data.gormDB.WithContext(ctx).Create(&po).Error
|
||||
}
|
||||
return r.data.gormDB.WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
|
||||
}
|
||||
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
|
||||
return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if len(added) > 0 {
|
||||
pos := make([]apiPO, 0, len(added))
|
||||
for _, item := range added {
|
||||
pos = append(pos, apiPO{Path: item.Path, Method: item.Method, Description: item.Description, APIGroup: item.APIGroup})
|
||||
}
|
||||
// Insert the submitted newApis slice as-is. Do not silently
|
||||
// collapse repeated/existing path+method pairs here; sys_apis itself
|
||||
// intentionally has no uniqueness constraint.
|
||||
if err := tx.Create(&pos).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range deleted {
|
||||
var ids []uint
|
||||
if err := tx.Model(&apiPO{}).Where("path = ? AND method = ?", item.Path, item.Method).Pluck("id", &ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err := deletePoliciesForPath(tx, item.Path, item.Method); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("id IN ?", ids).Delete(&apiPO{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ func TestLogViewerReadsNestedCategoryFiles(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 1 || files[0].Path != filepath.Join("http", "access.log") {
|
||||
if len(files) != 1 || files[0].Path != "http/access.log" {
|
||||
t.Fatalf("unexpected nested log files: %+v", files)
|
||||
}
|
||||
content, err := repo.LogContent(context.Background(), date, "http/access.log", nil)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package service
|
||||
package routeinfo
|
||||
|
||||
import "strings"
|
||||
|
||||
|
|
@ -171,15 +171,18 @@ var apiMetadata = map[string]apiMetadataValue{
|
|||
"PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息"},
|
||||
}
|
||||
|
||||
func routeMetadata(method, path string) (string, string) {
|
||||
// Metadata returns the administration group and description for a route.
|
||||
func Metadata(method, path string) (string, string) {
|
||||
if value, ok := apiMetadata[strings.ToUpper(method)+" "+path]; ok {
|
||||
return value.group, value.description
|
||||
}
|
||||
return routeGroup(path), ""
|
||||
}
|
||||
|
||||
// RouteMetadata exposes the administration API grouping and description to
|
||||
// transport-level documentation without leaking the metadata table itself.
|
||||
func RouteMetadata(method, path string) (string, string) {
|
||||
return routeMetadata(method, path)
|
||||
func routeGroup(path string) string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(parts) > 0 && parts[0] != "" {
|
||||
return parts[0]
|
||||
}
|
||||
return "base"
|
||||
}
|
||||
|
|
@ -25,45 +25,6 @@ func NewAudit(service *service.AuditService, recorder *service.AuditRecorder, lo
|
|||
return &Audit{service: service, recorder: recorder, logs: logs, logger: logger}
|
||||
}
|
||||
|
||||
func page(c *gin.Context) (int, int, error) {
|
||||
var value, size int
|
||||
if raw, exists := c.GetQuery("page"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
if raw, exists := c.GetQuery("pageSize"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
size = parsed
|
||||
}
|
||||
return value, size, nil
|
||||
}
|
||||
|
||||
func queryUintValue(c *gin.Context, key string) (uint, bool, error) {
|
||||
raw, ok := c.GetQuery(key)
|
||||
if !ok || raw == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 10, 0)
|
||||
return uint(value), true, err
|
||||
}
|
||||
func IDsFromQuery(c *gin.Context) []uint {
|
||||
values := c.QueryArray("IDs[]")
|
||||
ids := make([]uint, 0, len(values))
|
||||
for _, value := range values {
|
||||
id, _ := strconv.ParseUint(value, 10, 64)
|
||||
if id > 0 {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (h *Audit) Operations(c *gin.Context) {
|
||||
var req dto.OperationRecordSearchRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package handler
|
|||
import (
|
||||
"strconv"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
"kra/internal/service"
|
||||
"kra/internal/service/dto"
|
||||
|
|
@ -175,7 +174,7 @@ func (h *Dictionary) Details(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
}
|
||||
filter := biz.DictionaryDetailFilter{DictionaryID: uint(dictionaryID), Label: c.Query("label"), Value: c.Query("value")}
|
||||
filter := dto.DictionaryDetailListRequest{DictionaryID: uint(dictionaryID), Label: c.Query("label"), Value: c.Query("value")}
|
||||
if raw, exists := c.GetQuery("status"); exists {
|
||||
value, parseErr := strconv.ParseBool(raw)
|
||||
if parseErr != nil {
|
||||
|
|
@ -201,7 +200,7 @@ func (h *Dictionary) Details(c *gin.Context) {
|
|||
}
|
||||
filter.Level = &value
|
||||
}
|
||||
items, total, err := h.service.DictionaryDetails(c.Request.Context(), p, size, filter)
|
||||
items, total, err := h.service.DictionaryDetailsRequest(c.Request.Context(), p, size, filter)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package handler
|
|||
import (
|
||||
"strconv"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/server/httpx"
|
||||
servermiddleware "kra/internal/server/middleware"
|
||||
"kra/internal/service"
|
||||
|
|
@ -47,7 +46,7 @@ func (h *Media) List(c *gin.Context) {
|
|||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.MediaList(c.Request.Context(), biz.MediaFilter{Page: req.Page, PageSize: req.PageSize, Keyword: req.Keyword, CategoryID: req.ClassID, Tag: req.Tag, UserID: req.UserID, StartCreatedAt: req.StartCreatedAt, EndCreatedAt: req.EndCreatedAt, OrderKey: req.OrderKey, Desc: req.Desc})
|
||||
items, total, err := h.service.MediaListRequest(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "获取失败")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
|
|||
httpx.Fail(c, "已存在数据库配置")
|
||||
return
|
||||
}
|
||||
var input service.DatabaseInit
|
||||
var input dto.DatabaseInitRequest
|
||||
if c.ShouldBindJSON(&input) != nil {
|
||||
httpx.Fail(c, "参数校验不通过")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func page(c *gin.Context) (int, int, error) {
|
||||
var value, size int
|
||||
if raw, exists := c.GetQuery("page"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
if raw, exists := c.GetQuery("pageSize"); exists && raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
size = parsed
|
||||
}
|
||||
return value, size, nil
|
||||
}
|
||||
|
||||
func IDsFromQuery(c *gin.Context) []uint {
|
||||
values := c.QueryArray("IDs[]")
|
||||
ids := make([]uint, 0, len(values))
|
||||
for _, value := range values {
|
||||
id, _ := strconv.ParseUint(value, 10, 64)
|
||||
if id > 0 {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
|
@ -108,8 +108,7 @@ func (h *User) UpdateSelf(c *gin.Context) {
|
|||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
value := service.UserInput{ID: claims.ID, NickName: req.NickName, HeaderImg: req.HeaderImg, Phone: req.Phone, Email: req.Email, Enable: req.Enable}
|
||||
if err := h.service.UpdateSelfUser(c.Request.Context(), value); err != nil {
|
||||
if err := h.service.UpdateSelfUserRequest(c.Request.Context(), claims.ID, &req); err != nil {
|
||||
httpx.Fail(c, "修改失败")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"kra/internal/service"
|
||||
"kra/internal/routeinfo"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
|
|
@ -83,7 +83,7 @@ func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string
|
|||
apiPath = "/"
|
||||
}
|
||||
documentPath := swaggerPathParameter.ReplaceAllString(apiPath, `{$1}`)
|
||||
group, description := service.RouteMetadata(route.Method, apiPath)
|
||||
group, description := routeinfo.Metadata(route.Method, apiPath)
|
||||
if description == "" {
|
||||
description = route.Method + " " + apiPath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,6 @@ import (
|
|||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type LoginResult struct {
|
||||
User *dto.UserResponse `json:"user"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
NeedChangePassword bool `json:"needChangePassword"`
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
uc *biz.AuthenticationUsecase
|
||||
}
|
||||
|
|
@ -22,11 +15,11 @@ func NewAuthService(uc *biz.AuthenticationUsecase) *AuthService {
|
|||
return &AuthService{uc: uc}
|
||||
}
|
||||
|
||||
func loginResult(value *biz.AuthenticationResult) *LoginResult {
|
||||
return &LoginResult{User: convertUser(value.User), Token: value.Token, ExpiresAt: value.ExpiresAt.UnixMilli(), NeedChangePassword: value.NeedChangePassword}
|
||||
func loginResult(value *biz.AuthenticationResult) *dto.LoginResponse {
|
||||
return &dto.LoginResponse{User: convertUser(value.User), Token: value.Token, ExpiresAt: value.ExpiresAt.UnixMilli(), NeedChangePassword: value.NeedChangePassword}
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, ip, agent string) (*LoginResult, error) {
|
||||
func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, ip, agent string) (*dto.LoginResponse, error) {
|
||||
value, err := s.uc.Login(ctx, &biz.LoginAttempt{Username: req.Username, Password: req.Password, CaptchaID: req.CaptchaID, Captcha: req.Captcha, IP: ip, Agent: agent})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -34,7 +27,7 @@ func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, ip, agen
|
|||
return loginResult(value), nil
|
||||
}
|
||||
|
||||
func (s *AuthService) SwitchAuthority(ctx context.Context, claims *biz.AuthClaims, authorityID uint) (*LoginResult, error) {
|
||||
func (s *AuthService) SwitchAuthority(ctx context.Context, claims *biz.AuthClaims, authorityID uint) (*dto.LoginResponse, error) {
|
||||
value, err := s.uc.SwitchAuthority(ctx, claims, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -119,6 +119,16 @@ func (s *DictionaryService) DictionaryDetails(ctx context.Context, page, size in
|
|||
}
|
||||
return out, total, nil
|
||||
}
|
||||
func (s *DictionaryService) DictionaryDetailsRequest(ctx context.Context, page, size int, request dto.DictionaryDetailListRequest) ([]*dto.DictionaryDetailResponse, int64, error) {
|
||||
return s.DictionaryDetails(ctx, page, size, biz.DictionaryDetailFilter{
|
||||
DictionaryID: request.DictionaryID,
|
||||
Label: request.Label,
|
||||
Value: request.Value,
|
||||
Status: request.Status,
|
||||
ParentID: request.ParentID,
|
||||
Level: request.Level,
|
||||
})
|
||||
}
|
||||
func (s *DictionaryService) DictionaryDetail(ctx context.Context, id uint) (*dto.DictionaryDetailResponse, error) {
|
||||
v, err := s.uc.FindDictionaryDetail(ctx, id)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package dto
|
||||
|
||||
type LoginResponse struct {
|
||||
User *UserResponse `json:"user"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
NeedChangePassword bool `json:"needChangePassword"`
|
||||
}
|
||||
|
|
@ -30,6 +30,15 @@ type DictionaryDetailsByParentRequest struct {
|
|||
ParentID *uint `json:"parentID" form:"parentID"`
|
||||
IncludeChildren bool `json:"includeChildren" form:"includeChildren"`
|
||||
}
|
||||
|
||||
type DictionaryDetailListRequest struct {
|
||||
DictionaryID uint
|
||||
Label string
|
||||
Value string
|
||||
Status *bool
|
||||
ParentID *uint
|
||||
Level *int
|
||||
}
|
||||
type SystemParameterRequest struct {
|
||||
ID uint `json:"ID" form:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt" form:"CreatedAt"`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package dto
|
||||
|
||||
type DatabaseInitRequest struct {
|
||||
DBType string `json:"dbType"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
DBName string `json:"dbName" binding:"required"`
|
||||
DBPath string `json:"dbPath"`
|
||||
Template string `json:"template"`
|
||||
AdminPassword string `json:"adminPassword" binding:"required"`
|
||||
}
|
||||
|
|
@ -57,6 +57,14 @@ func (s *MediaService) MediaList(ctx context.Context, filter biz.MediaFilter) ([
|
|||
}
|
||||
return out, total, nil
|
||||
}
|
||||
func (s *MediaService) MediaListRequest(ctx context.Context, request *dto.MediaListRequest) ([]*dto.MediaResponse, int64, error) {
|
||||
return s.MediaList(ctx, biz.MediaFilter{
|
||||
Page: request.Page, PageSize: request.PageSize, Keyword: request.Keyword,
|
||||
CategoryID: request.ClassID, Tag: request.Tag, UserID: request.UserID,
|
||||
StartCreatedAt: request.StartCreatedAt, EndCreatedAt: request.EndCreatedAt,
|
||||
OrderKey: request.OrderKey, Desc: request.Desc,
|
||||
})
|
||||
}
|
||||
func (s *MediaService) Delete(ctx context.Context, id uint) error { return s.uc.Delete(ctx, id) }
|
||||
func (s *MediaService) Rename(ctx context.Context, id uint, name string) error {
|
||||
return s.uc.UpdateMediaName(ctx, id, name)
|
||||
|
|
|
|||
|
|
@ -5,22 +5,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/routeinfo"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type DatabaseInit struct {
|
||||
DBType string `json:"dbType"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
UserName string `json:"userName"`
|
||||
Password string `json:"password"`
|
||||
DBName string `json:"dbName" binding:"required"`
|
||||
DBPath string `json:"dbPath"`
|
||||
Template string `json:"template"`
|
||||
AdminPassword string `json:"adminPassword" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error {
|
||||
func (s *SystemConfigService) Initialize(ctx context.Context, input *dto.DatabaseInitRequest, apis []*biz.API) error {
|
||||
driver, config := input.DBType, ""
|
||||
switch driver {
|
||||
case "mysql":
|
||||
|
|
@ -37,7 +26,7 @@ func (s *SystemConfigService) Initialize(ctx context.Context, input *DatabaseIni
|
|||
return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: driver, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, Template: input.Template, AdminPassword: input.AdminPassword, APIs: apis})
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error {
|
||||
func (s *SystemConfigService) InitializeRoutes(ctx context.Context, input *dto.DatabaseInitRequest, routes []dto.Route) error {
|
||||
apis := make([]*biz.API, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
path := route.Path
|
||||
|
|
@ -47,16 +36,8 @@ func (s *SystemConfigService) InitializeRoutes(ctx context.Context, input *Datab
|
|||
path = "/"
|
||||
}
|
||||
}
|
||||
group, description := routeMetadata(route.Method, path)
|
||||
group, description := routeinfo.Metadata(route.Method, path)
|
||||
apis = append(apis, &biz.API{Path: path, Method: route.Method, APIGroup: group, Description: description})
|
||||
}
|
||||
return s.Initialize(ctx, input, apis)
|
||||
}
|
||||
|
||||
func routeGroup(path string) string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(parts) > 0 && parts[0] != "" {
|
||||
return parts[0]
|
||||
}
|
||||
return "base"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type UserInput struct {
|
||||
type userInput struct {
|
||||
ID uint
|
||||
Username, Password, NickName, HeaderImg, Phone, Email string
|
||||
AuthorityID uint
|
||||
|
|
@ -24,17 +24,20 @@ func NewUserService(uc *biz.UserUsecase, settings *SecurityService) *UserService
|
|||
return &UserService{uc: uc, settings: settings}
|
||||
}
|
||||
|
||||
func userInput(value *dto.UserRequest) UserInput {
|
||||
return UserInput{ID: value.ID, Username: value.Username, Password: value.Password, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, AuthorityIDs: value.AuthorityIDs, Enable: value.Enable, Phone: value.Phone, Email: value.Email}
|
||||
func userRequestInput(value *dto.UserRequest) userInput {
|
||||
return userInput{ID: value.ID, Username: value.Username, Password: value.Password, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, AuthorityIDs: value.AuthorityIDs, Enable: value.Enable, Phone: value.Phone, Email: value.Email}
|
||||
}
|
||||
func (s *UserService) ListUsersRequest(ctx context.Context, value *dto.UserListRequest) ([]*dto.UserResponse, int64, error) {
|
||||
return s.ListUsers(ctx, value.Page, value.PageSize, &biz.UserListFilter{Username: value.Username, NickName: value.NickName, Phone: value.Phone, Email: value.Email, OrderKey: value.OrderKey, Desc: value.Desc})
|
||||
}
|
||||
func (s *UserService) CreateUserRequest(ctx context.Context, value *dto.UserRequest) (*dto.UserResponse, error) {
|
||||
return s.CreateUser(ctx, userInput(value))
|
||||
return s.createUser(ctx, userRequestInput(value))
|
||||
}
|
||||
func (s *UserService) UpdateUserRequest(ctx context.Context, value *dto.UserRequest) error {
|
||||
return s.UpdateUser(ctx, userInput(value))
|
||||
return s.updateUser(ctx, userRequestInput(value))
|
||||
}
|
||||
func (s *UserService) UpdateSelfUserRequest(ctx context.Context, id uint, value *dto.SelfUserRequest) error {
|
||||
return s.updateSelfUser(ctx, userInput{ID: id, NickName: value.NickName, HeaderImg: value.HeaderImg, Phone: value.Phone, Email: value.Email, Enable: value.Enable})
|
||||
}
|
||||
|
||||
func (s *UserService) User(ctx context.Context, id uint) (*dto.UserResponse, error) {
|
||||
|
|
@ -85,7 +88,7 @@ func (s *UserService) Authorities(ctx context.Context) ([]*dto.AuthorityResponse
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, input UserInput) (*dto.UserResponse, error) {
|
||||
func (s *UserService) createUser(ctx context.Context, input userInput) (*dto.UserResponse, error) {
|
||||
if err := s.settings.ValidatePassword(ctx, input.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -97,12 +100,12 @@ func (s *UserService) CreateUser(ctx context.Context, input UserInput) (*dto.Use
|
|||
}
|
||||
return convertUser(user), nil
|
||||
}
|
||||
func (s *UserService) UpdateUser(ctx context.Context, input UserInput) error {
|
||||
func (s *UserService) updateUser(ctx context.Context, input userInput) error {
|
||||
// The compatible ChangeUserInfo payload uses authorityIds for role assignment; the
|
||||
// standalone authorityId field is not applied by setUserInfo.
|
||||
return s.uc.UpdateUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable}, input.AuthorityIDs)
|
||||
}
|
||||
func (s *UserService) UpdateSelfUser(ctx context.Context, input UserInput) error {
|
||||
func (s *UserService) updateSelfUser(ctx context.Context, input userInput) error {
|
||||
return s.uc.UpdateSelfUser(ctx, &biz.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable})
|
||||
}
|
||||
func (s *UserService) DeleteUser(ctx context.Context, id uint) error {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ func TestErrorSinkSkipsGORMBridge(t *testing.T) {
|
|||
})}
|
||||
base := zapcore.NewNopCore()
|
||||
core := &routedFileCore{base: base, encoder: zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), level: zapcore.ErrorLevel, root: root, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: state}
|
||||
defer core.Close()
|
||||
for _, filename := range []string{"/tmp/gorm_logger_writer.go", "/workspace/internal/data/gorm_logger.go"} {
|
||||
if err := core.Write(zapcore.Entry{Level: zapcore.ErrorLevel, Message: "database failed", Caller: zapcore.EntryCaller{Defined: true, File: filename, Line: 10}}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"github.com/go-kratos/kratos/v3/middleware"
|
||||
"go.einride.tech/aip/fieldbehavior"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// Middleware is a middleware that validates the request message with [FieldBehavior](https://google.aip.dev/203)
|
||||
func Middleware() middleware.Middleware {
|
||||
return func(handler middleware.Handler) middleware.Handler {
|
||||
return func(ctx context.Context, req any) (reply any, err error) {
|
||||
if msg, ok := req.(proto.Message); ok {
|
||||
if err := fieldbehavior.ValidateRequiredFields(msg); err != nil {
|
||||
return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err)
|
||||
}
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue