服务端
Some checks failed
CI / init (push) Has been cancelled
CI / Frontend node 18.16.0 (push) Has been cancelled
CI / Backend go (1.22) (push) Has been cancelled
CI / devops-test (1.22, 18.16.0) (push) Has been cancelled
CI / release-pr (push) Has been cancelled
CI / release-please (push) Has been cancelled
CI / devops-prod (1.22, 18.x) (push) Has been cancelled
CI / docker (push) Has been cancelled

This commit is contained in:
2026-04-22 15:49:50 +08:00
parent 05ee541420
commit 8164eec650
18 changed files with 1115 additions and 251 deletions

View File

@@ -0,0 +1,96 @@
# API 鉴权与权限修改规范
## 适用范围
- 新增接口时,需要判定它是“公开访问”“仅登录可访问”还是“登录且有权限才可访问”时,使用本文。
- 已有接口在“不需要登录 / 需要登录 / 需要权限”之间切换时,使用本文。
## 现有鉴权基线
- `initialize/router.go` 当前只创建两类顶层路由组:
- `PublicGroup`:不挂鉴权中间件。
- `PrivateGroup`:统一挂 `middleware.JWTAuth()``middleware.CasbinHandler()`
- `JWTAuth` 定义在 `middleware/jwt.go`
- `CasbinHandler` 定义在 `middleware/casbin_rbac.go`
- 结论:
- 挂到 `PublicGroup` = 不需要登录。
- 挂到 `PrivateGroup` = 需要登录,且需要角色权限。
- 当前仓库没有默认内置的“只登录、不校验权限”公共组。
- 不存在“只校验权限、不登录”的合法模式;`CasbinHandler` 依赖 `JWTAuth` 写入的用户 claims。
## 权限模式判定
| 模式 | 路由挂载方式 | JWT | Casbin | 后续要求 |
|:---|:---|:---|:---|:---|
| 不需要登录 | `PublicGroup` / `PublicRouter` | 否 | 否 | 默认不作为角色权限点长期维护 |
| 需要登录,不需要权限 | 基于 `PublicRouter` 单独加 `middleware.JWTAuth()` | 是 | 否 | 默认不进入角色权限维护 |
| 需要登录且需要权限 | `PrivateGroup` / `Router` | 是 | 是 | 必须进入 `sys_apis` 并分配角色 |
## 强制规则
- 同一个 `path + method` 只能落一种鉴权模式;禁止同时注册到公开组和鉴权组。
- `middleware.OperationRecord()` 只负责操作审计,不改变登录与权限判定。
- 鉴权逻辑统一落在 `router` 层;禁止在 `API``Service` 中靠 `if token != ""``if authorityId == ...` 兜底模拟权限。
- 修改接口鉴权时Swagger 注解必须同步:
- 不需要登录:移除 `@Security ApiKeyAuth`
- 需要登录:保留 `@Security ApiKeyAuth`
- 业务路由统一在 `router/<module>` 调整;业务模块接入点统一在 `initialize/router_biz.go` 注册。
## 改成不需要登录
- 把路由挂载从 `PrivateGroup` 或“仅登录组”移到 `PublicRouter`
- 如果只是业务模块内部路由调整,不要改 `API``Service` 方法签名。
- 该接口如果不再作为后台角色权限点维护,必须把对应 `path + method` 从权限维护中移走,二选一即可:
- 删除对应 `sys_apis` 记录。
- 加入 `sys_ignore_apis`,避免后续 `SyncApi` 再次纳入权限页。
- 若历史上已经给该接口分配过角色,必须同步清理对应 Casbin 规则,避免后台权限配置出现“看起来要授权、实际上公开可调”的假象。
## 改成需要登录,不需要权限
- 当前项目没有全局现成的 `LoginGroup`;少量接口优先在对应 router 中基于 `PublicRouter` 单独挂 `JWTAuth`,不要为了一两个接口改全局入口。
- 推荐写法:
```go
func (r *BookRouter) InitBookRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
bookLoginRouter := PublicRouter.Group("book").Use(middleware.JWTAuth())
{
bookLoginRouter.GET("profile", bookApi.GetProfile)
}
}
```
- 同一模块里如果“仅登录接口”很多,再考虑在 `initialize/router.go` 新增单独的 `LoginGroup`,并把它显式传入对应 router 初始化函数。
- 这类接口禁止挂 `middleware.CasbinHandler()`
- 这类接口默认不进入 `sys_apis` 的角色权限维护;如果历史上已经进入,必须删除或加入 `sys_ignore_apis`,不要继续让后台把它当作“需要分配角色”的接口。
## 改成需要登录且需要权限
- 把路由挂到 `PrivateGroup` 或业务 router 的 `Router` 参数上。
- 只把路由挪进 `PrivateGroup` 还不够;`CasbinHandler``path + method` 校验权限,没有角色策略就会直接返回“权限不足”。
- 因此必须同时完成以下动作:
- 确保该 `path + method` 存在于 `sys_apis`
- 确保该接口已分配允许访问的角色。
- 刷新 Casbin 策略缓存。
- 系统现成接口的完整路径 = `<router-prefix>/api/...`;如果 `router-prefix` 为空,则完整路径就是 `/api/...`。不要把是否存在额外前缀写死成项目事实。
- 常用管理接口:
- `GET <router-prefix>/api/syncApi`:对比内存路由和 `sys_apis`
- `POST <router-prefix>/api/enterSyncApi`:确认把新增路由写入 `sys_apis`
- `POST <router-prefix>/api/setApiRoles`:全量覆盖某个接口的角色列表
- `GET <router-prefix>/api/freshCasbin`:刷新 Casbin 策略缓存
- 若接口此前在 `sys_ignore_apis` 中,改成“需要权限”前必须先取消 ignore再同步到 `sys_apis`
- 若不给任何角色分配该接口,则结果不是“仅登录可访问”,而是“所有已登录用户都权限不足”。
## 验证要求
- 修改完成后,至少验证下面三类请求结果:
- 公开接口:不带 token 可访问成功。
- 仅登录接口:不带 token 返回未登录;带有效 token 可访问成功。
- 权限接口:不带 token 返回未登录;带无权角色 token 返回“权限不足”;带有权角色 token 可访问成功。
- 如果改了接口鉴权方式,同时该接口出现在 Swagger、前端权限配置页、角色配置页也必须同步核对展示结果是否一致。
## 禁止事项
- 禁止只改 `sys_apis``casbin_rule`,却不改真实 router 挂载。
- 禁止只把路由从 `PublicGroup` 挪到 `PrivateGroup`,却不补 `sys_apis` 和角色策略。
- 禁止把“仅登录接口”偷懒挂到 `PrivateGroup`,再靠给所有角色放权来模拟“无权限限制”。
- 禁止让公开接口长期保留在角色权限维护页里,造成权限含义失真。

