Compare commits

...
38 Commits
Author SHA1 Message Date
PiexlMax(奇淼 a6882210a8 Merge pull request #2235 from kobihikri/ci/pin-github-push-action
ci: pin ad-m/github-push-action to a full commit SHA
2026-07-14 15:56:15 +08:00
kobihikri 005a7b747f ci: pin ad-m/github-push-action to a full commit SHA
The release-pr job passes GITHUB_TOKEN to ad-m/github-push-action, which
is referenced by the mutable `@master` branch. A mutable ref means the
exact code that runs with the token can change without any change in this
repository. Pinning to a full commit SHA makes the action immutable while
keeping the current behaviour.

881a6320fdb16eb5318c5054f31c218aec2b324c is the commit `@master` resolves
to today, which is also tagged v1.3.0, so this changes nothing about what
runs now — it just fixes the version in place. The trailing `# v1.3.0`
comment keeps the human-readable version visible for future updates.

This mirrors GitHub's own hardening guidance for third-party actions.
2026-07-14 10:53:57 +03:00
PiexlMax(奇淼 4445d0e583 Merge pull request #2225 from estel-li/codex/fix-select-image-max-update-count
fix: respect SelectImage maxUpdateCount in media library
2026-06-30 17:34:09 +08:00
PiexlMax(奇淼 e04452c99e Merge pull request #2218 from AbbyJL/codex/fix-db-migrate-exit-code-clean
gorm.go: fix migrate failure exit code
2026-06-30 17:33:42 +08:00
李觉 06f859c13a fix select image max update count 2026-06-30 17:28:00 +08:00
PiexlMax(奇淼 87f6838a08 Merge pull request #2219 from 1220lanxi-glitch/main
fix:MCP生成代码创建字典失败bug
2026-06-30 15:42:28 +08:00
lanxi 463f6b7dfe fix:MCP生成代码创建字典失败bug 2026-06-16 13:46:37 +08:00
PiexlMax(奇淼 7d3f0c7564 clear: 清理对用户无意义的ai记忆 2026-06-15 10:35:05 +08:00
PiexlMax(奇淼 086fbdae04 clear: 清理无用的docs文件 2026-06-15 10:33:17 +08:00
Codex 07f1beb4b3 gorm.go: fix migrate failure exit code 2026-06-08 16:08:08 +08:00
PiexlMax(奇淼 78b3bd0c67 docs: 更新README.md,简化章节标题和图片描述 2026-05-28 11:43:04 +08:00
PiexlMax(奇淼 53e72c9660 Merge branch 'main' of github.com:flipped-aurora/gin-vue-admin 2026-05-28 11:42:36 +08:00
PiexlMax(奇淼andCopilot a333332e6c docs: 更新README.md,优化格式和内容,增加文档可读性
Co-authored-by: Copilot <copilot@github.com>
2026-05-28 11:41:43 +08:00
PiexlMax(奇淼 d1e6d9c642 Update README.md 2026-05-28 11:34:43 +08:00
PiexlMax(奇淼 7c8b623ab2 Update README.md 2026-05-28 10:46:00 +08:00
Zmzblink e4b76383ef Preload Authorities when fetching user by ID (#2215)
修复多角色时,签发其他角色token失败的问题
2026-05-28 09:31:56 +08:00
PiexlMax(奇淼 cdfd2a1901 docs: 更新许可证信息为BSL 1.1,明确非商用和商用使用要求 2026-05-28 09:29:41 +08:00
PiexlMax(奇淼 03f4daf5d2 docs: add BSL migration design 2026-05-27 16:13:43 +08:00
PiexlMax(奇淼 0094f677a1 fix: 更新记忆管理规则,确保长期记忆与业务记忆独立维护 2026-05-26 13:52:29 +08:00
PiexlMax(奇淼 3fda90acac Merge branch 'main' of github.com:flipped-aurora/gin-vue-admin 2026-05-26 12:32:19 +08:00
Robert SilénandCursor ee2de98fab docs: mention MariaDB in README (zh) (#2213)
Note MySQL or MariaDB 5.7+ in the tech stack database line; the server
already detects MariaDB versions in basetypes.go.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 12:03:14 +08:00
PiexlMax(奇淼andCopilot fcb10a9b6e feat: 添加构建设置项检查脚本,优化主题样式变量
Co-authored-by: Copilot <copilot@github.com>
2026-05-26 12:01:38 +08:00
PiexlMax(奇淼 f4fa0b7a40 fix: 更新预览脚本以支持开发模式 2026-05-26 11:22:25 +08:00
PiexlMax(奇淼 e77e0603db fix: 更新 @vueuse/core 和 @vueuse/integrations 依赖版本 2026-05-26 11:21:55 +08:00
15e6e7685c update:发布2.9.2版本 (#2211)
* feat: 调整AI工作模式,更符合harness基准

* feat: 添加BusinessDB字段到PluginInitializeGorm结构体,并增加相关测试用例

* [middleware/jwt.go]: fix #2192 issues bug

* docs: add auto plugin design spec

* chore: ignore worktrees directory

* feat: 调整代码辅助能力至插件

* feat: 添加警告条组件,提示授权用户访问限制

* fix: 修正商业用途版权声明链接

* feat: 调整agent.md 更加节省token

* fix: 修改casbin版本为v3

* feat: 更新JSONMap和JSONSlice类型,优化GORM数据类型处理

* feat: 添加数据库就绪通知机制,优化插件注册流程

* feat: 重构API路径和描述,优化代码生成器和模板配置分组

* feat: 优化 person 页面的 css 作用域限制

* feat: 更新 vite 至 vite8

* 新增:MCP工具为指定URL的角色ID授权

* fix: 增加文件名合法性检查,拒绝包含非法字符的文件写入

* chore: 更新CI配置,升级Node.js和Go版本,调整checkout和setup动作版本

* feat: 为数据库连接增加最大复用时间配置

* feat: 为各数据库连接配置增加最大连接生命周期设置

* feat: 更新插件注册逻辑并优化API和菜单组件的标签显示

* feat: 更新版本号至v2.9.2并添加新插件路径信息

---------

Co-authored-by: taincheng <zhangtc@gmail.com>
Co-authored-by: Azir-11 <2075125282@qq.com>
Co-authored-by: lanxi <1220lanxi@gmail.com>
2026-05-11 13:48:18 +08:00
PiexlMax(奇淼 699817becc Merge pull request #2204 from Azir-11/main
feat: 提高对深色主题的适配
2026-05-05 15:44:54 +08:00
Azir-11 fbd742ae03 feat: 提高对深色主题的适配 2026-04-30 14:20:05 +08:00
pixelmaxQM 0025a1a002 fix: 修正商业用途版权声明链接 2026-04-28 14:25:04 +08:00
pixelmaxQM 36c7f0bdd7 feat: 添加警告条组件,提示授权用户访问限制 2026-04-26 11:22:16 +08:00
pixelmaxQM 51b4ea284e feat: 添加获取 MCP 路由列表的接口 2026-04-16 10:08:21 +08:00
pixelmaxQM 60c1ec12b9 feat: 添加获取和设置API及菜单关联角色的接口 2026-04-16 10:05:11 +08:00
pixelmaxQM 61a3f8297c feat: 更新版本号至v2.9.1 2026-03-27 05:46:07 +01:00
PiexlMax(奇淼 375b21c222 发布2.9.1版本 (#2189)
* feat: mcp能力调整为 streamhttp的独立服务 不再依赖gva本体

* feat: 增加MCP启动能力,增加AI工作流需求整理能力。

* feat: 增加需求梳理能力

* feat: 清理无用日志

* feat: 移除无用的Dify Chatflow文本和代码生成器按钮
2026-03-27 12:14:13 +08:00
pixelmaxQM 324aaa3057 feat: 优化图片路径处理,使用getUrl函数替代硬编码路径 2026-03-21 07:32:55 +01:00
pixelmaxQM 24c2013caf feat: 修复错误的预览路径 2026-03-21 05:43:06 +01:00
pixelmaxQM 708a568427 feat:修复自动化代码导致注释位置错乱的bug 2026-03-21 05:25:17 +01:00
PiexlMax(奇淼 a75ab546ee Update README.md 2026-03-10 22:22:10 +08:00
PiexlMax(奇淼 f8c40b5b21 Update README.md 2026-03-10 22:21:21 +08:00
178 changed files with 10801 additions and 5745 deletions
+16 -893
View File
@@ -1,902 +1,25 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
tool: claude
role: compatibility-adapter
canonical_source: /AGENT.MD
structured_context: /aiDoc
---
#### **角色与目标**
# Claude 规则适配层
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
本文件只用于兼容 Claude 现有的自动加载路径。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
## 真实规则入口
---
请按下面顺序读取:
### **🚀 重要提示:GVA Helper MCP 支持**
1. `/AGENT.MD`
2. `/aiDoc/README.md`
3. `/aiDoc/relations/`、`/aiDoc/modules/`、`/aiDoc/frontend-backend/`、`/aiDoc/examples/`、`/aiDoc/memory/` 中与当前任务相关的文件
**在开始任何GVA开发工作之前,请务必注意以下重要工作流程:**
## 适配层约束
1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得:
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住:GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**:Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**:
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体(DTOs)。
- **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能,避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范:
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态,必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用,提供统一的接口服务
- **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织,每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
- **必须**优先使用UnoCSS原子化类名进行样式设计
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名:kebab-case(短横线命名)
- 组件名:PascalCase(大驼峰)
- 变量名:camelCase(小驼峰)
- 常量名:UPPER_SNAKE_CASE(大写下划线)
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **⚠️ 前端工具库使用规范(强制)**
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
### **工具文件清单**
#### `request.js` — HTTP 请求封装(核心)
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
- 用法:`import service from '@/utils/request'`
#### `date.js` — 日期格式化
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
- 用法:`import { formatTimeToStr } from '@/utils/date'`
#### `format.js` — 数据展示格式化(综合工具)
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
- `getDictFunc(type)` — 异步获取指定类型的字典数据
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
- `onDownloadFile(url)` — 触发文件下载
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
- `CreateUUID()` — 生成 UUID v4 字符串
- `getBaseUrl()` — 获取当前环境的 API BaseURL
- **以上所有格式化场景优先使用此文件中的工具函数**
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
#### `dictionary.js` — 字典数据获取
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
- 用法:`import { getDict } from '@/utils/dictionary'`
#### `stringFun.js` — 字符串处理
- `toUpperCase(str)` — 首字母转大写
- `toLowerCase(str)` — 首字母转小写
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
#### `params.js` — 系统参数获取
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
- **获取系统配置参数时,优先使用此工具**
- 用法:`import { getParams } from '@/utils/params'`
#### `bus.js` — 全局事件总线
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
- 用法:`import { emitter } from '@/utils/bus'`
#### `closeThisPage.js` — 关闭当前标签页
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
- **在需要程序化关闭当前页面时,必须使用此工具**
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
#### `downloadImg.js` — 图片下载
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
- **需要下载图片时,优先使用此工具**
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
#### `image.js` — 图片压缩
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
- **上传图片前需要做压缩处理时,使用此工具**
- 用法:`import ImageCompress from '@/utils/image'`
#### `event.js` — DOM 事件监听管理
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
- **手动操作 DOM 事件时,使用此工具以确保安全性**
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
#### `env.js` — 环境判断
- `isDev` — 是否为开发环境(Boolean)
- `isProd` — 是否为生产环境(Boolean)
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
- 用法:`import { isDev, isProd } from '@/utils/env'`
#### `doc.js` — 外部文档跳转
- `toDoc(url)` — 在新标签页打开指定 URL
- 用法:`import { toDoc } from '@/utils/doc'`
#### `fmtRouterTitle.js` — 路由标题格式化
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
#### `page.js` — 页面标题生成
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
- 用法:`import getPageTitle from '@/utils/page'`
#### `asyncRouter.js` — 异步路由处理
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
#### `btnAuth.js` — 按钮权限
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
- **实现按钮级别权限控制时,必须使用此 Hook**
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
### **使用强制要求**
| 场景 | 必须使用的工具 |
|------|----------------|
| 发送 HTTP 请求 | `@/utils/request` |
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式:ISO 8601标准
- **⚠️ 数据类型一致性**:
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型:feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范,建议AI在开发gin-vue-admin项目时:
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制
- 不要在这里扩写项目级规则
- 项目级规则变更时,先更新 `/AGENT.MD` 与 `/aiDoc/`
- 工具目录只保留薄适配层职责,不再保存独立 project rule 副本
- 代码读取约束以 `/AGENT.MD` 为准,其中包括:无论什么情况,都不要直接读取 `node_modules/` 中的代码
+15 -893
View File
@@ -1,902 +1,24 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
tool: codex
role: compatibility-adapter
canonical_source: /AGENT.MD
structured_context: /aiDoc
---
#### **角色与目标**
# Codex 规则适配层
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
本文件只用于兼容 Codex 现有的自动加载路径。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
## 真实规则入口
---
请按下面顺序读取:
### **🚀 重要提示:GVA Helper MCP 支持**
1. `/AGENT.MD`
2. `/aiDoc/README.md`
3. `/aiDoc/relations/`、`/aiDoc/modules/`、`/aiDoc/frontend-backend/`、`/aiDoc/examples/`、`/aiDoc/memory/` 中与当前任务相关的文件
**在开始任何GVA开发工作之前,请务必注意以下重要工作流程:**
## 适配层约束
1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得:
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住:GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**:Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**:
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体(DTOs)。
- **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能,避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范:
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态,必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用,提供统一的接口服务
- **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织,每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
- **必须**优先使用UnoCSS原子化类名进行样式设计
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名:kebab-case(短横线命名)
- 组件名:PascalCase(大驼峰)
- 变量名:camelCase(小驼峰)
- 常量名:UPPER_SNAKE_CASE(大写下划线)
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **⚠️ 前端工具库使用规范(强制)**
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
### **工具文件清单**
#### `request.js` — HTTP 请求封装(核心)
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
- 用法:`import service from '@/utils/request'`
#### `date.js` — 日期格式化
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
- 用法:`import { formatTimeToStr } from '@/utils/date'`
#### `format.js` — 数据展示格式化(综合工具)
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
- `getDictFunc(type)` — 异步获取指定类型的字典数据
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
- `onDownloadFile(url)` — 触发文件下载
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
- `CreateUUID()` — 生成 UUID v4 字符串
- `getBaseUrl()` — 获取当前环境的 API BaseURL
- **以上所有格式化场景优先使用此文件中的工具函数**
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
#### `dictionary.js` — 字典数据获取
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
- 用法:`import { getDict } from '@/utils/dictionary'`
#### `stringFun.js` — 字符串处理
- `toUpperCase(str)` — 首字母转大写
- `toLowerCase(str)` — 首字母转小写
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
#### `params.js` — 系统参数获取
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
- **获取系统配置参数时,优先使用此工具**
- 用法:`import { getParams } from '@/utils/params'`
#### `bus.js` — 全局事件总线
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
- 用法:`import { emitter } from '@/utils/bus'`
#### `closeThisPage.js` — 关闭当前标签页
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
- **在需要程序化关闭当前页面时,必须使用此工具**
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
#### `downloadImg.js` — 图片下载
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
- **需要下载图片时,优先使用此工具**
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
#### `image.js` — 图片压缩
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
- **上传图片前需要做压缩处理时,使用此工具**
- 用法:`import ImageCompress from '@/utils/image'`
#### `event.js` — DOM 事件监听管理
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
- **手动操作 DOM 事件时,使用此工具以确保安全性**
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
#### `env.js` — 环境判断
- `isDev` — 是否为开发环境(Boolean)
- `isProd` — 是否为生产环境(Boolean)
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
- 用法:`import { isDev, isProd } from '@/utils/env'`
#### `doc.js` — 外部文档跳转
- `toDoc(url)` — 在新标签页打开指定 URL
- 用法:`import { toDoc } from '@/utils/doc'`
#### `fmtRouterTitle.js` — 路由标题格式化
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
#### `page.js` — 页面标题生成
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
- 用法:`import getPageTitle from '@/utils/page'`
#### `asyncRouter.js` — 异步路由处理
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
#### `btnAuth.js` — 按钮权限
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
- **实现按钮级别权限控制时,必须使用此 Hook**
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
### **使用强制要求**
| 场景 | 必须使用的工具 |
|------|----------------|
| 发送 HTTP 请求 | `@/utils/request` |
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式:ISO 8601标准
- **⚠️ 数据类型一致性**:
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型:feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范,建议AI在开发gin-vue-admin项目时:
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制
- 不要在这里扩写项目级规则
- 项目级规则变更时,先更新 `/AGENT.MD` 与 `/aiDoc/`
- 工具目录只保留薄适配层职责,不再保存独立 project rule 副本
+15 -893
View File
@@ -1,902 +1,24 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
tool: cursor
role: compatibility-adapter
canonical_source: /AGENT.MD
structured_context: /aiDoc
---
#### **角色与目标**
# Cursor 规则适配层
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
本文件只用于兼容 Cursor 现有的自动加载路径。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
## 真实规则入口
---
请按下面顺序读取:
### **🚀 重要提示:GVA Helper MCP 支持**
1. `/AGENT.MD`
2. `/aiDoc/README.md`
3. `/aiDoc/relations/`、`/aiDoc/modules/`、`/aiDoc/frontend-backend/`、`/aiDoc/examples/`、`/aiDoc/memory/` 中与当前任务相关的文件
**在开始任何GVA开发工作之前,请务必注意以下重要工作流程:**
## 适配层约束
1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得:
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住:GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**:Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**:
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体(DTOs)。
- **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能,避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范:
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态,必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用,提供统一的接口服务
- **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织,每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
- **必须**优先使用UnoCSS原子化类名进行样式设计
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名:kebab-case(短横线命名)
- 组件名:PascalCase(大驼峰)
- 变量名:camelCase(小驼峰)
- 常量名:UPPER_SNAKE_CASE(大写下划线)
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **⚠️ 前端工具库使用规范(强制)**
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
### **工具文件清单**
#### `request.js` — HTTP 请求封装(核心)
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
- 用法:`import service from '@/utils/request'`
#### `date.js` — 日期格式化
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
- 用法:`import { formatTimeToStr } from '@/utils/date'`
#### `format.js` — 数据展示格式化(综合工具)
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
- `getDictFunc(type)` — 异步获取指定类型的字典数据
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
- `onDownloadFile(url)` — 触发文件下载
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
- `CreateUUID()` — 生成 UUID v4 字符串
- `getBaseUrl()` — 获取当前环境的 API BaseURL
- **以上所有格式化场景优先使用此文件中的工具函数**
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
#### `dictionary.js` — 字典数据获取
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
- 用法:`import { getDict } from '@/utils/dictionary'`
#### `stringFun.js` — 字符串处理
- `toUpperCase(str)` — 首字母转大写
- `toLowerCase(str)` — 首字母转小写
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
#### `params.js` — 系统参数获取
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
- **获取系统配置参数时,优先使用此工具**
- 用法:`import { getParams } from '@/utils/params'`
#### `bus.js` — 全局事件总线
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
- 用法:`import { emitter } from '@/utils/bus'`
#### `closeThisPage.js` — 关闭当前标签页
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
- **在需要程序化关闭当前页面时,必须使用此工具**
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
#### `downloadImg.js` — 图片下载
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
- **需要下载图片时,优先使用此工具**
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
#### `image.js` — 图片压缩
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
- **上传图片前需要做压缩处理时,使用此工具**
- 用法:`import ImageCompress from '@/utils/image'`
#### `event.js` — DOM 事件监听管理
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
- **手动操作 DOM 事件时,使用此工具以确保安全性**
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
#### `env.js` — 环境判断
- `isDev` — 是否为开发环境(Boolean)
- `isProd` — 是否为生产环境(Boolean)
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
- 用法:`import { isDev, isProd } from '@/utils/env'`
#### `doc.js` — 外部文档跳转
- `toDoc(url)` — 在新标签页打开指定 URL
- 用法:`import { toDoc } from '@/utils/doc'`
#### `fmtRouterTitle.js` — 路由标题格式化
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
#### `page.js` — 页面标题生成
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
- 用法:`import getPageTitle from '@/utils/page'`
#### `asyncRouter.js` — 异步路由处理
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
#### `btnAuth.js` — 按钮权限
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
- **实现按钮级别权限控制时,必须使用此 Hook**
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
### **使用强制要求**
| 场景 | 必须使用的工具 |
|------|----------------|
| 发送 HTTP 请求 | `@/utils/request` |
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式:ISO 8601标准
- **⚠️ 数据类型一致性**:
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型:feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范,建议AI在开发gin-vue-admin项目时:
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制
- 不要在这里扩写项目级规则
- 项目级规则变更时,先更新 `/AGENT.MD` 与 `/aiDoc/`
- 工具目录只保留薄适配层职责,不再保存独立 project rule 副本
+18 -18
View File
@@ -26,13 +26,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.16.0]
node-version: [20.19.0]
steps:
- name: Check out branch
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
@@ -51,13 +51,13 @@ jobs:
go-version: [1.22]
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v1
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Check out branch
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Download dependencies
run: |
@@ -83,11 +83,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.16.0]
node-version: [20.19.0]
go-version: [1.22]
steps:
- name: Check out branch
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Sed Config
env:
PROD: ${{ secrets.PROD }}
@@ -99,14 +99,14 @@ jobs:
sed -i "s/${PROD}/${TESTING}/g" web/.env.production
sed -i 's/${basePath}:${basePort}/${basePath}/g' web/src/view/systemTools/formCreate/index.vue
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build-Node
run: |
cd web/ && yarn install && yarn run build
- name: Use Go ${{ matrix.go-version }}
uses: actions/setup-go@v1
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Build-go
@@ -133,7 +133,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out branch
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Sed Config
env:
GVA_VERSION: ${{ inputs.gva_version }}
@@ -147,7 +147,7 @@ jobs:
git config --local user.name "github-actions[bot]"
git add . && git commit -m "release: v${GVA_VERSION##v}"
- name: Push
uses: ad-m/github-push-action@master
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # v1.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
@@ -194,28 +194,28 @@ jobs:
name: devops-prod
strategy:
matrix:
node-version: ['18.x']
node-version: ['20.x']
go-version: ['1.22']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: tag major and minor versions
run: |
echo " ${{ needs.release-please.outputs.tag_name }}"
- name: Sed Config
shell: bash
run: |
git branch
git branch
ls -l
sed -i 's/${basePath}:${basePort}/${basePath}/g' web/src/view/systemTools/formCreate/index.vue
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build-Node
run: |
cd web/ && yarn install && yarn run build
- name: Use Go ${{ matrix.go-version }}
uses: actions/setup-go@v1
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Build-go
@@ -246,9 +246,9 @@ jobs:
- release-please
steps:
- name: Check out branch
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Login to Aliyun Registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ${{ secrets.ALIYUN_REGISTRY }}
username: ${{ secrets.ALIYUN_DOCKERHUB_USER }}
+1
View File
@@ -41,3 +41,4 @@ web/pnpm-lock.yaml
*.db
*.sqlite
*.sqlite3
worktrees/
+15 -893
View File
@@ -1,902 +1,24 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
tool: trae
role: compatibility-adapter
canonical_source: /AGENT.MD
structured_context: /aiDoc
---
#### **角色与目标**
# Trae 规则适配层
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
本文件只用于兼容 Trae 现有的自动加载路径。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
## 真实规则入口
---
请按下面顺序读取:
### **🚀 重要提示:GVA Helper MCP 支持**
1. `/AGENT.MD`
2. `/aiDoc/README.md`
3. `/aiDoc/relations/`、`/aiDoc/modules/`、`/aiDoc/frontend-backend/`、`/aiDoc/examples/`、`/aiDoc/memory/` 中与当前任务相关的文件
**在开始任何GVA开发工作之前,请务必注意以下重要工作流程:**
## 适配层约束
1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得:
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住:GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**:Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**:
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体(DTOs)。
- **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能,避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范:
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态,必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用,提供统一的接口服务
- **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织,每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
- **必须**优先使用UnoCSS原子化类名进行样式设计
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名:kebab-case(短横线命名)
- 组件名:PascalCase(大驼峰)
- 变量名:camelCase(小驼峰)
- 常量名:UPPER_SNAKE_CASE(大写下划线)
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **⚠️ 前端工具库使用规范(强制)**
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。**
`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明:
### **工具文件清单**
#### `request.js` — HTTP 请求封装(核心)
- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截
- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios**
- 用法:`import service from '@/utils/request'`
#### `date.js` — 日期格式化
- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss`
- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串
- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑**
- 用法:`import { formatTimeToStr } from '@/utils/date'`
#### `format.js` — 数据展示格式化(综合工具)
- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示
- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串
- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label
- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找
- `getDictFunc(type)` — 异步获取指定类型的字典数据
- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀
- `onDownloadFile(url)` — 触发文件下载
- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式)
- `CreateUUID()` — 生成 UUID v4 字符串
- `getBaseUrl()` — 获取当前环境的 API BaseURL
- **以上所有格式化场景优先使用此文件中的工具函数**
- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'`
#### `dictionary.js` — 字典数据获取
- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求
- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具**
- 用法:`import { getDict } from '@/utils/dictionary'`
#### `stringFun.js` — 字符串处理
- `toUpperCase(str)` — 首字母转大写
- `toLowerCase(str)` — 首字母转小写
- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name`
- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName`
- **进行命名格式转换时必须使用此工具,禁止使用正则手写**
- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'`
#### `params.js` — 系统参数获取
- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存
- **获取系统配置参数时,优先使用此工具**
- 用法:`import { getParams } from '@/utils/params'`
#### `bus.js` — 全局事件总线
- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信
- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia**
- 用法:`import { emitter } from '@/utils/bus'`
#### `closeThisPage.js` — 关闭当前标签页
- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件)
- **在需要程序化关闭当前页面时,必须使用此工具**
- 用法:`import { closeThisPage } from '@/utils/closeThisPage'`
#### `downloadImg.js` — 图片下载
- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域
- **需要下载图片时,优先使用此工具**
- 用法:`import { downloadImage } from '@/utils/downloadImg'`
#### `image.js` — 图片压缩
- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小
- **上传图片前需要做压缩处理时,使用此工具**
- 用法:`import ImageCompress from '@/utils/image'`
#### `event.js` — DOM 事件监听管理
- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听
- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听
- **手动操作 DOM 事件时,使用此工具以确保安全性**
- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'`
#### `env.js` — 环境判断
- `isDev` — 是否为开发环境(Boolean)
- `isProd` — 是否为生产环境(Boolean)
- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`**
- 用法:`import { isDev, isProd } from '@/utils/env'`
#### `doc.js` — 外部文档跳转
- `toDoc(url)` — 在新标签页打开指定 URL
- 用法:`import { toDoc } from '@/utils/doc'`
#### `fmtRouterTitle.js` — 路由标题格式化
- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值)
- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'`
#### `page.js` — 页面标题生成
- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`)
- 用法:`import getPageTitle from '@/utils/page'`
#### `asyncRouter.js` — 异步路由处理
- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录
- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现**
- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'`
#### `btnAuth.js` — 按钮权限
- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示
- **实现按钮级别权限控制时,必须使用此 Hook**
- 用法:`import { useBtnAuth } from '@/utils/btnAuth'`
### **使用强制要求**
| 场景 | 必须使用的工具 |
|------|----------------|
| 发送 HTTP 请求 | `@/utils/request` |
| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` |
| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` |
| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` |
| 生成 UUID | `@/utils/format` 中的 `CreateUUID` |
| 驼峰/下划线命名转换 | `@/utils/stringFun` |
| 获取系统参数 | `@/utils/params` 中的 `getParams` |
| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` |
| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` |
| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` |
| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` |
| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` |
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式:ISO 8601标准
- **⚠️ 数据类型一致性**:
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型:feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范,建议AI在开发gin-vue-admin项目时:
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制
- 不要在这里扩写项目级规则
- 项目级规则变更时,先更新 `/AGENT.MD` 与 `/aiDoc/`
- 工具目录只保留薄适配层职责,不再保存独立 project rule 副本
+114
View File
@@ -0,0 +1,114 @@
# AGENT.MD
## 目的
本文件是本仓库内 AI 协作规则的唯一真源。
`.codex/`、`.claude/`、`.cursor/`、`.trae/` 下的规则文件仅作为兼容适配层,不能再次演变成各自独立维护的 project rule 副本。
## 读取顺序
按下面顺序加载项目上下文:
1. `AGENT.MD`
2. `aiDoc/README.md`
3. 按任务读取以下目录中的相关文件:
- `aiDoc/relations/`
- `aiDoc/modules/`
- `aiDoc/frontend-backend/`
- `aiDoc/examples/`
- `aiDoc/memory/`
4. 仅在当前工具确实依赖时,再读取工具目录下的适配文件
若内容冲突,以 `AGENT.MD` 为准。
## 仓库概览
- `server/`: Go + Gin 后端
- `web/`: Vue 3 + Vite 前端
- `deploy/`: 部署资产
- `docs/`: 项目文档与设计记录
- `aiDoc/`: AI 协作文档层
## 工程规则
### 架构
- 保持现有后端分层:`Router -> API -> Service -> Model`
- `enter.go` 继续作为分组注册与组合入口
- API 层处理 HTTP 相关逻辑,Service 层不要依赖 `gin.Context`
- 对外接口的 Swagger 注释必须和真实行为保持一致
- 优先沿用 gin-vue-admin 现有模式,不做无关的大改
### 前后端协作
- 明确请求与响应契约
- 保持统一响应结构:`{ code, data, msg }`
- 保持统一分页结构:`{ page, pageSize, total, list }`
- 前后端字段名和数据类型保持一致
- 优先复用 `web/src/utils/` 里的工具函数
- 涉及跨栈边界变更时,同步更新 `aiDoc/frontend-backend/`
### 插件与模块
- 后端插件放在 `server/plugin/<name>/`
- 前端插件放在 `web/src/plugin/<name>/`
- 稳定的模块职责、入口和边界说明放到 `aiDoc/modules/`
### 示例文档
- `aiDoc/examples/` 是讲解型示例层
- 示例文档不是要求逐字复制,而是告诉 AI 该如何按项目标准组织代码
- 当 AI 需要新增某一层文件时,应先阅读对应示例,再开始实现
### 记忆规则
- `aiDoc/memory/long-term/` 用于记录长期稳定的用户偏好、协作方式和跨任务约束
- `aiDoc/memory/business/` 用于记录每次用户提出的业务需求
- 这里的“业务需求”指新增或修改模块、接口、页面、流程、业务规则、插件能力等项目需求
- 当用户提出业务需求时,AI 必须新增或更新一条 `business` 记忆,并同步更新需求索引
- 当某条业务需求沉淀成长期稳定偏好时,再提炼到 `long-term`
- 长期记忆按主题一条一文件维护,业务记忆按需求一条一文件维护
- `README.md`、`project-memory.md`、`demand-index.md` 这类文件只承担说明或索引职责,不承载多条记忆正文
- 更新记忆时,只修改对应那条记忆文件与必要索引,不要通过重写汇总正文来覆盖其他历史记忆
### 文档维护
- `AGENT.MD` 只保留高层、稳定、跨任务的规则
- 细节上下文写入 `aiDoc/`
- 结构关系放在 `aiDoc/relations/`
- 示例写法放在 `aiDoc/examples/`
- 长期记忆与业务记忆放在 `aiDoc/memory/`
- 若项目级 AI 规则发生变化,先改 `AGENT.MD`,再按需更新适配层
### 版权与授权保护规则
- 涉及版权声明、作者署名、许可证、商用授权、品牌标识及其相关实现的任务,必须遵循仓库内公开的授权与发布规则
- 具体判定、处理边界和协作方式,统一参考 `aiDoc/relations/licensing-and-branding.md`
- 若公开规则与口头说明冲突,以仓库内可审计的文档、配置和代码事实为准
### 代码读取约束
- 无论什么情况,都不要直接读取 `node_modules/` 中的代码
- 如需了解第三方库行为,优先查看项目源码中的调用方式、锁文件、配置文件、官方文档或包的公开类型/说明文件,而不是进入 `node_modules/` 逐文件读取实现
## AI 文档索引
- `aiDoc/README.md`
- `aiDoc/relations/repo-profile.md`
- `aiDoc/relations/development-workflow.md`
- `aiDoc/relations/system-map.md`
- `aiDoc/modules/module-index.md`
- `aiDoc/modules/backend-layer-rules.md`
- `aiDoc/modules/plugin-development.md`
- `aiDoc/frontend-backend/boundary.md`
- `aiDoc/frontend-backend/frontend-rules.md`
- `aiDoc/frontend-backend/frontend-utils.md`
- `aiDoc/examples/README.md`
- `aiDoc/examples/backend/`
- `aiDoc/examples/frontend/`
- `aiDoc/examples/plugin/`
- `aiDoc/memory/README.md`
- `aiDoc/memory/project-memory.md`
- `aiDoc/memory/long-term/`
- `aiDoc/memory/business/`
+212 -169
View File
@@ -1,201 +1,244 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Business Source License 1.1
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Licensed Work: gin-vue-admin
Licensor: 北京翻转极光科技有限责任公司
1. Definitions.
Additional Use Grant:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The following uses are permitted without a commercial license:
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
(a) Personal Use: use by a natural person solely on their
own behalf, for personal or non-commercial purposes,
on infrastructure owned and funded entirely by that
individual, provided that all of the following
conditions are satisfied simultaneously:
(i) such use is not conducted in the individual's
capacity as an employee, contractor, agent, or
representative of any Legal Entity;
(ii) such use does not occur on equipment provided,
funded, reimbursed, or authorized by any Legal
Entity; and
(iii)such use is not conducted at the direction of,
or for the benefit of, any Legal Entity.
Personal Use includes individual personal projects
accessible via public internet, provided all
conditions above are satisfied.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
(b) Evaluation and Development Use: use solely for
internal technical evaluation, testing, or development
purposes, provided that all of the following
conditions are satisfied simultaneously:
(i) the Licensed Work does not process real user
data or real business data;
(ii) the Licensed Work is not used to support any
actual business operations or workflows;
(iii)the Licensed Work is not deployed or delivered
to any third party; and
(iv) the Licensed Work is not accessible via public
internet.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
(c) Educational Use: use by educational institutions or
individuals for non-commercial teaching, research,
or academic purposes, provided such use is not
conducted on behalf of or for the benefit of any
commercial Legal Entity.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Any use not expressly described in (a), (b), or (c)
above constitutes Production Use and requires a
commercial license.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
Change Date: The Change Date for each version is the
date falling three (3) years after the date on which
that version was first publicly released. The release
date of each version shall be determined by reference
to the version release records published by the
Licensor in the official code repository or on the
official website.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
Change License: Apache License, Version 2.0
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
------------------------------------------------------------------------
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
Definitions
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
"Licensed Work" means the software and associated
documentation files made available under this License.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
"Licensor" means the copyright holder(s) of the
Licensed Work.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
"Legal Entity" means any corporation, partnership,
limited liability company, sole proprietorship,
individually registered business, or other organization
recognized as a legal entity under applicable law,
including any entity engaged in commercial activity
regardless of profit motive.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
"Production Use" means any of the following:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(a) Deployment for External Parties: deploying,
installing, hosting, configuring, integrating, or
otherwise making the Licensed Work available to any
third party — whether or not for compensation,
whether or not under a separate agreement, and
whether or not the deployment is accessible via
public internet — including but not limited to
providing the Licensed Work as part of a service,
product, solution, or deliverable to any client,
customer, or other party;
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(b) Organizational Deployment: any installation,
execution, or operation of the Licensed Work in
any of the following ways:
(i) running on infrastructure owned, leased,
managed, or controlled by a Legal Entity,
including but not limited to servers, virtual
machines, containers, cloud instances, or
shared computing resources; or
(ii) by an individual acting in their capacity as
an employee, contractor, agent, or
representative of a Legal Entity, where such
use occurs on equipment provided by, or under
the direction or authorization of, such Legal
Entity; or
(iii)in a manner that is funded by, reimbursed by,
or conducted at the expense of a Legal Entity;
regardless of whether such use processes real data,
is integrated into any system, or is accessible
via any network;
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(c) Commercial Integration: incorporating the Licensed
Work into any product, service, or system that is
sold, licensed, subscribed to, or otherwise
monetized, directly or indirectly.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
"Non-Production Use" means use that satisfies all
conditions specified in Additional Use Grant section
(b) above simultaneously. To facilitate compliance,
users are advised to retain written records sufficient
to demonstrate that all conditions of Non-Production
Use are satisfied. The Licensor shall bear the burden
of proof in accordance with applicable law when
asserting any claim of breach or infringement.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
"Third Party" means any natural person or Legal Entity
other than the user themselves. For the avoidance of
doubt, a Legal Entity and its affiliates, subsidiaries,
parent companies, or related entities each constitute
separate parties for the purposes of this License,
unless they are incorporated as a single unified legal
entity under applicable law.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
------------------------------------------------------------------------
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
Terms
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
The Licensor hereby grants you the right to copy,
modify, create derivative works, redistribute, and
make Non-Production Use and Personal Use of the
Licensed Work. The foregoing rights are subject to
the following conditions:
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
Section 1 Production Use Restriction
You may not make Production Use of the Licensed Work
except as expressly permitted by a separate commercial
license obtained from the Licensor. For the avoidance
of doubt:
(a) the scale of deployment, number of users,
transaction volume, or revenue generated does not
affect the applicability of this restriction;
(b) absence of commercial intent or profit motive does
not exempt any use from this restriction; and
(c) any Production Use, regardless of scale, intent,
or organizational structure, requires a commercial
license.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
Section 2 Automatic Termination
Any use of the Licensed Work in violation of this
License will automatically terminate your rights
under this License for the current version and all
other versions of the Licensed Work released after
the date of first violation. For the avoidance of
doubt, versions of the Licensed Work released under
a prior open source license before the adoption of
this License remain governed by that prior license
and are not affected by termination under this
section.
END OF TERMS AND CONDITIONS
Section 3 Distribution Requirements
You must provide all recipients of the Licensed Work
with a copy of this License.
APPENDIX: How to apply the Apache License to your work.
Section 4 Modification Notices
You must cause any modified files to carry prominent
notices stating that you changed the files and the
date of such changes.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Section 5 Notice Preservation
You must not remove, alter, or obscure any licensing,
copyright, attribution, or other notices of the
Licensor in the Licensed Work except as expressly
permitted by a separate commercial license.
Copyright 2019 北京翻转极光科技有限责任公司
Section 6 No Trademark License
This License does not grant you any right in the
trademarks, service marks, trade names, or logos
of the Licensor.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Section 7 Disclaimer of Warranties
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE
LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS.
LICENSOR HEREBY DISCLAIMS ALL WARRANTIES, WHETHER
EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, AND
SPECIFICALLY DISCLAIMS ALL IMPLIED WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
TITLE, AND NON-INFRINGEMENT. THE FOREGOING DISCLAIMER
SHALL NOT APPLY TO THE EXTENT PROHIBITED BY
APPLICABLE MANDATORY LAW.
http://www.apache.org/licenses/LICENSE-2.0
Section 8 Limitation of Liability
TO THE EXTENT PERMITTED BY APPLICABLE LAW, IN NO
EVENT WILL THE LICENSOR BE LIABLE FOR ANY INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE
OR THE USE OR INABILITY TO USE THE LICENSED WORK,
EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES. THE FOREGOING LIMITATION
SHALL NOT APPLY TO THE EXTENT PROHIBITED BY
APPLICABLE MANDATORY LAW.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Section 9 Change Date
If the Change Date applicable to a specific version
has passed, this License shall, with respect to that
version, be replaced by the Change License, and from
that date you may use that version of the Licensed
Work under the terms of the Change License.
Section 10 Governing Interpretation
This License shall be interpreted in accordance with
its plain meaning and the overall purpose of this
License. The Licensor hereby declares that the core
purpose of this License is to require a commercial
license for all Production Use as defined herein.
This License shall be interpreted in accordance with
applicable law.
This License constitutes a standard form contract.
The Licensor has drawn the attention of users to all
terms that exclude or limit the Licensor's liability,
restrict users' rights, or impose obligations on
users, by presenting such terms prominently in this
License, in fulfillment of the Licensor's obligation
to give notice and explanation as required by
applicable law.
------------------------------------------------------------------------
For information about the Business Source License,
see https://mariadb.com/bsl11/
For commercial licensing inquiries, contact:
Beijing Flipped Aurora Technology Co., Ltd.
(北京翻转极光科技有限责任公司)
Email: business@gin-vue-admin.com
Website: https://www.gin-vue-admin.com
+89 -216
View File
@@ -1,29 +1,38 @@
<div align=center>
<img src="http://qmplusimg.henrongyi.top/gvalogo.jpg" width="300" height="300" />
<div align="center">
<img src="http://qmplusimg.henrongyi.top/gvalogo.jpg" width="300" height="300" />
</div>
<div align=center>
<img src="https://img.shields.io/badge/golang-1.20-blue"/>
<img src="https://img.shields.io/badge/gin-1.9.1-lightBlue"/>
<img src="https://img.shields.io/badge/vue-3.3.4-brightgreen"/>
<img src="https://img.shields.io/badge/element--plus-2.3.8-green"/>
<img src="https://img.shields.io/badge/gorm-1.25.2-red"/>
<img src="https://gitcode.com/flipped-aurora/gin-vue-admin/star/badge.svg"/>
<div align="center">
<img src="https://img.shields.io/badge/golang-1.20-blue" />
<img src="https://img.shields.io/badge/gin-1.9.1-lightBlue" />
<img src="https://img.shields.io/badge/vue-3.3.4-brightgreen" />
<img src="https://img.shields.io/badge/element--plus-2.3.8-green" />
<img src="https://img.shields.io/badge/gorm-1.25.2-red" />
<img src="https://gitcode.com/flipped-aurora/gin-vue-admin/star/badge.svg" />
</div>
<div align=center>
<a href="https://trendshift.io/repositories/3250" target="_blank"><img src="https://trendshift.io/api/badge/repositories/3250" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<br>
<div align="center">
<a href="https://trendshift.io/repositories/3250" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/3250" alt="Trendshift" width="250" height="55" />
</a>
</div>
[English](./README-en.md) | 简体中文
<p align="center">
<a href="./README-en.md">English</a> | 简体中文
</p>
## ✨一分钟生成前后端基础代码
## 支持 Claw 生态
[🦞 GvaClaw](https://plugin.gin-vue-admin.com/details/159)
## 一分钟生成前后端基础代码
<table>
<tr>
<td width="250">
<p>⭐️ <a href="https://www.bilibili.com/video/BV1B3htzqEf1/?spm_id_from=333.1387.homepage.video_card.click" target="__blank"> 高度适配AI编辑器的MCP </a></p>
<p>⭐️ <a href="https://www.bilibili.com/video/BV1B3htzqEf1/?spm_id_from=333.1387.homepage.video_card.click" target="__blank"> 高度适配AI编辑器的MCP </a></p>
<p>📄 创建基础模板</p>
<p>🤖 AI生成结构</p>
<p>⏰ 生成代码</p>
@@ -31,41 +40,35 @@
<p>🎉 基础CURD生成完成</p>
</td>
<td>
<video src="https://private-user-images.githubusercontent.com/165128580/384700666-4d039215-af29-4f86-bb4f-60dbab38f58e.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzEyNTIxNDYsIm5iZiI6MTczMTI1MTg0NiwicGF0aCI6Ii8xNjUxMjg1ODAvMzg0NzAwNjY2LTRkMDM5MjE1LWFmMjktNGY4Ni1iYjRmLTYwZGJhYjM4ZjU4ZS5tcDQ_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMTEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTExMFQxNTE3MjZaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00NjJkMDcwZjJkMjAyMmU1N2I2MzQxY2RhODFlNzgzNGRiMDFhMmY2NTYyM2ZmODdhNDVmMWE1NzlhMDdlOTI5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.ZJbswpLzF2RHjemcGirKOP0L1fvpl3FUqIiQ_-yjeUo" data-canonical-src="https://private-user-images.githubusercontent.com/165128580/384700666-4d039215-af29-4f86-bb4f-60dbab38f58e.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzEyNTIxNDYsIm5iZiI6MTczMTI1MTg0NiwicGF0aCI6Ii8xNjUxMjg1ODAvMzg0NzAwNjY2LTRkMDM5MjE1LWFmMjktNGY4Ni1iYjRmLTYwZGJhYjM4ZjU4ZS5tcDQ_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMTEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTExMFQxNTE3MjZaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00NjJkMDcwZjJkMjAyMmU1N2I2MzQxY2RhODFlNzgzNGRiMDFhMmY2NTYyM2ZmODdhNDVmMWE1NzlhMDdlOTI5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.ZJbswpLzF2RHjemcGirKOP0L1fvpl3FUqIiQ_-yjeUo" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px">
</video>
<video src="https://private-user-images.githubusercontent.com/165128580/384700666-4d039215-af29-4f86-bb4f-60dbab38f58e.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzEyNTIxNDYsIm5iZiI6MTczMTI1MTg0NiwicGF0aCI6Ii8xNjUxMjg1ODAvMzg0NzAwNjY2LTRkMDM5MjE1LWFmMjktNGY4Ni1iYjRmLTYwZGJhYjM4ZjU4ZS5tcDQ_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMTEwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTExMFQxNTE3MjZaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00NjJkMDcwZjJkMjAyMmU1N2I2MzQxY2RhODFlNzgzNGRiMDFhMmY2NTYyM2ZmODdhNDVmMWE1NzlhMDdlOTI5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.ZJbswpLzF2RHjemcGirKOP0L1fvpl3FUqIiQ_-yjeUo" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px">
</video>
</td>
</tr>
</table>
## 项目文档
# 项目文档
[在线文档](https://www.gin-vue-admin.com) : https://www.gin-vue-admin.com
- **在线文档**: [https://www.gin-vue-admin.com](https://www.gin-vue-admin.com)
- **初始化指南**: [点击查看](https://www.gin-vue-admin.com/guide/start-quickly/initialization.html)
- **从环境到部署教学视频**: [bilibili](https://www.bilibili.com/video/BV1Rg411u7xH)
- **开发教学**: [开发教学说明](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (贡献者: <a href="https://github.com/LLemonGreen">LLemonGreen</a> And <a href="https://github.com/fkk0509">Fann</a>)
- **交流社区**: [点击进入](https://support.qq.com/products/371961)
- **插件市场**: [点击进入](https://plugin.gin-vue-admin.com/)
- **软件著作权证书**: [查看证书](https://www.gin-vue-admin.com/copyright.pdf)
[初始化](https://www.gin-vue-admin.com/guide/start-quickly/initialization.html)
[从环境到部署教学视频](https://www.bilibili.com/video/BV1Rg411u7xH)
## 重要提示
[开发教学](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (贡献者: <a href="https://github.com/LLemonGreen">LLemonGreen</a> And <a href="https://github.com/fkk0509">Fann</a>)
1. 本项目从起步到开发到部署均有文档和详细视频教程。
2. 本项目需要您有一定的 golang 和 vue 基础。
3. 您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html)。
4. 本项目采用 BSL 1.1 授权。非商用、学习、评估等用途请遵循仓库许可证;如将此项目用于商用或生产环境,或需要移除版权与署名信息,请[购买授权](https://plugin.gin-vue-admin.com/license)。您需保留仓库、日志和代码中的版权声明信息。
[交流社区](https://support.qq.com/products/371961)
<br>
[插件市场](https://plugin.gin-vue-admin.com/)
[软件著作权证书](https://www.gin-vue-admin.com/copyright.pdf)
# 重要提示
1.本项目从起步到开发到部署均有文档和详细视频教程
2.本项目需要您有一定的golang和vue基础
3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html)
4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html)
\
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="1000">
<img src="https://qmplusimg.henrongyi.top/openSource/dashboard.jpg" width="1000">
<div align="center">
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="49%" alt="登录界面" style="border-radius: 8px;" />
<img src="https://qmplusimg.henrongyi.top/openSource/dashboard.jpg" width="49%" alt="仪表盘" style="border-radius: 8px;" />
</div>
## 1. 基本介绍
@@ -73,13 +76,12 @@
> Gin-vue-admin是一个基于 [vue](https://vuejs.org) 和 [gin](https://gin-gonic.com) 开发的全栈前后端分离的开发基础平台,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。
[在线预览](http://demo.gin-vue-admin.com): http://demo.gin-vue-admin.com
测试用户名:admin
测试密码:123456
- **在线预览**: [http://demo.gin-vue-admin.com](http://demo.gin-vue-admin.com)
- **测试用户名**: `admin`
- **测试密码**: `123456`
### 1.2 贡献指南
Hi! 首先感谢你使用 gin-vue-admin。
Gin-vue-admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。
@@ -88,32 +90,25 @@ Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-adm
#### 1.2.1 Issue 规范
- issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。
- 在提交 issue 之前,请搜索相关内容是否已被提出。
#### 1.2.2 Pull Request 规范
- 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。
- commit 信息要以`[文件名]: 描述信息` 的形式填写,例如 `README.md: fix xxx bug`。
- commit 信息要以 `[文件名]: 描述信息` 的形式填写,例如 `README.md: fix xxx bug`。
- 如果是修复 bug,请在 PR 中给出描述信息。
- 合并代码需要两名维护人员参与:一人进行 review 后 approve,另一人再次 review,通过后即可合并。
## 2. 使用说明
```
- node版本 > v18.16.0
- golang版本 >= v1.22
- IDE推荐:Goland
```
### 2.1 server项目
使用 `Goland` 等编辑工具,打开server目录,不可以打开 gin-vue-admin 根目录
```bash
# 克隆项目
git clone https://github.com/flipped-aurora/gin-vue-admin.git
# 进入server文件夹
@@ -124,7 +119,6 @@ go generate
# 运行
go run .
```
### 2.2 web项目
@@ -144,32 +138,29 @@ npm run serve
#### 2.3.1 安装 swagger
``` shell
```bash
go install github.com/swaggo/swag/cmd/swag@latest
```
#### 2.3.2 生成API文档
```` shell
```bash
cd server
swag init
````
```
> 执行上面的命令后,server目录下会出现docs文件夹里的 `docs.go`, `swagger.json`, `swagger.yaml` 三个文件更新,启动go服务之后, 在浏览器输入 [http://localhost:8888/swagger/index.html](http://localhost:8888/swagger/index.html) 即可查看swagger文档
### 2.4 VSCode工作区
#### 2.4.1 开发
使用`VSCode`打开根目录下的工作区文件`gin-vue-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。
使用 `VSCode` 打开根目录下的工作区文件 `gin-vue-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。
#### 2.4.2 运行/调试
在运行和调试中也可以看到三个task:`Backend`、`Frontend`、`Both (Backend & Frontend)`。运行`Both (Backend & Frontend)`可以同时启动前后端项目。
在运行和调试中也可以看到三个 task:`Backend`、`Frontend`、`Both (Backend & Frontend)`。运行 `Both (Backend & Frontend)` 可以同时启动前后端项目。
#### 2.4.3 settings
在工作区配置文件中有`go.toolsEnvVars`字段,是用于`VSCode`自身的go工具环境变量。此外在多go版本的系统中,可以通过`gopath`、`go.goroot`指定运行版本。
在工作区配置文件中有 `go.toolsEnvVars` 字段,是用于 `VSCode` 自身的 go 工具环境变量。此外在多 go 版本的系统中,可以通过 `gopath`、`go.goroot` 指定运行版本。
```json
"go.gopath": null,
@@ -180,10 +171,10 @@ swag init
- 前端:用基于 [Vue](https://vuejs.org) 的 [Element](https://github.com/ElemeFE/element) 构建基础页面。
- 后端:用 [Gin](https://gin-gonic.com/) 快速搭建基础restful风格API,[Gin](https://gin-gonic.com/) 是一个go语言编写的Web框架。
- 数据库:采用`MySql` > (5.7) 版本 数据库引擎 InnoDB,使用 [gorm](http://gorm.cn) 实现对数据库的基本操作。
- 缓存:使用`Redis`实现记录当前活跃用户的`jwt`令牌并实现多点登录限制。
- API文档:使用`Swagger`构建自动化文档。
- 配置文件:使用 [fsnotify](https://github.com/fsnotify/fsnotify) 和 [viper](https://github.com/spf13/viper) 实现`yaml`格式的配置文件。
- 数据库:采用 `MySQL` 或 `MariaDB`(5.7+),数据库引擎 InnoDB,使用 [gorm](http://gorm.cn) 实现对数据库的基本操作。
- 缓存:使用 `Redis` 实现记录当前活跃用户的 jwt 令牌并实现多点登录限制。
- API文档:使用 `Swagger` 构建自动化文档。
- 配置文件:使用 [fsnotify](https://github.com/fsnotify/fsnotify) 和 [viper](https://github.com/spf13/viper) 实现 yaml 格式的配置文件。
- 日志:使用 [zap](https://github.com/uber-go/zap) 实现日志记录。
## 4. 项目架构
@@ -192,188 +183,70 @@ swag init
![系统架构图](http://qmplusimg.henrongyi.top/gva/gin-vue-admin.png)
### 4.2 前端详细设计图 (提供者:<a href="https://github.com/baobeisuper">baobeisuper</a>)
### 4.2 详细设计图 (提供者:<a href="https://github.com/baobeisuper">baobeisuper</a>)
![前端详细设计图](http://qmplusimg.henrongyi.top/naotu.png)
![详细设计图](http://qmplusimg.henrongyi.top/naotu.png)
### 4.3 目录结构
```
├── server
├── api (api层)
│ └── v1 (v1版本接口)
├── config (配置包)
├── core (核心文件)
├── docs (swagger文档目录)
├── global (全局对象)
├── initialize (初始化)
│ └── internal (初始化内部函数)
├── middleware (中间件层)
├── model (模型层)
│ ├── request (入参结构体)
│ └── response (出参结构体)
├── packfile (静态文件打包)
├── resource (静态资源文件夹)
│ ├── excel (excel导入导出默认路径)
│ ├── page (表单生成器)
│ └── template (模板)
├── router (路由层)
├── service (service层)
├── source (source层)
└── utils (工具包)
├── timer (定时器接口封装)
└── upload (oss接口封装)
web
├── babel.config.js
├── Dockerfile
├── favicon.ico
├── index.html -- 主页面
├── limit.js -- 助手代码
├── package.json -- 包管理器代码
├── src -- 源代码
│ ├── api -- api 组
│ ├── App.vue -- 主页面
│ ├── assets -- 静态资源
│ ├── components -- 全局组件
│ ├── core -- gva 组件包
│ │ ├── config.js -- gva网站配置文件
│ │ ├── gin-vue-admin.js -- 注册欢迎文件
│ │ └── global.js -- 统一导入文件
│ ├── directive -- v-auth 注册文件
│ ├── main.js -- 主文件
│ ├── permission.js -- 路由中间件
│ ├── pinia -- pinia 状态管理器,取代vuex
│ │ ├── index.js -- 入口文件
│ │ └── modules -- modules
│ │ ├── dictionary.js
│ │ ├── router.js
│ │ └── user.js
│ ├── router -- 路由声明文件
│ │ └── index.js
│ ├── style -- 全局样式
│ │ ├── base.scss
│ │ ├── basics.scss
│ │ ├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式
│ │ ├── iconfont.css -- 顶部几个icon的样式文件
│ │ ├── main.scss
│ │ ├── mobile.scss
│ │ └── newLogin.scss
│ ├── utils -- 方法包库
│ │ ├── asyncRouter.js -- 动态路由相关
│ │ ├── btnAuth.js -- 动态权限按钮相关
│ │ ├── bus.js -- 全局mitt声明文件
│ │ ├── date.js -- 日期相关
│ │ ├── dictionary.js -- 获取字典方法
│ │ ├── downloadImg.js -- 下载图片方法
│ │ ├── format.js -- 格式整理相关
│ │ ├── image.js -- 图片相关方法
│ │ ├── page.js -- 设置页面标题
│ │ ├── request.js -- 请求
│ │ └── stringFun.js -- 字符串文件
| ├── view -- 主要view代码
| | ├── about -- 关于我们
| | ├── dashboard -- 面板
| | ├── error -- 错误
| | ├── example --上传案例
| | ├── iconList -- icon列表
| | ├── init -- 初始化数据
| | | ├── index -- 新版本
| | | ├── init -- 旧版本
| | ├── layout -- layout约束页面
| | | ├── aside
| | | ├── bottomInfo -- bottomInfo
| | | ├── screenfull -- 全屏设置
| | | ├── setting -- 系统设置
| | | └── index.vue -- base 约束
| | ├── login --登录
| | ├── person --个人中心
| | ├── superAdmin -- 超级管理员操作
| | ├── system -- 系统检测页面
| | ├── systemTools -- 系统配置相关页面
| | └── routerHolder.vue -- page 入口页面
├── vite.config.js -- vite 配置文件
└── yarn.lock
```
*(详细目录结构请见源码...)*
## 5. 主要功能
- 权限管理:基于`jwt`和`casbin`实现的权限管理。
- 文件上传下载:实现基于`七牛云`, `阿里云`, `腾讯云` 的文件上传操作(请开发自己去各个平台的申请对应 `token` 或者对应`key`)。
- 分页封装:前端使用 `mixins` 封装分页,分页方法调用 `mixins` 即可。
- 权限管理:基于 `jwt` 和 `casbin` 实现的权限管理。
- 文件上传下载:实现基于 `七牛云`, `阿里云`, `腾讯云` 的文件上传操作。
- 分页封装:前端使用 `mixins` 封装分页,分页方法调用即可。
- 用户管理:系统管理员分配用户角色和角色权限。
- 角色管理:创建权限控制的主要对象,可以给角色分配不同api权限和菜单权限。
- 角色管理:创建权限控制的主要对象,可以给角色分配不同 api 权限和菜单权限。
- 菜单管理:实现用户动态菜单配置,实现不同角色不同菜单。
- api管理:不同用户可调用的api接口的权限不同。
- api管理:不同用户可调用的 api 接口的权限不同。
- 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。
- 条件搜索:增加条件搜索示例。
- restful示例:可以参考用户管理模块中的示例API。
- 前端文件参考: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-vue-admin/blob/master/web/src/view/superAdmin/api/api.vue)
- 后台文件参考: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/router/sys_api.go)
- 多点登录限制:需要在`config.yaml`中把`system`中的`use-multipoint`修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。
- restful示例:可以参考用户管理模块中的示例 API。
- 多点登录限制:借助 Redis 配合对应配置限制多端登录状态。
- 分片上传:提供文件分片上传和大文件分片上传功能示例。
- 表单生成器:表单生成器借助 [@Variant Form](https://github.com/vform666/variant-form) 。
- 代码生成器:后台基础逻辑以及简单curd的代码生成器。
- 表单生成器:表单生成器借助 [@Variant Form](https://github.com/vform666/variant-form)。
- 代码生成器:后台基础逻辑以及简单 curd 的代码生成器。
## 6. 知识库
## 6.1 团队博客
### 6.1 团队博客
> https://www.yuque.com/flipped-aurora
>
>内有前端框架教学视频。如果觉得项目对您有所帮助可以添加我的个人微信:shouzi_1994,欢迎您提出宝贵的需求。
> [https://www.yuque.com/flipped-aurora](https://www.yuque.com/flipped-aurora)
> 内有前端框架教学视频。如果觉得项目对您有所帮助可以添加我的个人微信: shouzi_1994。
## 6.2 教学视频
### 6.2 教学视频
(1)手把手教学视频
> https://www.bilibili.com/video/BV1Rg411u7xH/
(2)后端目录结构调整介绍以及使用方法
> https://www.bilibili.com/video/BV1x44y117TT/
(3)golang基础教学视频
> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=108884
(4)gin框架基础教学
> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0
(5)gin-vue-admin 版本更新介绍视频
> bilibili:https://www.bilibili.com/video/BV1kv4y1g7nT
1. **手把手教学视频**: [https://www.bilibili.com/video/BV1Rg411u7xH/](https://www.bilibili.com/video/BV1Rg411u7xH/)
2. **后端目录结构调整介绍以及使用方法**: [https://www.bilibili.com/video/BV1x44y117TT/](https://www.bilibili.com/video/BV1x44y117TT/)
3. **golang基础教学视频**: [bilibili](https://space.bilibili.com/322210472/channel/detail?cid=108884)
4. **gin框架基础教学**: [bilibili](https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0)
5. **gin-vue-admin 版本更新介绍视频**: [bilibili](https://www.bilibili.com/video/BV1kv4y1g7nT)
## 7. 联系方式
### 7.1 技术群
### QQ交流群:971857775
- **QQ交流群**: `971857775`
- **[关于我们](https://www.gin-vue-admin.com/about/join.html)**
### 微信交流群
| 微信 |
| :---: |
| <img width="150" src="http://qmplusimg.henrongyi.top/qrjjz.png">
防止广告进群,添加微信,输入以下代码执行结果(请勿转码为string)
<img width="150" src="http://qmplusimg.henrongyi.top/qrjjz.png">
```
防止广告进群,添加微信,输入以下代码执行结果(请勿转码为string):
```go
str := "5Yqg5YWlR1ZB5Lqk5rWB576k"
decodeBytes, err := base64.StdEncoding.DecodeString(str)
fmt.Println(decodeBytes, err)
```
### [关于我们](https://www.gin-vue-admin.com/about/join.html)
## 8. 贡献者
感谢您对gin-vue-admin的贡献!
<a href="https://openomy.app/github/flipped-aurora/gin-vue-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=flipped-aurora/gin-vue-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
</a>
## 9. 捐赠
@@ -381,5 +254,5 @@ fmt.Println(decodeBytes, err)
## 10. 注意事项
请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license)
未授权去除版权信息将依法追究法律责任
请遵守 BSL 1.1 许可证要求并保留作品声明;商用、生产使用或去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license)
未授权超出许可范围使用将依法追究法律责任。
+41
View File
@@ -0,0 +1,41 @@
# aiDoc
`aiDoc/` 是本仓库的结构化 AI 文档层,用于把长期有效的项目上下文从工具目录中抽离出来,并按主题拆分成可维护的约束文档。
## 使用方式
1. 先读取 `AGENT.MD`
2. 再查看本索引文件
3. 按任务只打开相关子目录
4. 不再把项目级规则塞回 `.codex/`、`.claude/`、`.cursor/`、`.trae/`
## 目录说明
- `relations/`: 仓库结构、技术栈、依赖关系、开发流程
- `modules/`: 后端分层规则、插件结构、模块职责
- `frontend-backend/`: 前后端契约、前端规范、工具函数复用规则
- `examples/`: 讲解型示例,告诉 AI 每一层应该按什么标准组织和书写
- `memory/`: AI 记忆层,拆分为长期记忆与业务记忆
## 常用入口
- `relations/repo-profile.md`: 项目定位、技术栈、核心特性
- `relations/development-workflow.md`: GVA 开发流程、分支与提交规范
- `relations/licensing-and-branding.md`: 版权、授权与品牌相关任务的协作边界
- `modules/backend-layer-rules.md`: 后端分层、`enter.go`、统一响应、Swagger 约束
- `modules/plugin-development.md`: 前后端插件结构与开发流程
- `frontend-backend/boundary.md`: 前后端契约与字段类型约束
- `frontend-backend/frontend-rules.md`: 前端代码、状态、路由、样式规范
- `frontend-backend/frontend-utils.md`: `src/utils/` 工具库的强制复用规则
- `examples/README.md`: 示例层总入口
- `memory/project-memory.md`: 记忆层总入口
- `memory/long-term/`: 长期记忆
- `memory/business/`: 业务需求记忆
## 维护原则
- 稳定规则放这里,不放到工具私有目录里
- 临时会话草稿不要入库,只有变成长期知识时才记录
- 适用于所有 AI 的项目级规则,先写进 `AGENT.MD`
- 细节说明再拆到 `aiDoc/` 对应子目录
- 只要用户提出业务需求,就要同步更新 `memory/business/`
+46
View File
@@ -0,0 +1,46 @@
# 示例层
`aiDoc/examples/` 是讲解型示例层。
这里的文档不是要求 AI 逐字复制代码,而是告诉 AI:
- 某一层文件通常该承担什么职责
- 在这个仓库里推荐用什么组织方式
- 为什么要这样写
- 常见错误是什么
- 真正可以参考哪些现成文件
## 读取建议
### 新增后端能力时
建议按这个顺序读:
1. `backend/request-example.md`
2. `backend/model-example.md`
3. `backend/service-example.md`
4. `backend/api-example.md`
5. `backend/router-example.md`
6. `backend/enter-go-example.md`
### 新增前端能力时
建议按这个顺序读:
1. `frontend/api-example.md`
2. `frontend/pinia-example.md`
3. `frontend/view-example.md`
4. `frontend/utils-usage-example.md`
### 新增插件时
建议至少先读:
1. `plugin/full-plugin-example.md`
2. `backend/plugin-go-example.md`
## 使用原则
- 优先参考仓库内真实文件,其次参考本目录的示例文档
- 示例关注结构与写法,不替代业务设计
- 如果真实代码与旧示例不一致,应以当前仓库真实结构为准,并及时更新示例
+23
View File
@@ -0,0 +1,23 @@
# 后端示例
这个目录聚焦 gin-vue-admin 后端的分层写法。
## 包含内容
- `request-example.md`: 请求结构写法
- `model-example.md`: 数据模型写法
- `service-example.md`: 业务层写法
- `api-example.md`: API 层写法
- `router-example.md`: 路由层写法
- `enter-go-example.md`: `enter.go` 分组聚合写法
- `plugin-go-example.md`: 插件入口写法
## 适用范围
当 AI 需要新增或修改以下文件时,应优先阅读本目录:
- `server/model/**`
- `server/service/**`
- `server/api/**`
- `server/router/**`
- `server/plugin/**`
+103
View File
@@ -0,0 +1,103 @@
# API 示例
## 这个文件负责什么
API 层负责接收 HTTP 请求、从合适的位置提取参数、调用 Service、统一返回响应,并补全 Swagger 注释。
## 什么时候应该这样写
- 新增对外接口
- 为某个模块补 CRUD 接口
- 需要统一响应结构和错误处理
## 核心原则
参数从哪里取,不是由“AI 习惯”决定,而是由下面几件事共同决定:
1. 前端怎么传
2. 接口协议怎么约定
3. 当前逻辑到底需要什么数据
4. 这个数据放在哪个位置更合理、更安全
也就是说,API 层不应该机械地一律使用 `ShouldBindJSON`,而应该先判断参数来源,再选择对应取法。
## 常见参数来源与推荐取法
- JSON body: `ShouldBindJSON`
- Query string: `ShouldBindQuery`、`c.Query(...)`、`c.DefaultQuery(...)`
- Path params: `c.Param(...)`
- `multipart/form-data`: `c.FormFile(...)`、`c.DefaultPostForm(...)`、`c.Request.FormValue(...)`
- Header: `c.GetHeader(...)`、`c.Request.Header.Get(...)`
- Cookie: `c.Cookie(...)`
## 推荐写法示例
下面这个示例演示的是 `POST + JSON body` 场景,所以这里使用 `ShouldBindJSON`。
```go
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/gin-gonic/gin"
)
// GetOrderList
// @Tags Order
// @Summary 分页获取订单列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body systemReq.OrderSearch true "分页和筛选参数"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "返回列表、总数、分页信息"
// @Router /order/getOrderList [post]
func (o *OrderApi) GetOrderList(c *gin.Context) {
var pageInfo systemReq.OrderSearch
if err := c.ShouldBindJSON(&pageInfo); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, total, err := orderService.GetOrderList(pageInfo)
if err != nil {
response.FailWithMessage("获取失败", c)
return
}
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: pageInfo.Page,
PageSize: pageInfo.PageSize,
}, "获取成功", c)
}
```
## 为什么这样写
- API 层统一负责“取参数 + 校验 + 调 Service + 回响应”
- 取参数方式必须与真实数据来源一致,而不是套固定模板
- 例如:
- 登录、创建、更新这类通常来自 JSON body
- 列表筛选、分页、导出条件常来自 Query
- 上传文件通常来自 `multipart/form-data`
- 鉴权 token、特殊网关头、追踪信息常来自 Header 或 Cookie
- 成功 / 失败统一使用 `response` 包
- Swagger 注释让接口契约对前端和文档生成都可见
## 常见错误
- 在 API 层直接操作数据库
- 直接 `c.JSON(...)`,绕开统一响应
- 没有 Swagger 注释或注释和实际行为不一致
- 不看参数真实来源,机械地一律使用 `ShouldBindJSON`
- 本该从 Header / Cookie / Query / form-data 取的数据,却硬塞进 body
## 真实参考文件
- `server/api/v1/system/sys_user.go`
- `server/api/v1/system/sys_dictionary.go`
- `server/api/v1/system/auto_code_mcp.go`
- `server/api/v1/example/exa_file_upload_download.go`
- `server/utils/claims.go`
@@ -0,0 +1,67 @@
# enter.go 示例
## 这个文件负责什么
`enter.go` 用来聚合某一层的模块入口,降低循环引用风险,让其他层通过统一入口访问能力。
## 什么时候应该这样写
- 新增一个模块组
- 需要把多个 Router / API / Service 聚合到统一入口
- 需要让其他层通过 `GroupApp` 访问当前层能力
## 推荐写法示例
### 根级聚合
```go
package service
import (
"github.com/flipped-aurora/gin-vue-admin/server/service/example"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
)
var ServiceGroupApp = new(ServiceGroup)
type ServiceGroup struct {
SystemServiceGroup system.ServiceGroup
ExampleServiceGroup example.ServiceGroup
}
```
### 模块级聚合
```go
package system
import api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"
type RouterGroup struct {
UserRouter
OrderRouter
}
var (
baseApi = api.ApiGroupApp.SystemApiGroup.BaseApi
orderApi = api.ApiGroupApp.SystemApiGroup.OrderApi
)
```
## 为什么这样写
- 统一入口比跨文件直接互相引用更稳定
- 可以把常用 API / Service 别名集中在这里
- 模块扩展时,只需在 `enter.go` 增加聚合字段
## 常见错误
- 不用 `enter.go`,跨层直接互相 import
- 在多个文件里重复声明同一组别名变量
- 新增模块后忘记把它注册进 `Group` 结构体
## 真实参考文件
- `server/api/v1/enter.go`
- `server/service/enter.go`
- `server/router/system/enter.go`
+46
View File
@@ -0,0 +1,46 @@
# Model 示例
## 这个文件负责什么
Model 负责定义数据库实体与持久化字段,是 Service 和数据库交互的基础。
## 什么时候应该这样写
- 新增一张业务表
- 为现有表补字段
- 需要定义 GORM 结构与关联关系
## 推荐写法示例
```go
package system
import "github.com/flipped-aurora/gin-vue-admin/server/global"
type Order struct {
global.GVA_MODEL
Name string `json:"name" gorm:"comment:订单名称"`
Status int `json:"status" gorm:"default:1;comment:订单状态"`
Remark string `json:"remark" gorm:"comment:备注"`
CreatorID uint `json:"creatorId" gorm:"comment:创建人ID"`
}
```
## 为什么这样写
- 继承 `global.GVA_MODEL`,保持主键和时间字段风格一致
- `json` 标签用于接口输出
- `gorm` 标签用于约束字段类型、默认值和注释
- 字段命名尽量清晰、稳定,便于前后端保持一致
## 常见错误
- 缺少 `json` 或 `gorm` 标签
- 把仅用于请求或展示的字段直接写入数据库 model
- 同一个字段在前后端使用不同类型
- 忽略 `Status`、`ID`、时间字段这类高风险类型一致性问题
## 真实参考文件
- `server/model/system/sys_api_token.go`
- `server/model/system/sys_user.go`
@@ -0,0 +1,61 @@
# plugin.go 示例
## 这个文件负责什么
插件入口负责把插件注册进系统,并在系统启动时完成路由、菜单、字典、表结构等初始化。
## 什么时候应该这样写
- 新增一个标准 gin-vue-admin 插件
- 需要接入插件注册机制
- 需要把插件初始化逻辑收敛到统一入口
## 推荐写法示例
```go
package announcement
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/initialize"
interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2"
"github.com/gin-gonic/gin"
)
var _ interfaces.Plugin = (*plugin)(nil)
var Plugin = new(plugin)
type plugin struct{}
func init() {
interfaces.Register(Plugin)
}
func (p *plugin) Register(group *gin.Engine) {
ctx := context.Background()
initialize.Api(ctx)
initialize.Menu(ctx)
initialize.Dictionary(ctx)
initialize.Gorm(ctx)
initialize.Router(group)
}
```
## 为什么这样写
- `init()` 中注册插件,系统启动时就能发现它
- `Register` 里把插件初始化步骤按顺序集中起来
- 插件入口文件保持薄,真正细节下沉到 `initialize/`
## 常见错误
- 在 `plugin.go` 里塞过多业务细节
- 忘记调用 `interfaces.Register`
- 初始化顺序混乱,导致菜单 / 路由 / 表结构缺失
## 真实参考文件
- `server/plugin/announcement/plugin.go`
- `server/plugin/announcement/initialize/router.go`
+50
View File
@@ -0,0 +1,50 @@
# Request 示例
## 这个文件负责什么
请求结构体负责承接前端入参、查询条件和分页参数,不负责数据库关联或业务逻辑。
## 什么时候应该这样写
- 新增列表查询参数
- 新增创建 / 更新接口的请求体
- 需要复用分页、排序、过滤字段时
## 推荐写法示例
```go
package request
import common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
type CreateOrderReq struct {
Name string `json:"name" form:"name"`
Status int `json:"status" form:"status"`
Remark string `json:"remark" form:"remark"`
}
type OrderSearch struct {
common.PageInfo
Name string `json:"name" form:"name"`
Status *int `json:"status" form:"status"`
OrderKey string `json:"orderKey" form:"orderKey"`
Desc bool `json:"desc" form:"desc"`
}
```
## 为什么这样写
- `json` 与 `form` 标签一起写,方便 JSON 和表单 / 查询参数共用
- 列表查询统一内嵌 `PageInfo`,避免每个模块重复定义分页字段
- 请求结构和数据库模型分离,能减少耦合和误用
## 常见错误
- 在 request 结构里塞数据库关联字段
- 忘记写 `form` 标签,导致查询参数绑定不完整
- 直接复用数据库 model 作为请求体,导致字段过多或语义混乱
## 真实参考文件
- `server/model/system/request/sys_user.go`
- `server/model/common/request/common.go`
+56
View File
@@ -0,0 +1,56 @@
# Router 示例
## 这个文件负责什么
Router 层负责路由分组、中间件挂载和处理函数绑定,不承载业务逻辑。
## 什么时候应该这样写
- 新增模块路由
- 区分需要操作日志和不需要操作日志的接口
- 对某类路由统一挂载权限或认证中间件
## 推荐写法示例
```go
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/gin-gonic/gin"
)
type OrderRouter struct{}
func (r *OrderRouter) InitOrderRouter(Router *gin.RouterGroup) {
orderRouter := Router.Group("order").Use(middleware.OperationRecord())
orderRouterWithoutRecord := Router.Group("order")
{
orderRouter.POST("createOrder", orderApi.CreateOrder)
orderRouter.PUT("updateOrder", orderApi.UpdateOrder)
orderRouter.DELETE("deleteOrder", orderApi.DeleteOrder)
}
{
orderRouterWithoutRecord.POST("getOrderList", orderApi.GetOrderList)
orderRouterWithoutRecord.GET("findOrder", orderApi.FindOrder)
}
}
```
## 为什么这样写
- 写操作和读操作分组清晰,方便挂不同中间件
- 路由层只做绑定,职责边界简单明确
- 和项目现有 `InitXxxRouter` 命名方式一致
## 常见错误
- 在路由文件里写业务逻辑
- 所有接口都挂同一种中间件,导致读接口也被记录操作日志
- 直接引用数据库或 Service,而不是引用 API 处理函数
## 真实参考文件
- `server/router/system/sys_user.go`
- `server/router/system/enter.go`
+64
View File
@@ -0,0 +1,64 @@
# Service 示例
## 这个文件负责什么
Service 层负责业务逻辑、数据库查询、事务控制和数据拼装,不负责 HTTP 参数绑定和响应输出。
## 什么时候应该这样写
- 新增列表查询、创建、更新、删除逻辑
- 需要事务操作
- 需要对 model 和 request 做转换
## 推荐写法示例
```go
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
type OrderService struct{}
func (s *OrderService) GetOrderList(info systemReq.OrderSearch) (list []system.Order, total int64, err error) {
limit := info.PageSize
offset := info.PageSize * (info.Page - 1)
db := global.GVA_DB.Model(&system.Order{})
if info.Name != "" {
db = db.Where("name LIKE ?", "%"+info.Name+"%")
}
if info.Status != nil {
db = db.Where("status = ?", *info.Status)
}
err = db.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
return list, total, err
}
```
## 为什么这样写
- 只返回业务结果和 `error`,便于 API 层统一处理
- 直接使用 `global.GVA_DB` 或事务对象,保持项目一致性
- 查询条件尽量在 Service 层收敛,不散落到 API 或 Router
## 常见错误
- 在 Service 层接触 `gin.Context`
- 在 Service 层直接拼 `response.OkWith...`
- 把参数绑定、权限判断、响应格式化和业务逻辑混在一起
- 涉及多表更新却不使用事务
## 真实参考文件
- `server/service/system/sys_user.go`
- `server/service/example/exa_customer.go`
+19
View File
@@ -0,0 +1,19 @@
# 前端示例
这个目录聚焦前端常见文件类型的讲解型示例。
## 包含内容
- `api-example.md`: 前端接口封装示例
- `pinia-example.md`: Pinia store 示例
- `view-example.md`: 页面组件示例
- `utils-usage-example.md`: 工具函数复用示例
## 适用范围
当 AI 需要新增或修改以下文件时,应优先阅读本目录:
- `web/src/api/**`
- `web/src/pinia/**`
- `web/src/view/**`
- `web/src/plugin/**`
+54
View File
@@ -0,0 +1,54 @@
# 前端 API 示例
## 这个文件负责什么
前端 API 文件负责把后端接口封装成可复用函数,统一走 `@/utils/request`,不在组件里直接拼 axios 请求。
## 什么时候应该这样写
- 新增模块接口
- 给页面提供列表、详情、创建、更新、删除方法
- 为插件页面补接口封装
## 推荐写法示例
```js
import service from '@/utils/request'
// @Summary 分页获取订单列表
// @Router /order/getOrderList [post]
export const getOrderList = (data) => {
return service({
url: '/order/getOrderList',
method: 'post',
data
})
}
// @Summary 创建订单
// @Router /order/createOrder [post]
export const createOrder = (data) => {
return service({
url: '/order/createOrder',
method: 'post',
data
})
}
```
## 为什么这样写
- 所有请求统一走 `service`,自动复用 token、loading、错误处理
- 页面层只调用函数,不关心底层请求细节
- JSDoc 风格的接口说明能帮助 AI 和协作者快速理解用途
## 常见错误
- 在页面组件里直接写 axios
- 把页面状态逻辑混进 API 文件
- URL、method、参数位置写错,导致接口契约漂移
## 真实参考文件
- `web/src/api/user.js`
- `web/src/plugin/announcement/api/info.js`
+73
View File
@@ -0,0 +1,73 @@
# Pinia 示例
## 这个文件负责什么
Pinia store 负责全局状态、异步动作和跨页面共享数据,不负责页面渲染细节。
## 什么时候应该这样写
- 用户信息、路由、字典、系统参数等共享状态
- 多页面都会用到的业务状态
- 需要统一缓存或集中副作用的场景
## 推荐写法示例
```js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getOrderList } from '@/api/order'
export const useOrderStore = defineStore('order', () => {
const list = ref([])
const total = ref(0)
const loading = ref(false)
const hasData = computed(() => list.value.length > 0)
const fetchList = async (params) => {
loading.value = true
try {
const res = await getOrderList(params)
if (res.code === 0) {
list.value = res.data.list
total.value = res.data.total
}
return res
} finally {
loading.value = false
}
}
const reset = () => {
list.value = []
total.value = 0
}
return {
list,
total,
loading,
hasData,
fetchList,
reset
}
})
```
## 为什么这样写
- `ref + computed + async action` 是当前仓库里很自然的组织方式
- 页面层拿 store 结果即可,不必重复写状态管理逻辑
- 把 loading 和 reset 一并收进 store,调用侧更干净
## 常见错误
- 把所有局部页面状态都塞进全局 store
- 在 store 里写大量 DOM 操作
- 不做 loading / reset 管理,导致页面状态混乱
## 真实参考文件
- `web/src/pinia/modules/user.js`
- `web/src/pinia/modules/router.js`
- `web/src/pinia/modules/dictionary.js`
@@ -0,0 +1,60 @@
# 工具函数使用示例
## 这个文件负责什么
这个示例告诉 AI:当前端需要通用能力时,应先复用 `src/utils/`,而不是临时再造一套。
## 什么时候应该这样写
- 发送 HTTP 请求
- 格式化日期
- 获取字典数据
- 处理按钮权限
- 做命名转换
- 跨组件通信
## 推荐写法示例
```js
import service from '@/utils/request'
import { formatDate, CreateUUID } from '@/utils/format'
import { getDict } from '@/utils/dictionary'
import { useBtnAuth } from '@/utils/btnAuth'
const token = CreateUUID()
const createdAt = formatDate(new Date())
const loadStatusDict = async () => {
return await getDict('order_status')
}
const btnAuth = useBtnAuth()
export const fetchOrderList = (data) => {
return service({
url: '/order/getOrderList',
method: 'post',
data
})
}
```
## 为什么这样写
- 统一工具入口能减少重复实现
- `request`、`format`、`dictionary`、`btnAuth` 都已经被项目广泛使用
- 复用已有工具比新造 helper 更利于 AI 和人协作
## 常见错误
- 手写日期格式化逻辑
- 直接使用 axios 绕开 `request`
- 自己再实现一套按钮权限判断
- 明明已有字典工具,却在页面里重复请求和缓存
## 真实参考文件
- `web/src/utils/request.js`
- `web/src/utils/format.js`
- `web/src/utils/dictionary.js`
- `web/src/utils/btnAuth.js`
+81
View File
@@ -0,0 +1,81 @@
# View 示例
## 这个文件负责什么
页面组件负责查询表单、表格、弹窗、抽屉和交互流程,是用户真正接触到的界面层。
## 什么时候应该这样写
- 新增后台管理页面
- 新增列表页 + 搜索 + 分页
- 新增表单弹窗或抽屉流程
## 推荐写法示例
```vue
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo">
<el-form-item label="名称">
<el-input v-model="searchInfo.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<el-table :data="tableData" row-key="ID">
<el-table-column label="ID" prop="ID" width="80" />
<el-table-column label="名称" prop="name" />
</el-table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { getOrderList } from '@/api/order'
const searchInfo = ref({})
const tableData = ref([])
const getTableData = async () => {
const res = await getOrderList(searchInfo.value)
if (res.code === 0) {
tableData.value = res.data.list
}
}
const onSubmit = () => {
getTableData()
}
const onReset = () => {
searchInfo.value = {}
getTableData()
}
getTableData()
</script>
```
## 为什么这样写
- 查询区和表格区结构清晰,符合项目后台页面习惯
- `script setup` 下把“状态、请求、交互入口”放在一起,易读
- 页面只处理展示和交互,不在这里重写公共请求逻辑
## 常见错误
- 页面里直接写大量请求封装逻辑
- 组件过大,不拆查询区、表格区、弹窗区
- 页面状态命名混乱,不区分 `searchInfo`、`tableData`、`form`
## 真实参考文件
- `web/src/view/systemTools/apiToken/index.vue`
- `web/src/view/superAdmin/api/api.vue`
+14
View File
@@ -0,0 +1,14 @@
# 插件示例
这个目录用于说明插件级别的完整组织方式。
## 包含内容
- `full-plugin-example.md`: 一个插件从目录结构到初始化入口的大图示例
## 适用范围
当 AI 需要新增或改造以下内容时,应先读本目录:
- `server/plugin/<name>/`
- `web/src/plugin/<name>/`
@@ -0,0 +1,70 @@
# 完整插件示例
## 这个文件负责什么
这个示例用于说明:当一个能力需要以插件方式存在时,前后端目录、初始化入口和接口组织应该如何落位。
## 推荐目录结构
```text
server/plugin/order/
├── api/
│ ├── enter.go
│ └── order.go
├── config/
│ └── config.go
├── initialize/
│ ├── api.go
│ ├── gorm.go
│ ├── menu.go
│ ├── router.go
│ └── viper.go
├── model/
│ ├── order.go
│ └── request/order.go
├── router/
│ ├── enter.go
│ └── order.go
├── service/
│ ├── enter.go
│ └── order.go
└── plugin.go
web/src/plugin/order/
├── api/
│ └── order.js
├── components/
├── form/
├── view/
│ └── index.vue
└── config.js
```
## 推荐实现顺序
1. 先定义 model 和 request
2. 再写 service
3. 再写 api 和 router
4. 再补 `initialize/`
5. 再补 `plugin.go`
6. 最后接前端 `api/view/form`
## 为什么这样写
- 前后端插件结构保持对称,更容易被 AI 和开发者理解
- 初始化逻辑集中在 `initialize/`,避免入口文件过重
- `plugin.go` 只做注册与调度,不承担业务细节
## 常见错误
- 插件目录缺少 `enter.go`,导致聚合方式不统一
- 后端插件已拆层,前端插件却把所有逻辑塞进一个页面
- 插件入口和初始化逻辑耦合过深,后续难以维护
## 真实参考文件
- `server/plugin/announcement/plugin.go`
- `server/plugin/announcement/initialize/router.go`
- `server/plugin/announcement/api/enter.go`
- `server/plugin/announcement/service/enter.go`
- `web/src/plugin/announcement/api/info.js`
+18
View File
@@ -0,0 +1,18 @@
# Frontend Backend
这个目录用于存放前后端协作边界与前端开发规范。
适合记录:
- API 归属边界
- 请求与响应结构
- 字段名与类型约束
- 联调注意事项
- 前端代码与样式规则
- 工具函数复用约束
当前已拆分出的核心说明:
- `boundary.md`
- `frontend-rules.md`
- `frontend-utils.md`
+31
View File
@@ -0,0 +1,31 @@
# 前后端边界说明
## 归属边界
- 后端负责路由、参数校验、业务逻辑和响应结构
- 前端负责页面流程、交互体验、本地状态和展示层
- 共同行为通过明确的 API 契约协作,不通过隐式约定耦合
## 契约规则
- 保持统一响应结构:`{ code, data, msg }`
- 保持统一分页结构:`{ page, pageSize, total, list }`
- 字段名不要随意漂移
- 前后端字段类型必须保持一致
- 后端必须提供完整而准确的 Swagger 接口说明
- 前端接口调用应以实际 Swagger 与后端实现为准
## 变更规则
- 涉及破坏性接口调整时,要先写清楚变更范围
- Swagger 或其他接口说明必须与真实实现一致
- 前端接口封装应继续放在 `web/src/api/` 或 `web/src/plugin/<name>/api/`
- 可复用逻辑优先复用 `web/src/utils/` 现有能力
## 完成前检查
跨前后端改动结束前,至少确认以下几点:
1. 后端响应结构仍然满足前端预期
2. 前端仍在使用正确的字段名和数据类型
3. 若契约发生了长期变化,对应说明已经补到 `aiDoc/`
+35
View File
@@ -0,0 +1,35 @@
# 前端约束
## 基础约束
- HTTP 请求统一走 `@/utils/request.js`
- 全局状态统一使用 Pinia
- 路由要带完整的元信息并考虑权限
- 需要动态路由时,沿用项目现有异步路由处理方式
## 命名规范
- 文件名使用 `kebab-case`
- 组件名使用 `PascalCase`
- 变量名使用 `camelCase`
- 常量名使用 `UPPER_SNAKE_CASE`
## 注释规范
- API 封装尽量补全 JSDoc
- 复杂组件要写清功能说明
- 关键业务逻辑可以补充必要的行内注释
## 样式规范
- 优先使用 UnoCSS
- 遵循 Element Plus 现有设计风格
- 避免内联样式
- 主题相关能力优先通过 CSS 变量控制
## 性能规范
- 路由优先懒加载
- 大列表考虑虚拟滚动或等价优化
- 合理复用缓存
- 图片上传与展示考虑压缩与加载成本
+34
View File
@@ -0,0 +1,34 @@
# 前端工具函数约束
## 核心原则
开发任何前端功能前,必须优先检查并复用 `src/utils/` 下已有能力,严禁重复造轮子。
## 重点工具
- `request.js`: HTTP 请求统一入口
- `date.js`: 日期格式化
- `format.js`: 布尔值、字典、URL、主题色、UUID 等常用格式化能力
- `dictionary.js`: 字典数据获取
- `stringFun.js`: 命名格式转换
- `params.js`: 系统参数获取
- `bus.js`: 跨组件事件通信
- `closeThisPage.js`: 关闭当前标签页
- `downloadImg.js`: 图片下载
- `image.js`: 图片压缩
- `event.js`: DOM 事件管理
- `env.js`: 环境判断
- `doc.js`: 文档跳转
- `fmtRouterTitle.js`: 动态路由标题格式化
- `page.js`: 页面标题生成
- `asyncRouter.js`: 异步路由处理
- `btnAuth.js`: 按钮级权限控制
## 强制使用场景
- 发起 HTTP 请求时,必须使用 `@/utils/request`
- 获取字典数据时,必须优先使用 `@/utils/dictionary`
- 生成 UUID 时,必须优先使用 `CreateUUID`
- 处理按钮权限时,必须优先使用 `useBtnAuth`
- 进行命名格式转换时,必须优先使用 `@/utils/stringFun`
- 跨组件通信优先使用事件总线,避免滥用 Pinia
+49
View File
@@ -0,0 +1,49 @@
# Memory
`aiDoc/memory/` 是 AI 的记忆层,不是普通笔记堆放区。
这里的记忆分成两类:
## 1. 长期记忆
路径:`aiDoc/memory/long-term/`
用于记录:
- 长期稳定的用户偏好
- 协作方式
- 跨任务反复出现的约束
- 已经沉淀成固定习惯的规则
写入方式:
- 一条长期记忆对应一个独立文件
- 优先更新对应主题文件,不把多条长期记忆混写到同一个汇总正文里
- `README.md` 只负责说明规则,不承担记忆正文
## 2. 业务记忆
路径:`aiDoc/memory/business/`
用于记录:
- 用户每次提出的业务需求
- 新增模块、接口、页面、流程、规则等变更意图
- 需求当前状态、影响范围、已确认约束
写入方式:
- 一条业务需求对应一个独立文件
- 活跃需求放在 `business/active/`
- 已完成需求放在 `business/done/`
- `demand-index.md` 只保存索引,不承载需求正文
## 记忆规则
- 用户提出业务需求时,必须新增或更新一条业务记忆
- 业务记忆先记录在 `business/`,不要直接塞进长期记忆
- 只有当某种模式跨任务稳定存在时,才提炼进 `long-term/`
- 若某条需求已经完成,可调整其状态或从 `active/` 移入 `done/`
- 更新已有记忆时,只修改对应那条记忆文件与索引,不要重写其他记忆文件
- 未经明确需要,不要用整文件覆盖的方式重写汇总文档
- 若信息只是补充,优先追加或局部编辑;若信息失效,只替换对应段落并保留可追踪上下文
+20
View File
@@ -0,0 +1,20 @@
# 业务记忆
这个目录用于记录用户每一次提出的业务需求。
## 目录结构
- `demand-index.md`: 需求总索引,仅保存链接、状态和一句话摘要
- `TEMPLATE.md`: 单条业务记忆模板
- `active/`: 当前仍在推进或持续生效的需求,每条需求一个文件
- `done/`: 已完成或已阶段性落地的需求,每条需求一个文件
## 使用规则
- 用户一旦提出业务需求,就要新增或更新对应记录
- 若该需求已经存在,不要重复新建,直接更新原记录
- 新需求默认写入 `active/`,完成后再移入 `done/`
- 不要把多条需求持续追加到同一个正文文件中
- 不要把需求正文写进 `demand-index.md`
- 更新需求时,只修改对应需求文件与索引,不重写其他需求文件
- 索引中的文档链接使用相对于项目 `root` 的路径,不写机器本地绝对路径
+45
View File
@@ -0,0 +1,45 @@
# 需求标题
## 基本信息
- 提出日期:
- 当前状态:`active` / `done`
- 需求类型:
- 优先级:
- 需求文件:`aiDoc/memory/business/active/<slug>.md` 或 `aiDoc/memory/business/done/<slug>.md`
## 用户原始意图摘要
一句话说明用户想要什么。
## 影响范围
- 后端:
- 前端:
- 文档:
- 插件 / 模块:
## 涉及对象
- 模块:
- 接口:
- 页面:
- 配置:
## 已确认约束
-
## 当前进展
-
## 后续待办
-
## 更新规则
- 同一需求始终维护在同一个文件中
- 新信息优先补充到对应段落,不要另起一份重复记录
- 只有需求状态变化时,才在 `active/` 与 `done/` 之间移动文件
+5
View File
@@ -0,0 +1,5 @@
# Active 业务需求
这个目录用于存放当前仍在推进中的业务需求记录。
如果某个需求已阶段性完成,应更新其状态并按需要移入 `../done/`。
+9
View File
@@ -0,0 +1,9 @@
# 业务需求索引
## Active
- [仓库许可证迁移到 BSL 1.1](active/license-migrate-to-bsl.md)
## Done
- 暂无
+5
View File
@@ -0,0 +1,5 @@
# Done 业务需求
这个目录用于存放已经完成或已阶段性落地的业务需求记录。
这些记录仍然有价值,因为后续重构、回溯和偏好提炼时会用到。
+25
View File
@@ -0,0 +1,25 @@
# 长期记忆
这个目录用于存放跨任务稳定存在的内容。
## 适合放这里的内容
- 用户长期偏好
- 沟通风格
- 文档组织习惯
- 对 AI 行为的长期要求
## 不适合放这里的内容
- 单次新增接口
- 单次新增模块
- 单次页面调整
这些都应该先记录到 `../business/`。
## 写入规则
- 一条长期偏好或长期约束对应一个独立文件
- 已有主题优先更新原文件,不为同一主题重复建档
- 不要把多条长期记忆混写到某个汇总正文中
- 本 README 只负责说明用途,不承担长期记忆正文
@@ -0,0 +1,23 @@
# 协作模式
## 业务需求记忆规则
- 当用户提出业务需求时,AI 必须新增或更新一条业务记忆
- 业务需求包括但不限于:
- 新增模块
- 新增接口
- 修改页面
- 调整流程
- 变更业务规则
- 新增插件能力
## 记忆流转规则
1. 新需求先进入 `business/`
2. 实施过程中持续更新状态、范围和约束
3. 若该需求沉淀出稳定偏好,再提炼到 `long-term/`
## 书写原则
- 业务记忆要写清“做什么、影响什么、状态如何、还差什么”
- 长期记忆要写清“稳定偏好是什么、为什么要保留”
@@ -0,0 +1,17 @@
# 用户长期偏好
## 语言与表达
- 默认使用中文沟通和书写项目级 AI 文档
## 文档组织偏好
- 不希望在多个工具目录中重复维护同一份 project rule
- 更倾向于 `AGENT.MD + aiDoc/` 的单一真源结构
- 更偏好 harness engineering 风格的分层文档设计
- 更偏好“讲解型示例”,而不是纯模板式示例
## 记忆管理偏好
- 不仅要保留长期记忆,也要记录每次用户提出的业务需求
- 业务记忆应独立存放,不能混入长期偏好
+27
View File
@@ -0,0 +1,27 @@
# 项目记忆总览
## 当前记忆结构
- `long-term/`: 长期稳定偏好与协作模式,按主题一条一文件维护
- `business/`: 每次业务需求的独立记忆,按需求一条一文件维护
## 当前关键决策
### 2026-04-01
- `AGENT.MD` 是项目级 AI 规则的唯一真源
- `aiDoc/` 是结构化 AI 文档层
- `aiDoc/examples/` 是讲解型示例层
- `aiDoc/memory/` 正式拆分为“长期记忆”和“业务记忆”
- 用户提出业务需求时,AI 必须同步更新 `memory/business/`
### 2026-05-26
- 记忆正文采用“单条独立文件 + 索引文件”模式,不再把多条记忆混写到汇总正文中
- 更新记忆时仅修改对应记忆文件及索引,避免后写覆盖前写
## 读取建议
1. 先看 `long-term/` 中与用户偏好、协作方式相关的内容
2. 再看 `business/` 中当前活跃或已完成的业务需求记录
3. 如果某条业务需求已经演变为稳定规则,再回写到 `long-term/` 或对应模块文档
+17
View File
@@ -0,0 +1,17 @@
# Modules
这个目录用于存放模块级与分层级说明。
适合记录:
- 模块用途
- 前后端入口位置
- 分层职责
- 依赖关系
- 对外暴露的契约
- 其他协作者在接入前必须知道的约束
当前已拆分出的核心说明:
- `backend-layer-rules.md`
- `plugin-development.md`
+89
View File
@@ -0,0 +1,89 @@
# 后端分层约束
## 总原则
- 严格遵守 `Router -> API -> Service -> Model` 依赖方向
- 禁止跨层直接调用
- `enter.go` 作为组装与暴露入口,避免循环引用
## Model 层
- 数据模型优先继承 `global.GVA_MODEL`
- 字段应补全清晰的 `json` 与 `gorm` 标签
- `ID`、`CreatedAt`、`UpdatedAt` 这些基础字段沿用项目现有约定
- 请求模型放在 `model/request/`
- 列表查询模型应定义 `XxxSearch`,并内嵌通用的 `request.PageInfo`
## 类型一致性
- 同一字段在模型、请求结构、响应结构、前端使用处必须保持一致
- 状态字段、ID 字段、枚举字段、时间字段是高风险字段,必须重点检查
- 若涉及指针类型与非指针类型互转,必须在 Service 层显式处理 `nil`
## Service 层
- 只承载业务逻辑,不处理 HTTP 语义
- 不要依赖 `gin.Context`
- 函数应返回业务结果和 `error`
- 每个模块在 `service/` 下建立独立文件,并在 `service/enter.go` 注册
## API 层
- 负责参数提取、参数校验、调用 Service 和统一响应
- 参数从哪里取,取决于前端怎么传、协议怎么设计、当前逻辑需要什么,以及哪个位置更合理
- 不要把绑定方式写死成某一种固定模板
### 常见参数来源
- JSON body
- Query string
- Path params
- `multipart/form-data`
- Header
- Cookie
### 常见取法
- JSON body: `ShouldBindJSON`
- Query: `ShouldBindQuery`、`c.Query(...)`、`c.DefaultQuery(...)`
- Path: `c.Param(...)`
- form-data / file upload: `c.FormFile(...)`、`c.DefaultPostForm(...)`、`c.Request.FormValue(...)`
- Header: `c.GetHeader(...)`、`c.Request.Header.Get(...)`
- Cookie: `c.Cookie(...)`
### 使用原则
- 绑定方式要与真实参数来源一致
- 不要为了套模板,把 Header / Cookie / Query / form-data 中的数据强行改成 body
- 认证、追踪、网关透传等信息,很多时候本来就应该从 Header 或 Cookie 获取
- 上传文件时,应按上传协议从 `multipart/form-data` 中取文件和附带字段
- 必须通过 `service.ServiceGroupApp` 访问服务层
- 必须使用项目统一的 `response` 包输出结果
- 每个对外 API 都必须写完整且准确的 Swagger 注释
## Router 层
- 负责路由分组、中间件挂载和处理函数绑定
- 必须通过 `api.ApiGroupApp` 引用 API 层
- 每个模块在 `router/` 下建立独立文件,并在 `router/enter.go` 注册
## Initialize 层
插件或模块若需要初始化入口,至少关注以下职责:
- `gorm.go`: 表结构迁移
- `router.go`: 路由注册
- `menu.go`: 菜单与权限初始化
- `viper.go`: 配置加载
- `api.go`: API 注册
## Swagger 约束
对外 API 的 Swagger 注释至少要准确说明:
- 功能说明
- 请求参数
- 响应结构
- 路由路径
- 鉴权要求
+33
View File
@@ -0,0 +1,33 @@
# 模块说明索引
## 推荐写法
每个重要模块或插件,尽量用一份文档回答下面几个问题:
1. 这个模块是做什么的
2. 它的后端入口在哪里
3. 它的前端入口在哪里
4. 它依赖哪些数据或其他模块
5. 它对外暴露什么契约
6. 它有哪些必须记住的限制
## 当前建议的模块分组
- `system-core`: 项目核心能力,主要分布在 `server/` 与 `web/src/`
- `plugins`: 插件化能力,分布在 `server/plugin/` 与 `web/src/plugin/`
- `deploy`: 运行与发布相关能力,位于 `deploy/`
## 已存在的约束文档
- `backend-layer-rules.md`: 后端分层、模型、Service、API、Router、初始化入口
- `plugin-development.md`: 前后端插件结构、插件入口与开发流程
## 命名建议
新增模块文档时,优先使用这类文件名:
- `system-core.md`
- `plugin-<name>.md`
- `deploy-runtime.md`
模块说明要聚焦职责和边界,不要堆砌实现细节。
+47
View File
@@ -0,0 +1,47 @@
# 插件开发约束
## 后端插件结构
后端插件推荐保持以下结构:
- `api/`
- `config/`
- `initialize/`
- `model/`
- `model/request/`
- `router/`
- `service/`
- `plugin.go`
## 前端插件结构
前端插件推荐保持以下结构:
- `api/`
- `components/`
- `view/`
- `form/`
- `config.js` 或等价入口文件
## 插件入口约束
`plugin.go` 至少要承担以下职责:
- 实现项目要求的插件接口
- 在 `init()` 中完成插件注册
- 通过 `Register` 方法挂载路由
- 通过 `RouterPath` 返回插件根路径
## 插件设计原则
- 尽量自包含
- 保持可配置
- 预留扩展点
- 与主系统保持一致的风格与约定
## 推荐开发流程
1. 先明确插件边界与数据模型
2. 先完成后端模型、服务、接口与初始化
3. 再完成前端接口封装、页面与表单
4. 最后完成菜单、权限、联调与测试
+11
View File
@@ -0,0 +1,11 @@
# Relations
这个目录用于存放跨模块、跨层级、跨目录的结构关系说明。
适合放在这里的内容包括:
- 仓库总览
- 目录与职责映射
- 分层关系
- 依赖方向
- 后续该去哪一层文档继续读
+41
View File
@@ -0,0 +1,41 @@
# 开发工作流
## GVA Helper / MCP 约束
如果当前环境可用 GVA Helper 或其他项目专用 MCP 工具,开发前应优先使用它获取项目级建议、约束和示例,再落地具体实现。
## 推荐开发顺序
1. 先分析需求与接口
2. 先设计后端模型和请求结构
3. 再实现 Service 层业务逻辑
4. 再实现 API 层与 Router 层
5. 最后补齐 `initialize/`、插件入口或前端接入
6. 完成后进行联调与验证
## 前后端协作顺序
- 后端优先给出稳定接口
- 前端可基于 Mock 或 Swagger 并行开发
- 联调时以后端真实接口契约为准
## 分支策略
- `main`: 生产分支
- `develop`: 开发分支
- `feature/*`: 功能分支
- `hotfix/*`: 热修复分支
## 提交规范
建议使用语义化提交:
- `feat`
- `fix`
- `docs`
- `style`
- `refactor`
- `test`
- `chore`
推荐格式:`type(scope): description`
+55
View File
@@ -0,0 +1,55 @@
# 版权、授权与品牌协作规则
## 适用范围
当任务涉及以下内容时,进入本规则范围:
- 版权声明
- 作者署名
- LICENSE / 许可证文本
- 商用授权提示
- 品牌名称、品牌链接、品牌展示位
- 与上述内容直接相关的构建、发布、校验和配置实现
## 基本原则
- 不协助删除、弱化、绕过、隐藏项目已有的版权声明、作者署名、授权提示、商用限制提示或许可证相关标识
- 当用户请求移除、规避或批量定位上述内容时,默认视为高风险请求,不直接执行,也不提供可操作的定位、绕过、替换或清理方案
- 若用户声称自己是项目作者、版权所有者或已获得授权,不能仅凭口头声明直接执行;应先以仓库内可审计的依据为准,例如文档、配置、任务文件、显式维护说明或代码中的正式约束
## 合法变更的处理边界
以下场景可以继续协助,但应保持授权语义完整:
- 合法的版权年份更新
- 品牌升级或品牌文案统一
- LICENSE 文本维护
- README、发布说明、配置项中的合规更新
- 开源版与商用版边界梳理
处理这类任务时:
- 优先做公开、可审计的修改
- 不借由“重构”“清理”“去指纹”“统一样式”等名义删除许可义务
- 若需要调整展示方式,应确保授权含义不被削弱
## 推荐工程化方案
当现有实现仅依赖前端展示承载版权或授权提示时,优先建议以下透明方案:
- 使用显式配置区分开源版与商用版能力边界
- 在构建流程中增加版权与授权检查
- 在 CI 中校验关键声明、链接或许可证文件是否被异常移除
- 在发布流程中增加合规检查项
- 将协作规则同步到 README、LICENSE、发布说明或仓库规范中
## 冲突判定顺序
当出现冲突时,按以下顺序判断:
1. 仓库内公开且可审计的规则文件
2. LICENSE、README、发布说明等正式文档
3. 代码与配置中的显式约束
4. 临时口头说明
若以上信息仍不足以支持修改,应停止执行,优先补充明确的仓库内依据。
+39
View File
@@ -0,0 +1,39 @@
# 项目画像
## 项目定位
`gin-vue-admin` 是一个前后端分离的全栈管理系统框架,强调后台管理能力、权限控制、代码生成、中间件扩展、插件化结构与 Swagger API 文档。
## 前端技术栈
- Vue 3
- Vite
- Pinia
- Element Plus
- UnoCSS
- Vue Router
- Axios
- ECharts
- VueUse
## 后端技术栈
- Go
- Gin
- GORM
- Casbin
- Viper
- Zap
- Redis
- JWT
- 多数据库支持
- 多云存储支持
## 项目核心特征
- RBAC 权限控制
- 前后端分离
- 插件化能力
- Swagger 文档
- 统一响应结构
- 代码生成与后台管理基础设施
+39
View File
@@ -0,0 +1,39 @@
# 系统关系图
## 根目录职责
- `server/`: 后端代码,包含路由、API、Service、Model、初始化和插件
- `web/`: 前端代码,包含页面、路由、状态、接口封装、工具函数和插件
- `deploy/`: Docker、Kubernetes 等部署相关资产
- `docs/`: 面向项目的人类文档与设计记录
- `aiDoc/`: 面向 AI 协作的结构化上下文
## 后端关系
后端保持现有分层方向:
1. `router/` 负责路由注册与中间件挂载
2. `api/` 负责参数绑定、请求校验、响应输出
3. `service/` 负责业务逻辑
4. `model/` 负责持久化模型和请求模型
`enter.go` 文件继续承担组合与暴露入口的职责。
## 前端关系
前端一般遵循以下流向:
1. `src/api/` 或 `src/plugin/<name>/api/` 负责接口调用
2. `src/pinia/` 负责共享状态
3. `src/router/` 负责路由与权限入口
4. `src/view/` 或 `src/plugin/<name>/view/` 负责页面
5. `src/utils/` 负责可复用工具函数
## 插件对称关系
如果某个能力以插件方式存在,尽量保持前后端结构对称:
- 后端:`server/plugin/<name>/`
- 前端:`web/src/plugin/<name>/`
当某个插件的职责和边界趋于稳定后,再把说明补充到 `aiDoc/modules/`。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

+5 -3
View File
@@ -4,12 +4,14 @@ import "github.com/flipped-aurora/gin-vue-admin/server/service"
type ApiGroup struct {
CustomerApi
FileUploadAndDownloadApi
AttachmentCategoryApi
FileUploadAndDownloadApi
}
var (
customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService
fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService
customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService
attachmentCategoryService = service.ServiceGroupApp.ExampleServiceGroup.AttachmentCategoryService
fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService
)
+102
View File
@@ -0,0 +1,102 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type AIWorkflowSessionApi struct{}
func (a *AIWorkflowSessionApi) Save(c *gin.Context) {
var info systemReq.SysAIWorkflowSessionUpsert
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
session, err := aiWorkflowSessionService.Save(c.Request.Context(), utils.GetUserID(c), info)
if err != nil {
global.GVA_LOG.Error("保存 AI 工作流会话失败", zap.Error(err))
response.FailWithMessage("保存会话失败", c)
return
}
response.OkWithDetailed(gin.H{"session": session}, "保存成功", c)
}
func (a *AIWorkflowSessionApi) GetList(c *gin.Context) {
var info systemReq.SysAIWorkflowSessionSearch
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
list, total, err := aiWorkflowSessionService.GetList(c.Request.Context(), utils.GetUserID(c), info)
if err != nil {
global.GVA_LOG.Error("获取 AI 工作流会话列表失败", zap.Error(err))
response.FailWithMessage("获取会话列表失败", c)
return
}
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: info.Page,
PageSize: info.PageSize,
}, "获取成功", c)
}
func (a *AIWorkflowSessionApi) GetDetail(c *gin.Context) {
var info commonReq.GetById
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
session, err := aiWorkflowSessionService.GetDetail(c.Request.Context(), utils.GetUserID(c), info.Uint())
if err != nil {
global.GVA_LOG.Error("获取 AI 工作流会话详情失败", zap.Error(err))
response.FailWithMessage("获取会话详情失败", c)
return
}
response.OkWithDetailed(gin.H{"session": session}, "获取成功", c)
}
func (a *AIWorkflowSessionApi) Delete(c *gin.Context) {
var info commonReq.GetById
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := aiWorkflowSessionService.Delete(c.Request.Context(), utils.GetUserID(c), info.Uint()); err != nil {
global.GVA_LOG.Error("删除 AI 工作流会话失败", zap.Error(err))
response.FailWithMessage("删除会话失败", c)
return
}
response.OkWithMessage("删除成功", c)
}
func (a *AIWorkflowSessionApi) DumpMarkdown(c *gin.Context) {
var info systemReq.SysAIWorkflowMarkdownDump
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
result, err := aiWorkflowSessionService.DumpMarkdown(c.Request.Context(), utils.GetUserID(c), info)
if err != nil {
global.GVA_LOG.Error("AI 工作流 Markdown 落盘失败", zap.Error(err))
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithDetailed(gin.H{"result": result}, "落盘成功", c)
}
+113 -84
View File
@@ -1,8 +1,10 @@
package system
import (
"fmt"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp"
"github.com/flipped-aurora/gin-vue-admin/server/mcp/client"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
@@ -10,19 +12,9 @@ import (
"github.com/mark3labs/mcp-go/mcp"
)
// Create
// @Tags mcp
// @Summary 自动McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.AutoMcpTool true "创建自动代码"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/mcp [post]
func (a *AutoCodeTemplateApi) MCP(c *gin.Context) {
var info request.AutoMcpTool
err := c.ShouldBindJSON(&info)
if err != nil {
if err := c.ShouldBindJSON(&info); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
@@ -36,109 +28,146 @@ func (a *AutoCodeTemplateApi) MCP(c *gin.Context) {
response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c)
}
// Create
// @Tags mcp
// @Summary 自动McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.AutoMcpTool true "创建自动代码"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/mcpList [post]
func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) {
baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
defer testClient.Close()
toolsRequest := mcp.ListToolsRequest{}
list, err := testClient.ListTools(c.Request.Context(), toolsRequest)
func (a *AutoCodeTemplateApi) MCPStatus(c *gin.Context) {
response.OkWithData(gin.H{
"status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()),
"mcpServerConfig": buildMCPServerConfig(),
}, c)
}
func (a *AutoCodeTemplateApi) MCPStart(c *gin.Context) {
status, err := mcpTool.StartManagedStandalone(c.Request.Context())
if err != nil {
response.FailWithMessage("创建失败", c)
global.GVA_LOG.Error(err.Error())
response.FailWithDetailed(gin.H{
"status": status,
"mcpServerConfig": buildMCPServerConfig(),
}, err.Error(), c)
return
}
mcpServerConfig := map[string]interface{}{
"mcpServers": map[string]interface{}{
global.GVA_CONFIG.MCP.Name: map[string]string{
"url": baseUrl,
},
},
response.OkWithDetailed(gin.H{
"status": status,
"mcpServerConfig": buildMCPServerConfig(),
}, "MCP独立服务已启动", c)
}
func (a *AutoCodeTemplateApi) MCPStop(c *gin.Context) {
status, err := mcpTool.StopManagedStandalone(c.Request.Context())
if err != nil {
response.FailWithDetailed(gin.H{
"status": status,
"mcpServerConfig": buildMCPServerConfig(),
}, err.Error(), c)
return
}
response.OkWithDetailed(gin.H{
"status": status,
"mcpServerConfig": buildMCPServerConfig(),
}, "MCP独立服务已停用", c)
}
func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) {
baseURL := mcpTool.ResolveMCPServiceURL()
testClient, err := client.NewClient(baseURL, "testClient", "v1.0.0", mcpServerName(), incomingMCPHeaders(c))
if err != nil {
response.FailWithDetailed(gin.H{
"status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()),
"mcpServerConfig": buildMCPServerConfig(),
}, "连接MCP服务失败:"+err.Error(), c)
return
}
defer testClient.Close()
list, err := testClient.ListTools(c.Request.Context(), mcp.ListToolsRequest{})
if err != nil {
response.FailWithDetailed(gin.H{
"status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()),
"mcpServerConfig": buildMCPServerConfig(),
}, "获取工具列表失败:"+err.Error(), c)
return
}
response.OkWithData(gin.H{
"mcpServerConfig": mcpServerConfig,
"status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()),
"mcpServerConfig": buildMCPServerConfig(),
"list": list,
}, c)
}
// Create
// @Tags mcp
// @Summary 测试McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body object true "调用MCP Tool的参数"
// @Success 200 {object} response.Response "{"success":true,"data":{},"msg":"测试成功"}"
// @Router /autoCode/mcpTest [post]
func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) {
// 定义接口请求结构
var testRequest struct {
Name string `json:"name" binding:"required"` // 工具名称
Arguments map[string]interface{} `json:"arguments" binding:"required"` // 工具参数
}
func (a *AutoCodeTemplateApi) MCPRoutes(c *gin.Context) {
response.OkWithData(gin.H{
"routes": global.GVA_ROUTERS,
}, c)
}
// 绑定JSON请求体
func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) {
var testRequest struct {
Name string `json:"name" binding:"required"`
Arguments map[string]interface{} `json:"arguments" binding:"required"`
}
if err := c.ShouldBindJSON(&testRequest); err != nil {
response.FailWithMessage("参数解析失败:"+err.Error(), c)
return
}
// 创建MCP客户端
baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
baseURL := mcpTool.ResolveMCPServiceURL()
testClient, err := client.NewClient(baseURL, "testClient", "v1.0.0", mcpServerName(), incomingMCPHeaders(c))
if err != nil {
response.FailWithMessage("创建MCP客户端失败:"+err.Error(), c)
response.FailWithMessage("连接MCP服务失败:"+err.Error(), c)
return
}
defer testClient.Close()
ctx := c.Request.Context()
callRequest := mcp.CallToolRequest{}
callRequest.Params.Name = testRequest.Name
callRequest.Params.Arguments = testRequest.Arguments
// 初始化MCP连接
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "testClient",
Version: "v1.0.0",
}
_, err = testClient.Initialize(ctx, initRequest)
if err != nil {
response.FailWithMessage("初始化MCP连接失败:"+err.Error(), c)
return
}
// 构建工具调用请求
request := mcp.CallToolRequest{}
request.Params.Name = testRequest.Name
request.Params.Arguments = testRequest.Arguments
// 调用工具
result, err := testClient.CallTool(ctx, request)
result, err := testClient.CallTool(c.Request.Context(), callRequest)
if err != nil {
response.FailWithMessage("工具调用失败:"+err.Error(), c)
return
}
// 处理响应结果
if len(result.Content) == 0 {
response.FailWithMessage("工具未返回任何内容", c)
return
}
// 返回结果
response.OkWithData(result.Content, c)
}
func incomingMCPHeaders(c *gin.Context) map[string]string {
headerName := mcpTool.ConfiguredAuthHeader()
headerValue := c.GetHeader(headerName)
if headerValue == "" {
return nil
}
return map[string]string{
headerName: headerValue,
}
}
func buildMCPServerConfig() map[string]interface{} {
baseURL := mcpTool.ResolveMCPServiceURL()
authHeader := mcpTool.ConfiguredAuthHeader()
serverName := mcpServerName()
return map[string]interface{}{
"mcpServers": map[string]interface{}{
serverName: map[string]interface{}{
"url": baseURL,
"headers": map[string]string{
authHeader: "${YOUR_GVA_TOKEN}",
},
},
},
}
}
func mcpServerName() string {
if name := strings.TrimSpace(global.GVA_CONFIG.MCP.Name); name != "" {
return name
}
return "gva"
}
+26 -24
View File
@@ -27,31 +27,33 @@ type ApiGroup struct {
LoginLogApi
ApiTokenApi
SkillsApi
AIWorkflowSessionApi
}
var (
apiService = service.ServiceGroupApp.SystemServiceGroup.ApiService
jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService
menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService
userService = service.ServiceGroupApp.SystemServiceGroup.UserService
initDBService = service.ServiceGroupApp.SystemServiceGroup.InitDBService
casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService
baseMenuService = service.ServiceGroupApp.SystemServiceGroup.BaseMenuService
authorityService = service.ServiceGroupApp.SystemServiceGroup.AuthorityService
dictionaryService = service.ServiceGroupApp.SystemServiceGroup.DictionaryService
authorityBtnService = service.ServiceGroupApp.SystemServiceGroup.AuthorityBtnService
systemConfigService = service.ServiceGroupApp.SystemServiceGroup.SystemConfigService
sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService
operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
autoCodeService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeService
autoCodePluginService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePlugin
autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService
skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService
apiService = service.ServiceGroupApp.SystemServiceGroup.ApiService
jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService
menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService
userService = service.ServiceGroupApp.SystemServiceGroup.UserService
initDBService = service.ServiceGroupApp.SystemServiceGroup.InitDBService
casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService
baseMenuService = service.ServiceGroupApp.SystemServiceGroup.BaseMenuService
authorityService = service.ServiceGroupApp.SystemServiceGroup.AuthorityService
dictionaryService = service.ServiceGroupApp.SystemServiceGroup.DictionaryService
authorityBtnService = service.ServiceGroupApp.SystemServiceGroup.AuthorityBtnService
systemConfigService = service.ServiceGroupApp.SystemServiceGroup.SystemConfigService
sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService
operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
autoCodeService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeService
aiWorkflowSessionService = service.ServiceGroupApp.SystemServiceGroup.AIWorkflowSession
autoCodePluginService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePlugin
autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService
skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService
)
+139 -37
View File
@@ -1,52 +1,43 @@
package system
import (
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"go.uber.org/zap"
)
type AutoCodeApi struct{}
// GetDB
// @Tags AutoCode
// @Summary 获取当前所有数据库
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前所有数据库"
// @Router /autoCode/getDB [get]
func (autoApi *AutoCodeApi) GetDB(c *gin.Context) {
businessDB := c.Query("businessDB")
dbs, err := autoCodeService.Database(businessDB).GetDB(businessDB)
var dbList []map[string]interface{}
for _, db := range global.GVA_CONFIG.DBList {
var item = make(map[string]interface{})
item["aliasName"] = db.AliasName
item["dbName"] = db.Dbname
item["disable"] = db.Disable
item["dbtype"] = db.Type
item := map[string]interface{}{
"aliasName": db.AliasName,
"dbName": db.Dbname,
"disable": db.Disable,
"dbtype": db.Type,
}
dbList = append(dbList, item)
}
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
} else {
response.OkWithDetailed(gin.H{"dbs": dbs, "dbList": dbList}, "获取成功", c)
return
}
response.OkWithDetailed(gin.H{"dbs": dbs, "dbList": dbList}, "获取成功", c)
}
// GetTables
// @Tags AutoCode
// @Summary 获取当前数据库所有表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前数据库所有表"
// @Router /autoCode/getTables [get]
func (autoApi *AutoCodeApi) GetTables(c *gin.Context) {
dbName := c.Query("dbName")
businessDB := c.Query("businessDB")
@@ -65,19 +56,11 @@ func (autoApi *AutoCodeApi) GetTables(c *gin.Context) {
if err != nil {
global.GVA_LOG.Error("查询table失败!", zap.Error(err))
response.FailWithMessage("查询table失败", c)
} else {
response.OkWithDetailed(gin.H{"tables": tables}, "获取成功", c)
return
}
response.OkWithDetailed(gin.H{"tables": tables}, "获取成功", c)
}
// GetColumn
// @Tags AutoCode
// @Summary 获取当前表所有字段
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前表所有字段"
// @Router /autoCode/getColumn [get]
func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
businessDB := c.Query("businessDB")
dbName := c.Query("dbName")
@@ -96,9 +79,9 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
} else {
response.OkWithDetailed(gin.H{"columns": columns}, "获取成功", c)
return
}
response.OkWithDetailed(gin.H{"columns": columns}, "获取成功", c)
}
func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) {
@@ -107,11 +90,130 @@ func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) {
response.FailWithMessage(err.Error(), c)
return
}
if shouldStreamLLM(c, llm) {
if err := autoApi.proxyLLMStream(c, llm); err != nil {
global.GVA_LOG.Error("大模型流式代理失败!", zap.Error(err))
if c.Writer.Written() {
writeLLMStreamError(c, err)
return
}
response.FailWithMessage(err.Error(), c)
}
return
}
data, err := autoCodeService.LLMAuto(c.Request.Context(), llm)
if err != nil {
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
response.FailWithMessage("大模型生成失败"+err.Error(), c)
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(data, c)
}
func shouldStreamLLM(c *gin.Context, llm common.JSONMap) bool {
responseMode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", llm["response_mode"])))
if responseMode == "streaming" || responseMode == "sse" {
return true
}
if stream, ok := llm["stream"].(bool); ok && stream {
return true
}
return strings.Contains(strings.ToLower(c.GetHeader("Accept")), "text/event-stream")
}
func (autoApi *AutoCodeApi) proxyLLMStream(c *gin.Context, llm common.JSONMap) error {
res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr)
}
return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body))
}
flusher, ok := c.Writer.(http.Flusher)
if !ok {
return errors.New("当前响应不支持流式输出")
}
copyLLMStreamHeaders(c.Writer.Header(), res.Header)
if c.Writer.Header().Get("Content-Type") == "" {
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
}
if c.Writer.Header().Get("Cache-Control") == "" {
c.Writer.Header().Set("Cache-Control", "no-cache")
}
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("X-Accel-Buffering", "no")
c.Status(res.StatusCode)
flusher.Flush()
buf := make([]byte, 32*1024)
for {
n, readErr := res.Body.Read(buf)
if n > 0 {
if _, writeErr := c.Writer.Write(buf[:n]); writeErr != nil {
return fmt.Errorf("向客户端写入流式响应失败: %w", writeErr)
}
flusher.Flush()
}
if readErr != nil {
if errors.Is(readErr, io.EOF) {
return nil
}
return fmt.Errorf("读取上游流式响应失败: %w", readErr)
}
}
}
func copyLLMStreamHeaders(dst, src http.Header) {
for _, key := range []string{
"Content-Type",
"Cache-Control",
"Content-Encoding",
"Content-Language",
"X-Accel-Buffering",
} {
if value := src.Get(key); value != "" {
dst.Set(key, value)
}
}
}
func writeLLMStreamError(c *gin.Context, err error) {
payload, marshalErr := json.Marshal(gin.H{
"message": err.Error(),
})
if marshalErr != nil {
payload = []byte(`{"message":"流式代理失败"}`)
}
_, _ = c.Writer.WriteString("event: error\n")
_, _ = c.Writer.WriteString("data: ")
_, _ = c.Writer.Write(payload)
_, _ = c.Writer.WriteString("\n\n")
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
}
}
func previewResponseBody(body []byte) string {
text := strings.TrimSpace(string(body))
text = strings.ReplaceAll(text, "\r", " ")
text = strings.ReplaceAll(text, "\n", " ")
text = strings.Join(strings.Fields(text), " ")
if text == "" {
return "<empty>"
}
runes := []rune(text)
if len(runes) > 300 {
return string(runes[:300]) + "..."
}
return text
}
+210
View File
@@ -0,0 +1,210 @@
package system
import (
"bufio"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"go.uber.org/zap"
)
func (autoApi *AutoCodeApi) LLMAutoSSE(c *gin.Context) {
var llm common.JSONMap
if err := c.ShouldBindJSON(&llm); err != nil {
global.GVA_LOG.Error("LLMAutoSSE 参数绑定失败!", zap.Error(err))
response.FailWithMessage(err.Error(), c)
return
}
if llm == nil {
llm = common.JSONMap{}
}
llm["response_mode"] = "streaming"
global.GVA_LOG.Info("LLMAutoSSE 收到请求", zap.Any("mode", llm["mode"]))
if err := autoApi.streamLLMAsSSE(c, llm); err != nil {
global.GVA_LOG.Error("大模型 SSE 代理失败!", zap.Error(err))
if c.Writer.Written() {
writeLLMStreamError(c, err)
return
}
response.FailWithMessage(err.Error(), c)
}
}
func (autoApi *AutoCodeApi) streamLLMAsSSE(c *gin.Context, llm common.JSONMap) error {
res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm)
if err != nil {
return fmt.Errorf("调用上游大模型失败: %w", err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr)
}
return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body))
}
ct := res.Header.Get("Content-Type")
global.GVA_LOG.Info("LLMAutoSSE 上游返回成功,开始 SSE 流式转发",
zap.Int("status", res.StatusCode),
zap.String("content-type", ct))
// 如果上游返回的不是 SSE 流(可能是 blocking 模式返回的 JSON),直接读取并转发
if !strings.Contains(ct, "text/event-stream") && !strings.Contains(ct, "text/plain") {
body, readErr := io.ReadAll(res.Body)
if readErr != nil {
return fmt.Errorf("读取上游非流式响应失败: %w", readErr)
}
global.GVA_LOG.Warn("LLMAutoSSE 上游返回非 SSE 流,Content-Type: "+ct+", 将以单次事件转发",
zap.String("body_preview", previewResponseBody(body)))
flusher, ok := c.Writer.(http.Flusher)
if !ok {
return errors.New("当前响应不支持流式输出")
}
prepareSSEHeaders(c)
c.Status(http.StatusOK)
var payload any
if err := json.Unmarshal(body, &payload); err != nil {
payload = string(body)
}
if err := renderSSE(c, sse.Event{Event: "message", Data: payload}); err != nil {
return err
}
if err := renderSSE(c, sse.Event{Event: "done", Data: gin.H{"done": true}}); err != nil {
return err
}
flusher.Flush()
return nil
}
flusher, ok := c.Writer.(http.Flusher)
if !ok {
return errors.New("当前响应不支持流式输出")
}
prepareSSEHeaders(c)
c.Status(http.StatusOK)
flusher.Flush()
reader := bufio.NewReader(res.Body)
lines := make([]string, 0, 8)
blockCount := 0
global.GVA_LOG.Info("LLMAutoSSE 开始读取上游流数据...")
for {
global.GVA_LOG.Debug("LLMAutoSSE 等待读取下一行...")
line, readErr := reader.ReadString('\n')
if readErr != nil && !errors.Is(readErr, io.EOF) {
global.GVA_LOG.Error("LLMAutoSSE 读取上游流失败", zap.Int("已转发块数", blockCount), zap.Error(readErr))
return fmt.Errorf("读取上游流式响应失败: %w", readErr)
}
line = strings.TrimRight(line, "\r\n")
if line == "" {
if len(lines) > 0 {
blockCount++
if blockCount <= 3 {
global.GVA_LOG.Debug("LLMAutoSSE 转发 SSE 块", zap.Int("block", blockCount), zap.Strings("lines", lines))
}
}
if err := emitSSEBlock(c, lines); err != nil {
return err
}
lines = lines[:0]
} else {
lines = append(lines, line)
}
if errors.Is(readErr, io.EOF) {
if err := emitSSEBlock(c, lines); err != nil {
return err
}
if err := renderSSE(c, sse.Event{
Event: "done",
Data: gin.H{"done": true},
}); err != nil {
return err
}
flusher.Flush()
global.GVA_LOG.Info("LLMAutoSSE 流式转发完成", zap.Int("总块数", blockCount))
return nil
}
}
}
func prepareSSEHeaders(c *gin.Context) {
header := c.Writer.Header()
header.Set("Content-Type", "text/event-stream; charset=utf-8")
header.Set("Cache-Control", "no-cache, no-transform")
header.Set("Connection", "keep-alive")
header.Set("X-Accel-Buffering", "no")
}
func emitSSEBlock(c *gin.Context, lines []string) error {
if len(lines) == 0 {
return nil
}
eventName := "message"
eventID := ""
dataLines := make([]string, 0, len(lines))
for _, line := range lines {
switch {
case strings.HasPrefix(line, "event:"):
eventName = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "id:"):
eventID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
case strings.HasPrefix(line, "data:"):
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
}
}
rawData := strings.TrimSpace(strings.Join(dataLines, "\n"))
if rawData == "" {
return nil
}
if rawData == "[DONE]" {
return renderSSE(c, sse.Event{
Id: eventID,
Event: "done",
Data: gin.H{"done": true},
})
}
var payload interface{}
if err := json.Unmarshal([]byte(rawData), &payload); err != nil {
payload = rawData
}
return renderSSE(c, sse.Event{
Id: eventID,
Event: eventName,
Data: payload,
})
}
func renderSSE(c *gin.Context, event sse.Event) error {
if err := event.Render(c.Writer); err != nil {
return fmt.Errorf("写入 SSE 事件失败: %w", err)
}
if flusher, ok := c.Writer.(http.Flusher); ok {
flusher.Flush()
}
return nil
}
+192
View File
@@ -0,0 +1,192 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"gopkg.in/yaml.v3"
)
type standaloneConfig struct {
MCP config.MCP `yaml:"mcp"`
AutoCode config.Autocode `yaml:"autocode"`
}
func loadStandaloneConfig() (string, error) {
configPath, err := resolveConfigPath()
if err != nil {
return "", err
}
content, err := os.ReadFile(configPath)
if err != nil {
return "", fmt.Errorf("读取 MCP 配置失败: %w", err)
}
var cfg standaloneConfig
if err := yaml.Unmarshal(content, &cfg); err != nil {
return "", fmt.Errorf("解析 MCP 配置失败: %w", err)
}
applyStandaloneDefaults(configPath, &cfg)
global.GVA_CONFIG.MCP = cfg.MCP
global.GVA_CONFIG.AutoCode = cfg.AutoCode
return configPath, nil
}
func resolveConfigPath() (string, error) {
explicit, err := parseConfigFlag(os.Args[1:])
if err != nil {
return "", err
}
if explicit != "" {
return filepath.Abs(explicit)
}
if envPath := strings.TrimSpace(os.Getenv("GVA_MCP_CONFIG")); envPath != "" {
return filepath.Abs(envPath)
}
wd, _ := os.Getwd()
exe, _ := os.Executable()
exeDir := filepath.Dir(exe)
candidates := []string{
filepath.Join(wd, "config.yaml"),
filepath.Join(wd, "mcp.yaml"),
filepath.Join(wd, "cmd", "mcp", "config.yaml"),
filepath.Join(wd, "server", "cmd", "mcp", "config.yaml"),
filepath.Join(exeDir, "config.yaml"),
filepath.Join(exeDir, "mcp.yaml"),
}
for _, candidate := range candidates {
if candidate == "" {
continue
}
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
return filepath.Abs(candidate)
}
}
return "", errors.New("未找到 MCP 独立配置文件,请在当前目录、cmd/mcp 目录或通过 -config / GVA_MCP_CONFIG 指定 config.yaml")
}
func parseConfigFlag(args []string) (string, error) {
fs := flag.NewFlagSet("gva-mcp", flag.ContinueOnError)
fs.SetOutput(io.Discard)
var configPath string
fs.StringVar(&configPath, "c", "", "MCP config file path")
fs.StringVar(&configPath, "config", "", "MCP config file path")
if err := fs.Parse(args); err != nil {
return "", err
}
return strings.TrimSpace(configPath), nil
}
func applyStandaloneDefaults(configPath string, cfg *standaloneConfig) {
if cfg.MCP.Name == "" {
cfg.MCP.Name = "GVA_MCP"
}
if cfg.MCP.Version == "" {
cfg.MCP.Version = "v1.0.0"
}
if cfg.MCP.Path == "" {
cfg.MCP.Path = "/mcp"
}
if cfg.MCP.Addr == 0 {
cfg.MCP.Addr = 8889
}
if cfg.MCP.AuthHeader == "" {
cfg.MCP.AuthHeader = "x-token"
}
if cfg.MCP.RequestTimeout <= 0 {
cfg.MCP.RequestTimeout = 15
}
if cfg.MCP.UpstreamBaseURL == "" {
cfg.MCP.UpstreamBaseURL = "http://127.0.0.1:8888"
}
if cfg.MCP.BaseURL == "" {
cfg.MCP.BaseURL = fmt.Sprintf("http://127.0.0.1:%d%s", cfg.MCP.Addr, cfg.MCP.Path)
}
configDir := filepath.Dir(configPath)
if cfg.AutoCode.Server == "" {
cfg.AutoCode.Server = "server"
}
if cfg.AutoCode.Web == "" {
cfg.AutoCode.Web = "web/src"
}
if cfg.AutoCode.Root == "" {
if root, err := detectProjectRoot(configDir); err == nil {
cfg.AutoCode.Root = root
}
} else if !filepath.IsAbs(cfg.AutoCode.Root) {
cfg.AutoCode.Root = filepath.Clean(filepath.Join(configDir, cfg.AutoCode.Root))
}
if cfg.AutoCode.Module == "" && cfg.AutoCode.Root != "" {
goModPath := filepath.Join(cfg.AutoCode.Root, cfg.AutoCode.Server, "go.mod")
if module, err := detectGoModule(goModPath); err == nil {
cfg.AutoCode.Module = module
}
}
}
func detectProjectRoot(startDir string) (string, error) {
dir := startDir
for {
serverDir := filepath.Join(dir, "server")
webDir := filepath.Join(dir, "web")
if isDir(serverDir) && isDir(webDir) {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", errors.New("未能自动识别项目根目录,请在 MCP 配置中设置 autocode.root")
}
func detectGoModule(goModPath string) (string, error) {
file, err := os.Open(goModPath)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", errors.New("go.mod 中未找到 module 定义")
}
func isDir(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
+14
View File
@@ -0,0 +1,14 @@
mcp:
name: GVA_MCP
version: v1.0.0
path: /mcp
addr: 8889
base_url: http://127.0.0.1:8889/mcp
upstream_base_url: http://127.0.0.1:8888
auth_header: x-token
request_timeout: 15
autocode:
root: ../../..
server: server
web: web/src
+17
View File
@@ -0,0 +1,17 @@
package main
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"go.uber.org/zap"
)
func initializeStandaloneLogger() error {
logger, err := zap.NewDevelopment()
if err != nil {
return err
}
global.GVA_LOG = logger
zap.ReplaceGlobals(logger)
return nil
}
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp"
_ "go.uber.org/automaxprocs"
"go.uber.org/zap"
)
func main() {
configPath, err := loadStandaloneConfig()
if err != nil {
panic(err)
}
if err := initializeStandaloneLogger(); err != nil {
panic(err)
}
addr := fmt.Sprintf(":%d", global.GVA_CONFIG.MCP.Addr)
server := mcpTool.NewStreamableHTTPServer()
global.GVA_LOG.Info("mcp独立服务启动",
zap.String("config", configPath),
zap.String("addr", addr),
zap.String("path", global.GVA_CONFIG.MCP.Path),
zap.String("upstream", global.GVA_CONFIG.MCP.UpstreamBaseURL),
)
if err := server.Start(addr); err != nil {
global.GVA_LOG.Fatal("mcp独立服务启动失败", zap.Error(err))
}
}
-3
View File
@@ -278,8 +278,5 @@ cors:
mcp:
name: GVA_MCP
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''
addr: 8889
separate: false
+6 -3
View File
@@ -109,6 +109,7 @@ mysql:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
@@ -123,6 +124,7 @@ pgsql:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
oracle:
@@ -134,6 +136,7 @@ oracle:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
mssql:
@@ -145,6 +148,7 @@ mssql:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
sqlite:
@@ -156,6 +160,7 @@ sqlite:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
db-list:
@@ -170,6 +175,7 @@ db-list:
password: ""
max-idle-conns: 10
max-open-conns: 100
conn-max-lifetime: 0
log-mode: ""
log-zap: false
@@ -279,8 +285,5 @@ cors:
mcp:
name: GVA_MCP
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''
addr: 8889
separate: false
+14 -13
View File
@@ -15,19 +15,20 @@ type DsnProvider interface {
// GeneralDB 也被 Pgsql 和 Mysql 原样使用
type GeneralDB struct {
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀
Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口
Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名
Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库账号
Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
Path string `mapstructure:"path" json:"path" yaml:"path"` // 数据库地址
Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启
LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀
Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口
Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置
Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名
Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库账号
Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码
Path string `mapstructure:"path" json:"path" yaml:"path"` // 数据库地址
Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB
LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志
MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数
MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数
ConnMaxLifetime int `mapstructure:"conn-max-lifetime" json:"conn-max-lifetime" yaml:"conn-max-lifetime"` // 连接最长复用时间,单位秒
Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启
LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件
}
func (c GeneralDB) LogLevel() logger.LogLevel {
+14 -7
View File
@@ -1,11 +1,18 @@
package config
type MCP struct {
Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称
Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本
SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径
MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径
UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 独立MCP服务端口
Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` // 是否独立运行MCP服务
Name string `mapstructure:"name" json:"name" yaml:"name"`
Version string `mapstructure:"version" json:"version" yaml:"version"`
Path string `mapstructure:"path" json:"path" yaml:"path"`
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"`
BaseURL string `mapstructure:"base_url" json:"base_url" yaml:"base_url"`
UpstreamBaseURL string `mapstructure:"upstream_base_url" json:"upstream_base_url" yaml:"upstream_base_url"`
AuthHeader string `mapstructure:"auth_header" json:"auth_header" yaml:"auth_header"`
RequestTimeout int `mapstructure:"request_timeout" json:"request_timeout" yaml:"request_timeout"`
// Deprecated fields kept for backward compatibility with older configs.
SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"`
MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"`
UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"`
Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"`
}
+11 -17
View File
@@ -2,16 +2,17 @@ package core
import (
"fmt"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize"
mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
"go.uber.org/zap"
"time"
)
func RunServer() {
if global.GVA_CONFIG.System.UseRedis {
// 初始化redis服务
initialize.Redis()
if global.GVA_CONFIG.System.UseMultipoint {
initialize.RedisList()
@@ -19,36 +20,29 @@ func RunServer() {
}
if global.GVA_CONFIG.System.UseMongo {
err := initialize.Mongo.Initialization()
if err != nil {
if err := initialize.Mongo.Initialization(); err != nil {
zap.L().Error(fmt.Sprintf("%+v", err))
}
}
// 从db加载jwt数据
if global.GVA_DB != nil {
system.LoadAll()
}
Router := initialize.Routers()
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
mcpBaseURL := mcpTool.ResolveMCPServiceURL()
fmt.Printf(`
欢迎使用 gin-vue-admin
当前版本:%s
加群方式:微信号:shouzi_1994 QQ群:470239250
项目地址:https://github.com/flipped-aurora/gin-vue-admin
项目地址:https://github.com/flipped-aurora/gin-vue-admin
插件市场:https://plugin.gin-vue-admin.com
GVA讨论社区:https://support.qq.com/products/371961
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认MCP SSE地址:http://127.0.0.1%s%s
默认MCP Message地址:http://127.0.0.1%s%s
MCP 独立服务请手动启动: go run ./cmd/mcp -config ./cmd/mcp/config.yaml
默认MCP StreamHTTP地址:%s
默认前端文件运行地址:http://127.0.0.1:8080
--------------------------------------版权声明--------------------------------------
** 版权所有方:flipped-aurora开源团队 **
** 版权持有公司:北京翻转极光科技有限责任公司 **
** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license **
** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展**
`, global.Version, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
`, global.Version, address, mcpBaseURL)
initServer(address, Router, 10*time.Minute, 10*time.Minute)
}
+1 -1
View File
@@ -4,7 +4,7 @@ package global
// 目前只有Version正式使用 其余为预留
const (
// Version 当前版本号
Version = "v2.9.0"
Version = "v2.9.2"
// AppName 应用名称
AppName = "Gin-Vue-Admin"
// Description 应用描述
+28 -27
View File
@@ -11,13 +11,14 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1
github.com/casbin/casbin/v2 v2.103.0
github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/casbin/casbin/v3 v3.10.0
github.com/casbin/gorm-adapter/v3 v3.41.0
github.com/dzwvip/gorm-oracle v0.1.2
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-contrib/sse v1.0.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/go-sql-driver/mysql v1.8.1
github.com/go-sql-driver/mysql v1.9.3
github.com/goccy/go-json v0.10.4
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
@@ -37,7 +38,7 @@ require (
github.com/shirou/gopsutil/v3 v3.24.5
github.com/songzhibin97/gkit v1.2.13
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
@@ -47,16 +48,17 @@ require (
go.mongodb.org/mongo-driver v1.17.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.37.0
golang.org/x/sync v0.13.0
golang.org/x/text v0.24.0
golang.org/x/crypto v0.46.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/datatypes v1.2.5
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlserver v1.5.4
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.3
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
gorm.io/gorm v1.31.1
)
require (
@@ -82,14 +84,14 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.0 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
@@ -100,7 +102,6 @@ require (
github.com/emirpasic/gods v1.12.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -124,7 +125,7 @@ require (
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -138,7 +139,7 @@ require (
github.com/magiconair/properties v1.8.9 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.8.0 // indirect
github.com/microsoft/go-mssqldb v1.9.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minlz v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -147,7 +148,7 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/mozillazg/go-httpheader v0.4.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
@@ -161,6 +162,7 @@ require (
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sijms/go-ora/v2 v2.7.17 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -188,20 +190,19 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/hints v1.1.2 // indirect
gorm.io/plugin/dbresolver v1.5.3 // indirect
modernc.org/fileutil v1.3.0 // indirect
modernc.org/libc v1.61.9 // indirect
gorm.io/plugin/dbresolver v1.6.2 // indirect
modernc.org/fileutil v1.3.40 // indirect
modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.42.2 // indirect
)
+100 -87
View File
@@ -19,25 +19,28 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
@@ -97,8 +100,8 @@ github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4p
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ=
github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
@@ -116,12 +119,13 @@ github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOE
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic=
github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus=
github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/casbin/v3 v3.10.0 h1:039ORla55vCeIZWd0LfzWFt1yiEA5X4W41xBW2bQuHs=
github.com/casbin/casbin/v3 v3.10.0/go.mod h1:5rJbQr2e6AuuDDNxnPc5lQlC9nIgg6nS1zYwKXhpHC8=
github.com/casbin/gorm-adapter/v3 v3.41.0 h1:Xhpi0tfRP9aKPDWDf6dgBxHZ9UM6IophxxPIEGWqCNM=
github.com/casbin/gorm-adapter/v3 v3.41.0/go.mod h1:BQZRJhwUnwMpI+pT2m7/cUJwXxrHfzpBpPcNTyMGeGA=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -203,16 +207,14 @@ github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn
github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -230,8 +232,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -256,12 +256,11 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -295,8 +294,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
@@ -360,13 +359,14 @@ github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZ
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY=
github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E=
@@ -392,8 +392,8 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w=
github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
@@ -438,6 +438,7 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -453,6 +454,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU=
github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/songzhibin97/gkit v1.2.13 h1:paY0XJkdRuy9/8k9nTnbdrzo8pC22jIIFldUkOQv5nU=
@@ -482,8 +485,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
@@ -567,16 +572,15 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -585,8 +589,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
@@ -608,11 +612,12 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -636,13 +641,14 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -660,9 +666,10 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -689,15 +696,15 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -707,12 +714,12 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -730,10 +737,12 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
@@ -766,8 +775,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -823,53 +832,57 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=
modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=
modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+2
View File
@@ -43,6 +43,7 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
sysModel.JwtBlacklist{},
sysModel.SysDictionary{},
sysModel.SysAutoCodeHistory{},
sysModel.SysAIWorkflowSession{},
sysModel.SysOperationRecord{},
sysModel.SysDictionaryDetail{},
sysModel.SysBaseMenuParameter{},
@@ -88,6 +89,7 @@ func (e *ensureTables) TableCreated(ctx context.Context) bool {
sysModel.JwtBlacklist{},
sysModel.SysDictionary{},
sysModel.SysAutoCodeHistory{},
sysModel.SysAIWorkflowSession{},
sysModel.SysOperationRecord{},
sysModel.SysDictionaryDetail{},
sysModel.SysBaseMenuParameter{},
+2 -2
View File
@@ -74,14 +74,14 @@ func RegisterTables() {
)
if err != nil {
global.GVA_LOG.Error("register table failed", zap.Error(err))
os.Exit(0)
os.Exit(1)
}
err = bizModel()
if err != nil {
global.GVA_LOG.Error("register biz_table failed", zap.Error(err))
os.Exit(0)
os.Exit(1)
}
global.GVA_LOG.Info("register table success")
}
+5
View File
@@ -10,9 +10,12 @@ package initialize
*/
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
@@ -37,6 +40,7 @@ func GormMssql() *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(m.ConnMaxLifetime) * time.Second)
return db
}
}
@@ -59,6 +63,7 @@ func GormMssqlByConfig(m config.Mssql) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(m.ConnMaxLifetime) * time.Second)
return db
}
}
+3
View File
@@ -5,6 +5,8 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
_ "github.com/go-sql-driver/mysql"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
@@ -43,6 +45,7 @@ func initMysqlDatabase(m config.Mysql) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(m.ConnMaxLifetime) * time.Second)
return db
}
}
+3
View File
@@ -1,6 +1,8 @@
package initialize
import (
"time"
oracle "github.com/dzwvip/gorm-oracle"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
@@ -32,6 +34,7 @@ func initOracleDatabase(m config.Oracle) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(m.ConnMaxLifetime) * time.Second)
return db
}
}
+3
View File
@@ -4,6 +4,8 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@@ -38,6 +40,7 @@ func initPgSqlDatabase(p config.Pgsql) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(p.MaxIdleConns)
sqlDB.SetMaxOpenConns(p.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(p.ConnMaxLifetime) * time.Second)
return db
}
}
+3
View File
@@ -4,6 +4,8 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
"time"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
@@ -33,6 +35,7 @@ func initSqliteDatabase(s config.Sqlite) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(s.MaxIdleConns)
sqlDB.SetMaxOpenConns(s.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(s.ConnMaxLifetime) * time.Second)
return db
}
}
-25
View File
@@ -1,25 +0,0 @@
package initialize
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp"
"github.com/mark3labs/mcp-go/server"
)
func McpRun() *server.SSEServer {
config := global.GVA_CONFIG.MCP
s := server.NewMCPServer(
config.Name,
config.Version,
)
global.GVA_MCP_SERVER = s
mcpTool.RegisterAllTools(s)
return server.NewSSEServer(s,
server.WithSSEEndpoint(config.SSEPath),
server.WithMessageEndpoint(config.MessagePath),
server.WithBaseURL(config.UrlPrefix))
}
+24 -1
View File
@@ -6,10 +6,33 @@ import (
)
func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) {
notifier := GetDBReadyNotifier()
// 保存插件初始化参数
notifier.SetPluginParams(&PluginParams{
PrivateGroup: PrivateGroup,
PublicRouter: PublicRouter,
Engine: engine,
})
if global.GVA_DB == nil {
global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装")
global.GVA_LOG.Info("项目暂未初始化,插件将在数据库初始化完成后自动注册")
// 订阅数据库就绪事件
notifier.Subscribe(func() {
global.GVA_LOG.Info("数据库已就绪,开始注册插件")
params := notifier.GetPluginParams()
if params != nil {
bizPluginV1(params.PrivateGroup, params.PublicRouter)
bizPluginV2(params.Engine)
// 重新同步全局路由表,包含插件注册的路由
global.GVA_ROUTERS = params.Engine.Routes()
global.GVA_LOG.Info("插件注册完成")
}
})
return
}
// 数据库已存在,直接注册
bizPluginV1(PrivateGroup, PublicRouter)
bizPluginV2(engine)
}
+112
View File
@@ -0,0 +1,112 @@
package initialize
import (
"sync"
"github.com/flipped-aurora/gin-vue-admin/server/service/system"
"github.com/gin-gonic/gin"
)
func init() {
// 注入数据库就绪回调到 service 层
system.SetDBReadyCallback(func() {
GetDBReadyNotifier().NotifyDBReady()
})
}
var (
dbReadyNotifier *DBReadyNotifier
dbReadyNotifierOnce sync.Once
)
// DBReadyNotifier 数据库就绪通知器
type DBReadyNotifier struct {
mu sync.RWMutex
subscribers []func()
dbReady bool
pluginParams *PluginParams // 保存插件初始化所需的参数
}
// PluginParams 插件初始化参数
type PluginParams struct {
PrivateGroup *gin.RouterGroup
PublicRouter *gin.RouterGroup
Engine *gin.Engine
}
// GetDBReadyNotifier 获取全局通知器实例(单例)
func GetDBReadyNotifier() *DBReadyNotifier {
dbReadyNotifierOnce.Do(func() {
dbReadyNotifier = &DBReadyNotifier{
subscribers: make([]func(), 0),
dbReady: false,
}
})
return dbReadyNotifier
}
// Subscribe 订阅数据库就绪事件
// 如果数据库已经就绪,立即执行回调
// 否则将回调加入订阅列表,等待通知
func (n *DBReadyNotifier) Subscribe(callback func()) {
n.mu.Lock()
defer n.mu.Unlock()
if n.dbReady {
// 数据库已就绪,立即执行
callback()
} else {
// 数据库未就绪,加入订阅列表
n.subscribers = append(n.subscribers, callback)
}
}
// SetPluginParams 设置插件初始化参数
func (n *DBReadyNotifier) SetPluginParams(params *PluginParams) {
n.mu.Lock()
defer n.mu.Unlock()
n.pluginParams = params
}
// GetPluginParams 获取插件初始化参数
func (n *DBReadyNotifier) GetPluginParams() *PluginParams {
n.mu.RLock()
defer n.mu.RUnlock()
return n.pluginParams
}
// NotifyDBReady 通知数据库已就绪
// 会触发所有订阅的回调函数
func (n *DBReadyNotifier) NotifyDBReady() {
n.mu.Lock()
if n.dbReady {
// 已经通知过了,避免重复通知
n.mu.Unlock()
return
}
n.dbReady = true
// 复制订阅列表,避免在执行回调时持有锁
callbacks := make([]func(), len(n.subscribers))
copy(callbacks, n.subscribers)
// 清空订阅列表
n.subscribers = nil
// 释放锁后再执行回调,避免死锁
n.mu.Unlock()
// 执行所有订阅的回调
for _, callback := range callbacks {
callback()
}
}
// IsDBReady 检查数据库是否已就绪
func (n *DBReadyNotifier) IsDBReady() bool {
n.mu.RLock()
defer n.mu.RUnlock()
return n.dbReady
}
+3 -20
View File
@@ -24,7 +24,7 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) {
}
stat, err := f.Stat()
if stat.IsDir() {
if err == nil && stat.IsDir() {
return nil, os.ErrPermission
}
@@ -41,20 +41,6 @@ func Routers() *gin.Engine {
Router.Use(gin.Logger())
}
if !global.GVA_CONFIG.MCP.Separate {
sseServer := McpRun()
// 注册mcp服务
Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) {
sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request)
})
Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) {
sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request)
})
}
systemRouter := router.RouterGroupApp.System
exampleRouter := router.RouterGroupApp.Example
// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
@@ -65,7 +51,8 @@ func Routers() *gin.Engine {
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)})
// Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
// 跨域,如需跨域可以打开下面的注释
// Router.Use(middleware.Cors()) // 直接放行全部跨域请求
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
@@ -99,10 +86,8 @@ func Routers() *gin.Engine {
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理
systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史
systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理
@@ -111,11 +96,9 @@ func Routers() *gin.Engine {
systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志
systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志
systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发
systemRouter.InitSkillsRouter(PrivateGroup,PublicGroup) // Skills 定义器
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类
}
//插件路由安装
+1 -1
View File
@@ -21,7 +21,7 @@ import (
// @Tag.Description 用户
// @title Gin-Vue-Admin Swagger API接口文档
// @version v2.9.0
// @version v2.9.2
// @description 使用gin+vue进行极速开发的全栈开发基础平台
// @securityDefinitions.apikey ApiKeyAuth
// @in header
+40 -72
View File
@@ -6,27 +6,23 @@ import (
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
RegisterTool(&ApiCreator{})
}
// ApiCreateRequest API创建请求结构
type ApiCreateRequest struct {
Path string `json:"path"` // API路径
Description string `json:"description"` // API中文描述
ApiGroup string `json:"apiGroup"` // API组
Method string `json:"method"` // HTTP方法
Path string `json:"path"`
Description string `json:"description"`
ApiGroup string `json:"apiGroup"`
Method string `json:"method"`
}
// ApiCreateResponse API创建响应结构
type ApiCreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -35,10 +31,8 @@ type ApiCreateResponse struct {
Method string `json:"method"`
}
// ApiCreator API创建工具
type ApiCreator struct{}
// New 创建API创建工具
func (a *ApiCreator) New() mcp.Tool {
return mcp.NewTool("create_api",
mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
@@ -68,37 +62,31 @@ func (a *ApiCreator) New() mcp.Tool {
)
}
// Handle 处理API创建请求
func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
var apis []ApiCreateRequest
// 检查是否是批量创建
if apisStr, ok := args["apis"].(string); ok && apisStr != "" {
if err := json.Unmarshal([]byte(apisStr), &apis); err != nil {
return nil, fmt.Errorf("apis 参数格式错误: %v", err)
return nil, fmt.Errorf("apis 参数格式错误: %w", err)
}
} else {
// 单个API创建
path, ok := args["path"].(string)
if !ok || path == "" {
return nil, errors.New("path 参数是必需的")
}
description, ok := args["description"].(string)
if !ok || description == "" {
return nil, errors.New("description 参数是必需的")
}
apiGroup, ok := args["apiGroup"].(string)
if !ok || apiGroup == "" {
return nil, errors.New("apiGroup 参数是必需的")
}
method := "POST"
if val, ok := args["method"].(string); ok && val != "" {
method = val
if value, ok := args["method"].(string); ok && value != "" {
method = value
}
apis = append(apis, ApiCreateRequest{
@@ -113,79 +101,59 @@ func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*
return nil, errors.New("没有要创建的API")
}
// 创建API记录
apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService
var responses []ApiCreateResponse
responses := make([]ApiCreateResponse, 0, len(apis))
successCount := 0
for _, apiReq := range apis {
api := system.SysApi{
_, err := postUpstream[map[string]any](ctx, "/api/createApi", system.SysApi{
Path: apiReq.Path,
Description: apiReq.Description,
ApiGroup: apiReq.ApiGroup,
Method: apiReq.Method,
}
err := apiService.CreateApi(api)
})
if err != nil {
global.GVA_LOG.Warn("创建API失败",
zap.String("path", apiReq.Path),
zap.String("method", apiReq.Method),
zap.Error(err))
responses = append(responses, ApiCreateResponse{
Success: false,
Message: fmt.Sprintf("创建API失败: %v", err),
Path: apiReq.Path,
Method: apiReq.Method,
})
} else {
// 获取创建的API ID
var createdApi system.SysApi
err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error
if err != nil {
global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err))
}
responses = append(responses, ApiCreateResponse{
Success: true,
Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
ApiID: createdApi.ID,
Path: apiReq.Path,
Method: apiReq.Method,
})
successCount++
continue
}
lookupResp, lookupErr := postUpstream[pageResultData[[]system.SysApi]](ctx, "/api/getApiList", systemReq.SearchApiParams{
SysApi: system.SysApi{
Path: apiReq.Path,
Method: apiReq.Method,
},
PageInfo: commonReq.PageInfo{
Page: 1,
PageSize: 1,
},
})
var apiID uint
if lookupErr == nil && len(lookupResp.Data.List) > 0 {
apiID = lookupResp.Data.List[0].ID
}
responses = append(responses, ApiCreateResponse{
Success: true,
Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path),
ApiID: apiID,
Path: apiReq.Path,
Method: apiReq.Method,
})
successCount++
}
// 构建总体响应
var resultMessage string
if len(apis) == 1 {
resultMessage = responses[0].Message
} else {
resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount)
}
result := map[string]interface{}{
result := map[string]any{
"success": successCount > 0,
"message": resultMessage,
"totalCount": len(apis),
"successCount": successCount,
"failedCount": len(apis) - successCount,
"details": responses,
}
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("API创建结果:\n\n%s", string(resultJSON)),
},
},
}, nil
return textResultWithJSON("API创建结果:", result)
}
+36 -109
View File
@@ -2,44 +2,39 @@ package mcpTool
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
"github.com/gin-gonic/gin"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
// 注册工具将在enter.go中统一处理
RegisterTool(&ApiLister{})
}
// ApiInfo API信息结构
type ApiInfo struct {
ID uint `json:"id,omitempty"` // 数据库ID(仅数据库API有)
Path string `json:"path"` // API路径
Description string `json:"description,omitempty"` // API描述
ApiGroup string `json:"apiGroup,omitempty"` // API组
Method string `json:"method"` // HTTP方法
Source string `json:"source"` // 来源:database 或 gin
ID uint `json:"id,omitempty"`
Path string `json:"path"`
Description string `json:"description,omitempty"`
ApiGroup string `json:"apiGroup,omitempty"`
Method string `json:"method"`
Source string `json:"source"`
}
// ApiListResponse API列表响应结构
type ApiListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API
GinApis []ApiInfo `json:"ginApis"` // gin框架中的API
TotalCount int `json:"totalCount"` // 总数量
DatabaseApis []ApiInfo `json:"databaseApis"`
GinApis []ApiInfo `json:"ginApis"`
TotalCount int `json:"totalCount"`
}
type mcpRoutesResponse struct {
Routes gin.RoutesInfo `json:"routes"`
}
// ApiLister API列表工具
type ApiLister struct{}
// New 创建API列表工具
func (a *ApiLister) New() mcp.Tool {
return mcp.NewTool("list_all_apis",
mcp.WithDescription(`获取系统中所有的API接口,分为两组:
@@ -54,92 +49,24 @@ func (a *ApiLister) New() mcp.Tool {
- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`),
mcp.WithString("_placeholder",
mcp.Description("占位符,防止json schema校验失败"),
),
),
)
}
// Handle 处理API列表请求
func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取数据库中的API
databaseApis, err := a.getDatabaseApis()
if err != nil {
global.GVA_LOG.Error("获取数据库API失败", zap.Error(err))
errorResponse := ApiListResponse{
Success: false,
Message: "获取数据库API失败: " + err.Error(),
}
resultJSON, _ := json.Marshal(errorResponse)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// 获取gin路由中的API
ginApis, err := a.getGinApis()
if err != nil {
global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err))
errorResponse := ApiListResponse{
Success: false,
Message: "获取gin路由API失败: " + err.Error(),
}
resultJSON, _ := json.Marshal(errorResponse)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// 构建响应
response := ApiListResponse{
Success: true,
Message: "获取API列表成功",
DatabaseApis: databaseApis,
GinApis: ginApis,
TotalCount: len(databaseApis) + len(ginApis),
}
global.GVA_LOG.Info("API列表获取成功",
zap.Int("数据库API数量", len(databaseApis)),
zap.Int("gin路由API数量", len(ginApis)),
zap.Int("总数量", response.TotalCount))
resultJSON, err := json.Marshal(response)
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// getDatabaseApis 获取数据库中的所有API
func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) {
var apis []system.SysApi
err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error
func (a *ApiLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
apiResp, err := postUpstream[systemRes.SysAPIListResponse](ctx, "/api/getAllApis", map[string]any{})
if err != nil {
return nil, err
}
// 转换为ApiInfo格式
var result []ApiInfo
for _, api := range apis {
result = append(result, ApiInfo{
routeResp, err := postUpstream[mcpRoutesResponse](ctx, "/autoCode/mcpRoutes", map[string]any{})
if err != nil {
return nil, err
}
databaseApis := make([]ApiInfo, 0, len(apiResp.Data.Apis))
for _, api := range apiResp.Data.Apis {
databaseApis = append(databaseApis, ApiInfo{
ID: api.ID,
Path: api.Path,
Description: api.Description,
@@ -149,20 +76,20 @@ func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) {
})
}
return result, nil
}
// getGinApis 获取gin路由中的所有API(包含被忽略的API)
func (a *ApiLister) getGinApis() ([]ApiInfo, error) {
// 从gin路由信息中获取所有API
var result []ApiInfo
for _, route := range global.GVA_ROUTERS {
result = append(result, ApiInfo{
ginApis := make([]ApiInfo, 0, len(routeResp.Data.Routes))
for _, route := range routeResp.Data.Routes {
ginApis = append(ginApis, ApiInfo{
Path: route.Path,
Method: route.Method,
Source: "gin",
})
}
return result, nil
return textResultWithJSON("", ApiListResponse{
Success: true,
Message: "获取API列表成功",
DatabaseApis: databaseApis,
GinApis: ginApis,
TotalCount: len(databaseApis) + len(ginApis),
})
}
+48
View File
@@ -0,0 +1,48 @@
package mcpTool
import (
"context"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
func fetchAutoCodePackages(ctx context.Context) ([]model.SysAutoCodePackage, error) {
resp, err := postUpstream[map[string][]model.SysAutoCodePackage](ctx, "/autoCode/getPackage", map[string]any{})
if err != nil {
return nil, err
}
return resp.Data["pkgs"], nil
}
func fetchAutoCodeHistories(ctx context.Context) ([]model.SysAutoCodeHistory, error) {
resp, err := postUpstream[pageResultData[[]model.SysAutoCodeHistory]](ctx, "/autoCode/getSysHistory", commonReq.PageInfo{
Page: 1,
PageSize: 10000,
})
if err != nil {
return nil, err
}
return resp.Data.List, nil
}
func createAutoCodePackage(ctx context.Context, info *systemReq.SysAutoCodePackageCreate) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/createPackage", info)
return err
}
func createAutoCodeModule(ctx context.Context, info systemReq.AutoCode) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/createTemp", info)
return err
}
func deleteAutoCodePackage(ctx context.Context, id uint) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/delPackage", commonReq.GetById{ID: int(id)})
return err
}
func deleteAutoCodeHistory(ctx context.Context, id uint) error {
_, err := postUpstream[map[string]any](ctx, "/autoCode/delSysHistory", commonReq.GetById{ID: int(id)})
return err
}
+10 -5
View File
@@ -3,24 +3,28 @@ package client
import (
"context"
"errors"
mcpClient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
)
func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) {
client, err := mcpClient.NewSSEMCPClient(baseUrl)
func NewClient(baseURL, name, version, serverName string, headers ...map[string]string) (*mcpClient.Client, error) {
options := make([]transport.StreamableHTTPCOption, 0, 1)
if len(headers) > 0 && len(headers[0]) > 0 {
options = append(options, transport.WithHTTPHeaders(headers[0]))
}
client, err := mcpClient.NewStreamableHttpClient(baseURL, options...)
if err != nil {
return nil, err
}
ctx := context.Background()
// 启动client
if err := client.Start(ctx); err != nil {
return nil, err
}
// 初始化
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
@@ -35,5 +39,6 @@ func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, er
if result.ServerInfo.Name != serverName {
return nil, errors.New("server name mismatch")
}
return client, nil
}
+66
View File
@@ -0,0 +1,66 @@
package mcpTool
import (
"context"
"net/http"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
type mcpContextKey string
const authTokenContextKey mcpContextKey = "mcp-auth-token"
func WithHTTPRequestContext(ctx context.Context, r *http.Request) context.Context {
token := extractIncomingAuthToken(r.Header)
return context.WithValue(ctx, authTokenContextKey, token)
}
func configuredAuthHeader() string {
if header := strings.TrimSpace(global.GVA_CONFIG.MCP.AuthHeader); header != "" {
return header
}
return "x-token"
}
func ConfiguredAuthHeader() string {
return configuredAuthHeader()
}
func authTokenFromContext(ctx context.Context) string {
token, _ := ctx.Value(authTokenContextKey).(string)
return strings.TrimSpace(token)
}
func extractIncomingAuthToken(headers http.Header) string {
candidates := []string{
configuredAuthHeader(),
"x-token",
"token",
"authorization",
}
seen := make(map[string]struct{}, len(candidates))
for _, name := range candidates {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
value := strings.TrimSpace(headers.Get(name))
if value == "" {
continue
}
if key == "authorization" {
return strings.TrimSpace(strings.TrimPrefix(value, "Bearer "))
}
return value
}
return ""
}
+29 -84
View File
@@ -6,38 +6,30 @@ import (
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
"gorm.io/gorm"
)
func init() {
RegisterTool(&DictionaryOptionsGenerator{})
}
// DictionaryOptionsGenerator 字典选项生成器
type DictionaryOptionsGenerator struct{}
// DictionaryOption 字典选项结构
type DictionaryOption struct {
Label string `json:"label"`
Value string `json:"value"`
Sort int `json:"sort"`
}
// DictionaryGenerateRequest 字典生成请求
type DictionaryGenerateRequest struct {
DictType string `json:"dictType"` // 字典类型
FieldDesc string `json:"fieldDesc"` // 字段描述
Options []DictionaryOption `json:"options"` // AI生成的字典选项
DictName string `json:"dictName"` // 字典名称(可选)
Description string `json:"description"` // 字典描述(可选)
DictType string `json:"dictType"`
FieldDesc string `json:"fieldDesc"`
Options []DictionaryOption `json:"options"`
DictName string `json:"dictName"`
Description string `json:"description"`
}
// DictionaryGenerateResponse 字典生成响应
type DictionaryGenerateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -45,7 +37,6 @@ type DictionaryGenerateResponse struct {
OptionsCount int `json:"optionsCount"`
}
// New 返回工具注册信息
func (d *DictionaryOptionsGenerator) New() mcp.Tool {
return mcp.NewTool("generate_dictionary_options",
mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"),
@@ -70,79 +61,52 @@ func (d *DictionaryOptionsGenerator) New() mcp.Tool {
)
}
// Handle 处理工具调用
func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 解析请求参数
args := request.GetArguments()
dictType, ok := args["dictType"].(string)
if !ok || dictType == "" {
return nil, errors.New("dictType 参数是必需的")
}
fieldDesc, ok := args["fieldDesc"].(string)
if !ok || fieldDesc == "" {
return nil, errors.New("fieldDesc 参数是必需的")
}
optionsStr, ok := args["options"].(string)
if !ok || optionsStr == "" {
return nil, errors.New("options 参数是必需的")
}
// 解析options JSON字符串
var options []DictionaryOption
if err := json.Unmarshal([]byte(optionsStr), &options); err != nil {
return nil, fmt.Errorf("options 参数格式错误: %v", err)
}
if len(options) == 0 {
return nil, errors.New("options 不能为空")
}
dictName, _ := args["dictName"].(string)
description, _ := args["description"].(string)
// 构建请求对象
req := &DictionaryGenerateRequest{
DictType: dictType,
FieldDesc: fieldDesc,
Options: options,
DictName: dictName,
Description: description,
DictName: stringValue(args["dictName"]),
Description: stringValue(args["description"]),
}
// 创建字典
response, err := d.createDictionaryWithOptions(ctx, req)
result, err := d.createDictionaryWithOptions(ctx, req)
if err != nil {
return nil, err
}
// 构建响应
resultJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("字典选项生成结果:\n\n%s", string(resultJSON)),
},
},
}, nil
return textResultWithJSON("字典选项生成结果:", result)
}
// createDictionaryWithOptions 创建字典和字典选项
func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) {
// 检查字典是否已存在
exists, err := d.checkDictionaryExists(req.DictType)
existingDict, err := findDictionaryByType(ctx, req.DictType)
if err != nil {
return nil, fmt.Errorf("检查字典是否存在失败: %v", err)
}
if exists {
if existingDict != nil {
return &DictionaryGenerateResponse{
Success: false,
Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType),
@@ -151,50 +115,38 @@ func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Con
}, nil
}
// 生成字典名称
dictName := req.DictName
if dictName == "" {
dictName = d.generateDictionaryName(req.DictType, req.FieldDesc)
}
// 创建字典
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
dictionary := system.SysDictionary{
if err := createDictionary(ctx, system.SysDictionary{
Name: dictName,
Type: req.DictType,
Status: &[]bool{true}[0], // 默认启用
Status: enabledBoolPointer(),
Desc: req.Description,
}
err = dictionaryService.CreateSysDictionary(dictionary)
if err != nil {
}); err != nil {
return nil, fmt.Errorf("创建字典失败: %v", err)
}
// 获取刚创建的字典ID
var createdDict system.SysDictionary
err = global.GVA_DB.Where("type = ?", req.DictType).First(&createdDict).Error
createdDict, err := findDictionaryByType(ctx, req.DictType)
if err != nil {
return nil, fmt.Errorf("获取创建的字典失败: %v", err)
}
if createdDict == nil {
return nil, fmt.Errorf("获取创建的字典失败")
}
// 创建字典详情项
dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
successCount := 0
for _, option := range req.Options {
dictionaryDetail := system.SysDictionaryDetail{
err := createDictionaryDetail(ctx, system.SysDictionaryDetail{
Label: option.Label,
Value: option.Value,
Status: &[]bool{true}[0], // 默认启用
Status: enabledBoolPointer(),
Sort: option.Sort,
SysDictionaryID: int(createdDict.ID),
}
err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail)
if err != nil {
global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err))
} else {
})
if err == nil {
successCount++
}
}
@@ -207,23 +159,16 @@ func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Con
}, nil
}
// checkDictionaryExists 检查字典是否存在
func (d *DictionaryOptionsGenerator) checkDictionaryExists(dictType string) (bool, error) {
var dictionary system.SysDictionary
err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil // 字典不存在
}
return false, err // 其他错误
}
return true, nil // 字典存在
}
// generateDictionaryName 生成字典名称
func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string {
if fieldDesc != "" {
return fmt.Sprintf("%s字典", fieldDesc)
}
return fmt.Sprintf("%s字典", dictType)
}
func stringValue(value any) string {
if str, ok := value.(string); ok {
return str
}
return ""
}
+73
View File
@@ -0,0 +1,73 @@
package mcpTool
import (
"context"
"net/url"
"strconv"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
)
type exportedDictionary struct {
Name string `json:"name"`
Type string `json:"type"`
Status *bool `json:"status"`
Desc string `json:"desc"`
SysDictionaryDetails []system.SysDictionaryDetail `json:"sysDictionaryDetails"`
}
func fetchDictionaryList(ctx context.Context, keyword string) ([]system.SysDictionary, error) {
query := url.Values{}
if keyword != "" {
query.Set("name", keyword)
}
resp, err := getUpstream[[]system.SysDictionary](ctx, "/sysDictionary/getSysDictionaryList", query)
if err != nil {
return nil, err
}
return resp.Data, nil
}
func findDictionaryByType(ctx context.Context, dictType string) (*system.SysDictionary, error) {
dictionaries, err := fetchDictionaryList(ctx, dictType)
if err != nil {
return nil, err
}
for _, dictionary := range dictionaries {
if dictionary.Type == dictType {
dict := dictionary
return &dict, nil
}
}
return nil, nil
}
func exportDictionary(ctx context.Context, id uint) (*exportedDictionary, error) {
query := url.Values{}
query.Set("id", strconv.FormatUint(uint64(id), 10))
resp, err := getUpstream[exportedDictionary](ctx, "/sysDictionary/exportSysDictionary", query)
if err != nil {
return nil, err
}
return &resp.Data, nil
}
func createDictionary(ctx context.Context, dictionary system.SysDictionary) error {
_, err := postUpstream[map[string]any](ctx, "/sysDictionary/createSysDictionary", dictionary)
return err
}
func createDictionaryDetail(ctx context.Context, detail system.SysDictionaryDetail) error {
_, err := postUpstream[map[string]any](ctx, "/sysDictionaryDetail/createSysDictionaryDetail", detail)
return err
}
func enabledBoolPointer() *bool {
return utils.Pointer(true)
}
+67 -167
View File
@@ -2,48 +2,38 @@ package mcpTool
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
"gorm.io/gorm"
)
// 注册工具
func init() {
RegisterTool(&DictionaryQuery{})
}
type DictionaryPre struct {
Type string `json:"type"` // 字典名(英)
Desc string `json:"desc"` // 描述
Type string `json:"type"`
Desc string `json:"desc"`
}
// DictionaryInfo 字典信息结构
type DictionaryInfo struct {
ID uint `json:"id"`
Name string `json:"name"` // 字典名(中)
Type string `json:"type"` // 字典名(英)
Status *bool `json:"status"` // 状态
Desc string `json:"desc"` // 描述
Details []DictionaryDetailInfo `json:"details"` // 字典详情
Name string `json:"name"`
Type string `json:"type"`
Status *bool `json:"status"`
Desc string `json:"desc"`
Details []DictionaryDetailInfo `json:"details"`
}
// DictionaryDetailInfo 字典详情信息结构
type DictionaryDetailInfo struct {
ID uint `json:"id"`
Label string `json:"label"` // 展示值
Value string `json:"value"` // 字典值
Extend string `json:"extend"` // 扩展值
Status *bool `json:"status"` // 启用状态
Sort int `json:"sort"` // 排序标记
Label string `json:"label"`
Value string `json:"value"`
Extend string `json:"extend"`
Status *bool `json:"status"`
Sort int `json:"sort"`
}
// DictionaryQueryResponse 字典查询响应结构
type DictionaryQueryResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -51,10 +41,8 @@ type DictionaryQueryResponse struct {
Dictionaries []DictionaryInfo `json:"dictionaries"`
}
// DictionaryQuery 字典查询工具
type DictionaryQuery struct{}
// New 创建字典查询工具
func (d *DictionaryQuery) New() mcp.Tool {
return mcp.NewTool("query_dictionaries",
mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"),
@@ -70,170 +58,82 @@ func (d *DictionaryQuery) New() mcp.Tool {
)
}
// Handle 处理字典查询请求
func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
// 获取参数
dictType := ""
if val, ok := args["dictType"].(string); ok {
dictType = val
dictType := stringValue(args["dictType"])
includeDisabled, _ := args["includeDisabled"].(bool)
detailsOnly, _ := args["detailsOnly"].(bool)
dictionaries, err := fetchDictionaryList(ctx, dictType)
if err != nil {
return nil, err
}
includeDisabled := false
if val, ok := args["includeDisabled"].(bool); ok {
includeDisabled = val
}
detailsOnly := false
if val, ok := args["detailsOnly"].(bool); ok {
detailsOnly = val
}
// 获取字典服务
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
var dictionaries []DictionaryInfo
var err error
if dictType != "" {
// 查询指定类型的字典
var status *bool
if !includeDisabled {
status = &[]bool{true}[0]
result := make([]DictionaryInfo, 0)
for _, dictionary := range dictionaries {
if dictType != "" && dictionary.Type != dictType {
continue
}
if !includeDisabled && dictionary.Status != nil && !*dictionary.Status {
continue
}
sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status)
dictInfo, err := buildDictionaryInfo(ctx, dictionary, includeDisabled)
if err != nil {
global.GVA_LOG.Error("查询字典失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
},
}, nil
}
// 转换为响应格式
dictInfo := DictionaryInfo{
ID: sysDictionary.ID,
Name: sysDictionary.Name,
Type: sysDictionary.Type,
Status: sysDictionary.Status,
Desc: sysDictionary.Desc,
}
// 获取字典详情
for _, detail := range sysDictionary.SysDictionaryDetails {
if includeDisabled || (detail.Status != nil && *detail.Status) {
dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
ID: detail.ID,
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
})
}
}
dictionaries = append(dictionaries, dictInfo)
} else {
// 查询所有字典
var sysDictionaries []system.SysDictionary
db := global.GVA_DB.Model(&system.SysDictionary{})
if !includeDisabled {
db = db.Where("status = ?", true)
}
err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
if includeDisabled {
return db.Order("sort")
} else {
return db.Where("status = ?", true).Order("sort")
}
}).Find(&sysDictionaries).Error
if err != nil {
global.GVA_LOG.Error("查询字典列表失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典列表失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
},
}, nil
}
// 转换为响应格式
for _, dict := range sysDictionaries {
dictInfo := DictionaryInfo{
ID: dict.ID,
Name: dict.Name,
Type: dict.Type,
Status: dict.Status,
Desc: dict.Desc,
}
// 获取字典详情
for _, detail := range dict.SysDictionaryDetails {
if includeDisabled || (detail.Status != nil && *detail.Status) {
dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{
ID: detail.ID,
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
})
}
}
dictionaries = append(dictionaries, dictInfo)
return nil, err
}
result = append(result, dictInfo)
}
// 如果只需要详情信息,则提取所有详情
if detailsOnly {
var allDetails []DictionaryDetailInfo
for _, dict := range dictionaries {
allDetails = append(allDetails, dict.Details...)
details := make([]DictionaryDetailInfo, 0)
for _, dictionary := range result {
details = append(details, dictionary.Details...)
}
response := map[string]interface{}{
return textResultWithJSON("", map[string]any{
"success": true,
"message": "查询字典详情成功",
"total": len(allDetails),
"details": allDetails,
}
responseJSON, _ := json.Marshal(response)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseJSON)),
},
}, nil
"total": len(details),
"details": details,
})
}
// 构建响应
response := DictionaryQueryResponse{
return textResultWithJSON("", DictionaryQueryResponse{
Success: true,
Message: "查询字典成功",
Total: len(dictionaries),
Dictionaries: dictionaries,
Total: len(result),
Dictionaries: result,
})
}
func buildDictionaryInfo(ctx context.Context, dictionary system.SysDictionary, includeDisabled bool) (DictionaryInfo, error) {
exported, err := exportDictionary(ctx, dictionary.ID)
if err != nil {
return DictionaryInfo{}, err
}
responseJSON, err := json.Marshal(response)
if err != nil {
global.GVA_LOG.Error("序列化响应失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "序列化响应失败: %v", "total": 0, "dictionaries": []}`, err.Error())),
},
}, nil
info := DictionaryInfo{
ID: dictionary.ID,
Name: exported.Name,
Type: exported.Type,
Status: exported.Status,
Desc: exported.Desc,
}
for _, detail := range exported.SysDictionaryDetails {
if !includeDisabled && detail.Status != nil && !*detail.Status {
continue
}
info.Details = append(info.Details, DictionaryDetailInfo{
ID: detail.ID,
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
})
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(string(responseJSON)),
},
}, nil
return info, nil
}
+62 -70
View File
@@ -1,11 +1,10 @@
package mcpTool
package mcpTool
import (
"context"
"encoding/json"
"errors"
"fmt"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"os"
"path/filepath"
"strings"
@@ -30,7 +29,7 @@ type AnalyzeRequest struct {
// AnalyzeResponse 分析响应结构体
type AnalyzeResponse struct {
ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
}
@@ -43,13 +42,13 @@ type ModuleInfo struct {
StructName string `json:"structName"` // 结构体名称
TableName string `json:"tableName"` // 表名
Description string `json:"description"` // 描述
FilePaths []string `json:"filePaths"` // 相关文件路径
FilePaths []string `json:"filePaths"` // 相关文件路径
}
// PackageInfo 包信息
type PackageInfo struct {
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
Template string `json:"template"` // 模板类型
Label string `json:"label"` // 标签
Desc string `json:"desc"` // 描述
Module string `json:"module"` // 模块
@@ -60,7 +59,7 @@ type PackageInfo struct {
type PredesignedModuleInfo struct {
ModuleName string `json:"moduleName"` // 模块名称
PackageName string `json:"packageName"` // 包名
Template string `json:"template"` // 模板类型
Template string `json:"template"` // 模板类型
FilePaths []string `json:"filePaths"` // 文件路径列表
Description string `json:"description"` // 描述
}
@@ -117,63 +116,65 @@ func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (
// performAnalysis 执行分析逻辑
func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
// 1. 获取数据库中的包信息
var packages []model.SysAutoCodePackage
if err := global.GVA_DB.Find(&packages).Error; err != nil {
_ = req
packages, err := fetchAutoCodePackages(ctx)
if err != nil {
return nil, fmt.Errorf("获取包信息失败: %v", err)
}
// 2. 获取历史记录
var histories []model.SysAutoCodeHistory
if err := global.GVA_DB.Find(&histories).Error; err != nil {
histories, err := fetchAutoCodeHistories(ctx)
if err != nil {
return nil, fmt.Errorf("获取历史记录失败: %v", err)
}
// 3. 检查空包并进行清理
cleanupInfo := &CleanupInfo{
DeletedPackages: []string{},
DeletedModules: []string{},
}
var validPackages []model.SysAutoCodePackage
validPackages := make([]PackageInfo, 0, len(packages))
var emptyPackageHistoryIDs []uint
for _, pkg := range packages {
isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template)
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err))
continue
}
if isEmpty {
// 删除空包文件夹
if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err))
} else {
cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName)
}
// 删除数据库记录
if err := global.GVA_DB.Delete(&pkg).Error; err != nil {
if err := deleteAutoCodePackage(ctx, pkg.ID); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
}
// 收集相关的历史记录ID
for _, history := range histories {
if history.Package == pkg.PackageName {
emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
}
}
} else {
validPackages = append(validPackages, pkg)
continue
}
validPackages = append(validPackages, PackageInfo{
PackageName: pkg.PackageName,
Template: pkg.Template,
Label: pkg.Label,
Desc: pkg.Desc,
Module: pkg.Module,
IsEmpty: false,
})
}
// 5. 清理空包相关的历史记录和脏历史记录
var dirtyHistoryIDs []uint
for _, history := range histories {
// 检查是否为空包相关的历史记录
for _, emptyID := range emptyPackageHistoryIDs {
if history.ID == emptyID {
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
@@ -182,28 +183,30 @@ func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (
}
}
// 删除脏历史记录
if len(dirtyHistoryIDs) > 0 {
if err := global.GVA_DB.Delete(&model.SysAutoCodeHistory{}, "id IN ?", dirtyHistoryIDs).Error; err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
} else {
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", len(dirtyHistoryIDs)))
deletedCount := 0
for _, historyID := range dirtyHistoryIDs {
if err := deleteAutoCodeHistory(ctx, historyID); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err))
continue
}
deletedCount++
}
if deletedCount > 0 {
global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", deletedCount))
}
// 清理相关的API和菜单记录
if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
}
}
// 6. 扫描预设计模块
predesignedModules, err := g.scanPredesignedModules()
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
predesignedModules = []PredesignedModuleInfo{} // 设置为空列表,不影响主流程
predesignedModules = []PredesignedModuleInfo{}
}
// 7. 过滤掉与已删除包相关的模块
filteredModules := []PredesignedModuleInfo{}
for _, module := range predesignedModules {
isDeleted := false
@@ -218,49 +221,38 @@ func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (
}
}
// 8. 构建分析结果消息
var analysisMessage strings.Builder
if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
analysisMessage.WriteString("**系统清理完成**\n\n")
if len(cleanupInfo.DeletedPackages) > 0 {
analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
}
if len(cleanupInfo.DeletedModules) > 0 {
analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
}
analysisMessage.WriteString("\n")
cleanupInfo.CleanupMessage = analysisMessage.String()
}
analysisMessage.WriteString(" **分析结果**\n\n")
analysisMessage.WriteString(fmt.Sprintf("- **现有包数量**: %d\n", len(validPackages)))
analysisMessage.WriteString(fmt.Sprintf("- **预设计模块数量**: %d\n\n", len(filteredModules)))
// 9. 转换包信息
existingPackages := make([]PackageInfo, len(validPackages))
for i, pkg := range validPackages {
existingPackages[i] = PackageInfo{
PackageName: pkg.PackageName,
Template: pkg.Template,
Label: pkg.Label,
Desc: pkg.Desc,
Module: pkg.Module,
IsEmpty: false, // 已经过滤掉空包
}
}
dictionaries := []DictionaryPre{} // 这里可以根据需要填充字典信息
err = global.GVA_DB.Table("sys_dictionaries").Find(&dictionaries, "deleted_at is null").Error
dictionaries := []DictionaryPre{}
dictEntities, err := fetchDictionaryList(ctx, "")
if err != nil {
global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
dictionaries = []DictionaryPre{} // 设置为空列表,不影响主流程
} else {
for _, dictionary := range dictEntities {
dictionaries = append(dictionaries, DictionaryPre{
Type: dictionary.Type,
Desc: dictionary.Desc,
})
}
}
var cleanupResult *CleanupInfo
if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 {
var message strings.Builder
message.WriteString("**系统清理完成**\n\n")
if len(cleanupInfo.DeletedPackages) > 0 {
message.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", ")))
}
if len(cleanupInfo.DeletedModules) > 0 {
message.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", ")))
}
cleanupInfo.CleanupMessage = message.String()
cleanupResult = cleanupInfo
}
// 10. 构建响应
response := &AnalyzeResponse{
ExistingPackages: existingPackages,
ExistingPackages: validPackages,
PredesignedModules: filteredModules,
Dictionaries: dictionaries,
CleanupInfo: cleanupResult,
}
return response, nil
@@ -278,7 +270,7 @@ func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool,
// 检查文件夹是否存在
if _, err := os.Stat(basePath); os.IsNotExist(err) {
return true, nil // 文件夹不存在,视为空
return true, nil // 文件夹不存在,认为空
} else if err != nil {
return false, err // 其他错误
}
@@ -485,7 +477,7 @@ func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string)
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
moduleName := strings.TrimSuffix(entry.Name(), ".go")
filePath := filepath.Join(dir, entry.Name())
filePath := outputPath(filepath.Join(dir, entry.Name()))
module := PredesignedModuleInfo{
ModuleName: moduleName,
+201 -242
View File
@@ -5,16 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
@@ -25,13 +22,13 @@ func init() {
// GVAExecutor GVA代码生成器
type GVAExecutor struct{}
// ExecuteRequest 执行请求结构
// ExecuteRequest 执行请求结构体
type ExecuteRequest struct {
ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
}
// ExecuteResponse 执行响应结构
// ExecuteResponse 执行响应结构体
type ExecuteResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -42,7 +39,7 @@ type ExecuteResponse struct {
NextActions []string `json:"nextActions,omitempty"`
}
// ExecutionPlan 执行计划结构
// ExecutionPlan 执行计划结构体
type ExecutionPlan struct {
PackageName string `json:"packageName"`
PackageType string `json:"packageType"` // "plugin" 或 "package"
@@ -57,7 +54,7 @@ type ExecutionPlan struct {
// New 创建GVA代码生成执行器工具
func (g *GVAExecutor) New() mcp.Tool {
return mcp.NewTool("gva_execute",
return mcp.NewTool("gva_execute",
mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤**
**核心功能:**
@@ -71,147 +68,147 @@ func (g *GVAExecutor) New() mcp.Tool {
- 字段使用字典类型时,系统会自动检查并创建字典
- 字典创建会在模块创建之前执行
- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`),
mcp.WithObject("executionPlan",
mcp.Description("执行计划,包含包信息、模块与字典信息"),
mcp.Required(),
mcp.Properties(map[string]interface{}{
"packageName": map[string]interface{}{
"type": "string",
"description": "包名(小写开头)",
},
"packageType": map[string]interface{}{
"type": "string",
"description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package",
"enum": []string{"package", "plugin"},
},
"needCreatedPackage": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建包,为true时packageInfo必需",
},
"needCreatedModules": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建模块,为true时modulesInfo必需",
},
"needCreatedDictionaries": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建字典,为true时dictionariesInfo必需",
},
"packageInfo": map[string]interface{}{
"type": "object",
"description": "包创建信息,当needCreatedPackage=true时必需",
"properties": map[string]interface{}{
"desc": map[string]interface{}{"type": "string", "description": "包描述"},
"label": map[string]interface{}{"type": "string", "description": "展示名"},
"template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}},
"packageName": map[string]interface{}{"type": "string", "description": "包名"},
},
},
"modulesInfo": map[string]interface{}{
"type": "array",
"description": "模块配置列表,支持批量创建多个模块",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"},
"tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"},
"businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"},
"structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"},
"packageName": map[string]interface{}{"type": "string", "description": "文件名称"},
"description": map[string]interface{}{"type": "string", "description": "中文描述"},
"abbreviation": map[string]interface{}{"type": "string", "description": "简称"},
"humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"},
"gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"},
"autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"},
"autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"},
"autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"},
"autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"},
"autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"},
"onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"},
"isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"},
"treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"},
"isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"},
"generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"},
"generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"},
"fields": map[string]interface{}{
"type": "array",
"description": "字段列表",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"},
"fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"},
"fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"},
"dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"},
"comment": map[string]interface{}{"type": "string", "description": "注释"},
"columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"},
"fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"},
"fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"},
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"},
"form": map[string]interface{}{"type": "boolean", "description": "表单显示"},
"table": map[string]interface{}{"type": "boolean", "description": "表格显示"},
"desc": map[string]interface{}{"type": "boolean", "description": "详情显示"},
"excel": map[string]interface{}{"type": "boolean", "description": "导入导出"},
"require": map[string]interface{}{"type": "boolean", "description": "是否必填"},
"defaultValue": map[string]interface{}{"type": "string", "description": "默认值"},
"errorText": map[string]interface{}{"type": "string", "description": "错误提示"},
"clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"},
"sort": map[string]interface{}{"type": "boolean", "description": "是否排序"},
"primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"},
"dataSource": map[string]interface{}{
"type": "object",
"description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空",
"properties": map[string]interface{}{
"dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"},
"table": map[string]interface{}{"type": "string", "description": "关联的表名"},
"label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"},
"value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"},
"association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"},
"hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"},
},
},
"checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"},
"fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"},
},
},
},
},
},
},
"paths": map[string]interface{}{
"type": "object",
"description": "生成的文件路径映射",
"additionalProperties": map[string]interface{}{"type": "string"},
},
"dictionariesInfo": map[string]interface{}{
"type": "array",
"description": "字典创建信息,字典创建会在模块创建之前执行",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"},
"dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"},
"description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"},
"status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"},
"options": map[string]interface{}{
"type": "array",
"description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"},
"value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"},
"sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"},
},
},
},
},
},
},
}),
mcp.AdditionalProperties(false),
),
mcp.WithObject("executionPlan",
mcp.Description("执行计划,包含包信息、模块与字典信息"),
mcp.Required(),
mcp.Properties(map[string]interface{}{
"packageName": map[string]interface{}{
"type": "string",
"description": "包名(小写开头)",
},
"packageType": map[string]interface{}{
"type": "string",
"description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package",
"enum": []string{"package", "plugin"},
},
"needCreatedPackage": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建包,为true时packageInfo必需",
},
"needCreatedModules": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建模块,为true时modulesInfo必需",
},
"needCreatedDictionaries": map[string]interface{}{
"type": "boolean",
"description": "是否需要创建字典,为true时dictionariesInfo必需",
},
"packageInfo": map[string]interface{}{
"type": "object",
"description": "包创建信息,当needCreatedPackage=true时必需",
"properties": map[string]interface{}{
"desc": map[string]interface{}{"type": "string", "description": "包描述"},
"label": map[string]interface{}{"type": "string", "description": "展示名"},
"template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}},
"packageName": map[string]interface{}{"type": "string", "description": "包名"},
},
},
"modulesInfo": map[string]interface{}{
"type": "array",
"description": "模块配置列表,支持批量创建多个模块",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"},
"tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"},
"businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"},
"structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"},
"packageName": map[string]interface{}{"type": "string", "description": "文件名称"},
"description": map[string]interface{}{"type": "string", "description": "中文描述"},
"abbreviation": map[string]interface{}{"type": "string", "description": "简称"},
"humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"},
"gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"},
"autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"},
"autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"},
"autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"},
"autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"},
"autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"},
"onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"},
"isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"},
"treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"},
"isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"},
"generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"},
"generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"},
"fields": map[string]interface{}{
"type": "array",
"description": "字段列表",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"},
"fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"},
"fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"},
"dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"},
"comment": map[string]interface{}{"type": "string", "description": "注释"},
"columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"},
"fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"},
"fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"},
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"},
"form": map[string]interface{}{"type": "boolean", "description": "表单显示"},
"table": map[string]interface{}{"type": "boolean", "description": "表格显示"},
"desc": map[string]interface{}{"type": "boolean", "description": "详情显示"},
"excel": map[string]interface{}{"type": "boolean", "description": "导入导出"},
"require": map[string]interface{}{"type": "boolean", "description": "是否必填"},
"defaultValue": map[string]interface{}{"type": "string", "description": "默认值"},
"errorText": map[string]interface{}{"type": "string", "description": "错误提示"},
"clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"},
"sort": map[string]interface{}{"type": "boolean", "description": "是否排序"},
"primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"},
"dataSource": map[string]interface{}{
"type": "object",
"description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空",
"properties": map[string]interface{}{
"dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"},
"table": map[string]interface{}{"type": "string", "description": "关联的表名"},
"label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"},
"value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"},
"association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"},
"hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"},
},
},
"checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"},
"fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"},
},
},
},
},
},
},
"paths": map[string]interface{}{
"type": "object",
"description": "生成的文件路径映射",
"additionalProperties": map[string]interface{}{"type": "string"},
},
"dictionariesInfo": map[string]interface{}{
"type": "array",
"description": "字典创建信息,字典创建会在模块创建之前执行",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"},
"dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"},
"description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"},
"status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"},
"fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"},
"options": map[string]interface{}{
"type": "array",
"description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"},
"value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"},
"sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"},
},
},
},
},
},
},
}),
mcp.AdditionalProperties(false),
),
mcp.WithString("requirement",
mcp.Description("原始需求描述(可选,用于日志记录)"),
),
@@ -294,7 +291,6 @@ func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (
// validateExecutionPlan 验证执行计划的完整性
func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
// 验证基本字段
if plan.PackageName == "" {
return errors.New("packageName 不能为空")
}
@@ -302,14 +298,10 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
return errors.New("packageType 必须是 'package' 或 'plugin'")
}
// 验证packageType和template字段的一致性
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
if plan.PackageType != plan.PackageInfo.Template {
return errors.New("packageType 和 packageInfo.template 必须保持一致")
}
if plan.NeedCreatedPackage && plan.PackageInfo != nil && plan.PackageType != plan.PackageInfo.Template {
return errors.New("packageType 和 packageInfo.template 必须保持一致")
}
// 验证包信息
if plan.NeedCreatedPackage {
if plan.PackageInfo == nil {
return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空")
@@ -328,13 +320,11 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
}
}
// 验证模块信息(批量验证)
if plan.NeedCreatedModules {
if len(plan.ModulesInfo) == 0 {
return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空")
}
// 遍历验证每个模块
for moduleIndex, moduleInfo := range plan.ModulesInfo {
if moduleInfo.Package == "" {
return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1)
@@ -357,8 +347,6 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
if moduleInfo.HumpPackageName == "" {
return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
}
// 验证字段信息
if len(moduleInfo.Fields) == 0 {
return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
}
@@ -367,8 +355,6 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
if field.FieldName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
}
// 确保字段名首字母大写
if len(field.FieldName) > 0 {
firstChar := string(field.FieldName[0])
if firstChar >= "a" && firstChar <= "z" {
@@ -388,7 +374,6 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
}
// 验证字段类型
validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
validType := false
for _, validFieldType := range validFieldTypes {
@@ -398,45 +383,36 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
}
}
if !validType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes)
return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持", moduleIndex+1, i+1, field.FieldType)
}
// 验证搜索类型(如果设置了)
if field.FieldSearchType != "" {
validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"}
validSearchType := false
for _, validType := range validSearchTypes {
if field.FieldSearchType == validType {
for _, validSearchTypeValue := range validSearchTypes {
if field.FieldSearchType == validSearchTypeValue {
validSearchType = true
break
}
}
if !validSearchType {
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes)
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持", moduleIndex+1, i+1, field.FieldSearchType)
}
}
// 验证 dataSource 字段配置
if field.DataSource != nil {
associationValue := field.DataSource.Association
// 当 association 为 2(一对多关联)时,强制修改 fieldType 为 array
if associationValue == 2 {
if field.FieldType != "array" {
global.GVA_LOG.Info(fmt.Sprintf("模块 %d 字段 %d:检测到一对多关联(association=2),自动将 fieldType 从 '%s' 修改为 'array'", moduleIndex+1, i+1, field.FieldType))
moduleInfo.Fields[i].FieldType = "array"
}
if associationValue == 2 && field.FieldType != "array" {
global.GVA_LOG.Info(fmt.Sprintf("module %d field %d association=2, force fieldType to array", moduleIndex+1, i+1))
moduleInfo.Fields[i].FieldType = "array"
}
// 验证 association 值的有效性
if associationValue != 1 && associationValue != 2 {
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1(一对一)或 2(一对多)", moduleIndex+1, i+1)
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1 或 2", moduleIndex+1, i+1)
}
}
}
// 验证主键设置
if !moduleInfo.GvaModel {
// 当不使用GVA模型时,必须有且仅有一个字段设置为主键
primaryKeyCount := 0
for _, field := range moduleInfo.Fields {
if field.PrimaryKey {
@@ -450,10 +426,9 @@ func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
}
} else {
// 当使用GVA模型时,所有字段的primaryKey都应该为false
for i, field := range moduleInfo.Fields {
if field.PrimaryKey {
return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1)
return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false", moduleIndex+1, i+1)
}
}
}
@@ -485,8 +460,7 @@ func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan)
// 创建包(如果需要)
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
err := packageService.Create(ctx, plan.PackageInfo)
err := createAutoCodePackage(ctx, plan.PackageInfo)
if err != nil {
result.Message = fmt.Sprintf("创建包失败: %v", err)
// 即使创建包失败,也要返回paths信息
@@ -503,8 +477,6 @@ func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan)
// 批量创建字典和模块(如果需要)
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
// 遍历所有模块进行创建
for _, moduleInfo := range plan.ModulesInfo {
@@ -515,7 +487,7 @@ func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan)
continue // 继续处理下一个模块
}
err = templateService.Create(ctx, *moduleInfo)
err = createAutoCodeModule(ctx, *moduleInfo)
if err != nil {
result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
continue // 继续处理下一个模块
@@ -576,76 +548,76 @@ func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]st
// 构建服务端路径
if serverPath != "" {
serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
serverBasePath := filepath.Join(rootPath, serverPath)
if packageType == "plugin" {
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录下
plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录中
plugingBasePath := filepath.Join(serverBasePath, "plugin", packageName)
// API 路径
paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
paths["api"] = outputPath(filepath.Join(plugingBasePath, "api"))
// Service 路径
paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
paths["service"] = outputPath(filepath.Join(plugingBasePath, "service"))
// Model 路径
paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
paths["model"] = outputPath(filepath.Join(plugingBasePath, "model"))
// Router 路径
paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
paths["router"] = outputPath(filepath.Join(plugingBasePath, "router"))
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
paths["request"] = outputPath(filepath.Join(plugingBasePath, "model", "request"))
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
paths["response"] = outputPath(filepath.Join(plugingBasePath, "model", "response"))
// Plugin 特有文件
paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath)
paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath)
paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath)
paths["plugin_main"] = outputPath(filepath.Join(plugingBasePath, "main.go"))
paths["plugin_config"] = outputPath(filepath.Join(plugingBasePath, "plugin.go"))
paths["plugin_initialize"] = outputPath(filepath.Join(plugingBasePath, "initialize"))
} else {
// Package 模式:传统的目录结构
// API 路径
paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
paths["api"] = outputPath(filepath.Join(serverBasePath, "api", "v1", packageName))
// Service 路径
paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
paths["service"] = outputPath(filepath.Join(serverBasePath, "service", packageName))
// Model 路径
paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
paths["model"] = outputPath(filepath.Join(serverBasePath, "model", packageName))
// Router 路径
paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
paths["router"] = outputPath(filepath.Join(serverBasePath, "router", packageName))
// Request 路径
paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
paths["request"] = outputPath(filepath.Join(serverBasePath, "model", packageName, "request"))
// Response 路径
paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
paths["response"] = outputPath(filepath.Join(serverBasePath, "model", packageName, "response"))
}
}
// 构建前端路径(两种模式相同)
if webPath != "" {
webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
webBasePath := filepath.Join(rootPath, webPath)
if packageType == "plugin" {
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录下
pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录中
pluginWebBasePath := filepath.Join(webBasePath, "plugin", packageName)
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
paths["vue_page"] = outputPath(filepath.Join(pluginWebBasePath, "view"))
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
paths["vue_api"] = outputPath(filepath.Join(pluginWebBasePath, "api"))
} else {
// Package 模式:传统的目录结构
// Vue 页面路径
paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
paths["vue_page"] = outputPath(filepath.Join(webBasePath, "view", packageName))
// API 路径
paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
paths["vue_api"] = outputPath(filepath.Join(webBasePath, "api", packageName))
}
}
@@ -707,46 +679,34 @@ func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
}
// checkDictionaryExists 检查字典是否存在
func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
_, err := dictionaryService.GetSysDictionary(dictType, 0, nil)
func (g *GVAExecutor) checkDictionaryExists(ctx context.Context, dictType string) (bool, error) {
dictionary, err := findDictionaryByType(ctx, dictType)
if err != nil {
// 如果是记录不存在的错误,返回false
if strings.Contains(err.Error(), "record not found") {
return false, nil
}
// 其他错误返回错误信息
return false, err
}
return true, nil
return dictionary != nil, nil
}
// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
var messages []string
dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService
dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
for _, dictInfo := range dictionariesInfo {
// 检查字典是否存在
exists, err := g.checkDictionaryExists(dictInfo.DictType)
exists, err := g.checkDictionaryExists(ctx, dictInfo.DictType)
if err != nil {
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
continue
}
if !exists {
// 字典不存在,创建字典
dictionary := model.SysDictionary{
err = createDictionary(ctx, system.SysDictionary{
Name: dictInfo.DictName,
Type: dictInfo.DictType,
Status: utils.Pointer(true),
Status: enabledBoolPointer(),
Desc: dictInfo.Description,
}
err = dictionaryService.CreateSysDictionary(dictionary)
})
if err != nil {
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
continue
@@ -754,30 +714,29 @@ func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionari
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
// 获取刚创建的字典ID
var createdDict model.SysDictionary
err = global.GVA_DB.Where("type = ?", dictInfo.DictType).First(&createdDict).Error
createdDict, err := findDictionaryByType(ctx, dictInfo.DictType)
if err != nil {
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err))
continue
}
if createdDict == nil {
messages = append(messages, fmt.Sprintf("获取创建的字典失败: %s; ", dictInfo.DictType))
continue
}
// 创建字典选项
if len(dictInfo.Options) > 0 {
successCount := 0
for _, option := range dictInfo.Options {
dictionaryDetail := model.SysDictionaryDetail{
dictionaryDetail := system.SysDictionaryDetail{
Label: option.Label,
Value: option.Value,
Status: &[]bool{true}[0], // 默认启用
Status: enabledBoolPointer(),
Sort: option.Sort,
SysDictionaryID: int(createdDict.ID),
}
err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail)
if err != nil {
global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err))
} else {
err = createDictionaryDetail(ctx, dictionaryDetail)
if err == nil {
successCount++
}
}
+153
View File
@@ -0,0 +1,153 @@
package mcpTool
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
type upstreamEnvelope[T any] struct {
Code int `json:"code"`
Data T `json:"data"`
Msg string `json:"msg"`
}
func ResolveMCPServiceURL() string {
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.BaseURL)
if baseURL != "" {
return strings.TrimRight(baseURL, "/")
}
addr := global.GVA_CONFIG.MCP.Addr
if addr <= 0 {
addr = 8889
}
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
if path == "" {
path = "/mcp"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return fmt.Sprintf("http://127.0.0.1:%d%s", addr, path)
}
func upstreamBaseURL() string {
baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.UpstreamBaseURL)
if baseURL != "" {
return strings.TrimRight(baseURL, "/")
}
return "http://127.0.0.1:8888"
}
func requestTimeout() time.Duration {
timeout := global.GVA_CONFIG.MCP.RequestTimeout
if timeout <= 0 {
timeout = 15
}
return time.Duration(timeout) * time.Second
}
func getUpstream[T any](ctx context.Context, endpoint string, query url.Values) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodGet, endpoint, query, nil)
}
func postUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodPost, endpoint, nil, body)
}
func deleteUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) {
return doUpstream[T](ctx, http.MethodDelete, endpoint, nil, body)
}
func doUpstream[T any](ctx context.Context, method, endpoint string, query url.Values, body any) (*upstreamEnvelope[T], error) {
token := authTokenFromContext(ctx)
if token == "" {
return nil, fmt.Errorf("缺少MCP鉴权请求头: %s", configuredAuthHeader())
}
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return nil, fmt.Errorf("上游接口路径不能为空")
}
if !strings.HasPrefix(endpoint, "/") {
endpoint = "/" + endpoint
}
baseURL := upstreamBaseURL()
requestURL, err := url.Parse(baseURL + endpoint)
if err != nil {
return nil, fmt.Errorf("构建上游请求地址失败: %w", err)
}
if len(query) > 0 {
requestURL.RawQuery = query.Encode()
}
var reader io.Reader
if body != nil {
payload, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("序列化上游请求失败: %w", err)
}
reader = bytes.NewReader(payload)
}
timeoutCtx, cancel := context.WithTimeout(ctx, requestTimeout())
defer cancel()
req, err := http.NewRequestWithContext(timeoutCtx, method, requestURL.String(), reader)
if err != nil {
return nil, fmt.Errorf("创建上游请求失败: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set(configuredAuthHeader(), token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("请求上游服务失败: %w", err)
}
defer resp.Body.Close()
rawBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取上游响应失败: %w", err)
}
var result upstreamEnvelope[T]
if len(rawBody) > 0 {
if err := json.Unmarshal(rawBody, &result); err != nil {
return nil, fmt.Errorf("解析上游响应失败: %w", err)
}
}
if resp.StatusCode >= http.StatusBadRequest {
if result.Msg != "" {
return nil, errors.New(result.Msg)
}
return nil, fmt.Errorf("上游请求失败,状态码: %d", resp.StatusCode)
}
if result.Code != 0 {
if result.Msg != "" {
return nil, errors.New(result.Msg)
}
return nil, fmt.Errorf("上游请求失败,业务码: %d", result.Code)
}
return &result, nil
}
+58 -107
View File
@@ -6,50 +6,42 @@ import (
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
RegisterTool(&MenuCreator{})
}
// MenuCreateRequest 菜单创建请求结构
type MenuCreateRequest struct {
ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单
Path string `json:"path"` // 路由path
Name string `json:"name"` // 路由name
Hidden bool `json:"hidden"` // 是否在列表隐藏
Component string `json:"component"` // 对应前端文件路径
Sort int `json:"sort"` // 排序标记
Title string `json:"title"` // 菜单名
Icon string `json:"icon"` // 菜单图标
KeepAlive bool `json:"keepAlive"` // 是否缓存
DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由
CloseTab bool `json:"closeTab"` // 自动关闭tab
ActiveName string `json:"activeName"` // 高亮菜单
Parameters []MenuParameterRequest `json:"parameters"` // 路由参数
MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮
ParentId uint `json:"parentId"`
Path string `json:"path"`
Name string `json:"name"`
Hidden bool `json:"hidden"`
Component string `json:"component"`
Sort int `json:"sort"`
Title string `json:"title"`
Icon string `json:"icon"`
KeepAlive bool `json:"keepAlive"`
DefaultMenu bool `json:"defaultMenu"`
CloseTab bool `json:"closeTab"`
ActiveName string `json:"activeName"`
Parameters []MenuParameterRequest `json:"parameters"`
MenuBtn []MenuButtonRequest `json:"menuBtn"`
}
// MenuParameterRequest 菜单参数请求结构
type MenuParameterRequest struct {
Type string `json:"type"` // 参数类型:params或query
Key string `json:"key"` // 参数key
Value string `json:"value"` // 参数值
Type string `json:"type"`
Key string `json:"key"`
Value string `json:"value"`
}
// MenuButtonRequest 菜单按钮请求结构
type MenuButtonRequest struct {
Name string `json:"name"` // 按钮名称
Desc string `json:"desc"` // 按钮描述
Name string `json:"name"`
Desc string `json:"desc"`
}
// MenuCreateResponse 菜单创建响应结构
type MenuCreateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
@@ -58,10 +50,8 @@ type MenuCreateResponse struct {
Path string `json:"path"`
}
// MenuCreator 菜单创建工具
type MenuCreator struct{}
// New 创建菜单创建工具
func (m *MenuCreator) New() mcp.Tool {
return mcp.NewTool("create_menu",
mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
@@ -121,75 +111,45 @@ func (m *MenuCreator) New() mcp.Tool {
)
}
// Handle 处理菜单创建请求
func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 解析请求参数
args := request.GetArguments()
// 必需参数
path, ok := args["path"].(string)
if !ok || path == "" {
return nil, errors.New("path 参数是必需的")
}
name, ok := args["name"].(string)
if !ok || name == "" {
return nil, errors.New("name 参数是必需的")
}
component, ok := args["component"].(string)
if !ok || component == "" {
return nil, errors.New("component 参数是必需的")
}
title, ok := args["title"].(string)
if !ok || title == "" {
return nil, errors.New("title 参数是必需的")
}
// 可选参数
parentId := uint(0)
if val, ok := args["parentId"].(float64); ok {
parentId = uint(val)
parentID := uint(0)
if value, ok := args["parentId"].(float64); ok {
parentID = uint(value)
}
hidden := false
if val, ok := args["hidden"].(bool); ok {
hidden = val
}
hidden, _ := args["hidden"].(bool)
sort := 1
if val, ok := args["sort"].(float64); ok {
sort = int(val)
if value, ok := args["sort"].(float64); ok {
sort = int(value)
}
icon := "menu"
if val, ok := args["icon"].(string); ok && val != "" {
icon = val
if value, ok := args["icon"].(string); ok && value != "" {
icon = value
}
keepAlive, _ := args["keepAlive"].(bool)
defaultMenu, _ := args["defaultMenu"].(bool)
closeTab, _ := args["closeTab"].(bool)
activeName, _ := args["activeName"].(string)
keepAlive := false
if val, ok := args["keepAlive"].(bool); ok {
keepAlive = val
}
defaultMenu := false
if val, ok := args["defaultMenu"].(bool); ok {
defaultMenu = val
}
closeTab := false
if val, ok := args["closeTab"].(bool); ok {
closeTab = val
}
activeName := ""
if val, ok := args["activeName"].(string); ok {
activeName = val
}
// 解析参数和按钮
var parameters []system.SysBaseMenuParameter
parameters := make([]system.SysBaseMenuParameter, 0)
if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" {
var paramReqs []MenuParameterRequest
if err := json.Unmarshal([]byte(parametersStr), &paramReqs); err != nil {
@@ -204,23 +164,22 @@ func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (
}
}
var menuBtn []system.SysBaseMenuBtn
menuBtns := make([]system.SysBaseMenuBtn, 0)
if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" {
var btnReqs []MenuButtonRequest
if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil {
var buttonReqs []MenuButtonRequest
if err := json.Unmarshal([]byte(menuBtnStr), &buttonReqs); err != nil {
return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err)
}
for _, btn := range btnReqs {
menuBtn = append(menuBtn, system.SysBaseMenuBtn{
Name: btn.Name,
Desc: btn.Desc,
for _, button := range buttonReqs {
menuBtns = append(menuBtns, system.SysBaseMenuBtn{
Name: button.Name,
Desc: button.Desc,
})
}
}
// 构建菜单对象
menu := system.SysBaseMenu{
ParentId: parentId,
ParentId: parentID,
Path: path,
Name: name,
Hidden: hidden,
@@ -235,43 +194,35 @@ func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (
ActiveName: activeName,
},
Parameters: parameters,
MenuBtn: menuBtn,
MenuBtn: menuBtns,
}
// 创建菜单
menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService
err := menuService.AddBaseMenu(menu)
if err != nil {
if _, err := postUpstream[map[string]any](ctx, "/menu/addBaseMenu", menu); err != nil {
return nil, fmt.Errorf("创建菜单失败: %v", err)
}
// 获取创建的菜单ID
var createdMenu system.SysBaseMenu
err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error
if err != nil {
global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err))
menuID := uint(0)
if menuListResp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{}); err == nil {
menuID = findMenuID(menuListResp.Data, name, path)
}
// 构建响应
response := &MenuCreateResponse{
return textResultWithJSON("菜单创建结果:", &MenuCreateResponse{
Success: true,
Message: fmt.Sprintf("成功创建菜单 %s", title),
MenuID: createdMenu.ID,
MenuID: menuID,
Name: name,
Path: path,
}
})
}
resultJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
func findMenuID(menus []system.SysBaseMenu, name, path string) uint {
for _, menu := range menus {
if menu.Name == name && menu.Path == path {
return menu.ID
}
if id := findMenuID(menu.Children, name, path); id != 0 {
return id
}
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("菜单创建结果:\n\n%s", string(resultJSON)),
},
},
}, nil
return 0
}
+17 -73
View File
@@ -2,34 +2,25 @@ package mcpTool
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
// 注册工具将在enter.go中统一处理
RegisterTool(&MenuLister{})
}
// MenuListResponse 菜单列表响应结构
type MenuListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Menus []system.SysBaseMenu `json:"menus"`
TotalCount int `json:"totalCount"`
Description string `json:"description"`
Success bool `json:"success"`
Message string `json:"message"`
Menus []system.SysBaseMenu `json:"menus"`
TotalCount int `json:"totalCount"`
Description string `json:"description"`
}
// MenuLister 菜单列表工具
type MenuLister struct{}
// New 创建菜单列表工具
func (m *MenuLister) New() mcp.Tool {
return mcp.NewTool("list_all_menus",
mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转
@@ -46,70 +37,23 @@ func (m *MenuLister) New() mcp.Tool {
- 菜单权限管理:了解系统中所有可用的菜单项
- 导航组件开发:构建动态导航菜单
- 系统架构分析:了解系统的菜单结构和页面组织`),
mcp.WithString("_placeholder",
mcp.WithString("_placeholder",
mcp.Description("占位符,防止json schema校验失败"),
),
),
)
}
// Handle 处理菜单列表请求
func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取所有基础菜单
allMenus, err := m.getAllMenus()
if err != nil {
global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("获取菜单列表失败: %v", err),
},
},
IsError: true,
}, nil
}
// 构建返回结果
response := MenuListResponse{
Success: true,
Message: "获取菜单列表成功",
Menus: allMenus,
TotalCount: len(allMenus),
Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
}
// 序列化响应
responseJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("序列化响应失败: %v", err),
},
},
IsError: true,
}, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(responseJSON),
},
},
}, nil
}
// getAllMenus 获取所有基础菜单
func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) {
var menus []system.SysBaseMenu
err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
func (m *MenuLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{})
if err != nil {
return nil, err
}
return menus, nil
}
return textResultWithJSON("", MenuListResponse{
Success: true,
Message: "获取菜单列表成功",
Menus: resp.Data,
TotalCount: len(resp.Data),
Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
})
}
+8
View File
@@ -0,0 +1,8 @@
package mcpTool
type pageResultData[T any] struct {
List T `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
+91
View File
@@ -0,0 +1,91 @@
package mcpTool
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/stretchr/testify/require"
)
func TestBuildDirectoryStructureReturnsRootRelativePaths(t *testing.T) {
original := global.GVA_CONFIG.AutoCode
t.Cleanup(func() {
global.GVA_CONFIG.AutoCode = original
})
root := filepath.Join(t.TempDir(), "repo")
global.GVA_CONFIG.AutoCode = config.Autocode{
Root: root,
Server: "server",
Web: "web",
Module: "demo-module",
}
paths := (&GVAExecutor{}).buildDirectoryStructure(&ExecutionPlan{
PackageName: "demo",
PackageType: "package",
})
require.Equal(t, "server/api/v1/demo", paths["api"])
require.Equal(t, "server/service/demo", paths["service"])
require.Equal(t, "server/model/demo", paths["model"])
require.Equal(t, "web/view/demo", paths["vue_page"])
require.Equal(t, "web/api/demo", paths["vue_api"])
require.NotContains(t, filepath.ToSlash(paths["api"]), filepath.ToSlash(root))
}
func TestCollectExpectedFilePathsReturnsRootRelativePaths(t *testing.T) {
original := global.GVA_CONFIG.AutoCode
t.Cleanup(func() {
global.GVA_CONFIG.AutoCode = original
})
root := filepath.Join(t.TempDir(), "repo")
global.GVA_CONFIG.AutoCode = config.Autocode{
Root: root,
Server: "server",
Web: "web",
}
paths := (&GVAExecutor{}).collectExpectedFilePaths(&ExecutionPlan{
PackageName: "demo",
PackageType: "package",
NeedCreatedModules: true,
ModulesInfo: []*request.AutoCode{
{StructName: "DemoItem"},
},
})
require.Contains(t, paths, "server/api/v1/demo/demoitem.go")
require.Contains(t, paths, "server/service/demo/demoitem.go")
require.Contains(t, paths, "server/model/demo/demoitem.go")
require.Contains(t, paths, "web/view/demo/demoitem.vue")
require.NotContains(t, strings.Join(paths, "\n"), filepath.ToSlash(root))
}
func TestScanModulesInDirectoryReturnsRootRelativeFilePaths(t *testing.T) {
original := global.GVA_CONFIG.AutoCode
t.Cleanup(func() {
global.GVA_CONFIG.AutoCode = original
})
root := t.TempDir()
global.GVA_CONFIG.AutoCode = config.Autocode{
Root: root,
Server: "server",
}
modelDir := filepath.Join(root, "server", "model", "demo")
require.NoError(t, os.MkdirAll(modelDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(modelDir, "demo_item.go"), []byte("package demo\n"), 0o644))
modules, err := (&GVAAnalyzer{}).scanModulesInDirectory(modelDir, "demo", "package")
require.NoError(t, err)
require.Len(t, modules, 1)
require.Equal(t, []string{"server/model/demo/demo_item.go"}, modules[0].FilePaths)
}
+43
View File
@@ -0,0 +1,43 @@
package mcpTool
import (
"path/filepath"
"strings"
"github.com/flipped-aurora/gin-vue-admin/server/global"
)
func outputPath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
if root != "" {
normalizedRoot := normalizeFilesystemPath(root)
normalizedPath := normalizeFilesystemPath(path)
if rel, err := filepath.Rel(normalizedRoot, normalizedPath); err == nil && !isParentTraversal(rel) {
return filepath.ToSlash(filepath.Clean(rel))
}
}
return filepath.ToSlash(filepath.Clean(path))
}
func normalizeFilesystemPath(path string) string {
cleaned := filepath.Clean(path)
if absPath, err := filepath.Abs(cleaned); err == nil {
return absPath
}
return cleaned
}
func isParentTraversal(path string) bool {
path = filepath.Clean(path)
if path == ".." {
return true
}
return strings.HasPrefix(path, ".."+string(filepath.Separator))
}
+37
View File
@@ -0,0 +1,37 @@
//go:build !windows
package mcpTool
import (
"errors"
"os/exec"
"syscall"
)
func prepareDetachedProcess(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
}
func processExists(pid int) bool {
if pid <= 0 {
return false
}
err := syscall.Kill(pid, 0)
return err == nil || errors.Is(err, syscall.EPERM)
}
func terminateProcess(pid int) error {
if pid <= 0 {
return nil
}
err := syscall.Kill(pid, syscall.SIGTERM)
if err == nil || errors.Is(err, syscall.ESRCH) {
return nil
}
return err
}
+58
View File
@@ -0,0 +1,58 @@
//go:build windows
package mcpTool
import (
"errors"
"os"
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
const windowsStillActive = 259
func prepareDetachedProcess(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x00000008 | 0x00000200 | 0x08000000,
}
}
func processExists(pid int) bool {
if pid <= 0 {
return false
}
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
return false
}
defer windows.CloseHandle(handle)
var code uint32
if err := windows.GetExitCodeProcess(handle, &code); err != nil {
return false
}
return code == windowsStillActive
}
func terminateProcess(pid int) error {
if pid <= 0 {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
return err
}
err = process.Kill()
if err == nil || errors.Is(err, os.ErrProcessDone) {
return nil
}
return err
}
+29
View File
@@ -0,0 +1,29 @@
package mcpTool
import (
"encoding/json"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
)
func textResultWithJSON(title string, payload any) (*mcp.CallToolResult, error) {
resultJSON, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %w", err)
}
text := string(resultJSON)
if title != "" {
text = fmt.Sprintf("%s\n\n%s", title, text)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: text,
},
},
}, nil
}
+163
View File
@@ -0,0 +1,163 @@
package mcpTool
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/mark3labs/mcp-go/mcp"
)
func init() {
RegisterTool(&RoleAPIAssigner{})
}
type RoleAPIAssigner struct{}
type roleAPIAssignResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
AuthorityID uint `json:"authorityId"`
Path string `json:"path"`
Method string `json:"method"`
Added bool `json:"added"`
AlreadyExists bool `json:"alreadyExists"`
TotalPolicies int `json:"totalPolicies"`
}
func (a *RoleAPIAssigner) New() mcp.Tool {
return mcp.NewTool("assign_api_to_role",
mcp.WithDescription(`将指定API权限追加分配给角色(仅追加,不覆盖原有权限)。
**功能说明:**
- 自动读取角色当前API权限
- 若目标权限不存在则追加并保存
- 若已存在则幂等返回成功,不重复写入
**适用场景:**
- create_api后将新API授权给指定角色
- 手动补齐某个角色缺失的API权限`),
mcp.WithNumber("authorityId",
mcp.Required(),
mcp.Description("角色ID,如:888"),
),
mcp.WithString("path",
mcp.Required(),
mcp.Description("API路径,如:/user/getCustomerUserList"),
),
mcp.WithString("method",
mcp.Description("HTTP方法,默认POST"),
mcp.DefaultString("POST"),
),
)
}
func (a *RoleAPIAssigner) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := request.GetArguments()
authorityID, err := parseAuthorityID(args["authorityId"])
if err != nil {
return nil, err
}
path, ok := args["path"].(string)
if !ok || strings.TrimSpace(path) == "" {
return nil, errors.New("path 参数是必需的")
}
method := "POST"
if value, ok := args["method"].(string); ok && strings.TrimSpace(value) != "" {
method = value
}
path, method = normalizePolicy(path, method)
currentResp, err := postUpstream[map[string][]systemReq.CasbinInfo](ctx, "/casbin/getPolicyPathByAuthorityId", map[string]any{
"authorityId": authorityID,
})
if err != nil {
return nil, fmt.Errorf("获取角色当前API权限失败: %w", err)
}
current := currentResp.Data["paths"]
updated, added := appendPolicyIfMissing(current, path, method)
if added {
if _, err = postUpstream[map[string]any](ctx, "/casbin/updateCasbin", map[string]any{
"authorityId": authorityID,
"casbinInfos": updated,
}); err != nil {
return nil, fmt.Errorf("分配API权限失败: %w", err)
}
}
msg := "权限已存在,无需重复分配"
if added {
msg = fmt.Sprintf("成功为角色 %d 分配权限 %s %s", authorityID, method, path)
}
return textResultWithJSON("角色API权限分配结果:", roleAPIAssignResponse{
Success: true,
Message: msg,
AuthorityID: authorityID,
Path: path,
Method: method,
Added: added,
AlreadyExists: !added,
TotalPolicies: len(updated),
})
}
func parseAuthorityID(v any) (uint, error) {
switch value := v.(type) {
case float64:
if value <= 0 {
return 0, errors.New("authorityId 必须大于0")
}
return uint(value), nil
case string:
id := strings.TrimSpace(value)
if id == "" {
return 0, errors.New("authorityId 参数是必需的")
}
parsed, err := strconv.ParseUint(id, 10, 64)
if err != nil || parsed == 0 {
return 0, errors.New("authorityId 格式不正确")
}
return uint(parsed), nil
default:
return 0, errors.New("authorityId 参数是必需的")
}
}
func appendPolicyIfMissing(current []systemReq.CasbinInfo, path, method string) ([]systemReq.CasbinInfo, bool) {
path, method = normalizePolicy(path, method)
for _, policy := range current {
p, m := normalizePolicy(policy.Path, policy.Method)
if p == path && m == method {
return current, false
}
}
updated := make([]systemReq.CasbinInfo, 0, len(current)+1)
updated = append(updated, current...)
updated = append(updated, systemReq.CasbinInfo{
Path: path,
Method: method,
})
return updated, true
}
func normalizePolicy(path, method string) (string, string) {
path = strings.TrimSpace(path)
if path == "" {
path = "/"
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
method = "POST"
}
return path, method
}

Some files were not shown because too many files have changed in this diff Show More