mirror of
https://github.com/flipped-aurora/gin-vue-admin.git
synced 2026-09-25 05:50:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a3f8297c | ||
|
|
375b21c222 | ||
|
|
324aaa3057 | ||
|
|
24c2013caf | ||
|
|
708a568427 | ||
|
|
a75ab546ee | ||
|
|
f8c40b5b21 | ||
|
|
98ccdc6295 | ||
|
|
d143b326b4 | ||
|
|
556b4431a2 | ||
|
|
9fea067931 | ||
|
|
fa9957c43c | ||
|
|
00224efd24 | ||
|
|
506c9ea7a6 | ||
|
|
05872354b8 | ||
|
|
b71cc443c0 | ||
|
|
1317e9504e | ||
|
|
670aff79cc | ||
|
|
52cd5e72ab | ||
|
|
68d6d3660b | ||
|
|
876017115c | ||
|
|
755373468c | ||
|
|
d2e64e618e | ||
|
|
fd2a6f45fd | ||
|
|
2242f5d6e1 | ||
|
|
567a8eb9bb | ||
|
|
2ebeec8765 | ||
|
|
70ee13e10c | ||
|
|
ed65e31cd8 | ||
|
|
f693f29326 | ||
|
|
2fda961643 | ||
|
|
fd9883e035 | ||
|
|
f41f068b8b | ||
|
|
799f411c73 | ||
|
|
97e7be9d3c | ||
|
|
5cfe04e5f5 | ||
|
|
5f0d59a02b | ||
|
|
805f319578 | ||
|
|
f23df92c18 | ||
|
|
41d9453110 | ||
|
|
48d0cae58b | ||
|
|
49af9439dc | ||
|
|
1994497ad4 | ||
|
|
4ce191cdfa | ||
|
|
ee8d8d7e04 | ||
|
|
cf022f492c | ||
|
|
96019e8a9b | ||
|
|
9ff1d4ca77 | ||
|
|
8bc631ece9 | ||
|
|
0edb567ded | ||
|
|
d404fcf746 | ||
|
|
8af78e3af7 | ||
|
|
3722a8e7f3 | ||
|
|
b86f7412d1 | ||
|
|
ad52418f99 | ||
|
|
08ae44158f | ||
|
|
a1f18e1386 | ||
|
|
c028ea3d15 | ||
|
|
f357c676bb | ||
|
|
8022c662e0 | ||
|
|
aa84605c4d | ||
|
|
287cb6ec9c | ||
|
|
b9788e9b2a | ||
|
|
a469ea23f4 | ||
|
|
c5f81d64c8 | ||
|
|
78d135c6bf | ||
|
|
ff06aca146 | ||
|
|
849d5760da | ||
|
|
8cce9edbde | ||
|
|
6bea7bc524 | ||
|
|
b6c0138dd2 | ||
|
|
7658ff46c3 | ||
|
|
bd1aa1f559 | ||
|
|
27fb85f630 | ||
|
|
0be88bb442 | ||
|
|
a8642f4582 | ||
|
|
5d67fc95d2 | ||
|
|
057ad2eeeb | ||
|
|
ca0a1250cd | ||
|
|
6c82b34e8b | ||
|
|
fdc5e3cbf6 | ||
|
|
a0643d8ded | ||
|
|
83fb5d5921 | ||
|
|
a1070a64e7 | ||
|
|
709cdef050 | ||
|
|
c95ece3fe2 | ||
|
|
d5befa1900 | ||
|
|
89ea284cc6 | ||
|
|
ffe1844ba6 | ||
|
|
d36eeb5134 | ||
|
|
b9009bbd48 | ||
|
|
5be32930f8 | ||
|
|
296c71290c | ||
|
|
2378971c8c | ||
|
|
5a30df558c | ||
|
|
2c21f3cd75 | ||
|
|
7d3e7b5b7a | ||
|
|
b01b27f1eb | ||
|
|
d40b1ba823 | ||
|
|
a1dfc360fc | ||
|
|
6da17433f2 | ||
|
|
32f21d8555 | ||
|
|
c59a0504ad | ||
|
|
182af9a1b6 | ||
|
|
424c5b23b4 | ||
|
|
86b420ce67 | ||
|
|
ea5709e5b6 | ||
|
|
4e9e3570ed | ||
|
|
58b123d316 |
@@ -0,0 +1,902 @@
|
||||
### 功能描述以及必要性描述
|
||||
|
||||
---
|
||||
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文档
|
||||
---
|
||||
|
||||
#### **角色与目标**
|
||||
|
||||
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
|
||||
|
||||
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
|
||||
|
||||
---
|
||||
|
||||
### **🚀 重要提示:GVA Helper MCP 支持**
|
||||
|
||||
**在开始任何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. **重视安全性**:实现完善的权限控制和数据验证机制
|
||||
@@ -194,6 +194,8 @@ web/
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt`
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
@@ -286,6 +288,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +306,11 @@ web/
|
||||
- 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):
|
||||
@@ -483,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -610,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `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` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
@@ -0,0 +1,902 @@
|
||||
### 功能描述以及必要性描述
|
||||
|
||||
---
|
||||
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文档
|
||||
---
|
||||
|
||||
#### **角色与目标**
|
||||
|
||||
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。
|
||||
|
||||
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
|
||||
|
||||
---
|
||||
|
||||
### **🚀 重要提示:GVA Helper MCP 支持**
|
||||
|
||||
**在开始任何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. **重视安全性**:实现完善的权限控制和数据验证机制
|
||||
+170
-29
@@ -189,23 +189,25 @@ web/
|
||||
#### **1. 模型层 (`model/`)**
|
||||
|
||||
- **数据模型 (`model/xxx.go`)**:
|
||||
|
||||
- 用于定义与数据库表映射的 GORM 结构体。
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
|
||||
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
|
||||
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
|
||||
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
|
||||
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
|
||||
- **⚠️ 指针类型处理**:
|
||||
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
|
||||
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
|
||||
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
|
||||
- 用于定义与数据库表映射的 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`)**:
|
||||
|
||||
@@ -225,9 +227,9 @@ web/
|
||||
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
|
||||
|
||||
- **⚠️ 数据类型处理注意事项**:
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
|
||||
|
||||
#### **3. API层 (`api/`)**
|
||||
@@ -286,6 +288,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +306,11 @@ web/
|
||||
- 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):
|
||||
@@ -483,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -610,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `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` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
@@ -620,16 +761,16 @@ src/plugin/[插件名]/
|
||||
- 接口变更**必须**提前通知并更新文档
|
||||
|
||||
2. **数据格式**:
|
||||
- **统一**使用JSON格式进行数据交换
|
||||
- **统一**响应格式:`{code, data, msg}`
|
||||
- **统一**分页格式:`{page, pageSize, total, list}`
|
||||
- **统一**时间格式:ISO 8601标准
|
||||
- **⚠️ 数据类型一致性**:
|
||||
- 前后端对于同一字段**必须**使用相同的数据类型
|
||||
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
|
||||
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
|
||||
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
|
||||
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
|
||||
- **统一**使用JSON格式进行数据交换
|
||||
- **统一**响应格式:`{code, data, msg}`
|
||||
- **统一**分页格式:`{page, pageSize, total, list}`
|
||||
- **统一**时间格式:ISO 8601标准
|
||||
- **⚠️ 数据类型一致性**:
|
||||
- 前后端对于同一字段**必须**使用相同的数据类型
|
||||
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
|
||||
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
|
||||
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
|
||||
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
|
||||
|
||||
3. **错误处理**:
|
||||
- 后端**必须**返回标准化的错误码和错误信息
|
||||
|
||||
@@ -27,6 +27,7 @@ rm_file/
|
||||
/server/server
|
||||
/server/latest_log
|
||||
/server/__debug_bin*
|
||||
/server/*.local.yaml
|
||||
server/uploads/
|
||||
|
||||
*.iml
|
||||
|
||||
+170
-29
@@ -189,23 +189,25 @@ web/
|
||||
#### **1. 模型层 (`model/`)**
|
||||
|
||||
- **数据模型 (`model/xxx.go`)**:
|
||||
|
||||
- 用于定义与数据库表映射的 GORM 结构体。
|
||||
|
||||
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
|
||||
|
||||
- 必须为字段添加清晰的 `json` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
|
||||
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
|
||||
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
|
||||
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
|
||||
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
|
||||
- **⚠️ 指针类型处理**:
|
||||
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
|
||||
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
|
||||
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
|
||||
- 用于定义与数据库表映射的 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`)**:
|
||||
|
||||
@@ -225,9 +227,9 @@ web/
|
||||
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
|
||||
|
||||
- **⚠️ 数据类型处理注意事项**:
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
|
||||
|
||||
#### **3. API层 (`api/`)**
|
||||
@@ -286,6 +288,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +306,11 @@ web/
|
||||
- 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):
|
||||
@@ -483,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重
|
||||
- **必须**进行响应式数据管理
|
||||
- **必须**处理加载状态和错误状态
|
||||
- **必须**遵循Element Plus组件规范
|
||||
- **必须**优先使用UnoCSS原子化类名进行样式设计
|
||||
- **必须**优先el-drawer组件进行编辑,新增,步骤等操作
|
||||
- **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染
|
||||
|
||||
#### **4. 状态管理 (`src/pinia/`)**
|
||||
|
||||
@@ -610,6 +627,130 @@ src/plugin/[插件名]/
|
||||
|
||||
---
|
||||
|
||||
## **⚠️ 前端工具库使用规范(强制)**
|
||||
|
||||
> **核心原则:在开发任何前端功能时,必须优先检查并使用 `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` |
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
@@ -620,16 +761,16 @@ src/plugin/[插件名]/
|
||||
- 接口变更**必须**提前通知并更新文档
|
||||
|
||||
2. **数据格式**:
|
||||
- **统一**使用JSON格式进行数据交换
|
||||
- **统一**响应格式:`{code, data, msg}`
|
||||
- **统一**分页格式:`{page, pageSize, total, list}`
|
||||
- **统一**时间格式:ISO 8601标准
|
||||
- **⚠️ 数据类型一致性**:
|
||||
- 前后端对于同一字段**必须**使用相同的数据类型
|
||||
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
|
||||
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
|
||||
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
|
||||
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
|
||||
- **统一**使用JSON格式进行数据交换
|
||||
- **统一**响应格式:`{code, data, msg}`
|
||||
- **统一**分页格式:`{page, pageSize, total, list}`
|
||||
- **统一**时间格式:ISO 8601标准
|
||||
- **⚠️ 数据类型一致性**:
|
||||
- 前后端对于同一字段**必须**使用相同的数据类型
|
||||
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
|
||||
- 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
|
||||
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
|
||||
- **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值
|
||||
|
||||
3. **错误处理**:
|
||||
- 后端**必须**返回标准化的错误码和错误信息
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct
|
||||
cd server
|
||||
go generate -run "go env -w .*?"
|
||||
|
||||
# 使用如下命令下载swag
|
||||
# Use the following command to download swag
|
||||
go get -u github.com/swaggo/swag/cmd/swag
|
||||
````
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
[English](./README-en.md) | 简体中文
|
||||
|
||||
## 支持claw生态
|
||||
|
||||
[🦞GvaClaw](https://plugin.gin-vue-admin.com/details/159)
|
||||
|
||||
## ✨一分钟生成前后端基础代码
|
||||
|
||||
<table>
|
||||
@@ -61,7 +65,7 @@
|
||||
|
||||
3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html)
|
||||
|
||||
4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://www.gin-vue-admin.com/empower/index.html)
|
||||
4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html)
|
||||
\
|
||||
<img src="https://qmplusimg.henrongyi.top/openSource/login.jpg" width="1000">
|
||||
|
||||
@@ -381,5 +385,5 @@ fmt.Println(decodeBytes, err)
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://www.gin-vue-admin.com/empower/)
|
||||
请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license)
|
||||
未授权去除版权信息将依法追究法律责任
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/example"
|
||||
|
||||
@@ -135,6 +136,11 @@ func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
// 路径穿越拦截
|
||||
if strings.Contains(file.FilePath, "..") || strings.Contains(file.FilePath, "../") || strings.Contains(file.FilePath, "./") || strings.Contains(file.FilePath, ".\\") {
|
||||
response.FailWithMessage("非法路径,禁止删除", c)
|
||||
return
|
||||
}
|
||||
err = utils.RemoveChunk(file.FileMd5)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("缓存切片删除失败!", zap.Error(err))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@ package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -117,3 +122,97 @@ func (a *AutoCodePluginApi) InitAPI(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("文件变更成功", c)
|
||||
}
|
||||
|
||||
// InitDictionary
|
||||
// @Tags AutoCodePlugin
|
||||
// @Summary 打包插件
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功"
|
||||
// @Router /autoCode/initDictionary [post]
|
||||
func (a *AutoCodePluginApi) InitDictionary(c *gin.Context) {
|
||||
var dictInfo request.InitDictionary
|
||||
err := c.ShouldBindJSON(&dictInfo)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = autoCodePluginService.InitDictionary(dictInfo)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建初始化Dictionary失败!", zap.Error(err))
|
||||
response.FailWithMessage("创建初始化Dictionary失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("文件变更成功", c)
|
||||
}
|
||||
|
||||
// GetPluginList
|
||||
// @Tags AutoCodePlugin
|
||||
// @Summary 获取插件列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=[]systemRes.PluginInfo} "获取插件列表成功"
|
||||
// @Router /autoCode/getPluginList [get]
|
||||
func (a *AutoCodePluginApi) GetPluginList(c *gin.Context) {
|
||||
serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin")
|
||||
webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin")
|
||||
|
||||
serverEntries, _ := os.ReadDir(serverDir)
|
||||
webEntries, _ := os.ReadDir(webDir)
|
||||
|
||||
configMap := make(map[string]string)
|
||||
|
||||
for _, entry := range serverEntries {
|
||||
if entry.IsDir() {
|
||||
configMap[entry.Name()] = "server"
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range webEntries {
|
||||
if entry.IsDir() {
|
||||
if val, ok := configMap[entry.Name()]; ok {
|
||||
if val == "server" {
|
||||
configMap[entry.Name()] = "full"
|
||||
}
|
||||
} else {
|
||||
configMap[entry.Name()] = "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var list []systemRes.PluginInfo
|
||||
for k, v := range configMap {
|
||||
apis, menus, dicts := utils.GetPluginData(k)
|
||||
list = append(list, systemRes.PluginInfo{
|
||||
PluginName: k,
|
||||
PluginType: v,
|
||||
Apis: apis,
|
||||
Menus: menus,
|
||||
Dictionaries: dicts,
|
||||
})
|
||||
}
|
||||
|
||||
response.OkWithDetailed(list, "获取成功", c)
|
||||
}
|
||||
|
||||
// Remove
|
||||
// @Tags AutoCodePlugin
|
||||
// @Summary 删除插件
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce application/json
|
||||
// @Param pluginName query string true "插件名称"
|
||||
// @Param pluginType query string true "插件类型"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除插件成功"
|
||||
// @Router /autoCode/removePlugin [post]
|
||||
func (a *AutoCodePluginApi) Remove(c *gin.Context) {
|
||||
pluginName := c.Query("pluginName")
|
||||
pluginType := c.Query("pluginType")
|
||||
err := autoCodePluginService.Remove(pluginName, pluginType)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
@@ -23,27 +23,37 @@ type ApiGroup struct {
|
||||
AutoCodeTemplateApi
|
||||
SysParamsApi
|
||||
SysVersionApi
|
||||
SysErrorApi
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
@@ -321,3 +321,61 @@ func (s *SystemApiApi) FreshCasbin(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("刷新成功", c)
|
||||
}
|
||||
|
||||
// GetApiRoles
|
||||
// @Tags SysApi
|
||||
// @Summary 获取拥有指定API权限的角色ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param path query string true "API路径"
|
||||
// @Param method query string true "请求方法"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
|
||||
// @Router /api/getApiRoles [get]
|
||||
func (s *SystemApiApi) GetApiRoles(c *gin.Context) {
|
||||
path := c.Query("path")
|
||||
method := c.Query("method")
|
||||
if path == "" || method == "" {
|
||||
response.FailWithMessage("API路径和请求方法不能为空", c)
|
||||
return
|
||||
}
|
||||
authorityIds, err := casbinService.GetAuthoritiesByApi(path, method)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if authorityIds == nil {
|
||||
authorityIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(authorityIds, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetApiRoles
|
||||
// @Tags SysApi
|
||||
// @Summary 全量覆盖某API关联的角色列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /api/setApiRoles [post]
|
||||
func (s *SystemApiApi) SetApiRoles(c *gin.Context) {
|
||||
var req systemReq.SetApiAuthorities
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.Path == "" || req.Method == "" {
|
||||
response.FailWithMessage("API路径和请求方法不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
// 刷新casbin缓存使策略立即生效
|
||||
_ = casbinService.FreshCasbin()
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
sysReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ApiTokenApi struct{}
|
||||
|
||||
// CreateApiToken 签发Token
|
||||
func (s *ApiTokenApi) CreateApiToken(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID uint `json:"userId"`
|
||||
AuthorityID uint `json:"authorityId"`
|
||||
Days int `json:"days"` // -1为永久, 其他为天数
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
token := system.SysApiToken{
|
||||
UserID: req.UserID,
|
||||
AuthorityID: req.AuthorityID,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
jwtStr, err := apiTokenService.CreateApiToken(token, req.Days)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("签发失败!", zap.Error(err))
|
||||
response.FailWithMessage("签发失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(gin.H{"token": jwtStr}, "签发成功", c)
|
||||
}
|
||||
|
||||
// GetApiTokenList 获取列表
|
||||
func (s *ApiTokenApi) GetApiTokenList(c *gin.Context) {
|
||||
var pageInfo sysReq.SysApiTokenSearch
|
||||
err := c.ShouldBindJSON(&pageInfo)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
list, total, err := apiTokenService.GetApiTokenList(pageInfo)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: pageInfo.Page,
|
||||
PageSize: pageInfo.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// DeleteApiToken 作废Token
|
||||
func (s *ApiTokenApi) DeleteApiToken(c *gin.Context) {
|
||||
var req system.SysApiToken
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = apiTokenService.DeleteApiToken(req.ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("作废失败!", zap.Error(err))
|
||||
response.FailWithMessage("作废失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("作废成功", c)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
|
||||
@@ -200,3 +201,57 @@ func (a *AuthorityApi) SetDataAuthority(c *gin.Context) {
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
// GetUsersByAuthority
|
||||
// @Tags Authority
|
||||
// @Summary 获取拥有指定角色的用户ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param authorityId query uint true "角色ID"
|
||||
// @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功"
|
||||
// @Router /authority/getUsersByAuthority [get]
|
||||
func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) {
|
||||
var req systemReq.SetRoleUsers
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if userIds == nil {
|
||||
userIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(userIds, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetRoleUsers
|
||||
// @Tags Authority
|
||||
// @Summary 全量覆盖某角色关联的用户列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /authority/setRoleUsers [post]
|
||||
func (a *AuthorityApi) SetRoleUsers(c *gin.Context) {
|
||||
var req systemReq.SetRoleUsers
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.AuthorityId == 0 {
|
||||
response.FailWithMessage("角色ID不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,43 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||
"github.com/goccy/go-json"
|
||||
"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/flipped-aurora/gin-vue-admin/server/utils/request"
|
||||
"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")
|
||||
@@ -70,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")
|
||||
@@ -101,55 +79,141 @@ 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) {
|
||||
var llm common.JSONMap
|
||||
err := c.ShouldBindJSON(&llm)
|
||||
if err != nil {
|
||||
if err := c.ShouldBindJSON(&llm); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if global.GVA_CONFIG.AutoCode.AiPath == "" {
|
||||
response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"]))
|
||||
res, err := request.HttpRequest(
|
||||
path,
|
||||
"POST",
|
||||
nil,
|
||||
nil,
|
||||
llm,
|
||||
)
|
||||
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
|
||||
}
|
||||
var resStruct response.Response
|
||||
b, err := io.ReadAll(res.Body)
|
||||
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 err != nil {
|
||||
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+err.Error(), c)
|
||||
return
|
||||
|
||||
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":"流式代理失败"}`)
|
||||
}
|
||||
err = json.Unmarshal(b, &resStruct)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+err.Error(), c)
|
||||
return
|
||||
_, _ = 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()
|
||||
}
|
||||
}
|
||||
|
||||
if resStruct.Code == 7 {
|
||||
global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+resStruct.Msg, c)
|
||||
return
|
||||
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]) + "..."
|
||||
}
|
||||
response.OkWithData(resStruct.Data, c)
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -116,10 +117,17 @@ func (s *DictionaryApi) FindSysDictionary(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.SysDictionarySearch true "字典 name 或者 type"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量"
|
||||
// @Router /sysDictionary/getSysDictionaryList [get]
|
||||
func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
|
||||
list, err := dictionaryService.GetSysDictionaryInfoList()
|
||||
var dictionary request.SysDictionarySearch
|
||||
err := c.ShouldBindQuery(&dictionary)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
@@ -127,3 +135,57 @@ func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
|
||||
}
|
||||
response.OkWithDetailed(list, "获取成功", c)
|
||||
}
|
||||
|
||||
// ExportSysDictionary
|
||||
// @Tags SysDictionary
|
||||
// @Summary 导出字典JSON(包含字典详情)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query system.SysDictionary true "字典ID"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "导出字典JSON"
|
||||
// @Router /sysDictionary/exportSysDictionary [get]
|
||||
func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) {
|
||||
var dictionary system.SysDictionary
|
||||
err := c.ShouldBindQuery(&dictionary)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if dictionary.ID == 0 {
|
||||
response.FailWithMessage("字典ID不能为空", c)
|
||||
return
|
||||
}
|
||||
exportData, err := dictionaryService.ExportSysDictionary(dictionary.ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导出失败!", zap.Error(err))
|
||||
response.FailWithMessage("导出失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(exportData, "导出成功", c)
|
||||
}
|
||||
|
||||
// ImportSysDictionary
|
||||
// @Tags SysDictionary
|
||||
// @Summary 导入字典JSON(包含字典详情)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.ImportSysDictionaryRequest true "字典JSON数据"
|
||||
// @Success 200 {object} response.Response{msg=string} "导入字典"
|
||||
// @Router /sysDictionary/importSysDictionary [post]
|
||||
func (s *DictionaryApi) ImportSysDictionary(c *gin.Context) {
|
||||
var req request.ImportSysDictionaryRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = dictionaryService.ImportSysDictionary(req.Json)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导入失败!", zap.Error(err))
|
||||
response.FailWithMessage("导入失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("导入成功", c)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
@@ -146,3 +148,120 @@ func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) {
|
||||
PageSize: pageInfo.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetDictionaryTreeList
|
||||
// @Tags SysDictionaryDetail
|
||||
// @Summary 获取字典详情树形结构
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param sysDictionaryID query int true "字典ID"
|
||||
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
|
||||
// @Router /sysDictionaryDetail/getDictionaryTreeList [get]
|
||||
func (s *DictionaryDetailApi) GetDictionaryTreeList(c *gin.Context) {
|
||||
sysDictionaryID := c.Query("sysDictionaryID")
|
||||
if sysDictionaryID == "" {
|
||||
response.FailWithMessage("字典ID不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
var id uint
|
||||
if idUint64, err := strconv.ParseUint(sysDictionaryID, 10, 32); err != nil {
|
||||
response.FailWithMessage("字典ID格式错误", c)
|
||||
return
|
||||
} else {
|
||||
id = uint(idUint64)
|
||||
}
|
||||
|
||||
list, err := dictionaryDetailService.GetDictionaryTreeList(id)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetDictionaryTreeListByType
|
||||
// @Tags SysDictionaryDetail
|
||||
// @Summary 根据字典类型获取字典详情树形结构
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param type query string true "字典类型"
|
||||
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构"
|
||||
// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get]
|
||||
func (s *DictionaryDetailApi) GetDictionaryTreeListByType(c *gin.Context) {
|
||||
dictType := c.Query("type")
|
||||
if dictType == "" {
|
||||
response.FailWithMessage("字典类型不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := dictionaryDetailService.GetDictionaryTreeListByType(dictType)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetDictionaryDetailsByParent
|
||||
// @Tags SysDictionaryDetail
|
||||
// @Summary 根据父级ID获取字典详情
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.GetDictionaryDetailsByParentRequest true "查询参数"
|
||||
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情列表"
|
||||
// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get]
|
||||
func (s *DictionaryDetailApi) GetDictionaryDetailsByParent(c *gin.Context) {
|
||||
var req request.GetDictionaryDetailsByParentRequest
|
||||
err := c.ShouldBindQuery(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := dictionaryDetailService.GetDictionaryDetailsByParent(req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"list": list}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetDictionaryPath
|
||||
// @Tags SysDictionaryDetail
|
||||
// @Summary 获取字典详情的完整路径
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id query uint true "字典详情ID"
|
||||
// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情路径"
|
||||
// @Router /sysDictionaryDetail/getDictionaryPath [get]
|
||||
func (s *DictionaryDetailApi) GetDictionaryPath(c *gin.Context) {
|
||||
idStr := c.Query("id")
|
||||
if idStr == "" {
|
||||
response.FailWithMessage("字典详情ID不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
var id uint
|
||||
if idUint64, err := strconv.ParseUint(idStr, 10, 32); err != nil {
|
||||
response.FailWithMessage("字典详情ID格式错误", c)
|
||||
return
|
||||
} else {
|
||||
id = uint(idUint64)
|
||||
}
|
||||
|
||||
path, err := dictionaryDetailService.GetDictionaryPath(id)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"path": path}, "获取成功", c)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SysErrorApi struct{}
|
||||
|
||||
// CreateSysError 创建错误日志
|
||||
// @Tags SysError
|
||||
// @Summary 创建错误日志
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.SysError true "创建错误日志"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /sysError/createSysError [post]
|
||||
func (sysErrorApi *SysErrorApi) CreateSysError(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var sysError system.SysError
|
||||
err := c.ShouldBindJSON(&sysError)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = sysErrorService.CreateSysError(ctx, &sysError)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建失败!", zap.Error(err))
|
||||
response.FailWithMessage("创建失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("创建成功", c)
|
||||
}
|
||||
|
||||
// DeleteSysError 删除错误日志
|
||||
// @Tags SysError
|
||||
// @Summary 删除错误日志
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.SysError true "删除错误日志"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /sysError/deleteSysError [delete]
|
||||
func (sysErrorApi *SysErrorApi) DeleteSysError(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ID := c.Query("ID")
|
||||
err := sysErrorService.DeleteSysError(ctx, ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// DeleteSysErrorByIds 批量删除错误日志
|
||||
// @Tags SysError
|
||||
// @Summary 批量删除错误日志
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{msg=string} "批量删除成功"
|
||||
// @Router /sysError/deleteSysErrorByIds [delete]
|
||||
func (sysErrorApi *SysErrorApi) DeleteSysErrorByIds(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
IDs := c.QueryArray("IDs[]")
|
||||
err := sysErrorService.DeleteSysErrorByIds(ctx, IDs)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("批量删除失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("批量删除成功", c)
|
||||
}
|
||||
|
||||
// UpdateSysError 更新错误日志
|
||||
// @Tags SysError
|
||||
// @Summary 更新错误日志
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body system.SysError true "更新错误日志"
|
||||
// @Success 200 {object} response.Response{msg=string} "更新成功"
|
||||
// @Router /sysError/updateSysError [put]
|
||||
func (sysErrorApi *SysErrorApi) UpdateSysError(c *gin.Context) {
|
||||
// 从ctx获取标准context进行业务行为
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var sysError system.SysError
|
||||
err := c.ShouldBindJSON(&sysError)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = sysErrorService.UpdateSysError(ctx, sysError)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("更新失败!", zap.Error(err))
|
||||
response.FailWithMessage("更新失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// FindSysError 用id查询错误日志
|
||||
// @Tags SysError
|
||||
// @Summary 用id查询错误日志
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param ID query uint true "用id查询错误日志"
|
||||
// @Success 200 {object} response.Response{data=system.SysError,msg=string} "查询成功"
|
||||
// @Router /sysError/findSysError [get]
|
||||
func (sysErrorApi *SysErrorApi) FindSysError(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
ID := c.Query("ID")
|
||||
resysError, err := sysErrorService.GetSysError(ctx, ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
||||
response.FailWithMessage("查询失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(resysError, c)
|
||||
}
|
||||
|
||||
// GetSysErrorList 分页获取错误日志列表
|
||||
// @Tags SysError
|
||||
// @Summary 分页获取错误日志列表
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /sysError/getSysErrorList [get]
|
||||
func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var pageInfo systemReq.SysErrorSearch
|
||||
err := c.ShouldBindQuery(&pageInfo)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
list, total, err := sysErrorService.GetSysErrorInfoList(ctx, pageInfo)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: pageInfo.Page,
|
||||
PageSize: pageInfo.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetSysErrorSolution 触发错误日志的异步处理
|
||||
// @Tags SysError
|
||||
// @Summary 根据ID触发处理:标记为处理中,1分钟后自动改为处理完成
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id query string true "错误日志ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "处理已提交"
|
||||
// @Router /sysError/getSysErrorSolution [get]
|
||||
func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) {
|
||||
// 创建业务用Context
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 兼容 id 与 ID 两种参数
|
||||
ID := c.Query("id")
|
||||
if ID == "" {
|
||||
response.FailWithMessage("缺少参数: id", c)
|
||||
return
|
||||
}
|
||||
|
||||
err := sysErrorService.GetSysErrorSolution(ctx, ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("处理触发失败!", zap.Error(err))
|
||||
response.FailWithMessage("处理触发失败:"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("已提交至AI处理", c)
|
||||
}
|
||||
@@ -50,6 +50,34 @@ type SysExportTemplateApi struct {
|
||||
|
||||
var sysExportTemplateService = service.ServiceGroupApp.SystemServiceGroup.SysExportTemplateService
|
||||
|
||||
// PreviewSQL 预览最终生成的SQL
|
||||
// @Tags SysExportTemplate
|
||||
// @Summary 预览最终生成的SQL(不执行查询,仅返回SQL字符串)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param templateID query string true "导出模板ID"
|
||||
// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件"
|
||||
// @Success 200 {object} response.Response{data=map[string]string} "获取成功"
|
||||
// @Router /sysExportTemplate/previewSQL [get]
|
||||
func (sysExportTemplateApi *SysExportTemplateApi) PreviewSQL(c *gin.Context) {
|
||||
templateID := c.Query("templateID")
|
||||
if templateID == "" {
|
||||
response.FailWithMessage("模板ID不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 直接复用导出接口的参数组织方式:使用 URL Query,其中 params 为内部编码的查询字符串
|
||||
queryParams := c.Request.URL.Query()
|
||||
|
||||
if sqlPreview, err := sysExportTemplateService.PreviewSQL(templateID, queryParams); err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
} else {
|
||||
response.OkWithData(gin.H{"sql": sqlPreview}, c)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSysExportTemplate 创建导出模板
|
||||
// @Tags SysExportTemplate
|
||||
// @Summary 创建导出模板
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type LoginLogApi struct{}
|
||||
|
||||
func (s *LoginLogApi) DeleteLoginLog(c *gin.Context) {
|
||||
var loginLog system.SysLoginLog
|
||||
err := c.ShouldBindJSON(&loginLog)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = loginLogService.DeleteLoginLog(loginLog)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
func (s *LoginLogApi) DeleteLoginLogByIds(c *gin.Context) {
|
||||
var SDS request.IdsReq
|
||||
err := c.ShouldBindJSON(&SDS)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = loginLogService.DeleteLoginLogByIds(SDS)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("批量删除失败!", zap.Error(err))
|
||||
response.FailWithMessage("批量删除失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("批量删除成功", c)
|
||||
}
|
||||
|
||||
func (s *LoginLogApi) FindLoginLog(c *gin.Context) {
|
||||
var loginLog system.SysLoginLog
|
||||
err := c.ShouldBindQuery(&loginLog)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
reLoginLog, err := loginLogService.GetLoginLog(loginLog.ID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("查询失败!", zap.Error(err))
|
||||
response.FailWithMessage("查询失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(reLoginLog, "查询成功", c)
|
||||
}
|
||||
|
||||
func (s *LoginLogApi) GetLoginLogList(c *gin.Context) {
|
||||
var pageInfo systemReq.SysLoginLogSearch
|
||||
err := c.ShouldBindQuery(&pageInfo)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
list, total, err := loginLogService.GetLoginLogInfoList(pageInfo)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: pageInfo.Page,
|
||||
PageSize: pageInfo.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
@@ -244,6 +244,76 @@ func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) {
|
||||
response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetMenuRoles
|
||||
// @Tags AuthorityMenu
|
||||
// @Summary 获取拥有指定菜单的角色ID列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param menuId query uint true "菜单ID"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功"
|
||||
// @Router /menu/getMenuRoles [get]
|
||||
func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) {
|
||||
var req systemReq.SetMenuAuthorities
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.MenuId == 0 {
|
||||
response.FailWithMessage("菜单ID不能为空", c)
|
||||
return
|
||||
}
|
||||
authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if authorityIds == nil {
|
||||
authorityIds = []uint{}
|
||||
}
|
||||
defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
if defaultRouterAuthorityIds == nil {
|
||||
defaultRouterAuthorityIds = []uint{}
|
||||
}
|
||||
response.OkWithDetailed(gin.H{
|
||||
"authorityIds": authorityIds,
|
||||
"defaultRouterAuthorityIds": defaultRouterAuthorityIds,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// SetMenuRoles
|
||||
// @Tags AuthorityMenu
|
||||
// @Summary 全量覆盖某菜单关联的角色列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表"
|
||||
// @Success 200 {object} response.Response{msg=string} "设置成功"
|
||||
// @Router /menu/setMenuRoles [post]
|
||||
func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) {
|
||||
var req systemReq.SetMenuAuthorities
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
if req.MenuId == 0 {
|
||||
response.FailWithMessage("菜单ID不能为空", c)
|
||||
return
|
||||
}
|
||||
if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil {
|
||||
global.GVA_LOG.Error("设置失败!", zap.Error(err))
|
||||
response.FailWithMessage("设置失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("设置成功", c)
|
||||
}
|
||||
|
||||
// GetMenuList
|
||||
// @Tags Menu
|
||||
// @Summary 分页获取基础menu列表
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SkillsApi struct{}
|
||||
|
||||
func (s *SkillsApi) GetTools(c *gin.Context) {
|
||||
data, err := skillsService.Tools(c.Request.Context())
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取工具列表失败", zap.Error(err))
|
||||
response.FailWithMessage("获取工具列表失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"tools": data}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetSkillList(c *gin.Context) {
|
||||
var req request.SkillToolRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
data, err := skillsService.List(c.Request.Context(), req.Tool)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取技能列表失败", zap.Error(err))
|
||||
response.FailWithMessage("获取技能列表失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"skills": data}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetSkillDetail(c *gin.Context) {
|
||||
var req request.SkillDetailRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
data, err := skillsService.Detail(c.Request.Context(), req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取技能详情失败", zap.Error(err))
|
||||
response.FailWithMessage("获取技能详情失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"detail": data}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveSkill(c *gin.Context) {
|
||||
var req request.SkillSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.Save(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存技能失败", zap.Error(err))
|
||||
response.FailWithMessage("保存技能失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) DeleteSkill(c *gin.Context) {
|
||||
var req request.SkillDeleteRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.Delete(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("删除技能失败", zap.Error(err))
|
||||
response.FailWithMessage("删除技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) CreateScript(c *gin.Context) {
|
||||
var req request.SkillScriptCreateRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
fileName, content, err := skillsService.CreateScript(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建脚本失败", zap.Error(err))
|
||||
response.FailWithMessage("创建脚本失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetScript(c *gin.Context) {
|
||||
var req request.SkillFileRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
content, err := skillsService.GetScript(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取脚本失败", zap.Error(err))
|
||||
response.FailWithMessage("读取脚本失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveScript(c *gin.Context) {
|
||||
var req request.SkillFileSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.SaveScript(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存脚本失败", zap.Error(err))
|
||||
response.FailWithMessage("保存脚本失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) CreateResource(c *gin.Context) {
|
||||
var req request.SkillResourceCreateRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
fileName, content, err := skillsService.CreateResource(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建资源失败", zap.Error(err))
|
||||
response.FailWithMessage("创建资源失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetResource(c *gin.Context) {
|
||||
var req request.SkillFileRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
content, err := skillsService.GetResource(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取资源失败", zap.Error(err))
|
||||
response.FailWithMessage("读取资源失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveResource(c *gin.Context) {
|
||||
var req request.SkillFileSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.SaveResource(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存资源失败", zap.Error(err))
|
||||
response.FailWithMessage("保存资源失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) CreateReference(c *gin.Context) {
|
||||
var req request.SkillReferenceCreateRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
fileName, content, err := skillsService.CreateReference(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建参考失败", zap.Error(err))
|
||||
response.FailWithMessage("创建参考失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetReference(c *gin.Context) {
|
||||
var req request.SkillFileRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
content, err := skillsService.GetReference(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取参考失败", zap.Error(err))
|
||||
response.FailWithMessage("读取参考失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveReference(c *gin.Context) {
|
||||
var req request.SkillFileSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.SaveReference(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存参考失败", zap.Error(err))
|
||||
response.FailWithMessage("保存参考失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) CreateTemplate(c *gin.Context) {
|
||||
var req request.SkillTemplateCreateRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
fileName, content, err := skillsService.CreateTemplate(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建模板失败", zap.Error(err))
|
||||
response.FailWithMessage("创建模板失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetTemplate(c *gin.Context) {
|
||||
var req request.SkillFileRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
content, err := skillsService.GetTemplate(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取模板失败", zap.Error(err))
|
||||
response.FailWithMessage("读取模板失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveTemplate(c *gin.Context) {
|
||||
var req request.SkillFileSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.SaveTemplate(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存模板失败", zap.Error(err))
|
||||
response.FailWithMessage("保存模板失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) GetGlobalConstraint(c *gin.Context) {
|
||||
var req request.SkillToolRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
content, exists, err := skillsService.GetGlobalConstraint(c.Request.Context(), req.Tool)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取全局约束失败", zap.Error(err))
|
||||
response.FailWithMessage("读取全局约束失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"content": content, "exists": exists}, "获取成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) {
|
||||
var req request.SkillGlobalConstraintSaveRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
if err := skillsService.SaveGlobalConstraint(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("保存全局约束失败", zap.Error(err))
|
||||
response.FailWithMessage("保存全局约束失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("保存成功", c)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) PackageSkill(c *gin.Context) {
|
||||
var req request.SkillPackageRequest
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
fileName, data, err := skillsService.Package(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("打包技能失败", zap.Error(err))
|
||||
response.FailWithMessage("打包技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/zip")
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+fileName+"\"")
|
||||
c.Data(http.StatusOK, "application/zip", data)
|
||||
}
|
||||
|
||||
func (s *SkillsApi) DownloadOnlineSkill(c *gin.Context) {
|
||||
var req request.DownloadOnlineSkillReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("参数错误", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := skillsService.DownloadOnlineSkill(c.Request.Context(), req); err != nil {
|
||||
global.GVA_LOG.Error("下载在线技能失败", zap.Error(err))
|
||||
response.FailWithMessage("下载在线技能失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("下载成功", c)
|
||||
}
|
||||
@@ -51,6 +51,14 @@ func (b *BaseApi) Login(c *gin.Context) {
|
||||
// 验证码次数+1
|
||||
global.BlackCache.Increment(key, 1)
|
||||
response.FailWithMessage("验证码错误", c)
|
||||
// 记录登录失败日志
|
||||
loginLogService.CreateLoginLog(system.SysLoginLog{
|
||||
Username: l.Username,
|
||||
Ip: c.ClientIP(),
|
||||
Agent: c.Request.UserAgent(),
|
||||
Status: false,
|
||||
ErrorMessage: "验证码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,6 +69,14 @@ func (b *BaseApi) Login(c *gin.Context) {
|
||||
// 验证码次数+1
|
||||
global.BlackCache.Increment(key, 1)
|
||||
response.FailWithMessage("用户名不存在或者密码错误", c)
|
||||
// 记录登录失败日志
|
||||
loginLogService.CreateLoginLog(system.SysLoginLog{
|
||||
Username: l.Username,
|
||||
Ip: c.ClientIP(),
|
||||
Agent: c.Request.UserAgent(),
|
||||
Status: false,
|
||||
ErrorMessage: "用户名不存在或者密码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
@@ -68,6 +84,15 @@ func (b *BaseApi) Login(c *gin.Context) {
|
||||
// 验证码次数+1
|
||||
global.BlackCache.Increment(key, 1)
|
||||
response.FailWithMessage("用户被禁止登录", c)
|
||||
// 记录登录失败日志
|
||||
loginLogService.CreateLoginLog(system.SysLoginLog{
|
||||
Username: l.Username,
|
||||
Ip: c.ClientIP(),
|
||||
Agent: c.Request.UserAgent(),
|
||||
Status: false,
|
||||
ErrorMessage: "用户被禁止登录",
|
||||
UserID: user.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
b.TokenNext(c, *user)
|
||||
@@ -81,6 +106,15 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) {
|
||||
response.FailWithMessage("获取token失败", c)
|
||||
return
|
||||
}
|
||||
// 记录登录成功日志
|
||||
loginLogService.CreateLoginLog(system.SysLoginLog{
|
||||
Username: user.Username,
|
||||
Ip: c.ClientIP(),
|
||||
Agent: c.Request.UserAgent(),
|
||||
Status: true,
|
||||
UserID: user.ID,
|
||||
ErrorMessage: "登录成功",
|
||||
})
|
||||
if !global.GVA_CONFIG.System.UseMultipoint {
|
||||
utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix()))
|
||||
response.OkWithDetailed(systemRes.LoginResponse{
|
||||
@@ -264,7 +298,7 @@ func (b *BaseApi) SetUserAuthority(c *gin.Context) {
|
||||
}
|
||||
c.Header("new-token", token)
|
||||
c.Header("new-expires-at", strconv.FormatInt(claims.ExpiresAt.Unix(), 10))
|
||||
utils.SetToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60))
|
||||
utils.SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix()))
|
||||
response.OkWithMessage("修改成功", c)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -278,6 +278,5 @@ cors:
|
||||
mcp:
|
||||
name: GVA_MCP
|
||||
version: v1.0.0
|
||||
sse_path: /sse
|
||||
message_path: /message
|
||||
url_prefix: ''
|
||||
addr: 8889
|
||||
separate: false
|
||||
|
||||
+4
-4
@@ -87,6 +87,8 @@ system:
|
||||
router-prefix: ""
|
||||
# 严格角色模式 打开后权限将会存在上下级关系
|
||||
use-strict-auth: false
|
||||
# 禁用自动迁移数据库表结构,生产环境建议设为true,手动迁移
|
||||
disable-auto-migrate: false
|
||||
|
||||
# captcha configuration
|
||||
captcha:
|
||||
@@ -268,7 +270,6 @@ cors:
|
||||
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
|
||||
allow-methods: POST, GET
|
||||
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
|
||||
|
||||
allow-credentials: true # 布尔值
|
||||
- allow-origin: example2.com
|
||||
allow-headers: content-type
|
||||
@@ -278,6 +279,5 @@ cors:
|
||||
mcp:
|
||||
name: GVA_MCP
|
||||
version: v1.0.0
|
||||
sse_path: /sse
|
||||
message_path: /message
|
||||
url_prefix: ''
|
||||
addr: 8889
|
||||
separate: false
|
||||
|
||||
+14
-5
@@ -1,9 +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前缀
|
||||
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,4 +11,5 @@ type System struct {
|
||||
UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis
|
||||
UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo
|
||||
UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式
|
||||
DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构,生产环境建议设为false,手动迁移
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -79,10 +80,13 @@ func (c *Cutter) Write(bytes []byte) (n int, err error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = removeNDaysFolders(c.director, c.retentionDay)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
err := removeNDaysFolders(c.director, c.retentionDay)
|
||||
if err != nil {
|
||||
fmt.Println("清理过期日志失败", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -60,7 +67,65 @@ func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
|
||||
}
|
||||
}
|
||||
return z.Core.Write(entry, fields)
|
||||
// 先写入原日志目标
|
||||
err := z.Core.Write(entry, fields)
|
||||
|
||||
// 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容
|
||||
if entry.Level >= zapcore.ErrorLevel {
|
||||
// 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
|
||||
if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
|
||||
return err
|
||||
}
|
||||
|
||||
form := "后端"
|
||||
level := entry.Level.String()
|
||||
// 生成基础信息
|
||||
info := entry.Message
|
||||
|
||||
// 提取 zap.Error(err) 内容
|
||||
var errStr string
|
||||
for i := 0; i < len(fields); i++ {
|
||||
f := fields[i]
|
||||
if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" {
|
||||
if f.Interface != nil {
|
||||
errStr = fmt.Sprintf("%v", f.Interface)
|
||||
} else if f.String != "" {
|
||||
errStr = f.String
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if errStr != "" {
|
||||
info = fmt.Sprintf("%s | 错误: %s", info, errStr)
|
||||
}
|
||||
|
||||
// 附加来源与堆栈信息
|
||||
if entry.Caller.File != "" {
|
||||
info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line)
|
||||
}
|
||||
stack := entry.Stack
|
||||
if stack != "" {
|
||||
info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
|
||||
// 解析最终业务调用方,并提取其方法源码
|
||||
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
|
||||
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
|
||||
if exErr == nil {
|
||||
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
|
||||
} else {
|
||||
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用后台上下文,避免依赖 gin.Context
|
||||
ctx := context.Background()
|
||||
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
|
||||
Form: &form,
|
||||
Info: &info,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (z *ZapCore) Sync() error {
|
||||
|
||||
+11
-17
@@ -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://gin-vue-admin.com/empower/index.html **
|
||||
** 感谢您对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)
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,13 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Zap 获取 zap.Logger
|
||||
@@ -24,9 +24,13 @@ func Zap() (logger *zap.Logger) {
|
||||
core := internal.NewZapCore(levels[i])
|
||||
cores = append(cores, core)
|
||||
}
|
||||
logger = zap.New(zapcore.NewTee(cores...))
|
||||
// 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理)
|
||||
logger = zap.New(zapcore.NewTee(cores...))
|
||||
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
|
||||
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
|
||||
if global.GVA_CONFIG.Zap.ShowLine {
|
||||
logger = logger.WithOptions(zap.AddCaller())
|
||||
opts = append(opts, zap.AddCaller())
|
||||
}
|
||||
logger = logger.WithOptions(opts...)
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ package global
|
||||
// 目前只有Version正式使用 其余为预留
|
||||
const (
|
||||
// Version 当前版本号
|
||||
Version = "v2.8.5"
|
||||
Version = "v2.9.1"
|
||||
// AppName 应用名称
|
||||
AppName = "Gin-Vue-Admin"
|
||||
// Description 应用描述
|
||||
Description = "使用gin+vue进行极速开发的全栈开发基础平台"
|
||||
)
|
||||
)
|
||||
|
||||
+36
-14
@@ -1,16 +1,21 @@
|
||||
module github.com/flipped-aurora/gin-vue-admin/server
|
||||
|
||||
go 1.23
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.23.9
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go v1.55.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
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/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
|
||||
@@ -20,7 +25,7 @@ require (
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/mark3labs/mcp-go v0.31.0
|
||||
github.com/mark3labs/mcp-go v0.41.1
|
||||
github.com/mholt/archives v0.1.1
|
||||
github.com/minio/minio-go/v7 v7.0.84
|
||||
github.com/mojocn/base64Captcha v1.3.8
|
||||
@@ -43,9 +48,10 @@ 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.32.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/text v0.24.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
|
||||
@@ -61,10 +67,27 @@ require (
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
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/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
|
||||
@@ -78,7 +101,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
|
||||
@@ -99,16 +121,16 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
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/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -153,6 +175,7 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
@@ -168,13 +191,12 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/hints v1.1.2 // indirect
|
||||
gorm.io/plugin/dbresolver v1.5.3 // indirect
|
||||
modernc.org/fileutil v1.3.0 // indirect
|
||||
|
||||
+64
-21
@@ -54,8 +54,48 @@ github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
|
||||
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 h1:+mQ8NQBh7B7c2FBtppRnwkrmuwFON1XQQ+5yblomZKk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3/go.mod h1:u67RKh3BRmS4FYLH+rN3N4T5fqpd9m2ttAwBJYEdosU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
|
||||
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
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=
|
||||
@@ -69,6 +109,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
@@ -247,6 +289,8 @@ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
||||
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -267,10 +311,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -282,8 +322,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -315,8 +355,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
|
||||
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
|
||||
github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
|
||||
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=
|
||||
@@ -473,6 +513,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
|
||||
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
@@ -533,8 +575,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
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.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
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/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=
|
||||
@@ -599,8 +641,8 @@ 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
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=
|
||||
@@ -618,8 +660,9 @@ 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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
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/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=
|
||||
@@ -653,8 +696,8 @@ golang.org/x/sys v0.17.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.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.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/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=
|
||||
@@ -688,8 +731,9 @@ 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.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
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/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=
|
||||
@@ -760,8 +804,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -772,7 +816,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -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{},
|
||||
@@ -54,6 +55,9 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
|
||||
sysModel.JoinTemplate{},
|
||||
sysModel.SysParams{},
|
||||
sysModel.SysVersion{},
|
||||
sysModel.SysError{},
|
||||
sysModel.SysLoginLog{},
|
||||
sysModel.SysApiToken{},
|
||||
adapter.CasbinRule{},
|
||||
|
||||
example.ExaFile{},
|
||||
@@ -85,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{},
|
||||
|
||||
@@ -35,6 +35,11 @@ func Gorm() *gorm.DB {
|
||||
}
|
||||
|
||||
func RegisterTables() {
|
||||
if global.GVA_CONFIG.System.DisableAutoMigrate {
|
||||
global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration")
|
||||
return
|
||||
}
|
||||
|
||||
db := global.GVA_DB
|
||||
err := db.AutoMigrate(
|
||||
|
||||
@@ -57,6 +62,9 @@ func RegisterTables() {
|
||||
system.JoinTemplate{},
|
||||
system.SysParams{},
|
||||
system.SysVersion{},
|
||||
system.SysError{},
|
||||
system.SysApiToken{},
|
||||
system.SysLoginLog{},
|
||||
|
||||
example.ExaFile{},
|
||||
example.ExaCustomer{},
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement"
|
||||
_ "github.com/flipped-aurora/gin-vue-admin/server/plugin"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -12,5 +12,5 @@ func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) {
|
||||
}
|
||||
}
|
||||
func bizPluginV2(engine *gin.Engine) {
|
||||
PluginInitV2(engine, announcement.Plugin)
|
||||
PluginInitV2(engine, plugin.Registered()...)
|
||||
}
|
||||
|
||||
+30
-36
@@ -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
|
||||
}
|
||||
|
||||
@@ -35,22 +35,12 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) {
|
||||
|
||||
func Routers() *gin.Engine {
|
||||
Router := gin.New()
|
||||
Router.Use(gin.Recovery())
|
||||
// 使用自定义的 Recovery 中间件,记录 panic 并入库
|
||||
Router.Use(middleware.GinRecovery(true))
|
||||
if gin.Mode() == gin.DebugMode {
|
||||
Router.Use(gin.Logger())
|
||||
}
|
||||
|
||||
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 下的
|
||||
@@ -61,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()) // 按照配置的规则放行跨域请求
|
||||
@@ -83,31 +74,34 @@ func Routers() *gin.Engine {
|
||||
})
|
||||
}
|
||||
{
|
||||
systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
|
||||
systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关
|
||||
systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
|
||||
systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关
|
||||
}
|
||||
|
||||
{
|
||||
systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由
|
||||
systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由
|
||||
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
||||
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
||||
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) // 按钮权限管理
|
||||
systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板
|
||||
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
|
||||
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
|
||||
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
|
||||
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类
|
||||
|
||||
systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由
|
||||
systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由
|
||||
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
||||
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
|
||||
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) // 按钮权限管理
|
||||
systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板
|
||||
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
|
||||
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
@@ -21,7 +21,7 @@ import (
|
||||
// @Tag.Description 用户
|
||||
|
||||
// @title Gin-Vue-Admin Swagger API接口文档
|
||||
// @version v2.8.5
|
||||
// @version v2.9.1
|
||||
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
|
||||
+40
-82
@@ -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,89 +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)
|
||||
}
|
||||
|
||||
// 添加权限分配提醒
|
||||
permissionReminder := "\n\n⚠️ 重要提醒:\n" +
|
||||
"API创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API权限," +
|
||||
"以确保用户能够正常访问新接口。\n" +
|
||||
"具体步骤:\n" +
|
||||
"1. 进入角色管理页面\n" +
|
||||
"2. 选择需要授权的角色\n" +
|
||||
"3. 在API权限中勾选新创建的API接口\n" +
|
||||
"4. 保存权限配置"
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("API创建结果:\n\n%s%s", string(resultJSON), permissionReminder),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
return textResultWithJSON("API创建结果:", result)
|
||||
}
|
||||
|
||||
+38
-108
@@ -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接口,分为两组:
|
||||
@@ -52,91 +47,26 @@ func (a *ApiLister) New() mcp.Tool {
|
||||
**返回数据结构:**
|
||||
- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息)
|
||||
- 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,
|
||||
@@ -146,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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -6,31 +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{}
|
||||
|
||||
// DictionaryGenerateRequest 字典生成请求
|
||||
type DictionaryOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -38,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("智能生成字典选项并自动创建字典和字典详情"),
|
||||
@@ -63,167 +61,52 @@ func (d *DictionaryOptionsGenerator) New() mcp.Tool {
|
||||
)
|
||||
}
|
||||
|
||||
// Name 返回工具名称
|
||||
func (d *DictionaryOptionsGenerator) Name() string {
|
||||
return "generate_dictionary_options"
|
||||
}
|
||||
|
||||
// Description 返回工具描述
|
||||
func (d *DictionaryOptionsGenerator) Description() string {
|
||||
return `字典选项生成工具 - 让AI生成并创建字典选项
|
||||
|
||||
此工具允许AI根据字典类型和字段描述生成合适的字典选项,并自动创建字典和字典详情。
|
||||
|
||||
参数说明:
|
||||
- dictType: 字典类型(必填)
|
||||
- fieldDesc: 字段描述(必填)
|
||||
- options: AI生成的字典选项数组(必填)
|
||||
- label: 选项标签
|
||||
- value: 选项值
|
||||
- sort: 排序号
|
||||
- dictName: 字典名称(可选,默认根据fieldDesc生成)
|
||||
- description: 字典描述(可选)
|
||||
|
||||
使用场景:
|
||||
1. 在创建模块时,如果字段需要字典类型,AI可以根据字段描述智能生成合适的选项
|
||||
2. 支持各种业务场景的字典选项生成,如状态、类型、等级等
|
||||
3. 自动创建字典和字典详情,无需手动配置
|
||||
|
||||
示例调用:
|
||||
{
|
||||
"dictType": "user_status",
|
||||
"fieldDesc": "用户状态",
|
||||
"options": [
|
||||
{"label": "正常", "value": "1", "sort": 1},
|
||||
{"label": "禁用", "value": "0", "sort": 2}
|
||||
],
|
||||
"dictName": "用户状态字典",
|
||||
"description": "用于管理用户账户状态的字典"
|
||||
}`
|
||||
}
|
||||
|
||||
// InputSchema 返回输入参数的JSON Schema
|
||||
func (d *DictionaryOptionsGenerator) InputSchema() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"dictType": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "字典类型,用于标识字典的唯一性",
|
||||
},
|
||||
"fieldDesc": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "字段描述,用于生成字典名称和理解字典用途",
|
||||
},
|
||||
"options": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "AI生成的字典选项数组",
|
||||
"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": "排序号,用于控制选项显示顺序",
|
||||
},
|
||||
},
|
||||
"required": []string{"label", "value", "sort"},
|
||||
},
|
||||
},
|
||||
"dictName": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "字典名称,可选,默认根据fieldDesc生成",
|
||||
},
|
||||
"description": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "字典描述,可选",
|
||||
},
|
||||
},
|
||||
"required": []string{"dictType", "fieldDesc", "options"},
|
||||
}
|
||||
}
|
||||
|
||||
// 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),
|
||||
@@ -232,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++
|
||||
}
|
||||
}
|
||||
@@ -288,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 ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+82
-177
@@ -2,54 +2,47 @@ 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{})
|
||||
}
|
||||
|
||||
// DictionaryInfo 字典信息结构
|
||||
type DictionaryPre struct {
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
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"` // 字典详情
|
||||
ID uint `json:"id"`
|
||||
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"`
|
||||
Total int `json:"total"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Total int `json:"total"`
|
||||
Dictionaries []DictionaryInfo `json:"dictionaries"`
|
||||
}
|
||||
|
||||
// DictionaryQuery 字典查询工具
|
||||
type DictionaryQuery struct{}
|
||||
|
||||
// New 创建字典查询工具
|
||||
func (d *DictionaryQuery) New() mcp.Tool {
|
||||
return mcp.NewTool("query_dictionaries",
|
||||
mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"),
|
||||
@@ -65,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
|
||||
}
|
||||
|
||||
sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status)
|
||||
if !includeDisabled && dictionary.Status != nil && !*dictionary.Status {
|
||||
continue
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
responseJSON, err := json.Marshal(response)
|
||||
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 {
|
||||
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
|
||||
return DictionaryInfo{}, err
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(string(responseJSON)),
|
||||
},
|
||||
}, 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 info, nil
|
||||
}
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
# ExecutionPlan 结构体格式说明
|
||||
|
||||
## 概述
|
||||
ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。
|
||||
|
||||
## 完整结构体定义
|
||||
|
||||
```go
|
||||
type ExecutionPlan struct {
|
||||
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
|
||||
PackageType string `json:"packageType"` // "plugin" 或 "package"
|
||||
NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包
|
||||
NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块
|
||||
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需)
|
||||
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建)
|
||||
Paths map[string]string `json:"paths,omitempty"` // 路径信息
|
||||
}
|
||||
```
|
||||
|
||||
## 子结构体详细说明
|
||||
|
||||
### 1. SysAutoCodePackageCreate 结构体
|
||||
|
||||
```go
|
||||
type SysAutoCodePackageCreate struct {
|
||||
Desc string `json:"desc"` // 描述,如:"用户管理模块"
|
||||
Label string `json:"label"` // 展示名,如:"用户管理"
|
||||
Template string `json:"template"` // 模板类型:"plugin" 或 "package"
|
||||
PackageName string `json:"packageName"` // 包名,如:"user"
|
||||
Module string `json:"-"` // 模块名(自动填充,无需设置)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AutoCode 结构体(核心字段)
|
||||
|
||||
```go
|
||||
type AutoCode struct {
|
||||
Package string `json:"package"` // 包名
|
||||
TableName string `json:"tableName"` // 数据库表名
|
||||
BusinessDB string `json:"businessDB"` // 业务数据库名
|
||||
StructName string `json:"structName"` // 结构体名称
|
||||
PackageName string `json:"packageName"` // 文件名称
|
||||
Description string `json:"description"` // 结构体中文名称
|
||||
Abbreviation string `json:"abbreviation"` // 结构体简称
|
||||
HumpPackageName string `json:"humpPackageName"` // 驼峰命名的包名
|
||||
GvaModel bool `json:"gvaModel"` // 是否使用GVA默认Model
|
||||
AutoMigrate bool `json:"autoMigrate"` // 是否自动迁移表结构
|
||||
AutoCreateResource bool `json:"autoCreateResource"` // 是否自动创建资源标识
|
||||
AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 是否自动创建API
|
||||
AutoCreateMenuToSql bool `json:"autoCreateMenuToSql"` // 是否自动创建菜单
|
||||
AutoCreateBtnAuth bool `json:"autoCreateBtnAuth"` // 是否自动创建按钮权限
|
||||
OnlyTemplate bool `json:"onlyTemplate"` // 是否只生成模板
|
||||
IsTree bool `json:"isTree"` // 是否树形结构
|
||||
TreeJson string `json:"treeJson"` // 树形结构JSON字段
|
||||
IsAdd bool `json:"isAdd"` // 是否新增
|
||||
Fields []*AutoCodeField `json:"fields"` // 字段列表
|
||||
GenerateWeb bool `json:"generateWeb"` // 是否生成前端代码
|
||||
GenerateServer bool `json:"generateServer"` // 是否生成后端代码
|
||||
Module string `json:"-"` // 模块(自动填充)
|
||||
DictTypes []string `json:"-"` // 字典类型(自动填充)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. AutoCodeField 结构体(字段定义)
|
||||
|
||||
```go
|
||||
type AutoCodeField struct {
|
||||
FieldName string `json:"fieldName"` // 字段名
|
||||
FieldDesc string `json:"fieldDesc"` // 字段中文描述
|
||||
FieldType string `json:"fieldType"` // 字段类型:string, int, bool, time.Time等
|
||||
FieldJson string `json:"fieldJson"` // JSON标签名
|
||||
DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度
|
||||
Comment string `json:"comment"` // 数据库字段注释
|
||||
ColumnName string `json:"columnName"` // 数据库列名
|
||||
FieldSearchType string `json:"fieldSearchType"` // 搜索类型:EQ, LIKE, BETWEEN等
|
||||
FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件
|
||||
DictType string `json:"dictType"` // 字典类型
|
||||
Form bool `json:"form"` // 是否在表单中显示
|
||||
Table bool `json:"table"` // 是否在表格中显示
|
||||
Desc bool `json:"desc"` // 是否在详情中显示
|
||||
Excel bool `json:"excel"` // 是否支持导入导出
|
||||
Require bool `json:"require"` // 是否必填
|
||||
DefaultValue string `json:"defaultValue"` // 默认值
|
||||
ErrorText string `json:"errorText"` // 校验失败提示
|
||||
Clearable bool `json:"clearable"` // 是否可清空
|
||||
Sort bool `json:"sort"` // 是否支持排序
|
||||
PrimaryKey bool `json:"primaryKey"` // 是否主键
|
||||
DataSource *DataSource `json:"dataSource"` // 数据源配置(用于关联其他表)
|
||||
CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源
|
||||
FieldIndexType string `json:"fieldIndexType"` // 索引类型
|
||||
}
|
||||
```
|
||||
|
||||
### 4. DataSource 结构体(关联表配置)
|
||||
|
||||
```go
|
||||
type DataSource struct {
|
||||
DBName string `json:"dbName"` // 关联的数据库名称
|
||||
Table string `json:"table"` // 关联的表名
|
||||
Label string `json:"label"` // 用于显示的字段名(如name、title等)
|
||||
Value string `json:"value"` // 用于存储的值字段名(通常是id)
|
||||
Association int `json:"association"` // 关联关系:1=一对一,2=一对多
|
||||
HasDeletedAt bool `json:"hasDeletedAt"` // 关联表是否有软删除字段
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例1:创建新包和批量创建多个模块
|
||||
|
||||
```json
|
||||
{
|
||||
"packageName": "user",
|
||||
"packageType": "package",
|
||||
"needCreatedPackage": true,
|
||||
"needCreatedModules": true,
|
||||
"packageInfo": {
|
||||
"desc": "用户管理模块",
|
||||
"label": "用户管理",
|
||||
"template": "package",
|
||||
"packageName": "user"
|
||||
},
|
||||
"modulesInfo": [
|
||||
{
|
||||
"package": "user",
|
||||
"tableName": "sys_users",
|
||||
"businessDB": "",
|
||||
"structName": "User",
|
||||
"packageName": "user",
|
||||
"description": "用户",
|
||||
"abbreviation": "user",
|
||||
"humpPackageName": "user",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"treeJson": "",
|
||||
"isAdd": true,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "Username",
|
||||
"fieldDesc": "用户名",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "username",
|
||||
"dataTypeLong": "50",
|
||||
"comment": "用户名",
|
||||
"columnName": "username",
|
||||
"fieldSearchType": "LIKE",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请输入用户名",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": {
|
||||
"dbName": "gva",
|
||||
"table": "sys_users",
|
||||
"label": "username",
|
||||
"value": "id",
|
||||
"association": 2,
|
||||
"hasDeletedAt": true
|
||||
},
|
||||
"checkDataSource": true,
|
||||
"fieldIndexType": ""
|
||||
},
|
||||
{
|
||||
"fieldName": "Email",
|
||||
"fieldDesc": "邮箱",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "email",
|
||||
"dataTypeLong": "100",
|
||||
"comment": "邮箱地址",
|
||||
"columnName": "email",
|
||||
"fieldSearchType": "EQ",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请输入邮箱",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": null,
|
||||
"checkDataSource": false,
|
||||
"fieldIndexType": "index"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"package": "user",
|
||||
"tableName": "user_profiles",
|
||||
"businessDB": "",
|
||||
"structName": "UserProfile",
|
||||
"packageName": "user",
|
||||
"description": "用户档案",
|
||||
"abbreviation": "userProfile",
|
||||
"humpPackageName": "user",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"treeJson": "",
|
||||
"isAdd": true,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "UserID",
|
||||
"fieldDesc": "用户ID",
|
||||
"fieldType": "int",
|
||||
"fieldJson": "userId",
|
||||
"dataTypeLong": "",
|
||||
"comment": "关联用户ID",
|
||||
"columnName": "user_id",
|
||||
"fieldSearchType": "EQ",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请选择用户",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": null,
|
||||
"checkDataSource": false,
|
||||
"fieldIndexType": "index"
|
||||
},
|
||||
{
|
||||
"fieldName": "Avatar",
|
||||
"fieldDesc": "头像",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "avatar",
|
||||
"dataTypeLong": "255",
|
||||
"comment": "用户头像URL",
|
||||
"columnName": "avatar",
|
||||
"fieldSearchType": "",
|
||||
"fieldSearchHide": true,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": false,
|
||||
"require": false,
|
||||
"defaultValue": "",
|
||||
"errorText": "",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": null,
|
||||
"checkDataSource": false,
|
||||
"fieldIndexType": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 示例2:仅在现有包中批量创建多个模块
|
||||
|
||||
```json
|
||||
{
|
||||
"packageName": "system",
|
||||
"packageType": "package",
|
||||
"needCreatedPackage": false,
|
||||
"needCreatedModules": true,
|
||||
"packageInfo": null,
|
||||
"modulesInfo": [
|
||||
{
|
||||
"package": "system",
|
||||
"tableName": "sys_roles",
|
||||
"businessDB": "",
|
||||
"structName": "Role",
|
||||
"packageName": "system",
|
||||
"description": "角色",
|
||||
"abbreviation": "role",
|
||||
"humpPackageName": "system",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "RoleName",
|
||||
"fieldDesc": "角色名称",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "roleName",
|
||||
"dataTypeLong": "50",
|
||||
"comment": "角色名称",
|
||||
"columnName": "role_name",
|
||||
"fieldSearchType": "LIKE",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"package": "system",
|
||||
"tableName": "sys_permissions",
|
||||
"businessDB": "",
|
||||
"structName": "Permission",
|
||||
"packageName": "system",
|
||||
"description": "权限",
|
||||
"abbreviation": "permission",
|
||||
"humpPackageName": "system",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "PermissionName",
|
||||
"fieldDesc": "权限名称",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "permissionName",
|
||||
"dataTypeLong": "100",
|
||||
"comment": "权限名称",
|
||||
"columnName": "permission_name",
|
||||
"fieldSearchType": "LIKE",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"fieldName": "PermissionCode",
|
||||
"fieldDesc": "权限代码",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "permissionCode",
|
||||
"dataTypeLong": "50",
|
||||
"comment": "权限代码",
|
||||
"columnName": "permission_code",
|
||||
"fieldSearchType": "=",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:模块关联关系配置详解
|
||||
|
||||
以下示例展示了如何配置不同类型的关联关系:
|
||||
|
||||
```json
|
||||
{
|
||||
"packageName": "order",
|
||||
"packageType": "package",
|
||||
"needCreatedPackage": true,
|
||||
"needCreatedModules": true,
|
||||
"packageInfo": {
|
||||
"desc": "订单管理模块",
|
||||
"label": "订单管理",
|
||||
"template": "package",
|
||||
"packageName": "order"
|
||||
},
|
||||
"modulesInfo": [
|
||||
{
|
||||
"package": "order",
|
||||
"tableName": "orders",
|
||||
"structName": "Order",
|
||||
"packageName": "order",
|
||||
"description": "订单",
|
||||
"abbreviation": "order",
|
||||
"humpPackageName": "order",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "UserID",
|
||||
"fieldDesc": "下单用户",
|
||||
"fieldType": "uint",
|
||||
"fieldJson": "userId",
|
||||
"columnName": "user_id",
|
||||
"fieldSearchType": "EQ",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true,
|
||||
"dataSource": {
|
||||
"dbName": "gva",
|
||||
"table": "sys_users",
|
||||
"label": "username",
|
||||
"value": "id",
|
||||
"association": 2,
|
||||
"hasDeletedAt": true
|
||||
},
|
||||
"checkDataSource": true
|
||||
},
|
||||
{
|
||||
"fieldName": "ProductID",
|
||||
"fieldDesc": "商品",
|
||||
"fieldType": "uint",
|
||||
"fieldJson": "productId",
|
||||
"columnName": "product_id",
|
||||
"fieldSearchType": "EQ",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true,
|
||||
"dataSource": {
|
||||
"dbName": "gva",
|
||||
"table": "products",
|
||||
"label": "name",
|
||||
"value": "id",
|
||||
"association": 2,
|
||||
"hasDeletedAt": false
|
||||
},
|
||||
"checkDataSource": true
|
||||
},
|
||||
{
|
||||
"fieldName": "Status",
|
||||
"fieldDesc": "订单状态",
|
||||
"fieldType": "int",
|
||||
"fieldJson": "status",
|
||||
"columnName": "status",
|
||||
"fieldSearchType": "EQ",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"require": true,
|
||||
"dictType": "order_status"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## DataSource 配置说明
|
||||
|
||||
### 关联关系类型
|
||||
- **association: 1** - 一对一关联(如用户与用户档案)
|
||||
- **association: 2** - 一对多关联(如用户与订单)
|
||||
|
||||
### 配置要点
|
||||
1. **dbName**: 通常为 "gva"(默认数据库)
|
||||
2. **table**: 关联表的实际表名
|
||||
3. **label**: 用于前端显示的字段(如用户名、商品名称)
|
||||
4. **value**: 用于存储关联ID的字段(通常是 "id")
|
||||
5. **hasDeletedAt**: 关联表是否支持软删除
|
||||
6. **checkDataSource**: 建议设为true,会验证关联表是否存在
|
||||
|
||||
### 常见关联场景
|
||||
- 用户关联:`{"table": "sys_users", "label": "username", "value": "id"}`
|
||||
- 角色关联:`{"table": "sys_authorities", "label": "authorityName", "value": "authorityId"}`
|
||||
- 部门关联:`{"table": "sys_departments", "label": "name", "value": "id"}`
|
||||
- 分类关联:`{"table": "categories", "label": "name", "value": "id"}`
|
||||
|
||||
## 重要注意事项
|
||||
|
||||
1. **PackageType**: 只能是 "plugin" 或 "package"
|
||||
2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供
|
||||
3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供
|
||||
4. **字段类型**: FieldType支持的类型包括:
|
||||
- string(字符串)
|
||||
- richtext(富文本)
|
||||
- int(整型)
|
||||
- bool(布尔值)
|
||||
- float64(浮点型)
|
||||
- time.Time(时间)
|
||||
- enum(枚举)
|
||||
- picture(单图片,字符串)
|
||||
- pictures(多图片,json字符串)
|
||||
- video(视频,字符串)
|
||||
- file(文件,json字符串)
|
||||
- json(JSON)
|
||||
- array(数组)
|
||||
5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等
|
||||
6. **索引类型**: FieldIndexType支持:index, unique等
|
||||
7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段
|
||||
8. **关联配置**: 使用dataSource时,确保关联表已存在,建议开启checkDataSource验证
|
||||
|
||||
## 常见错误避免
|
||||
|
||||
1. 确保PackageName和ModuleName符合Go语言命名规范
|
||||
2. 字段名使用大写开头的驼峰命名
|
||||
3. JSON标签使用小写开头的驼峰命名
|
||||
4. 数据库列名使用下划线分隔的小写命名
|
||||
5. 必填字段不要遗漏
|
||||
6. 字段类型要与实际需求匹配
|
||||
@@ -1,205 +0,0 @@
|
||||
# GAG工具使用示例 - 带用户确认流程
|
||||
|
||||
## 新的工作流程
|
||||
|
||||
现在GAG工具支持三步工作流程:
|
||||
1. `analyze` - 分析现有模块信息
|
||||
2. `confirm` - 请求用户确认创建计划
|
||||
3. `execute` - 执行创建操作(需要用户确认)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 第一步:分析
|
||||
```json
|
||||
{
|
||||
"action": "analyze",
|
||||
"requirement": "创建一个图书管理功能"
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:确认(支持批量创建多个模块)
|
||||
```json
|
||||
{
|
||||
"action": "confirm",
|
||||
"executionPlan": {
|
||||
"packageName": "library",
|
||||
"packageType": "package",
|
||||
"needCreatedPackage": true,
|
||||
"needCreatedModules": true,
|
||||
"packageInfo": {
|
||||
"desc": "图书管理包",
|
||||
"label": "图书管理",
|
||||
"template": "package",
|
||||
"packageName": "library"
|
||||
},
|
||||
"modulesInfo": [
|
||||
{
|
||||
"package": "library",
|
||||
"tableName": "library_books",
|
||||
"businessDB": "",
|
||||
"structName": "Book",
|
||||
"packageName": "library",
|
||||
"description": "图书信息",
|
||||
"abbreviation": "book",
|
||||
"humpPackageName": "Library",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"treeJson": "",
|
||||
"isAdd": false,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "title",
|
||||
"fieldDesc": "书名",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "title",
|
||||
"dataTypeLong": "255",
|
||||
"comment": "书名",
|
||||
"columnName": "title",
|
||||
"fieldSearchType": "LIKE",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请输入书名",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": {},
|
||||
"checkDataSource": false,
|
||||
"fieldIndexType": ""
|
||||
},
|
||||
{
|
||||
"fieldName": "AuthorID",
|
||||
"fieldDesc": "作者",
|
||||
"fieldType": "uint",
|
||||
"fieldJson": "authorId",
|
||||
"dataTypeLong": "",
|
||||
"comment": "作者ID",
|
||||
"columnName": "author_id",
|
||||
"fieldSearchType": "EQ",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请选择作者",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": {
|
||||
"dbName": "gva",
|
||||
"table": "library_authors",
|
||||
"label": "name",
|
||||
"value": "id",
|
||||
"association": 2,
|
||||
"hasDeletedAt": true
|
||||
},
|
||||
"checkDataSource": true,
|
||||
"fieldIndexType": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"package": "library",
|
||||
"tableName": "library_authors",
|
||||
"businessDB": "",
|
||||
"structName": "Author",
|
||||
"packageName": "library",
|
||||
"description": "作者信息",
|
||||
"abbreviation": "author",
|
||||
"humpPackageName": "Library",
|
||||
"gvaModel": true,
|
||||
"autoMigrate": true,
|
||||
"autoCreateResource": true,
|
||||
"autoCreateApiToSql": true,
|
||||
"autoCreateMenuToSql": true,
|
||||
"autoCreateBtnAuth": true,
|
||||
"onlyTemplate": false,
|
||||
"isTree": false,
|
||||
"treeJson": "",
|
||||
"isAdd": false,
|
||||
"generateWeb": true,
|
||||
"generateServer": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "name",
|
||||
"fieldDesc": "作者姓名",
|
||||
"fieldType": "string",
|
||||
"fieldJson": "name",
|
||||
"dataTypeLong": "100",
|
||||
"comment": "作者姓名",
|
||||
"columnName": "name",
|
||||
"fieldSearchType": "LIKE",
|
||||
"fieldSearchHide": false,
|
||||
"dictType": "",
|
||||
"form": true,
|
||||
"table": true,
|
||||
"desc": true,
|
||||
"excel": true,
|
||||
"require": true,
|
||||
"defaultValue": "",
|
||||
"errorText": "请输入作者姓名",
|
||||
"clearable": true,
|
||||
"sort": false,
|
||||
"primaryKey": false,
|
||||
"dataSource": {},
|
||||
"checkDataSource": false,
|
||||
"fieldIndexType": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第三步:执行(需要确认参数)
|
||||
```json
|
||||
{
|
||||
"action": "execute",
|
||||
"executionPlan": {
|
||||
// ... 同上面的executionPlan
|
||||
},
|
||||
"packageConfirm": "yes", // 确认创建包
|
||||
"modulesConfirm": "yes" // 确认创建模块
|
||||
}
|
||||
```
|
||||
|
||||
## 确认参数说明
|
||||
|
||||
- `packageConfirm`: 当`needCreatedPackage`为true时必需
|
||||
- "yes": 确认创建包
|
||||
- "no": 取消创建包(停止后续处理)
|
||||
|
||||
- `modulesConfirm`: 当`needCreatedModules`为true时必需
|
||||
- "yes": 确认创建模块
|
||||
- "no": 取消创建模块(停止后续处理)
|
||||
|
||||
## 取消操作的行为
|
||||
|
||||
1. 如果用户在`packageConfirm`中选择"no",系统将停止所有后续处理
|
||||
2. 如果用户在`modulesConfirm`中选择"no",系统将停止模块创建
|
||||
3. 任何取消操作都会返回相应的取消消息,不会执行任何创建操作
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 必须先调用`confirm`来获取确认信息
|
||||
2. 在`execute`时必须提供相应的确认参数
|
||||
3. 确认参数的值必须是"yes"或"no"
|
||||
4. 如果不需要创建包或模块,则不需要提供对应的确认参数
|
||||
5. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)
|
||||
@@ -0,0 +1,494 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAAnalyzer{})
|
||||
}
|
||||
|
||||
// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module
|
||||
type GVAAnalyzer struct{}
|
||||
|
||||
// AnalyzeRequest 分析请求结构体
|
||||
type AnalyzeRequest struct {
|
||||
Requirement string `json:"requirement" binding:"required"` // 用户需求描述
|
||||
}
|
||||
|
||||
// AnalyzeResponse 分析响应结构体
|
||||
type AnalyzeResponse struct {
|
||||
ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息
|
||||
PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息
|
||||
Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息
|
||||
CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有)
|
||||
}
|
||||
|
||||
// ModuleInfo 模块信息
|
||||
type ModuleInfo struct {
|
||||
ModuleName string `json:"moduleName"` // 模块名称
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
StructName string `json:"structName"` // 结构体名称
|
||||
TableName string `json:"tableName"` // 表名
|
||||
Description string `json:"description"` // 描述
|
||||
FilePaths []string `json:"filePaths"` // 相关文件路径
|
||||
}
|
||||
|
||||
// PackageInfo 包信息
|
||||
type PackageInfo struct {
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
Label string `json:"label"` // 标签
|
||||
Desc string `json:"desc"` // 描述
|
||||
Module string `json:"module"` // 模块
|
||||
IsEmpty bool `json:"isEmpty"` // 是否为空包
|
||||
}
|
||||
|
||||
// PredesignedModuleInfo 预设计模块信息
|
||||
type PredesignedModuleInfo struct {
|
||||
ModuleName string `json:"moduleName"` // 模块名称
|
||||
PackageName string `json:"packageName"` // 包名
|
||||
Template string `json:"template"` // 模板类型
|
||||
FilePaths []string `json:"filePaths"` // 文件路径列表
|
||||
Description string `json:"description"` // 描述
|
||||
}
|
||||
|
||||
// CleanupInfo 清理信息
|
||||
type CleanupInfo struct {
|
||||
DeletedPackages []string `json:"deletedPackages"` // 已删除的包
|
||||
DeletedModules []string `json:"deletedModules"` // 已删除的模块
|
||||
CleanupMessage string `json:"cleanupMessage"` // 清理消息
|
||||
}
|
||||
|
||||
// New 创建GVA分析器工具
|
||||
func (g *GVAAnalyzer) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_analyze",
|
||||
mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"),
|
||||
mcp.WithString("requirement",
|
||||
mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理分析请求
|
||||
func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// 解析请求参数
|
||||
requirementStr, ok := request.GetArguments()["requirement"].(string)
|
||||
if !ok || requirementStr == "" {
|
||||
return nil, errors.New("参数错误:requirement 必须是非空字符串")
|
||||
}
|
||||
|
||||
// 创建分析请求
|
||||
analyzeReq := AnalyzeRequest{
|
||||
Requirement: requirementStr,
|
||||
}
|
||||
|
||||
// 执行分析逻辑
|
||||
response, err := g.performAnalysis(ctx, analyzeReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("分析失败: %v", err)
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
responseJSON, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化响应失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(string(responseJSON)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// performAnalysis 执行分析逻辑
|
||||
func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) {
|
||||
_ = req
|
||||
|
||||
packages, err := fetchAutoCodePackages(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取包信息失败: %v", err)
|
||||
}
|
||||
|
||||
histories, err := fetchAutoCodeHistories(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取历史记录失败: %v", err)
|
||||
}
|
||||
|
||||
cleanupInfo := &CleanupInfo{
|
||||
DeletedPackages: []string{},
|
||||
DeletedModules: []string{},
|
||||
}
|
||||
|
||||
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))
|
||||
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 := deleteAutoCodePackage(ctx, pkg.ID); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err))
|
||||
}
|
||||
|
||||
for _, history := range histories {
|
||||
if history.Package == pkg.PackageName {
|
||||
emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID)
|
||||
cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
validPackages = append(validPackages, PackageInfo{
|
||||
PackageName: pkg.PackageName,
|
||||
Template: pkg.Template,
|
||||
Label: pkg.Label,
|
||||
Desc: pkg.Desc,
|
||||
Module: pkg.Module,
|
||||
IsEmpty: false,
|
||||
})
|
||||
}
|
||||
|
||||
var dirtyHistoryIDs []uint
|
||||
for _, history := range histories {
|
||||
for _, emptyID := range emptyPackageHistoryIDs {
|
||||
if history.ID == emptyID {
|
||||
dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(dirtyHistoryIDs) > 0 {
|
||||
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))
|
||||
}
|
||||
|
||||
if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
predesignedModules, err := g.scanPredesignedModules()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err))
|
||||
predesignedModules = []PredesignedModuleInfo{}
|
||||
}
|
||||
|
||||
filteredModules := []PredesignedModuleInfo{}
|
||||
for _, module := range predesignedModules {
|
||||
isDeleted := false
|
||||
for _, deletedPkg := range cleanupInfo.DeletedPackages {
|
||||
if module.PackageName == deletedPkg {
|
||||
isDeleted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isDeleted {
|
||||
filteredModules = append(filteredModules, module)
|
||||
}
|
||||
}
|
||||
|
||||
dictionaries := []DictionaryPre{}
|
||||
dictEntities, err := fetchDictionaryList(ctx, "")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err))
|
||||
} 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
|
||||
}
|
||||
|
||||
response := &AnalyzeResponse{
|
||||
ExistingPackages: validPackages,
|
||||
PredesignedModules: filteredModules,
|
||||
Dictionaries: dictionaries,
|
||||
CleanupInfo: cleanupResult,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// isPackageFolderEmpty 检查包文件夹是否为空
|
||||
func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) {
|
||||
// 根据模板类型确定基础路径
|
||||
var basePath string
|
||||
if template == "plugin" {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
|
||||
} else {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName)
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
return true, nil // 文件夹不存在,认为空
|
||||
} else if err != nil {
|
||||
return false, err // 其他错误
|
||||
}
|
||||
// 递归检查是否有.go文件
|
||||
return g.hasGoFilesRecursive(basePath)
|
||||
}
|
||||
|
||||
// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件
|
||||
func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return true, err // 读取失败,返回空
|
||||
}
|
||||
|
||||
// 检查当前目录下的.go文件
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
|
||||
return false, nil // 找到.go文件,不为空
|
||||
}
|
||||
}
|
||||
|
||||
// 递归检查子目录
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
subDirPath := filepath.Join(dirPath, entry.Name())
|
||||
isEmpty, err := g.hasGoFilesRecursive(subDirPath)
|
||||
if err != nil {
|
||||
continue // 忽略子目录的错误,继续检查其他目录
|
||||
}
|
||||
if !isEmpty {
|
||||
return false, nil // 子目录中找到.go文件,不为空
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // 没有找到.go文件,为空
|
||||
}
|
||||
|
||||
// removeEmptyPackageFolder 删除空包文件夹
|
||||
func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error {
|
||||
var basePath string
|
||||
if template == "plugin" {
|
||||
basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName)
|
||||
} else {
|
||||
// 对于package类型,需要删除多个目录
|
||||
paths := []string{
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName),
|
||||
filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName),
|
||||
}
|
||||
for _, path := range paths {
|
||||
if err := g.removeDirectoryIfExists(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return g.removeDirectoryIfExists(basePath)
|
||||
}
|
||||
|
||||
// removeDirectoryIfExists 删除目录(如果存在)
|
||||
func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需删除
|
||||
} else if err != nil {
|
||||
return err // 其他错误
|
||||
}
|
||||
|
||||
// 检查目录中是否包含go文件
|
||||
noGoFiles, err := g.hasGoFilesRecursive(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// hasGoFilesRecursive 返回 false 表示发现了 go 文件
|
||||
if noGoFiles {
|
||||
return os.RemoveAll(dirPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
|
||||
func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error {
|
||||
if len(historyIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 这里可以根据需要实现具体的API和菜单清理逻辑
|
||||
// 由于涉及到具体的业务逻辑,这里只做日志记录
|
||||
global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs))
|
||||
|
||||
// 可以调用service层的相关方法进行清理
|
||||
// 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs)
|
||||
// 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanPredesignedModules 扫描预设计模块
|
||||
func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) {
|
||||
// 获取autocode配置路径
|
||||
autocodeRoot := global.GVA_CONFIG.AutoCode.Root
|
||||
if autocodeRoot == "" {
|
||||
return nil, errors.New("autocode根路径未配置")
|
||||
}
|
||||
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
// 扫描plugin目录
|
||||
pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin"))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err))
|
||||
} else {
|
||||
modules = append(modules, pluginModules...)
|
||||
}
|
||||
|
||||
// 扫描model目录
|
||||
modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model"))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err))
|
||||
} else {
|
||||
modules = append(modules, modelModules...)
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanPluginModules 扫描插件模块
|
||||
func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
|
||||
return modules, nil // 目录不存在,返回空列表
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(pluginDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
pluginName := entry.Name()
|
||||
pluginPath := filepath.Join(pluginDir, pluginName)
|
||||
|
||||
// 查找model目录
|
||||
modelDir := filepath.Join(pluginPath, "model")
|
||||
if _, err := os.Stat(modelDir); err == nil {
|
||||
// 扫描model目录下的模块
|
||||
pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err))
|
||||
continue
|
||||
}
|
||||
modules = append(modules, pluginModules...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanModelModules 扫描模型模块
|
||||
func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
if _, err := os.Stat(modelDir); os.IsNotExist(err) {
|
||||
return modules, nil // 目录不存在,返回空列表
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(modelDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
packageName := entry.Name()
|
||||
packagePath := filepath.Join(modelDir, packageName)
|
||||
|
||||
// 扫描包目录下的模块
|
||||
packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err))
|
||||
continue
|
||||
}
|
||||
modules = append(modules, packageModules...)
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// scanModulesInDirectory 扫描目录中的模块
|
||||
func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) {
|
||||
var modules []PredesignedModuleInfo
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") {
|
||||
moduleName := strings.TrimSuffix(entry.Name(), ".go")
|
||||
filePath := filepath.Join(dir, entry.Name())
|
||||
|
||||
module := PredesignedModuleInfo{
|
||||
ModuleName: moduleName,
|
||||
PackageName: packageName,
|
||||
Template: template,
|
||||
FilePaths: []string{filePath},
|
||||
Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName),
|
||||
}
|
||||
modules = append(modules, module)
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,750 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAExecutor{})
|
||||
}
|
||||
|
||||
// GVAExecutor GVA代码生成器
|
||||
type GVAExecutor struct{}
|
||||
|
||||
// ExecuteRequest 执行请求结构体
|
||||
type ExecuteRequest struct {
|
||||
ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划
|
||||
Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录)
|
||||
}
|
||||
|
||||
// ExecuteResponse 执行响应结构体
|
||||
type ExecuteResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
PackageID uint `json:"packageId,omitempty"`
|
||||
HistoryID uint `json:"historyId,omitempty"`
|
||||
Paths map[string]string `json:"paths,omitempty"`
|
||||
GeneratedPaths []string `json:"generatedPaths,omitempty"`
|
||||
NextActions []string `json:"nextActions,omitempty"`
|
||||
}
|
||||
|
||||
// ExecutionPlan 执行计划结构体
|
||||
type ExecutionPlan struct {
|
||||
PackageName string `json:"packageName"`
|
||||
PackageType string `json:"packageType"` // "plugin" 或 "package"
|
||||
NeedCreatedPackage bool `json:"needCreatedPackage"`
|
||||
NeedCreatedModules bool `json:"needCreatedModules"`
|
||||
NeedCreatedDictionaries bool `json:"needCreatedDictionaries"`
|
||||
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"`
|
||||
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"`
|
||||
Paths map[string]string `json:"paths,omitempty"`
|
||||
DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"`
|
||||
}
|
||||
|
||||
// New 创建GVA代码生成执行器工具
|
||||
func (g *GVAExecutor) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_execute",
|
||||
mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤**
|
||||
|
||||
**核心功能:**
|
||||
根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。
|
||||
|
||||
**使用场景:**
|
||||
在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。
|
||||
|
||||
**重要提示:**
|
||||
- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具
|
||||
- 字段使用字典类型时,系统会自动检查并创建字典
|
||||
- 字典创建会在模块创建之前执行
|
||||
- 当字段配置了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.WithString("requirement",
|
||||
mcp.Description("原始需求描述(可选,用于日志记录)"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理执行请求(移除确认步骤)
|
||||
func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
executionPlanData, ok := request.GetArguments()["executionPlan"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:executionPlan 必须提供")
|
||||
}
|
||||
|
||||
// 解析执行计划
|
||||
planJSON, err := json.Marshal(executionPlanData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析执行计划失败: %v", err)
|
||||
}
|
||||
|
||||
var plan ExecutionPlan
|
||||
err = json.Unmarshal(planJSON, &plan)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err)
|
||||
}
|
||||
|
||||
// 验证执行计划的完整性
|
||||
if err := g.validateExecutionPlan(&plan); err != nil {
|
||||
return nil, fmt.Errorf("执行计划验证失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取原始需求(可选)
|
||||
var originalRequirement string
|
||||
if reqData, ok := request.GetArguments()["requirement"]; ok {
|
||||
if reqStr, ok := reqData.(string); ok {
|
||||
originalRequirement = reqStr
|
||||
}
|
||||
}
|
||||
|
||||
// 直接执行创建操作(无确认步骤)
|
||||
result := g.executeCreation(ctx, &plan)
|
||||
|
||||
// 如果执行成功且有原始需求,提供代码复检建议
|
||||
var reviewMessage string
|
||||
if result.Success && originalRequirement != "" {
|
||||
global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...")
|
||||
|
||||
// 构建文件路径信息供AI使用
|
||||
var pathsInfo []string
|
||||
for _, path := range result.GeneratedPaths {
|
||||
pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path))
|
||||
}
|
||||
|
||||
reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n"))
|
||||
} else if originalRequirement == "" {
|
||||
reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。"
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
response := ExecuteResponse{
|
||||
Success: result.Success,
|
||||
Message: result.Message,
|
||||
PackageID: result.PackageID,
|
||||
HistoryID: result.HistoryID,
|
||||
Paths: result.Paths,
|
||||
GeneratedPaths: result.GeneratedPaths,
|
||||
NextActions: result.NextActions,
|
||||
}
|
||||
|
||||
responseJSON, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateExecutionPlan 验证执行计划的完整性
|
||||
func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error {
|
||||
if plan.PackageName == "" {
|
||||
return errors.New("packageName 不能为空")
|
||||
}
|
||||
if plan.PackageType != "package" && plan.PackageType != "plugin" {
|
||||
return errors.New("packageType 必须是 'package' 或 'plugin'")
|
||||
}
|
||||
|
||||
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 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.PackageName == "" {
|
||||
return errors.New("packageInfo.packageName 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" {
|
||||
return errors.New("packageInfo.template 必须是 'package' 或 'plugin'")
|
||||
}
|
||||
if plan.PackageInfo.Label == "" {
|
||||
return errors.New("packageInfo.label 不能为空")
|
||||
}
|
||||
if plan.PackageInfo.Desc == "" {
|
||||
return errors.New("packageInfo.desc 不能为空")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if moduleInfo.StructName == "" {
|
||||
return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.TableName == "" {
|
||||
return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.Description == "" {
|
||||
return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.Abbreviation == "" {
|
||||
return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.PackageName == "" {
|
||||
return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if moduleInfo.HumpPackageName == "" {
|
||||
return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1)
|
||||
}
|
||||
if len(moduleInfo.Fields) == 0 {
|
||||
return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1)
|
||||
}
|
||||
|
||||
for i, field := range moduleInfo.Fields {
|
||||
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" {
|
||||
moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
|
||||
}
|
||||
}
|
||||
if field.FieldDesc == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.FieldType == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.FieldJson == "" {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1)
|
||||
}
|
||||
if field.ColumnName == "" {
|
||||
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 {
|
||||
if field.FieldType == validFieldType {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validType {
|
||||
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 _, validSearchTypeValue := range validSearchTypes {
|
||||
if field.FieldSearchType == validSearchTypeValue {
|
||||
validSearchType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validSearchType {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持", moduleIndex+1, i+1, field.FieldSearchType)
|
||||
}
|
||||
}
|
||||
|
||||
if field.DataSource != nil {
|
||||
associationValue := field.DataSource.Association
|
||||
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"
|
||||
}
|
||||
if associationValue != 1 && associationValue != 2 {
|
||||
return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1 或 2", moduleIndex+1, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !moduleInfo.GvaModel {
|
||||
primaryKeyCount := 0
|
||||
for _, field := range moduleInfo.Fields {
|
||||
if field.PrimaryKey {
|
||||
primaryKeyCount++
|
||||
}
|
||||
}
|
||||
if primaryKeyCount == 0 {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1)
|
||||
}
|
||||
if primaryKeyCount > 1 {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1)
|
||||
}
|
||||
} else {
|
||||
for i, field := range moduleInfo.Fields {
|
||||
if field.PrimaryKey {
|
||||
return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false", moduleIndex+1, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeCreation 执行创建操作
|
||||
func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse {
|
||||
result := &ExecuteResponse{
|
||||
Success: false,
|
||||
Paths: make(map[string]string),
|
||||
GeneratedPaths: []string{}, // 初始化生成文件路径列表
|
||||
}
|
||||
|
||||
// 无论如何都先构建目录结构信息,确保paths始终返回
|
||||
result.Paths = g.buildDirectoryStructure(plan)
|
||||
|
||||
// 记录预期生成的文件路径
|
||||
result.GeneratedPaths = g.collectExpectedFilePaths(plan)
|
||||
|
||||
if !plan.NeedCreatedModules {
|
||||
result.Success = true
|
||||
result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; "
|
||||
return result
|
||||
}
|
||||
|
||||
// 创建包(如果需要)
|
||||
if plan.NeedCreatedPackage && plan.PackageInfo != nil {
|
||||
err := createAutoCodePackage(ctx, plan.PackageInfo)
|
||||
if err != nil {
|
||||
result.Message = fmt.Sprintf("创建包失败: %v", err)
|
||||
// 即使创建包失败,也要返回paths信息
|
||||
return result
|
||||
}
|
||||
result.Message += "包创建成功; "
|
||||
}
|
||||
|
||||
// 创建指定字典(如果需要)
|
||||
if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 {
|
||||
dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo)
|
||||
result.Message += dictResult
|
||||
}
|
||||
|
||||
// 批量创建字典和模块(如果需要)
|
||||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||||
// 遍历所有模块进行创建
|
||||
for _, moduleInfo := range plan.ModulesInfo {
|
||||
|
||||
// 创建模块
|
||||
err := moduleInfo.Pretreatment()
|
||||
if err != nil {
|
||||
result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err)
|
||||
continue // 继续处理下一个模块
|
||||
}
|
||||
|
||||
err = createAutoCodeModule(ctx, *moduleInfo)
|
||||
if err != nil {
|
||||
result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err)
|
||||
continue // 继续处理下一个模块
|
||||
}
|
||||
result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName)
|
||||
}
|
||||
|
||||
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
|
||||
|
||||
// 添加重要提醒:不要使用其他MCP工具
|
||||
result.Message += "\n\n⚠️ 重要提醒:\n"
|
||||
result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n"
|
||||
result.Message += "- api_creator:API权限已在模块创建时自动生成\n"
|
||||
result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n"
|
||||
result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n"
|
||||
}
|
||||
|
||||
result.Message += "已构建目录结构信息; "
|
||||
result.Success = true
|
||||
|
||||
if result.Message == "" {
|
||||
result.Message = "执行计划完成"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildDirectoryStructure 构建目录结构信息
|
||||
func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string {
|
||||
paths := make(map[string]string)
|
||||
|
||||
// 获取配置信息
|
||||
autoCodeConfig := global.GVA_CONFIG.AutoCode
|
||||
|
||||
// 构建基础路径
|
||||
rootPath := autoCodeConfig.Root
|
||||
serverPath := autoCodeConfig.Server
|
||||
webPath := autoCodeConfig.Web
|
||||
moduleName := autoCodeConfig.Module
|
||||
|
||||
// 如果计划中有包名,使用计划中的包名,否则使用默认
|
||||
packageName := "example"
|
||||
if plan.PackageName != "" {
|
||||
packageName = plan.PackageName
|
||||
}
|
||||
|
||||
// 如果计划中有模块信息,获取第一个模块的结构名作为默认值
|
||||
structName := "ExampleStruct"
|
||||
if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" {
|
||||
structName = plan.ModulesInfo[0].StructName
|
||||
}
|
||||
|
||||
// 根据包类型构建不同的路径结构
|
||||
packageType := plan.PackageType
|
||||
if packageType == "" {
|
||||
packageType = "package" // 默认为package模式
|
||||
}
|
||||
|
||||
// 构建服务端路径
|
||||
if serverPath != "" {
|
||||
serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath)
|
||||
|
||||
if packageType == "plugin" {
|
||||
// Plugin 模式:所有文件都在 /plugin/packageName/ 目录中
|
||||
plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName)
|
||||
|
||||
// API 路径
|
||||
paths["api"] = fmt.Sprintf("%s/api", plugingBasePath)
|
||||
|
||||
// Service 路径
|
||||
paths["service"] = fmt.Sprintf("%s/service", plugingBasePath)
|
||||
|
||||
// Model 路径
|
||||
paths["model"] = fmt.Sprintf("%s/model", plugingBasePath)
|
||||
|
||||
// Router 路径
|
||||
paths["router"] = fmt.Sprintf("%s/router", plugingBasePath)
|
||||
|
||||
// Request 路径
|
||||
paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath)
|
||||
|
||||
// Response 路径
|
||||
paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath)
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
// Package 模式:传统的目录结构
|
||||
// API 路径
|
||||
paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName)
|
||||
|
||||
// Service 路径
|
||||
paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName)
|
||||
|
||||
// Model 路径
|
||||
paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName)
|
||||
|
||||
// Router 路径
|
||||
paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName)
|
||||
|
||||
// Request 路径
|
||||
paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName)
|
||||
|
||||
// Response 路径
|
||||
paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建前端路径(两种模式相同)
|
||||
if webPath != "" {
|
||||
webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath)
|
||||
|
||||
if packageType == "plugin" {
|
||||
// Plugin 模式:前端文件也在 /plugin/packageName/ 目录中
|
||||
pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName)
|
||||
|
||||
// Vue 页面路径
|
||||
paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath)
|
||||
|
||||
// API 路径
|
||||
paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath)
|
||||
} else {
|
||||
// Package 模式:传统的目录结构
|
||||
// Vue 页面路径
|
||||
paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName)
|
||||
|
||||
// API 路径
|
||||
paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模块信息
|
||||
paths["module"] = moduleName
|
||||
paths["package_name"] = packageName
|
||||
paths["package_type"] = packageType
|
||||
paths["struct_name"] = structName
|
||||
paths["root_path"] = rootPath
|
||||
paths["server_path"] = serverPath
|
||||
paths["web_path"] = webPath
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// collectExpectedFilePaths 收集预期生成的文件路径
|
||||
func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string {
|
||||
var paths []string
|
||||
|
||||
// 获取目录结构
|
||||
dirPaths := g.buildDirectoryStructure(plan)
|
||||
|
||||
// 如果需要创建模块,添加预期的文件路径
|
||||
if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 {
|
||||
for _, moduleInfo := range plan.ModulesInfo {
|
||||
structName := moduleInfo.StructName
|
||||
|
||||
// 后端文件
|
||||
if apiPath, ok := dirPaths["api"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName)))
|
||||
}
|
||||
if servicePath, ok := dirPaths["service"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName)))
|
||||
}
|
||||
if modelPath, ok := dirPaths["model"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName)))
|
||||
}
|
||||
if routerPath, ok := dirPaths["router"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName)))
|
||||
}
|
||||
if requestPath, ok := dirPaths["request"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName)))
|
||||
}
|
||||
if responsePath, ok := dirPaths["response"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName)))
|
||||
}
|
||||
|
||||
// 前端文件
|
||||
if vuePage, ok := dirPaths["vue_page"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName)))
|
||||
}
|
||||
if vueApi, ok := dirPaths["vue_api"]; ok {
|
||||
paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// checkDictionaryExists 检查字典是否存在
|
||||
func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) {
|
||||
dictionary, err := findDictionaryByType(context.Background(), dictType)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return dictionary != nil, nil
|
||||
}
|
||||
|
||||
// createDictionariesFromInfo 根据 DictionariesInfo 创建字典
|
||||
func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string {
|
||||
var messages []string
|
||||
|
||||
messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo)))
|
||||
|
||||
for _, dictInfo := range dictionariesInfo {
|
||||
exists, err := g.checkDictionaryExists(dictInfo.DictType)
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if !exists {
|
||||
err = createDictionary(ctx, system.SysDictionary{
|
||||
Name: dictInfo.DictName,
|
||||
Type: dictInfo.DictType,
|
||||
Status: enabledBoolPointer(),
|
||||
Desc: dictInfo.Description,
|
||||
})
|
||||
if err != nil {
|
||||
messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err))
|
||||
continue
|
||||
}
|
||||
|
||||
messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName))
|
||||
|
||||
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 := system.SysDictionaryDetail{
|
||||
Label: option.Label,
|
||||
Value: option.Value,
|
||||
Status: enabledBoolPointer(),
|
||||
Sort: option.Sort,
|
||||
SysDictionaryID: int(createdDict.ID),
|
||||
}
|
||||
|
||||
err = createDictionaryDetail(ctx, dictionaryDetail)
|
||||
if err == nil {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount))
|
||||
}
|
||||
} else {
|
||||
messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(messages, "")
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// GVAReviewer GVA代码审查工具
|
||||
type GVAReviewer struct{}
|
||||
|
||||
// init 注册工具
|
||||
func init() {
|
||||
RegisterTool(&GVAReviewer{})
|
||||
}
|
||||
|
||||
// ReviewRequest 审查请求结构
|
||||
type ReviewRequest struct {
|
||||
UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求
|
||||
GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表
|
||||
}
|
||||
|
||||
// ReviewResponse 审查响应结构
|
||||
type ReviewResponse struct {
|
||||
Success bool `json:"success"` // 是否审查成功
|
||||
Message string `json:"message"` // 审查结果消息
|
||||
AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示
|
||||
ReviewDetails string `json:"reviewDetails"` // 详细的审查结果
|
||||
}
|
||||
|
||||
// New 创建GVA代码审查工具
|
||||
func (g *GVAReviewer) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_review",
|
||||
mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用**
|
||||
|
||||
**核心功能:**
|
||||
- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表
|
||||
- 分析生成的代码是否满足用户的原始需求
|
||||
- 检查是否涉及到关联、交互等复杂功能
|
||||
- 如果代码不满足需求,提供调整建议和新的prompt
|
||||
|
||||
**使用场景:**
|
||||
- 在gva_execute成功执行后调用
|
||||
- 用于验证生成的代码是否完整满足用户需求
|
||||
- 检查模块间的关联关系是否正确实现
|
||||
- 发现缺失的交互功能或业务逻辑
|
||||
|
||||
**工作流程:**
|
||||
1. 接收用户原始需求和生成的文件列表
|
||||
2. 分析需求中的关键功能点
|
||||
3. 检查生成的文件是否覆盖所有功能
|
||||
4. 识别缺失的关联关系、交互功能等
|
||||
5. 生成调整建议和新的开发prompt
|
||||
|
||||
**输出内容:**
|
||||
- 审查结果和是否需要调整
|
||||
- 详细的缺失功能分析
|
||||
- 针对性的代码调整建议
|
||||
- 可直接使用的开发prompt
|
||||
|
||||
**重要提示:**
|
||||
- 本工具专门用于代码质量审查,不执行实际的代码修改
|
||||
- 重点关注模块间关联、用户交互、业务流程完整性
|
||||
- 提供的调整建议应该具体可执行`),
|
||||
mcp.WithString("userRequirement",
|
||||
mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString("generatedFiles",
|
||||
mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"),
|
||||
mcp.Required(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 处理审查请求
|
||||
func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
// 获取用户需求
|
||||
userRequirementData, ok := request.GetArguments()["userRequirement"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:userRequirement 必须提供")
|
||||
}
|
||||
|
||||
userRequirement, ok := userRequirementData.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:userRequirement 必须是字符串类型")
|
||||
}
|
||||
|
||||
// 获取生成的文件列表
|
||||
generatedFilesData, ok := request.GetArguments()["generatedFiles"]
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:generatedFiles 必须提供")
|
||||
}
|
||||
|
||||
generatedFilesStr, ok := generatedFilesData.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串")
|
||||
}
|
||||
|
||||
// 解析JSON字符串为字符串数组
|
||||
var generatedFiles []string
|
||||
err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析generatedFiles失败: %v", err)
|
||||
}
|
||||
|
||||
if len(generatedFiles) == 0 {
|
||||
return nil, errors.New("参数错误:generatedFiles 不能为空")
|
||||
}
|
||||
|
||||
// 直接生成调整提示,不进行复杂分析
|
||||
adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles)
|
||||
|
||||
// 构建简化的审查详情
|
||||
reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles))
|
||||
|
||||
// 构建审查结果
|
||||
reviewResult := &ReviewResponse{
|
||||
Success: true,
|
||||
Message: "代码审查完成",
|
||||
AdjustmentPrompt: adjustmentPrompt,
|
||||
ReviewDetails: reviewDetails,
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
responseJSON, err := json.MarshalIndent(reviewResult, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化审查结果失败: %v", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAdjustmentPrompt 生成调整代码的提示
|
||||
func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n")
|
||||
prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement))
|
||||
prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n")
|
||||
for _, file := range generatedFiles {
|
||||
prompt.WriteString(fmt.Sprintf("- %s\n", file))
|
||||
}
|
||||
prompt.WriteString("\n")
|
||||
|
||||
prompt.WriteString("**请帮我优化和完善代码,确保:**\n")
|
||||
prompt.WriteString("1. 代码完全满足用户的原始需求\n")
|
||||
prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n")
|
||||
prompt.WriteString("3. 实现所有必要的用户交互功能\n")
|
||||
prompt.WriteString("4. 保持代码的完整性和可维护性\n")
|
||||
prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n")
|
||||
prompt.WriteString("6. 确保前后端功能完整对接\n")
|
||||
prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n")
|
||||
prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n")
|
||||
prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n")
|
||||
prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n")
|
||||
prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n")
|
||||
prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n")
|
||||
prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
@@ -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
-117
@@ -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), ¶mReqs); 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,53 +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
|
||||
}
|
||||
}
|
||||
|
||||
// 添加权限分配提醒
|
||||
permissionReminder := "\n\n⚠️ 重要提醒:\n" +
|
||||
"菜单创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的菜单权限," +
|
||||
"以确保用户能够正常访问新菜单。\n" +
|
||||
"具体步骤:\n" +
|
||||
"1. 进入角色管理页面\n" +
|
||||
"2. 选择需要授权的角色\n" +
|
||||
"3. 在菜单权限中勾选新创建的菜单项\n" +
|
||||
"4. 保存权限配置"
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("菜单创建结果:\n\n%s%s", string(resultJSON), permissionReminder),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
return 0
|
||||
}
|
||||
|
||||
+18
-71
@@ -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,67 +37,23 @@ func (m *MenuLister) New() mcp.Tool {
|
||||
- 菜单权限管理:了解系统中所有可用的菜单项
|
||||
- 导航组件开发:构建动态导航菜单
|
||||
- 系统架构分析:了解系统的菜单结构和页面组织`),
|
||||
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: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,8 +20,6 @@ type RequirementAnalysisRequest struct {
|
||||
UserRequirement string `json:"userRequirement"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// RequirementAnalysisResponse 需求分析响应
|
||||
type RequirementAnalysisResponse struct {
|
||||
AIPrompt string `json:"aiPrompt"` // 给AI的提示词
|
||||
@@ -30,37 +28,36 @@ type RequirementAnalysisResponse struct {
|
||||
// New 返回工具注册信息
|
||||
func (t *RequirementAnalyzer) New() mcp.Tool {
|
||||
return mcp.NewTool("requirement_analyzer",
|
||||
mcp.WithDescription(`**🚀 需求分析工具 - 首选入口工具(最高优先级)**
|
||||
mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)**
|
||||
|
||||
**⭐ 重要提示:这是所有MCP工具的首选入口,请优先使用!**
|
||||
** 重要提示:这是所有MCP工具的首选入口,请优先使用!**
|
||||
|
||||
**🎯 核心职责:**
|
||||
将用户的自然语言需求转换为AI可理解的结构化提示词
|
||||
** 核心能力:**
|
||||
作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构
|
||||
|
||||
**📋 工作流程:**
|
||||
1. 接收用户自然语言需求描述
|
||||
2. 生成专业的AI提示词,要求AI将需求梳理为清晰的逻辑步骤:
|
||||
- **1. 第一步功能描述**
|
||||
- **2. 第二步功能描述**
|
||||
- **3. 第三步功能描述**
|
||||
- **...**
|
||||
3. 指导后续使用 gva_auto_generate 工具进行代码生成
|
||||
** 核心功能:**
|
||||
1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系
|
||||
2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能
|
||||
3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求
|
||||
4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议
|
||||
|
||||
**✅ 适用场景:**
|
||||
- 用户有新的业务需求需要开发
|
||||
- 需要创建新的功能模块
|
||||
- 想要快速搭建业务系统
|
||||
- 需求描述比较模糊,需要AI帮助梳理
|
||||
** 输出内容:**
|
||||
- 模块数量和架构设计
|
||||
- 每个模块的详细字段清单
|
||||
- 数据类型和关联关系设计
|
||||
- 字典需求和类型定义
|
||||
- 模块间关系图和扩展建议
|
||||
|
||||
**❌ 不负责的事情:**
|
||||
- 不生成具体的包名和模块名(交给 gva_auto_generate)
|
||||
- 不进行代码生成(交给 gva_auto_generate)
|
||||
- 不创建数据库表结构(交给 gva_auto_generate)
|
||||
** 适用场景:**
|
||||
- 用户需求描述不完整,需要智能补全
|
||||
- 复杂业务系统的模块架构设计
|
||||
- 需要专业的数据库设计建议
|
||||
- 想要快速搭建生产级业务系统
|
||||
|
||||
**🔄 推荐工作流:**
|
||||
requirement_analyzer → gva_auto_generate → 其他辅助工具
|
||||
|
||||
`),
|
||||
** 推荐工作流:**
|
||||
requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具
|
||||
|
||||
`),
|
||||
mcp.WithString("userRequirement",
|
||||
mcp.Required(),
|
||||
mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"),
|
||||
@@ -104,36 +101,99 @@ func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*Requi
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateAIPrompt 生成AI提示词 - 要求AI梳理逻辑为1xxx2xxx格式
|
||||
// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构
|
||||
func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string {
|
||||
prompt := fmt.Sprintf(`# 🤖 AI需求逻辑梳理任务
|
||||
prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务
|
||||
|
||||
## 📝 用户原始需求
|
||||
## 用户原始需求
|
||||
%s
|
||||
|
||||
## 🎯 AI任务要求
|
||||
请将上述用户需求梳理成清晰的逻辑步骤,格式要求:
|
||||
## 核心任务
|
||||
你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。
|
||||
|
||||
## 分析步骤
|
||||
|
||||
### 第一步:需求解构分析
|
||||
请仔细分析用户需求,识别出:
|
||||
1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等)
|
||||
2. **业务流程**(如:注册、购买、记录、管理等)
|
||||
3. **数据关系**(实体间的关联关系)
|
||||
4. **功能模块**(需要哪些独立的管理模块)
|
||||
|
||||
### 第二步:模块架构设计
|
||||
基于需求分析,设计出模块架构,格式如下:
|
||||
|
||||
**模块1:[模块名称]**
|
||||
- 功能描述:[该模块的核心功能]
|
||||
- 主要字段:[列出关键字段,注明数据类型]
|
||||
- 关联关系:[与其他模块的关系,明确一对一/一对多]
|
||||
- 字典需求:[需要哪些字典类型]
|
||||
|
||||
**模块2:[模块名称]**
|
||||
- 功能描述:[该模块的核心功能]
|
||||
- 主要字段:[列出关键字段,注明数据类型]
|
||||
- 关联关系:[与其他模块的关系]
|
||||
- 字典需求:[需要哪些字典类型]
|
||||
|
||||
**1. 第一步功能描述**
|
||||
**2. 第二步功能描述**
|
||||
**3. 第三步功能描述**
|
||||
**...**
|
||||
|
||||
## 📋 梳理要求
|
||||
- 将需求拆解为具体的功能步骤
|
||||
- 每个步骤用数字编号(1、2、3...)
|
||||
- 步骤描述要清晰、具体、可执行
|
||||
- 按照业务逻辑顺序排列
|
||||
- 考虑数据流和用户操作流程
|
||||
### 第三步:字段详细设计
|
||||
为每个模块详细设计字段:
|
||||
|
||||
## 🔄 后续流程
|
||||
梳理完成后,请使用 gva_auto_generate 工具进行代码生成:
|
||||
- gva_auto_generate 会根据梳理的逻辑步骤自动生成包名、模块名
|
||||
- gva_auto_generate 会设计数据表结构和API接口
|
||||
- gva_auto_generate 会生成完整的前后端代码
|
||||
#### 模块1字段清单:
|
||||
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- ...
|
||||
|
||||
#### 模块2字段清单:
|
||||
- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型]
|
||||
- ...
|
||||
|
||||
现在请开始梳理用户需求:"%s"`, userRequirement, userRequirement)
|
||||
## 智能分析指导原则
|
||||
|
||||
### 模块拆分原则
|
||||
1. **单一职责**:每个模块只负责一个核心业务实体
|
||||
2. **数据完整性**:相关数据应该在同一模块中
|
||||
3. **业务独立性**:模块应该能够独立完成特定业务功能
|
||||
4. **扩展性考虑**:为未来功能扩展预留空间
|
||||
|
||||
### 字段设计原则
|
||||
1. **必要性**:只包含业务必需的字段
|
||||
2. **规范性**:遵循数据库设计规范
|
||||
3. **关联性**:正确识别实体间关系
|
||||
4. **字典化**:状态、类型等枚举值使用字典
|
||||
|
||||
### 关联关系识别
|
||||
- **一对一**:一个实体只能关联另一个实体的一个记录
|
||||
- **一对多**:一个实体可以关联另一个实体的多个记录
|
||||
- **多对多**:通过中间表实现复杂关联
|
||||
|
||||
## 特殊场景处理
|
||||
|
||||
### 复杂实体识别
|
||||
当用户提到某个概念时,要判断它是否需要独立模块:
|
||||
- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等)
|
||||
- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录)
|
||||
|
||||
## 输出要求
|
||||
|
||||
### 必须包含的信息
|
||||
1. **模块数量**:明确需要几个模块
|
||||
2. **模块关系图**:用文字描述模块间关系
|
||||
3. **核心字段**:每个模块的关键字段(至少5-10个)
|
||||
4. **数据类型**:string、int、bool、time.Time、float64等
|
||||
5. **关联设计**:明确哪些字段是关联字段
|
||||
6. **字典需求**:列出需要创建的字典类型
|
||||
|
||||
### 严格遵循用户输入
|
||||
- 如果用户提供了具体字段,**必须使用**用户提供的字段
|
||||
- 如果用户提供了SQL文件,**严格按照**SQL结构设计
|
||||
- **不要**随意发散,**不要**添加用户未提及的功能
|
||||
---
|
||||
|
||||
**现在请开始深度分析用户需求:"%s"**
|
||||
|
||||
请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
mcpServer "github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func NewMCPServer() *mcpServer.MCPServer {
|
||||
config := global.GVA_CONFIG.MCP
|
||||
|
||||
s := mcpServer.NewMCPServer(
|
||||
config.Name,
|
||||
config.Version,
|
||||
)
|
||||
|
||||
global.GVA_MCP_SERVER = s
|
||||
RegisterAllTools(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStreamableHTTPServer() *mcpServer.StreamableHTTPServer {
|
||||
config := global.GVA_CONFIG.MCP
|
||||
path := config.Path
|
||||
if path == "" {
|
||||
path = "/mcp"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
httpSrv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
handler := mcpServer.NewStreamableHTTPServer(
|
||||
NewMCPServer(),
|
||||
mcpServer.WithHTTPContextFunc(WithHTTPRequestContext),
|
||||
mcpServer.WithStreamableHTTPServer(httpSrv),
|
||||
)
|
||||
mux.Handle(path, handler)
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
return handler
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
package mcpTool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
const (
|
||||
mcpRuntimeDirName = ".tmp"
|
||||
mcpRuntimeSubDir = "mcp"
|
||||
mcpRuntimeMetaName = "managed-process.json"
|
||||
mcpRuntimeLogName = "mcp.log"
|
||||
mcpHealthCheckTimeout = 2 * time.Second
|
||||
mcpStartWaitTimeout = 20 * time.Second
|
||||
mcpStopWaitTimeout = 8 * time.Second
|
||||
mcpBuildTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
type ManagedStandaloneStatus struct {
|
||||
State string `json:"state"`
|
||||
Managed bool `json:"managed"`
|
||||
Reachable bool `json:"reachable"`
|
||||
Starting bool `json:"starting"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
HealthURL string `json:"healthURL"`
|
||||
ListenAddr string `json:"listenAddr"`
|
||||
Path string `json:"path"`
|
||||
AuthHeader string `json:"authHeader"`
|
||||
PID int `json:"pid,omitempty"`
|
||||
LogPath string `json:"logPath,omitempty"`
|
||||
StartedAt string `json:"startedAt,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type managedProcessMeta struct {
|
||||
PID int `json:"pid"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
LogPath string `json:"logPath"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
WorkDir string `json:"workDir"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
}
|
||||
|
||||
func ResolveMCPListenAddr() string {
|
||||
addr := global.GVA_CONFIG.MCP.Addr
|
||||
if addr <= 0 {
|
||||
addr = 8889
|
||||
}
|
||||
return fmt.Sprintf(":%d", addr)
|
||||
}
|
||||
|
||||
func ResolveMCPPath() string {
|
||||
path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path)
|
||||
if path == "" {
|
||||
path = "/mcp"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func ResolveMCPHealthURL() string {
|
||||
baseURL, err := url.Parse(ResolveMCPServiceURL())
|
||||
if err != nil || baseURL.Scheme == "" || baseURL.Host == "" {
|
||||
return fmt.Sprintf("http://127.0.0.1%s/health", ResolveMCPListenAddr())
|
||||
}
|
||||
baseURL.Path = "/health"
|
||||
baseURL.RawQuery = ""
|
||||
baseURL.Fragment = ""
|
||||
return baseURL.String()
|
||||
}
|
||||
|
||||
func GetManagedStandaloneStatus(ctx context.Context) ManagedStandaloneStatus {
|
||||
reachable, reachErr := checkMCPHealth(ctx)
|
||||
meta, _ := readManagedProcessMeta()
|
||||
|
||||
if meta != nil && meta.PID > 0 && !processExists(meta.PID) {
|
||||
_ = removeManagedProcessMeta()
|
||||
meta = nil
|
||||
}
|
||||
|
||||
status := ManagedStandaloneStatus{
|
||||
Managed: meta != nil,
|
||||
Reachable: reachable,
|
||||
BaseURL: ResolveMCPServiceURL(),
|
||||
HealthURL: ResolveMCPHealthURL(),
|
||||
ListenAddr: ResolveMCPListenAddr(),
|
||||
Path: ResolveMCPPath(),
|
||||
AuthHeader: ConfiguredAuthHeader(),
|
||||
}
|
||||
|
||||
if meta != nil {
|
||||
status.PID = meta.PID
|
||||
status.LogPath = meta.LogPath
|
||||
status.StartedAt = meta.StartedAt
|
||||
}
|
||||
|
||||
switch {
|
||||
case meta != nil && reachable:
|
||||
status.State = "running"
|
||||
status.Message = "MCP 独立服务运行中"
|
||||
case meta != nil && processExists(meta.PID):
|
||||
status.State = "starting"
|
||||
status.Managed = true
|
||||
status.Starting = true
|
||||
status.Message = "MCP 独立服务启动中"
|
||||
case reachable:
|
||||
status.State = "external"
|
||||
status.Managed = false
|
||||
status.Message = "检测到 MCP 服务已运行,但不是由页面启动的托管进程"
|
||||
case meta != nil:
|
||||
status.State = "stopped"
|
||||
status.Managed = false
|
||||
status.Message = "上次托管的 MCP 进程已退出"
|
||||
default:
|
||||
status.State = "stopped"
|
||||
status.Message = "MCP 独立服务未启动"
|
||||
}
|
||||
|
||||
if !reachable && reachErr != nil && status.State != "stopped" {
|
||||
status.LastError = reachErr.Error()
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func StartManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
|
||||
current := GetManagedStandaloneStatus(ctx)
|
||||
if current.Reachable {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
meta, _ := readManagedProcessMeta()
|
||||
if meta != nil && meta.PID > 0 && processExists(meta.PID) {
|
||||
return waitForManagedProcess(ctx, meta)
|
||||
}
|
||||
|
||||
commandPath, commandArgs, workDir, configPath, err := resolveManagedStartCommand()
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
logPath := filepath.Join(runtimeDir, mcpRuntimeLogName)
|
||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
cmd := exec.Command(commandPath, commandArgs...)
|
||||
cmd.Dir = workDir
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.Env = append(os.Environ(), "GVA_MCP_CONFIG="+configPath)
|
||||
prepareDetachedProcess(cmd)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("启动 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
_ = cmd.Process.Release()
|
||||
|
||||
meta = &managedProcessMeta{
|
||||
PID: pid,
|
||||
StartedAt: time.Now().Format(time.RFC3339),
|
||||
LogPath: logPath,
|
||||
ConfigPath: configPath,
|
||||
WorkDir: workDir,
|
||||
Command: commandPath,
|
||||
Args: append([]string{}, commandArgs...),
|
||||
}
|
||||
if err := writeManagedProcessMeta(meta); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), err
|
||||
}
|
||||
|
||||
return waitForManagedProcess(ctx, meta)
|
||||
}
|
||||
|
||||
func StopManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) {
|
||||
meta, err := readManagedProcessMeta()
|
||||
if err != nil {
|
||||
status := GetManagedStandaloneStatus(ctx)
|
||||
if status.Reachable {
|
||||
return status, errors.New("当前 MCP 服务不是由页面启动的,无法自动停用")
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
if meta.PID > 0 && processExists(meta.PID) {
|
||||
if err := terminateProcess(meta.PID); err != nil {
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("停用 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
deadline := time.NewTimer(mcpStopWaitTimeout)
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
|
||||
case <-deadline.C:
|
||||
_ = removeManagedProcessMeta()
|
||||
return GetManagedStandaloneStatus(context.Background()), nil
|
||||
case <-ticker.C:
|
||||
if meta.PID <= 0 || !processExists(meta.PID) {
|
||||
_ = removeManagedProcessMeta()
|
||||
status := GetManagedStandaloneStatus(context.Background())
|
||||
if status.Reachable {
|
||||
status.State = "external"
|
||||
status.Message = "托管进程已停止,但检测到还有其他 MCP 服务在运行"
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkMCPHealth(ctx context.Context) (bool, error) {
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), mcpHealthCheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
if ctx != nil {
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeoutCtx, cancel = context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(timeoutCtx, http.MethodGet, ResolveMCPHealthURL(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("MCP 健康检查失败: %s", resp.Status)
|
||||
}
|
||||
|
||||
func waitForManagedProcess(ctx context.Context, meta *managedProcessMeta) (ManagedStandaloneStatus, error) {
|
||||
deadline := time.NewTimer(mcpStartWaitTimeout)
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer deadline.Stop()
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return GetManagedStandaloneStatus(context.Background()), ctx.Err()
|
||||
case <-deadline.C:
|
||||
return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("等待 MCP 独立服务启动超时,请查看日志: %s", meta.LogPath)
|
||||
case <-ticker.C:
|
||||
current := GetManagedStandaloneStatus(context.Background())
|
||||
if current.Reachable {
|
||||
return current, nil
|
||||
}
|
||||
if meta.PID > 0 && !processExists(meta.PID) {
|
||||
return current, fmt.Errorf("MCP 独立进程已退出,请查看日志: %s", meta.LogPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveManagedStartCommand() (string, []string, string, string, error) {
|
||||
serverRoot := resolveMCPServerRoot()
|
||||
if serverRoot == "" {
|
||||
return "", nil, "", "", errors.New("未找到 server 根目录,无法启动 MCP 独立服务")
|
||||
}
|
||||
|
||||
configPath, err := resolveMCPConfigPath(serverRoot)
|
||||
if err != nil {
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
if explicit := strings.TrimSpace(os.Getenv("GVA_MCP_BIN")); explicit != "" {
|
||||
if !fileExists(explicit) {
|
||||
return "", nil, "", "", fmt.Errorf("GVA_MCP_BIN 指向的文件不存在: %s", explicit)
|
||||
}
|
||||
return explicit, []string{"-config", configPath}, filepath.Dir(explicit), configPath, nil
|
||||
}
|
||||
|
||||
binaryPath, err := ensureManagedBinary(serverRoot)
|
||||
if err != nil {
|
||||
return "", nil, "", "", err
|
||||
}
|
||||
|
||||
return binaryPath, []string{"-config", configPath}, serverRoot, configPath, nil
|
||||
}
|
||||
|
||||
func ensureManagedBinary(serverRoot string) (string, error) {
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(runtimeDir, managedBinaryName())
|
||||
sourceDir := filepath.Join(serverRoot, "cmd", "mcp")
|
||||
|
||||
goBin, lookErr := exec.LookPath("go")
|
||||
if lookErr == nil && isDir(sourceDir) {
|
||||
buildCtx, cancel := context.WithTimeout(context.Background(), mcpBuildTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(buildCtx, goBin, "build", "-o", binaryPath, "./cmd/mcp")
|
||||
cmd.Dir = serverRoot
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
message := strings.TrimSpace(string(output))
|
||||
if message != "" {
|
||||
return "", fmt.Errorf("构建 MCP 独立服务失败: %w, 输出: %s", err, message)
|
||||
}
|
||||
return "", fmt.Errorf("构建 MCP 独立服务失败: %w", err)
|
||||
}
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
if fileExists(binaryPath) {
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
return "", errors.New("未检测到可用的 Go 环境,且本地没有可复用的 MCP 独立二进制")
|
||||
}
|
||||
|
||||
func resolveMCPServerRoot() string {
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
serverDir := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Server)
|
||||
if serverDir == "" {
|
||||
serverDir = "server"
|
||||
}
|
||||
|
||||
candidates := []string{}
|
||||
if root != "" {
|
||||
candidates = append(candidates, filepath.Join(root, filepath.FromSlash(serverDir)))
|
||||
candidates = append(candidates, root)
|
||||
}
|
||||
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
candidates = append(candidates, cwd)
|
||||
candidates = append(candidates, filepath.Join(cwd, "server"))
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if isDir(filepath.Join(candidate, "cmd", "mcp")) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) > 0 {
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveMCPConfigPath(serverRoot string) (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join(serverRoot, "cmd", "mcp", "config.yaml"),
|
||||
filepath.Join(serverRoot, "config.yaml"),
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if fileExists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("未找到 MCP 配置文件,请确认 cmd/mcp/config.yaml 或 server/config.yaml 存在")
|
||||
}
|
||||
|
||||
func ensureMCPRuntimeDir() (string, error) {
|
||||
runtimeDir := filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir)
|
||||
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return runtimeDir, nil
|
||||
}
|
||||
|
||||
func resolveMCPProjectRoot() string {
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
if root != "" {
|
||||
return root
|
||||
}
|
||||
|
||||
serverRoot := resolveMCPServerRoot()
|
||||
if serverRoot == "" {
|
||||
return "."
|
||||
}
|
||||
|
||||
if fileExists(filepath.Join(serverRoot, "go.mod")) {
|
||||
return filepath.Dir(serverRoot)
|
||||
}
|
||||
|
||||
return serverRoot
|
||||
}
|
||||
|
||||
func readManagedProcessMeta() (*managedProcessMeta, error) {
|
||||
data, err := os.ReadFile(managedProcessMetaPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var meta managedProcessMeta
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func writeManagedProcessMeta(meta *managedProcessMeta) error {
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(managedProcessMetaPath(), data, 0o644)
|
||||
}
|
||||
|
||||
func removeManagedProcessMeta() error {
|
||||
err := os.Remove(managedProcessMetaPath())
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func managedProcessMetaPath() string {
|
||||
runtimeDir, err := ensureMCPRuntimeDir()
|
||||
if err != nil {
|
||||
return filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir, mcpRuntimeMetaName)
|
||||
}
|
||||
return filepath.Join(runtimeDir, mcpRuntimeMetaName)
|
||||
}
|
||||
|
||||
func managedBinaryName() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "gva-mcp-standalone.exe"
|
||||
}
|
||||
return "gva-mcp-standalone"
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CasbinHandler 拦截器
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -9,6 +11,8 @@ import (
|
||||
"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/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -42,12 +46,27 @@ func GinRecovery(stack bool) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
if stack {
|
||||
form := "后端"
|
||||
info := fmt.Sprintf("Panic: %v\nRequest: %s\nStack: %s", err, string(httpRequest), string(debug.Stack()))
|
||||
level := "error"
|
||||
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{
|
||||
Form: &form,
|
||||
Info: &info,
|
||||
Level: level,
|
||||
})
|
||||
global.GVA_LOG.Error("[Recovery from panic]",
|
||||
zap.Any("error", err),
|
||||
zap.String("request", string(httpRequest)),
|
||||
zap.String("stack", string(debug.Stack())),
|
||||
)
|
||||
} else {
|
||||
form := "后端"
|
||||
info := fmt.Sprintf("Panic: %v\nRequest: %s", err, string(httpRequest))
|
||||
level := "error"
|
||||
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{
|
||||
Form: &form,
|
||||
Info: &info,
|
||||
Level: level,
|
||||
})
|
||||
global.GVA_LOG.Error("[Recovery from panic]",
|
||||
zap.Any("error", err),
|
||||
zap.String("request", string(httpRequest)),
|
||||
|
||||
@@ -2,11 +2,12 @@ package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -59,7 +60,7 @@ func JWTAuth() gin.HandlerFunc {
|
||||
newClaims, _ := j.ParseToken(newToken)
|
||||
c.Header("new-token", newToken)
|
||||
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
|
||||
utils.SetToken(c, newToken, int(dr.Seconds()))
|
||||
utils.SetToken(c, newToken, int(dr.Seconds()/60))
|
||||
if global.GVA_CONFIG.System.UseMultipoint {
|
||||
// 记录新的活跃jwt
|
||||
_ = utils.SetRedisJWT(newToken, newClaims.Username)
|
||||
|
||||
@@ -18,7 +18,6 @@ const (
|
||||
)
|
||||
|
||||
func Result(code int, data interface{}, msg string, c *gin.Context) {
|
||||
// 开始时间
|
||||
c.JSON(http.StatusOK, Response{
|
||||
code,
|
||||
data,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
common "github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
system "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
)
|
||||
|
||||
type SysAIWorkflowSessionUpsert struct {
|
||||
ID uint `json:"id"`
|
||||
Tab string `json:"tab"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
MessageID string `json:"messageId"`
|
||||
CurrentNodeID string `json:"currentNodeId"`
|
||||
Settings common.JSONMap `json:"settings"`
|
||||
FormData common.JSONMap `json:"formData"`
|
||||
ResultData common.JSONMap `json:"resultData"`
|
||||
Messages []system.AIWorkflowMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type SysAIWorkflowSessionSearch struct {
|
||||
commonReq.PageInfo
|
||||
Tab string `json:"tab" form:"tab"`
|
||||
}
|
||||
|
||||
type SysAIWorkflowMarkdownDump struct {
|
||||
ID uint `json:"id"`
|
||||
Tab string `json:"tab"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
MessageID string `json:"messageId"`
|
||||
CurrentNodeID string `json:"currentNodeId"`
|
||||
Settings common.JSONMap `json:"settings"`
|
||||
FormData common.JSONMap `json:"formData"`
|
||||
ResultData common.JSONMap `json:"resultData"`
|
||||
Messages []system.AIWorkflowMessage `json:"messages"`
|
||||
}
|
||||
@@ -12,3 +12,10 @@ type SearchApiParams struct {
|
||||
OrderKey string `json:"orderKey"` // 排序
|
||||
Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true
|
||||
}
|
||||
|
||||
// SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表
|
||||
type SetApiAuthorities struct {
|
||||
Path string `json:"path" form:"path"` // API路径
|
||||
Method string `json:"method" form:"method"` // 请求方法
|
||||
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
)
|
||||
|
||||
type SysApiTokenSearch struct {
|
||||
system.SysApiToken
|
||||
request.PageInfo
|
||||
Status *bool `json:"status" form:"status"`
|
||||
}
|
||||
@@ -280,6 +280,11 @@ type InitApi struct {
|
||||
APIs []uint `json:"apis"`
|
||||
}
|
||||
|
||||
type InitDictionary struct {
|
||||
PlugName string `json:"plugName"`
|
||||
Dictionaries []uint `json:"dictionaries"`
|
||||
}
|
||||
|
||||
type LLMAutoCode struct {
|
||||
Prompt string `json:"prompt" form:"prompt" gorm:"column:prompt;comment:提示语;type:text;"` //提示语
|
||||
Mode string `json:"mode" form:"mode" gorm:"column:mode;comment:模式;type:text;"` //模式
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package request
|
||||
|
||||
type SysDictionarySearch struct {
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
}
|
||||
|
||||
type ImportSysDictionaryRequest struct {
|
||||
Json string `json:"json" binding:"required"` // JSON字符串
|
||||
}
|
||||
@@ -8,4 +8,36 @@ import (
|
||||
type SysDictionaryDetailSearch struct {
|
||||
system.SysDictionaryDetail
|
||||
request.PageInfo
|
||||
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,用于查询指定父级下的子项
|
||||
Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据
|
||||
}
|
||||
|
||||
// CreateSysDictionaryDetailRequest 创建字典详情请求
|
||||
type CreateSysDictionaryDetailRequest struct {
|
||||
Label string `json:"label" form:"label" binding:"required"` // 展示值
|
||||
Value string `json:"value" form:"value" binding:"required"` // 字典值
|
||||
Extend string `json:"extend" form:"extend"` // 扩展值
|
||||
Status *bool `json:"status" form:"status"` // 启用状态
|
||||
Sort int `json:"sort" form:"sort"` // 排序标记
|
||||
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
|
||||
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
|
||||
}
|
||||
|
||||
// UpdateSysDictionaryDetailRequest 更新字典详情请求
|
||||
type UpdateSysDictionaryDetailRequest struct {
|
||||
ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID
|
||||
Label string `json:"label" form:"label" binding:"required"` // 展示值
|
||||
Value string `json:"value" form:"value" binding:"required"` // 字典值
|
||||
Extend string `json:"extend" form:"extend"` // 扩展值
|
||||
Status *bool `json:"status" form:"status"` // 启用状态
|
||||
Sort int `json:"sort" form:"sort"` // 排序标记
|
||||
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记
|
||||
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID
|
||||
}
|
||||
|
||||
// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求
|
||||
type GetDictionaryDetailsByParentRequest struct {
|
||||
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID
|
||||
ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,为空时获取顶级
|
||||
IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SysErrorSearch struct{
|
||||
CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"`
|
||||
Form *string `json:"form" form:"form"`
|
||||
Info *string `json:"info" form:"info"`
|
||||
request.PageInfo
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
)
|
||||
|
||||
type SysLoginLogSearch struct {
|
||||
system.SysLoginLog
|
||||
request.PageInfo
|
||||
}
|
||||
@@ -11,6 +11,12 @@ type AddMenuAuthorityInfo struct {
|
||||
AuthorityId uint `json:"authorityId"` // 角色ID
|
||||
}
|
||||
|
||||
// SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表
|
||||
type SetMenuAuthorities struct {
|
||||
MenuId uint `json:"menuId" form:"menuId"` // 菜单ID
|
||||
AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表
|
||||
}
|
||||
|
||||
func DefaultMenu() []system.SysBaseMenu {
|
||||
return []system.SysBaseMenu{{
|
||||
GVA_MODEL: global.GVA_MODEL{ID: 1},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package request
|
||||
|
||||
import "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
|
||||
type SkillToolRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
}
|
||||
|
||||
type SkillDetailRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillDeleteRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillPackageRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
}
|
||||
|
||||
type SkillSaveRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
Meta system.SkillMeta `json:"meta"`
|
||||
Markdown string `json:"markdown"`
|
||||
SyncTools []string `json:"syncTools"`
|
||||
}
|
||||
|
||||
type SkillScriptCreateRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
ScriptType string `json:"scriptType"`
|
||||
}
|
||||
|
||||
type SkillResourceCreateRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type SkillReferenceCreateRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type SkillTemplateCreateRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type SkillFileRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
type SkillFileSaveRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
FileName string `json:"fileName"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type SkillGlobalConstraintSaveRequest struct {
|
||||
Tool string `json:"tool"`
|
||||
Content string `json:"content"`
|
||||
SyncTools []string `json:"syncTools"`
|
||||
}
|
||||
|
||||
type DownloadOnlineSkillReq struct {
|
||||
Tool string `json:"tool" binding:"required"`
|
||||
ID uint `json:"id" binding:"required"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
}
|
||||
@@ -66,4 +66,12 @@ type GetUserList struct {
|
||||
NickName string `json:"nickName" form:"nickName"`
|
||||
Phone string `json:"phone" form:"phone"`
|
||||
Email string `json:"email" form:"email"`
|
||||
OrderKey string `json:"orderKey" form:"orderKey"` // 排序
|
||||
Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true
|
||||
}
|
||||
|
||||
// SetRoleUsers 通过角色ID全量覆盖关联用户列表
|
||||
type SetRoleUsers struct {
|
||||
AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID
|
||||
UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type SysAIWorkflowSessionListItem struct {
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
Tab string `json:"tab"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
CurrentNodeID string `json:"currentNodeId"`
|
||||
}
|
||||
|
||||
type AIWorkflowMarkdownDumpResult struct {
|
||||
FileName string `json:"fileName"`
|
||||
FilePath string `json:"filePath"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
Directory string `json:"directory"`
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package response
|
||||
|
||||
import "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
|
||||
type Db struct {
|
||||
Database string `json:"database" gorm:"column:database"`
|
||||
}
|
||||
@@ -15,3 +17,11 @@ type Column struct {
|
||||
ColumnComment string `json:"columnComment" gorm:"column:column_comment"`
|
||||
PrimaryKey bool `json:"primaryKey" gorm:"column:primary_key"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
PluginName string `json:"pluginName"`
|
||||
PluginType string `json:"pluginType"` // web, server, full
|
||||
Apis []system.SysApi `json:"apis"`
|
||||
Menus []system.SysBaseMenu `json:"menus"`
|
||||
Dictionaries []system.SysDictionary `json:"dictionaries"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
common "github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||
)
|
||||
|
||||
type AIWorkflowMessage struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Snapshot common.JSONMap `json:"snapshot"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
MessageID string `json:"messageId"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SysAIWorkflowSession struct {
|
||||
global.GVA_MODEL
|
||||
UserID uint `json:"userId" gorm:"column:user_id;index;comment:用户ID"`
|
||||
Tab string `json:"tab" gorm:"column:tab;size:32;index;comment:会话类型"`
|
||||
Title string `json:"title" gorm:"column:title;size:255;comment:会话标题"`
|
||||
Summary string `json:"summary" gorm:"column:summary;type:text;comment:摘要"`
|
||||
ConversationID string `json:"conversationId" gorm:"column:conversation_id;size:255;comment:Dify会话ID"`
|
||||
MessageID string `json:"messageId" gorm:"column:message_id;size:255;comment:Dify消息ID"`
|
||||
CurrentNodeID string `json:"currentNodeId" gorm:"column:current_node_id;size:64;comment:当前选中节点ID"`
|
||||
Settings common.JSONMap `json:"settings" gorm:"column:settings;type:longtext;comment:页面设置"`
|
||||
FormData common.JSONMap `json:"formData" gorm:"column:form_data;type:longtext;comment:表单数据"`
|
||||
ResultData common.JSONMap `json:"resultData" gorm:"column:result_data;type:longtext;comment:当前展示结果"`
|
||||
Messages []AIWorkflowMessage `json:"messages" gorm:"column:messages;serializer:json;type:longtext;comment:会话消息"`
|
||||
}
|
||||
|
||||
func (s *SysAIWorkflowSession) TableName() string {
|
||||
return "sys_ai_workflow_sessions"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SysApiToken struct {
|
||||
global.GVA_MODEL
|
||||
UserID uint `json:"userId" gorm:"comment:用户ID"`
|
||||
User SysUser `json:"user" gorm:"foreignKey:UserID;"`
|
||||
AuthorityID uint `json:"authorityId" gorm:"comment:角色ID"`
|
||||
Token string `json:"token" gorm:"type:text;comment:Token"`
|
||||
Status bool `json:"status" gorm:"default:true;comment:状态"` // true有效 false无效
|
||||
ExpiresAt time.Time `json:"expiresAt" gorm:"comment:过期时间"`
|
||||
Remark string `json:"remark" gorm:"comment:备注"`
|
||||
}
|
||||
@@ -13,7 +13,7 @@ type SysBaseMenu struct {
|
||||
Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏
|
||||
Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径
|
||||
Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记
|
||||
Meta `json:"meta" gorm:"embedded;comment:附加属性"` // 附加属性
|
||||
Meta `json:"meta" gorm:"embedded"` // 附加属性
|
||||
SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"`
|
||||
Children []SysBaseMenu `json:"children" gorm:"-"`
|
||||
Parameters []SysBaseMenuParameter `json:"parameters"`
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
// 如果含有time.Time 请自行import time包
|
||||
type SysDictionary struct {
|
||||
global.GVA_MODEL
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英)
|
||||
Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态
|
||||
Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英)
|
||||
Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态
|
||||
Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述
|
||||
ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID
|
||||
Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典
|
||||
SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,17 @@ import (
|
||||
// 如果含有time.Time 请自行import time包
|
||||
type SysDictionaryDetail struct {
|
||||
global.GVA_MODEL
|
||||
Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值
|
||||
Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值
|
||||
Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值
|
||||
Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态
|
||||
Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记
|
||||
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记
|
||||
Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值
|
||||
Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值
|
||||
Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值
|
||||
Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态
|
||||
Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记
|
||||
SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记
|
||||
ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID
|
||||
Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情
|
||||
Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度,从0开始
|
||||
Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3"
|
||||
Disabled bool `json:"disabled" gorm:"-"` // 禁用状态,根据status字段动态计算
|
||||
}
|
||||
|
||||
func (SysDictionaryDetail) TableName() string {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 自动生成模板SysError
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
// 错误日志 结构体 SysError
|
||||
type SysError struct {
|
||||
global.GVA_MODEL
|
||||
Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源
|
||||
Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容
|
||||
Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"`
|
||||
Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案
|
||||
Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成
|
||||
}
|
||||
|
||||
// TableName 错误日志 SysError自定义表名 sys_error
|
||||
func (SysError) TableName() string {
|
||||
return "sys_error"
|
||||
}
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
// 导出模板 结构体 SysExportTemplate
|
||||
type SysExportTemplate struct {
|
||||
global.GVA_MODEL
|
||||
DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称
|
||||
TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称
|
||||
TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识
|
||||
TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息
|
||||
DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称
|
||||
TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称
|
||||
TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识
|
||||
TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息
|
||||
SQL string `json:"sql" form:"sql" gorm:"column:sql;type:text;comment:自定义导出SQL;"` //自定义导出SQL
|
||||
ImportSQL string `json:"importSql" form:"importSql" gorm:"column:import_sql;type:text;comment:自定义导入SQL;"` //自定义导入SQL
|
||||
Limit *int `json:"limit" form:"limit" gorm:"column:limit;comment:导出限制"`
|
||||
Order string `json:"order" form:"order" gorm:"column:order;comment:排序"`
|
||||
Conditions []Condition `json:"conditions" form:"conditions" gorm:"foreignKey:TemplateID;references:TemplateID;comment:条件"`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
)
|
||||
|
||||
type SysLoginLog struct {
|
||||
global.GVA_MODEL
|
||||
Username string `json:"username" gorm:"column:username;comment:用户名"`
|
||||
Ip string `json:"ip" gorm:"column:ip;comment:请求ip"`
|
||||
Status bool `json:"status" gorm:"column:status;comment:登录状态"`
|
||||
ErrorMessage string `json:"errorMessage" gorm:"column:error_message;comment:错误信息"`
|
||||
Agent string `json:"agent" gorm:"column:agent;comment:代理"`
|
||||
UserID uint `json:"userId" gorm:"column:user_id;comment:用户id"`
|
||||
User SysUser `json:"user" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package system
|
||||
|
||||
type SkillMeta struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
AllowedTools string `json:"allowedTools" yaml:"allowed-tools,omitempty"`
|
||||
Context string `json:"context" yaml:"context,omitempty"`
|
||||
Agent string `json:"agent" yaml:"agent,omitempty"`
|
||||
}
|
||||
|
||||
type SkillDetail struct {
|
||||
Tool string `json:"tool"`
|
||||
Skill string `json:"skill"`
|
||||
Meta SkillMeta `json:"meta"`
|
||||
Markdown string `json:"markdown"`
|
||||
Scripts []string `json:"scripts"`
|
||||
Resources []string `json:"resources"`
|
||||
References []string `json:"references"`
|
||||
Templates []string `json:"templates"`
|
||||
}
|
||||
|
||||
type SkillTool struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
@@ -11,7 +11,7 @@ type SysVersion struct {
|
||||
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
|
||||
VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
|
||||
Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
|
||||
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:longtext;postgres:type:text;sqlite:type:text;"` //版本数据
|
||||
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
|
||||
}
|
||||
|
||||
// TableName 版本管理 SysVersion自定义表名 sys_versions
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
|
||||
)
|
||||
|
||||
func Dictionary(ctx context.Context) {
|
||||
entities := []model.SysDictionary{}
|
||||
utils.RegisterDictionaries(entities...)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user