View File

@@ -0,0 +1,42 @@
# 同模块 admin/app 接口分层规范
## 适用范围
- 同一业务模块同时存在 `admin` 管理端接口和 `app` 用户端接口时,使用本文。
## 强制规则
- 顶层目录一律按业务模块落点;禁止因为有 `app` 端,就单独新建顶层 `router/app``api/v1/app``service/app``model/app` 来承载 `book``order``author` 这类业务。
- 同一业务模块的实体仍统一放在 `model/<module>`;只有接口入参、出参和流程按 `admin` / `app` 分开。
- `router/<module>``api/v1/<module>``service/<module>` 内,`admin``app` 接口必须分文件或分承载结构体;禁止长期混写在同一个大文件里。
- 可复用逻辑优先下沉到 `service/<module>` 的公共方法;公共逻辑只抽业务共性,不要把 `admin` / `app` 的鉴权、返回、分页口径硬揉成一套。
## 推荐落点
- 推荐文件名:`<module>_admin.go``<module>_app.go``enter.go`
- 示例:
- `router/book/book_admin.go`
- `router/book/book_app.go`
- `api/v1/book/book_admin.go`
- `api/v1/book/book_app.go`
- `service/book/book_admin.go`
- `service/book/book_app.go`
- `service/book/book_common.go`
- `model/book/book.go`
- `model/book/request/book_admin.go`
- `model/book/request/book_app.go`
- `model/book/response/book_admin.go`
- `model/book/response/book_app.go`
## 路由与鉴权
- `admin` 接口默认挂业务 router 的 `PrivateGroup`,走后台 `JWT + Casbin`
- `app` 公开接口挂 `PublicGroup`
- `app` 仅登录接口,基于 `PublicGroup` 单独加 `middleware.JWTAuth()`;不要直接挂 `PrivateGroup`
- 如果 `app` 用户体系不等于 `sys_users`禁止直接复用后台登录、claims、角色权限链路必须单独实现用户端认证链路。
## 禁止事项
- 禁止把“同一业务模块的 app 端接口”误建成独立业务模块。
- 禁止为了省事,把所有 `app` 接口都挂到 `PrivateGroup`,再用后台角色权限去模拟用户端登录态。
- 禁止同一个业务实体在 `model` 层拆出两套重复表结构,仅因接口面向 `admin` / `app` 不同。

View File

@@ -0,0 +1,54 @@
# 业务 admin 端默认 CRUD 接口规范
## 适用范围
- 新增业务模块的 `admin` 端管理接口时,使用本文。
- 判断某个业务模块默认应该提供哪些后台 CRUD 接口时,使用本文。
- 业务模块没有明确说明“只读”“无详情”“禁止批量删除”等例外时,默认按本文落地。
## 默认接口基线
- 默认 CRUD = `创建``单删``批量删除``更新``详情``分页列表` 6 个接口。
- 完整路径 = `<router-prefix>/<abbreviation>/...`;如果 `router-prefix` 为空,则完整路径不带额外前缀。禁止把 `/api` 写死为项目事实。
- `admin` 接口默认挂 `PrivateGroup`,统一走后台 `JWT + Casbin`
- 写操作默认挂 `middleware.OperationRecord()`;读操作默认不挂操作审计。
| 动作 | Method | 默认路径 | API 方法名 | Service 方法名 |
|:---|:---|:---|:---|:---|
| 创建 | `POST` | `/<abbreviation>/create<StructName>` | `Create<StructName>` | `Create<StructName>` |
| 单删 | `DELETE` | `/<abbreviation>/delete<StructName>` | `Delete<StructName>` | `Delete<StructName>` |
| 批量删除 | `DELETE` | `/<abbreviation>/delete<StructName>ByIds` | `Delete<StructName>ByIds` | `Delete<StructName>ByIds` |
| 更新 | `PUT` | `/<abbreviation>/update<StructName>` | `Update<StructName>` | `Update<StructName>` |
| 详情 | `GET` | `/<abbreviation>/find<StructName>` | `Find<StructName>` | `Get<StructName>` |
| 分页列表 | `GET` | `/<abbreviation>/get<StructName>List` | `Get<StructName>List` | `Get<StructName>InfoList` |
## 参数约定
- `Create``Update` 默认使用 `body` 传实体或业务 `request`
- `Delete` 默认用主键 query 参数删除;主键名必须和实体主键字段保持一致。
- `DeleteByIds` 默认用主键数组 query 参数删除,格式统一为 `<primaryField>s[]`
- `Find` 默认用主键 query 参数查询详情。
- `GetList` 默认接收 `model/<module>/request` 下的搜索结构;普通列表返回分页结果,树形列表可不接分页参数,但接口名仍保持 `get<StructName>List`
## 强制规则
- 新增业务 `admin` 模块时,如无明确例外,先提供这 6 个接口,再叠加业务特有接口。
- 路由统一放在 `router/<module>`,接口统一放在 `api/v1/<module>`,业务统一放在 `service/<module>`,模型统一放在 `model/<module>`
- 新增业务路由后,必须同步在 `initialize/router_biz.go` 注册。
- 同一模块如果同时有 `admin/app` 两套接口,目录仍按业务模块落点,`admin``app` 必须分文件或分承载结构体,不能长期混写。
- Swagger 注解里的 `@Router``@Security ApiKeyAuth``Method` 必须和真实 router 挂载一致。
## 允许例外
- 业务天然只读时,可以不做 `Create``Update``Delete``DeleteByIds`,但必须在对应业务文档或接口文档中明确说明。
- 业务明确禁止批量删除时,可以去掉 `DeleteByIds`,但 `router``api``service`、前端调用、权限点必须同步移除,不能只删单层。
- 树形实体的列表接口可以返回整棵树,不强制分页;但接口名仍保持 `get<StructName>List`,不要随意改成 `tree``all``queryList`
- 配置型场景如果本质不是独立业务实体,优先复用已有能力,例如 `sys_params`;不要为了凑 CRUD 强行建完整业务模块。
## 禁止事项
- 禁止把默认 `admin` CRUD 直接挂到 `PublicGroup`
- 禁止同一批后台 CRUD 同时出现 `createXxx``addXxx``saveXxx` 多套命名。
- 禁止把列表接口随意命名成 `page``list``query`,导致不同模块接口风格漂移。
- 禁止只新增 `API` 或只新增 `Router`,不补齐 `Service``Model`、注册入口。
- 禁止因为有 `app` 端,就把 `admin` 端默认 CRUD 改落到 `router/app``api/v1/app` 这类顶层目录。

View File

@@ -0,0 +1,158 @@
# 系统参数SysParams使用规范
## 适用范围
- 需要读取或写入 `sys_params` 表中的系统参数时,使用本文。
- 需要给某一个固定参数提供独立业务 API但底层仍保存到 `sys_params` 时,使用本文。
## 现有落点
- 表结构:`model/system/sys_params.go`
- Service`service/system/sys_params.go`
- API`api/v1/system/sys_params.go`
- Router`router/system/sys_params.go`
## 现有接口
- 实际完整路径 = `<router-prefix>/sysParams/...`
- 如果环境把 `router-prefix` 配成 `/api`,则完整路径就是 `/api/sysParams/...`
- 当前仓库 `config.yaml``router-prefix` 默认值为空字符串;不要把 `/api` 写死为项目事实。
- 这组路由在 `initialize/router.go` 中挂到 `PrivateGroup`,默认需要鉴权。
- `GET /sysParams/getSysParam?key=xxx`:按 `key` 读取单条参数。
- `GET /sysParams/getSysParamsList`:分页读取参数列表。
- `POST /sysParams/createSysParams`:创建参数。
- `PUT /sysParams/updateSysParams`:按 `ID` 更新参数。
- `DELETE /sysParams/deleteSysParams`:按 `ID` 删除参数。
- 结论:这组接口适合“通用参数管理页”,不适合业务围绕某一个固定参数直接做长期读写。
## 强制规则
- 后端代码禁止通过 HTTP 反调自己的 `/sysParams` 接口;统一在 `Service` 层直接调用 `service.ServiceGroupApp.SystemServiceGroup.SysParamsService`
- `API` 层禁止直接操作 `global.GVA_DB`
- `Value` 是字符串存储;简单值可直接存字符串,结构化配置必须由业务 `Service` 负责 `json.Marshal` / `json.Unmarshal`
- 不要把 `sys_params``ID` 暴露给前端,作为某个固定配置的长期主键。
- 固定配置统一按 `key` 识别;`key` 必须稳定、唯一、可读,建议使用 `<module>.<scene>`,例如 `device.runtimeConfig`
- 参数不存在时必须显式处理:返回业务错误、初始化默认值、或创建后再返回;禁止静默吞掉。
- 通用参数管理页面可以直接调用 `/sysParams`;具体业务页面优先调用该业务自己的独立 API。
- 参数属于某个业务模块时,落点放到该业务模块;只有平台级通用参数才继续放在 `system` 模块。
## 代码内直接读取
- 读取动作统一放在 `service/<module>`
- 调用入口:
```go
param, err := service.ServiceGroupApp.SystemServiceGroup.SysParamsService.GetSysParam("device.runtimeConfig")
```
- `param.Value` 就是最终存储值。
- 如果 `Value` 存的是 JSON必须先反序列化为业务结构体再继续使用。
```go
package device
import (
"encoding/json"
"github.com/flipped-aurora/gin-vue-admin/server/service"
)
type RuntimeConfig struct {
Enable bool `json:"enable"`
Mode string `json:"mode"`
}
func (s *DeviceService) GetRuntimeConfig() (RuntimeConfig, error) {
param, err := service.ServiceGroupApp.SystemServiceGroup.SysParamsService.GetSysParam("device.runtimeConfig")
if err != nil {
return RuntimeConfig{}, err
}
var cfg RuntimeConfig
if err = json.Unmarshal([]byte(param.Value), &cfg); err != nil {
return RuntimeConfig{}, err
}
return cfg, nil
}
```
## 代码内直接写入
- 当前 `SysParamsService` 没有“按 `key` 直接写入”的现成能力。
- 现在只有 `CreateSysParams``UpdateSysParams``GetSysParam`
- 这意味着:业务写入固定参数时,必须先按 `key` 查询,再决定 `create` 还是 `update`
- 这段逻辑必须收口到业务 `Service`,不要放在 `API`,也不要交给前端拼 `ID`
```go
package device
import (
"encoding/json"
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"gorm.io/gorm"
)
const runtimeConfigKey = "device.runtimeConfig"
func (s *DeviceService) SaveRuntimeConfig(cfg RuntimeConfig) error {
valueBytes, err := json.Marshal(cfg)
if err != nil {
return err
}
sysParamsSvc := service.ServiceGroupApp.SystemServiceGroup.SysParamsService
param, err := sysParamsSvc.GetSysParam(runtimeConfigKey)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return sysParamsSvc.CreateSysParams(&system.SysParams{
Name: "设备运行配置",
Key: runtimeConfigKey,
Value: string(valueBytes),
Desc: "device 模块运行配置",
})
}
return err
}
param.Value = string(valueBytes)
param.Name = "设备运行配置"
param.Desc = "device 模块运行配置"
return sysParamsSvc.UpdateSysParams(param)
}
```
## 什么时候必须写独立 API
- 前端只关心某一个固定参数,不应该先查列表、拿 `ID`、再更新时。
- 参数值是 JSON需要强类型请求体和响应体时。
- 这个参数有明确业务语义,例如“设备运行配置”“首页配置”“第三方回调配置”时。
- 该参数需要单独做权限控制、默认值处理、字段校验、审计说明时。
## 独立 API 规范
- 独立 API 仍按 `Router → API → Service → GORM → Database` 分层实现。
- 底层存储仍使用 `sys_params`;不要因为一个配置项就单独建表,除非它已经演变成独立业务实体。
- 对外接口暴露业务语义,不暴露 `sys_params``ID`、通用 CRUD 细节。
- `Service` 负责固定 `key`、处理 JSON 转换、决定 create/update、处理不存在时的策略。
- 参数属于业务模块时,文件落点统一为 `model/<module>/request``model/<module>/response``service/<module>``api/v1/<module>``router/<module>`
- 新增业务路由统一注册到 `initialize/router_biz.go`
## 推荐落地方式
- 对外提供两个独立接口即可:一个“获取配置”,一个“保存配置”。
- 接口名使用业务语义,不使用 `sysParams` 命名。
- 示例接口:`GET <router-prefix>/deviceConfig/getRuntimeConfig``PUT <router-prefix>/deviceConfig/updateRuntimeConfig`
- `GET` 接口返回业务结构体。
- `PUT` 接口接收业务结构体。
- 底层统一在 `service/device` 中转成 `sys_params.value` 的 JSON 字符串。
## 禁止事项
- 禁止在业务代码里直接写 SQL 操作 `sys_params`,绕开现有 `Service` 分层。
- 禁止在业务 `API` 中直接处理 `sys_params``ID`
- 禁止让前端把一个固定配置当成“参数管理列表中的某一行”去长期维护。
- 禁止把结构化 JSON 配置直接裸透传为字符串给业务页面,除非该页面本身就是通用参数管理页。

View File

@@ -0,0 +1,54 @@
# Model / Request / Response 组织规范
## 适用范围
- 判断项目中的 `vo`、实体、接口入参、接口出参应该落在哪一层时,使用本文。
- 新增或修改业务模块的 `model/<module>``model/<module>/request``model/<module>/response` 时,使用本文。
- 判断某个 API 是直接复用实体,还是新增 `request/response` 结构时,使用本文。
## 结论基线
- 本项目不单独维护顶层 `vo` 目录。
- 项目里的数据载体统一归到 `model` 体系,不额外拆一套平行 `vo` 层。
- 标准落点如下:
| 场景 | 落点 | 说明 |
|:---|:---|:---|
| 数据库实体 / 业务实体 | `model/<module>` | 承载表结构映射、业务实体字段 |
| API 入参 | `model/<module>/request` | 承载查询条件、分页条件、保存参数等 |
| API 出参 | `model/<module>/response` | 承载详情包装、列表项包装、聚合展示结构等 |
| 跨模块通用入参 | `model/common/request` | 例如 `PageInfo``GetById``IdsReq` |
| 跨模块通用出参 | `model/common/response` | 例如统一响应壳、分页结果 |
## 判定规则
- 请求参数如果就是业务实体本身,且不会引入多余字段、敏感字段或语义歧义,可以直接复用 `model/<module>` 实体。
- 请求参数如果只是“分页 + 条件筛选 + 排序”这类接口 contract应定义到 `model/<module>/request`
- 返回结果如果只是直接返回实体本身,可以直接返回实体或列表,不强制为了“像 VO”再包一层空结构。
- 返回结果如果需要额外包装、聚合字段、嵌套结构、展示字段转换,应定义到 `model/<module>/response`
- 多个 API 只要 contract 一致,可以共用同一个 `request``response` 结构;不是每个 API 都必须单独建一份。
## 强制规则
- 新增业务模块时,实体统一放 `model/<module>`;禁止把实体落到 `api``service``router`
- `API` 层入参、出参需要独立结构时,统一放 `model/<module>/request``model/<module>/response`;禁止在 `API` 文件里长期维护临时匿名结构体当正式 contract。
- 跨模块都能稳定复用的分页、主键、统一响应、分页结果等结构,统一复用 `model/common/request``model/common/response`;不要每个模块各复制一份。
- 同一业务模块如果同时存在 `admin/app` 两套接口,实体仍统一放 `model/<module>`;只有 `request/response``admin` / `app` 分文件区分。
- 新增 `request/response` 前,先检查同模块现有结构是否可复用;只有接口字段、校验语义、返回语义明显不同,才新增结构。
- 改实体字段时,必须同步检查 `service` 查询/写入、`API` 绑定/返回、`doc-sql` 是否仍一致。
-`request/response` 时,必须同步检查 `API` 绑定、`Service` 方法签名、`doc-api` 是否仍一致。
## 推荐做法
- `Create``Update` 这类保存接口,如果直接面向实体字段,可优先复用实体。
- `GetList`、搜索、筛选、排序接口,优先使用 `request` 结构,不要把分页筛选字段硬塞进实体。
- `Find`、详情、聚合展示、树结构、联表结果等返回,优先使用 `response` 结构,不要把纯展示字段反向塞进数据库实体。
- `response` 结构命名优先体现业务语义,例如 `BookDetailResponse``BookListItem``AuthorOption`;不要机械统一叫 `XxxVO`
## 禁止事项
- 禁止单独新建顶层 `vo` 目录,与 `model` 并行维护两套数据结构体系。
- 禁止为了“每个 API 都有专属 VO”而机械性给每个接口复制一份几乎相同的 `request/response`
- 禁止把分页、排序、筛选字段直接加进数据库实体,只为了省掉 `request` 结构。
- 禁止把纯展示字段、聚合字段、临时返回字段长期塞进实体,只为了省掉 `response` 结构。
- 禁止 `API``Service` 长期返回 `map[string]interface{}``gin.H` 充当正式业务出参,导致 contract 漂移。