mirror of
https://github.com/flipped-aurora/gin-vue-admin.git
synced 2026-09-25 05:50:19 +00:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e272a9f8c | ||
|
|
7602673ed3 | ||
|
|
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 | ||
|
|
32f21d8555 |
@@ -0,0 +1,773 @@
|
||||
### 功能描述以及必要性描述
|
||||
|
||||
---
|
||||
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` 和 `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组件规范
|
||||
|
||||
#### **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. **性能要求**:
|
||||
- **必须**使用懒加载优化路由
|
||||
- **必须**对大列表进行虚拟滚动优化
|
||||
- **必须**合理使用缓存机制
|
||||
- **必须**优化图片和资源加载
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
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. **重视安全性**:实现完善的权限控制和数据验证机制
|
||||
@@ -286,6 +286,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +304,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):
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
### 功能描述以及必要性描述
|
||||
|
||||
---
|
||||
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` 和 `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组件规范
|
||||
|
||||
#### **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. **性能要求**:
|
||||
- **必须**使用懒加载优化路由
|
||||
- **必须**对大列表进行虚拟滚动优化
|
||||
- **必须**合理使用缓存机制
|
||||
- **必须**优化图片和资源加载
|
||||
|
||||
---
|
||||
|
||||
## **前后端协作规范**
|
||||
|
||||
### **接口协作规范**
|
||||
|
||||
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. **重视安全性**:实现完善的权限控制和数据验证机制
|
||||
@@ -189,23 +189,23 @@ 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` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
|
||||
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
|
||||
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
|
||||
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
|
||||
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
|
||||
- **⚠️ 指针类型处理**:
|
||||
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
|
||||
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
|
||||
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
|
||||
|
||||
- **请求模型 (`model/request/xxx.go`)**:
|
||||
|
||||
@@ -225,9 +225,9 @@ web/
|
||||
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
|
||||
|
||||
- **⚠️ 数据类型处理注意事项**:
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
|
||||
|
||||
#### **3. API层 (`api/`)**
|
||||
@@ -286,6 +286,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +304,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):
|
||||
@@ -620,16 +632,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. **错误处理**:
|
||||
- 后端**必须**返回标准化的错误码和错误信息
|
||||
|
||||
@@ -189,23 +189,23 @@ 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` 和 `gorm` 标签。
|
||||
|
||||
- **⚠️ 重要提醒:数据类型一致性**
|
||||
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
|
||||
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
|
||||
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
|
||||
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
|
||||
- **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
|
||||
- **⚠️ 指针类型处理**:
|
||||
- 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
|
||||
- **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址
|
||||
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
|
||||
|
||||
- **请求模型 (`model/request/xxx.go`)**:
|
||||
|
||||
@@ -225,9 +225,9 @@ web/
|
||||
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。
|
||||
|
||||
- **⚠️ 数据类型处理注意事项**:
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
- 在进行数据模型转换时,**必须确保**字段类型的一致性
|
||||
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
|
||||
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
|
||||
|
||||
|
||||
#### **3. API层 (`api/`)**
|
||||
@@ -286,6 +286,13 @@ web/
|
||||
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
|
||||
|
||||
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
|
||||
|
||||
- **插件注册**: **必须**调用 ```
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
```
|
||||
方法,让插件自动注册到本体中
|
||||
|
||||
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
|
||||
|
||||
@@ -297,6 +304,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):
|
||||
@@ -620,16 +632,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
|
||||
````
|
||||
|
||||
|
||||
@@ -61,7 +61,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 +381,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)
|
||||
未授权去除版权信息将依法追究法律责任
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,6 +23,10 @@ type ApiGroup struct {
|
||||
AutoCodeTemplateApi
|
||||
SysParamsApi
|
||||
SysVersionApi
|
||||
SysErrorApi
|
||||
LoginLogApi
|
||||
ApiTokenApi
|
||||
SkillsApi
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -46,4 +50,8 @@ var (
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
"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/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -108,48 +103,15 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
|
||||
|
||||
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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
var resStruct response.Response
|
||||
b, err := io.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(b, &resStruct)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if resStruct.Code == 7 {
|
||||
global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err))
|
||||
response.FailWithMessage("大模型生成失败"+resStruct.Msg, c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(resStruct.Data, c)
|
||||
response.OkWithData(data, c)
|
||||
}
|
||||
|
||||
@@ -135,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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
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/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) 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ system:
|
||||
router-prefix: ""
|
||||
# 严格角色模式 打开后权限将会存在上下级关系
|
||||
use-strict-auth: false
|
||||
# 自动迁移数据库表结构,生产环境建议设为false,手动迁移
|
||||
# 禁用自动迁移数据库表结构,生产环境建议设为true,手动迁移
|
||||
disable-auto-migrate: false
|
||||
|
||||
# captcha configuration
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -47,7 +47,7 @@ func RunServer() {
|
||||
--------------------------------------版权声明--------------------------------------
|
||||
** 版权所有方:flipped-aurora开源团队 **
|
||||
** 版权持有公司:北京翻转极光科技有限责任公司 **
|
||||
** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html **
|
||||
** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license **
|
||||
** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展**
|
||||
`, global.Version, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
|
||||
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,7 +4,7 @@ package global
|
||||
// 目前只有Version正式使用 其余为预留
|
||||
const (
|
||||
// Version 当前版本号
|
||||
Version = "v2.8.6"
|
||||
Version = "v2.8.9"
|
||||
// AppName 应用名称
|
||||
AppName = "Gin-Vue-Admin"
|
||||
// Description 应用描述
|
||||
|
||||
+9
-9
@@ -1,8 +1,8 @@
|
||||
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
|
||||
@@ -43,9 +43,9 @@ 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
|
||||
gorm.io/datatypes v1.2.5
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
@@ -111,7 +111,7 @@ require (
|
||||
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
|
||||
@@ -172,11 +172,11 @@ 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
|
||||
|
||||
+14
-12
@@ -288,8 +288,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=
|
||||
@@ -541,8 +541,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=
|
||||
@@ -607,8 +607,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=
|
||||
@@ -626,8 +626,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=
|
||||
@@ -661,8 +662,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=
|
||||
@@ -696,8 +697,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=
|
||||
@@ -768,8 +770,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=
|
||||
|
||||
@@ -54,6 +54,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{},
|
||||
|
||||
@@ -62,6 +62,9 @@ func RegisterTables() {
|
||||
system.JoinTemplate{},
|
||||
system.SysParams{},
|
||||
system.SysVersion{},
|
||||
system.SysError{},
|
||||
system.SysApiToken{},
|
||||
system.SysLoginLog{},
|
||||
|
||||
example.ExaFile{},
|
||||
example.ExaCustomer{},
|
||||
|
||||
@@ -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()...)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ 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())
|
||||
}
|
||||
@@ -107,6 +108,10 @@ func Routers() *gin.Engine {
|
||||
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) // 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.6
|
||||
// @version v2.8.9
|
||||
// @description 使用gin+vue进行极速开发的全栈开发基础平台
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
|
||||
@@ -52,6 +52,9 @@ func (a *ApiLister) New() mcp.Tool {
|
||||
**返回数据结构:**
|
||||
- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息)
|
||||
- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`),
|
||||
mcp.WithString("_placeholder",
|
||||
mcp.Description("占位符,防止json schema校验失败"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,93 +70,6 @@ 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) {
|
||||
// 解析请求参数
|
||||
|
||||
@@ -282,20 +282,38 @@ func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool,
|
||||
} else if err != nil {
|
||||
return false, err // 其他错误
|
||||
}
|
||||
// 递归检查是否有.go文件
|
||||
return g.hasGoFilesRecursive(basePath)
|
||||
}
|
||||
|
||||
// 读取文件夹内容
|
||||
entries, err := os.ReadDir(basePath)
|
||||
// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件
|
||||
func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return true, err // 读取失败,返回空
|
||||
}
|
||||
|
||||
// 检查是否有.go文件
|
||||
// 检查当前目录下的.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文件,为空
|
||||
}
|
||||
|
||||
@@ -331,7 +349,16 @@ func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error {
|
||||
return err // 其他错误
|
||||
}
|
||||
|
||||
return os.RemoveAll(dirPath)
|
||||
// 检查目录中是否包含go文件
|
||||
noGoFiles, err := g.hasGoFilesRecursive(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// hasGoFilesRecursive 返回 false 表示发现了 go 文件
|
||||
if noGoFiles {
|
||||
return os.RemoveAll(dirPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupRelatedApiAndMenus 清理相关的API和菜单记录
|
||||
|
||||
+149
-135
@@ -57,147 +57,161 @@ type ExecutionPlan struct {
|
||||
|
||||
// New 创建GVA代码生成执行器工具
|
||||
func (g *GVAExecutor) New() mcp.Tool {
|
||||
return mcp.NewTool("gva_execute",
|
||||
return mcp.NewTool("gva_execute",
|
||||
mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤**
|
||||
|
||||
**核心功能:**
|
||||
- 根据需求分析和当前的包信息判断是否调用,如果需要调用,则根据入参描述生成json,用于直接生成代码
|
||||
- 支持批量创建多个模块
|
||||
- 自动创建包、模块、字典等
|
||||
- 移除了确认步骤,提高执行效率
|
||||
根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。
|
||||
|
||||
**使用场景:**
|
||||
- 在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp
|
||||
- 根据分析结果直接生成代码
|
||||
- 适用于自动化代码生成流程
|
||||
在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。
|
||||
|
||||
**批量创建功能:**
|
||||
- 支持在单个ExecutionPlan中创建多个模块
|
||||
- modulesInfo字段为数组,可包含多个模块配置
|
||||
- 一次性处理多个模块的创建和字典生成
|
||||
|
||||
**新功能:自动字典创建**
|
||||
- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在
|
||||
- 如果字典不存在,会自动创建对应的字典及默认的字典详情项
|
||||
- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等)
|
||||
|
||||
**重要限制:**
|
||||
- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具
|
||||
- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具
|
||||
|
||||
重要:ExecutionPlan结构体格式要求(支持批量创建):
|
||||
{
|
||||
"packageName": "包名(string)",
|
||||
"packageType": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。",
|
||||
"needCreatedPackage": "是否需要创建包(bool)",
|
||||
"needCreatedModules": "是否需要创建模块(bool)",
|
||||
"needCreatedDictionaries": "是否需要创建字典(bool)",
|
||||
"packageInfo": {
|
||||
"desc": "描述(string)",
|
||||
"label": "展示名(string)",
|
||||
"template": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。",
|
||||
"packageName": "包名(string)"
|
||||
},
|
||||
"modulesInfo": [{
|
||||
"package": "包名(string,必然是小写开头)",
|
||||
"tableName": "数据库表名(string,使用蛇形命名法)",
|
||||
"businessDB": "业务数据库(string)",
|
||||
"structName": "结构体名(string)",
|
||||
"packageName": "文件名称(string)",
|
||||
"description": "中文描述(string)",
|
||||
"abbreviation": "简称(string)",
|
||||
"humpPackageName": "文件名称 一般是结构体名的小驼峰(string)",
|
||||
"gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at",
|
||||
"autoMigrate": "是否自动迁移(bool)",
|
||||
"autoCreateResource": "是否创建资源(bool,默认为false)",
|
||||
"autoCreateApiToSql": "是否创建API(bool,默认为true)",
|
||||
"autoCreateMenuToSql": "是否创建菜单(bool,默认为true)",
|
||||
"autoCreateBtnAuth": "是否创建按钮权限(bool,默认为false)",
|
||||
"onlyTemplate": "是否仅模板(bool,默认为false)",
|
||||
"isTree": "是否树形结构(bool,默认为false)",
|
||||
"treeJson": "树形JSON字段(string)",
|
||||
"isAdd": "是否新增(bool) 固定为false",
|
||||
"generateWeb": "是否生成前端(bool)",
|
||||
"generateServer": "是否生成后端(bool)",
|
||||
"fields": [{
|
||||
"fieldName": "字段名(string)必须大写开头",
|
||||
"fieldDesc": "字段描述(string)",
|
||||
"fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)",
|
||||
"fieldJson": "JSON标签(string)",
|
||||
"dataTypeLong": "数据长度(string)",
|
||||
"comment": "注释(string)",
|
||||
"columnName": "数据库列名(string)",
|
||||
"fieldSearchType": "搜索类型:=/>/</>=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)",
|
||||
"fieldSearchHide": "是否隐藏搜索(bool)",
|
||||
"dictType": "字典类型(string)",
|
||||
"form": "表单显示(bool)",
|
||||
"table": "表格显示(bool)",
|
||||
"desc": "详情显示(bool)",
|
||||
"excel": "导入导出(bool)",
|
||||
"require": "是否必填(bool)",
|
||||
"defaultValue": "默认值(string)",
|
||||
"errorText": "错误提示(string)",
|
||||
"clearable": "是否可清空(bool)",
|
||||
"sort": "是否排序(bool)",
|
||||
"primaryKey": "是否主键(bool)",
|
||||
"dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空",
|
||||
"checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性",
|
||||
"fieldIndexType": "索引类型(string)"
|
||||
}]
|
||||
}, {
|
||||
"package": "包名(string)",
|
||||
"tableName": "第二个模块的表名(string)",
|
||||
"structName": "第二个模块的结构体名(string)",
|
||||
"description": "第二个模块的描述(string)",
|
||||
"...": "更多模块配置..."
|
||||
}],
|
||||
"dictionariesInfo":[{
|
||||
"dictType": "字典类型(string) - 用于标识字典的唯一性",
|
||||
"dictName": "字典名称(string) - 必须生成,字典的中文名称",
|
||||
"description": "字典描述(string) - 字典的用途说明",
|
||||
"status": "字典状态(bool) - true启用,false禁用",
|
||||
"fieldDesc": "字段描述(string) - 用于AI理解字段含义并生成合适的选项",
|
||||
"options": [{
|
||||
"label": "显示名称(string) - 用户看到的选项名",
|
||||
"value": "选项值(string) - 实际存储的值",
|
||||
"sort": "排序号(int) - 数字越小越靠前"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
注意:
|
||||
1. needCreatedPackage=true时packageInfo必需
|
||||
2. needCreatedModules=true时modulesInfo必需
|
||||
3. needCreatedDictionaries=true时dictionariesInfo必需
|
||||
4. dictionariesInfo中的options字段可选,如果不提供将根据fieldDesc自动生成默认选项
|
||||
5. 字典创建会在模块创建之前执行,确保模块字段可以正确引用字典类型
|
||||
6. packageType只能是"package"或"plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。"
|
||||
7. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)
|
||||
8. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN
|
||||
9. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段
|
||||
10. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误
|
||||
11. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true
|
||||
12. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会:
|
||||
- 自动检查字典是否存在,如果不存在则创建字典
|
||||
- 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景
|
||||
- 为无法识别的字典类型提供通用默认选项
|
||||
13. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段:
|
||||
- **dbName**: 关联的数据库名称
|
||||
- **table**: 关联的表名
|
||||
- **label**: 用于显示的字段名(如name、title等)
|
||||
- **value**: 用于存储的值字段名(通常是id)
|
||||
- **association**: 关联关系类型(1=一对一关联,2=一对多关联)一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个,则选用一对一,如果他需要关联多个他的关联实体,则选用一对多。
|
||||
- **hasDeletedAt**: 关联表是否有软删除字段
|
||||
- **checkDataSource**: 设为true时会验证关联表的存在性
|
||||
- 示例:{"dbName":"","table":"sys_users","label":"username","value":"id","association":1,"hasDeletedAt":true}
|
||||
14. **自动字段类型修正**:系统会自动检查和修正字段类型:
|
||||
- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'
|
||||
- 这确保了一对多关联数据的正确存储和处理
|
||||
- 修正操作会记录在日志中,便于开发者了解变更情况`),
|
||||
mcp.WithObject("executionPlan",
|
||||
mcp.Description("执行计划,包含包信息和模块信息"),
|
||||
mcp.Required(),
|
||||
),
|
||||
**重要提示:**
|
||||
- 当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("原始需求描述(可选,用于日志记录)"),
|
||||
),
|
||||
|
||||
@@ -46,6 +46,9 @@ func (m *MenuLister) New() mcp.Tool {
|
||||
- 菜单权限管理:了解系统中所有可用的菜单项
|
||||
- 导航组件开发:构建动态导航菜单
|
||||
- 系统架构分析:了解系统的菜单结构和页面组织`),
|
||||
mcp.WithString("_placeholder",
|
||||
mcp.Description("占位符,防止json schema校验失败"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;"` //模式
|
||||
|
||||
@@ -3,3 +3,7 @@ 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字符串
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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 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"`
|
||||
}
|
||||
@@ -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,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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func Menu(ctx context.Context) {
|
||||
entities := []model.SysBaseMenu{
|
||||
{
|
||||
ParentId: 24,
|
||||
ParentId: 9,
|
||||
Path: "anInfo",
|
||||
Name: "anInfo",
|
||||
Hidden: false,
|
||||
|
||||
@@ -13,14 +13,20 @@ var Plugin = new(plugin)
|
||||
|
||||
type plugin struct{}
|
||||
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
|
||||
func (p *plugin) Register(group *gin.Engine) {
|
||||
ctx := context.Background()
|
||||
// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key
|
||||
// initialize.Viper()
|
||||
// 安装插件时候自动注册的api数据请到下方法.Api方法中实现
|
||||
initialize.Api(ctx)
|
||||
// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现
|
||||
// 安装插件时候自动注册的Menu数据请到下方法.Menu方法中实现
|
||||
initialize.Menu(ctx)
|
||||
// 安装插件时候自动注册的Dictionary数据请到下方法.Dictionary方法中实现
|
||||
initialize.Dictionary(ctx)
|
||||
initialize.Gorm(ctx)
|
||||
initialize.Router(group)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,138 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
)
|
||||
|
||||
var (
|
||||
ApiMap = make(map[string][]system.SysApi)
|
||||
MenuMap = make(map[string][]system.SysBaseMenu)
|
||||
DictMap = make(map[string][]system.SysDictionary)
|
||||
rw sync.Mutex
|
||||
)
|
||||
|
||||
func getPluginName() string {
|
||||
_, file, _, ok := runtime.Caller(2)
|
||||
pluginName := ""
|
||||
if ok {
|
||||
file = filepath.ToSlash(file)
|
||||
const key = "server/plugin/"
|
||||
if idx := strings.Index(file, key); idx != -1 {
|
||||
remain := file[idx+len(key):]
|
||||
parts := strings.Split(remain, "/")
|
||||
if len(parts) > 0 {
|
||||
pluginName = parts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return pluginName
|
||||
}
|
||||
|
||||
func RegisterApis(apis ...system.SysApi) {
|
||||
var count int64
|
||||
var apiPaths []string
|
||||
for i := range apis {
|
||||
apiPaths = append(apiPaths, apis[i].Path)
|
||||
name := getPluginName()
|
||||
if name != "" {
|
||||
rw.Lock()
|
||||
ApiMap[name] = apis
|
||||
rw.Unlock()
|
||||
}
|
||||
global.GVA_DB.Find(&[]system.SysApi{}, "path in (?)", apiPaths).Count(&count)
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
err := global.GVA_DB.Create(&apis).Error
|
||||
|
||||
err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
for _, api := range apis {
|
||||
err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error
|
||||
if err != nil {
|
||||
zap.L().Error("注册API失败", zap.Error(err), zap.String("api", api.Path), zap.String("method", api.Method), zap.String("apiGroup", api.ApiGroup))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
zap.L().Error("注册API失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterMenus(menus ...system.SysBaseMenu) {
|
||||
var count int64
|
||||
var menuNames []string
|
||||
name := getPluginName()
|
||||
if name != "" {
|
||||
rw.Lock()
|
||||
MenuMap[name] = menus
|
||||
rw.Unlock()
|
||||
}
|
||||
|
||||
parentMenu := menus[0]
|
||||
otherMenus := menus[1:]
|
||||
for i := range menus {
|
||||
menuNames = append(menuNames, menus[i].Name)
|
||||
}
|
||||
global.GVA_DB.Find(&[]system.SysBaseMenu{}, "name in (?)", menuNames).Count(&count)
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
err := global.GVA_DB.Create(&parentMenu).Error
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
for i := range otherMenus {
|
||||
err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Model(system.SysBaseMenu{}).Where("name = ? ", parentMenu.Name).FirstOrCreate(&parentMenu).Error
|
||||
if err != nil {
|
||||
zap.L().Error("注册菜单失败", zap.Error(err))
|
||||
return errors.Wrap(err, "注册菜单失败")
|
||||
}
|
||||
pid := parentMenu.ID
|
||||
otherMenus[i].ParentId = pid
|
||||
}
|
||||
err = global.GVA_DB.Create(&otherMenus).Error
|
||||
for i := range otherMenus {
|
||||
otherMenus[i].ParentId = pid
|
||||
err = tx.Model(system.SysBaseMenu{}).Where("name = ? ", otherMenus[i].Name).FirstOrCreate(&otherMenus[i]).Error
|
||||
if err != nil {
|
||||
zap.L().Error("注册菜单失败", zap.Error(err))
|
||||
return errors.Wrap(err, "注册菜单失败")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
zap.L().Error("注册菜单失败", zap.Error(err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func RegisterDictionaries(dictionaries ...system.SysDictionary) {
|
||||
name := getPluginName()
|
||||
if name != "" {
|
||||
rw.Lock()
|
||||
DictMap[name] = dictionaries
|
||||
rw.Unlock()
|
||||
}
|
||||
|
||||
err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
for _, dict := range dictionaries {
|
||||
details := dict.SysDictionaryDetails
|
||||
dict.SysDictionaryDetails = nil
|
||||
err := tx.Model(system.SysDictionary{}).Where("type = ?", dict.Type).FirstOrCreate(&dict).Error
|
||||
if err != nil {
|
||||
zap.L().Error("注册字典失败", zap.Error(err), zap.String("type", dict.Type))
|
||||
return err
|
||||
}
|
||||
for _, detail := range details {
|
||||
detail.SysDictionaryID = int(dict.ID)
|
||||
err = tx.Model(system.SysDictionaryDetail{}).Where("sys_dictionary_id = ? AND value = ?", dict.ID, detail.Value).FirstOrCreate(&detail).Error
|
||||
if err != nil {
|
||||
zap.L().Error("注册字典详情失败", zap.Error(err), zap.String("value", detail.Value))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
zap.L().Error("注册字典失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func Pointer[T any](in T) *T {
|
||||
return &in
|
||||
}
|
||||
|
||||
func GetPluginData(pluginName string) ([]system.SysApi, []system.SysBaseMenu, []system.SysDictionary) {
|
||||
rw.Lock()
|
||||
defer rw.Unlock()
|
||||
return ApiMap[pluginName], MenuMap[pluginName], DictMap[pluginName]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
_ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
model "{{.Module}}/model/system"
|
||||
"{{.Module}}/plugin/plugin-tool/utils"
|
||||
)
|
||||
|
||||
func Dictionary(ctx context.Context) {
|
||||
entities := []model.SysDictionary{}
|
||||
utils.RegisterDictionaries(entities...)
|
||||
}
|
||||
@@ -13,12 +13,19 @@ var Plugin = new(plugin)
|
||||
|
||||
type plugin struct{}
|
||||
|
||||
func init() {
|
||||
interfaces.Register(Plugin)
|
||||
}
|
||||
|
||||
|
||||
// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法
|
||||
// initialize.Viper()
|
||||
// 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法
|
||||
// initialize.Api(ctx)
|
||||
// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法
|
||||
// initialize.Menu(ctx)
|
||||
// 安装插件时候自动注册的api数据请到下方法.Dictionary方法中实现并添加如下方法
|
||||
// initialize.Dictionary(ctx)
|
||||
func (p *plugin) Register(group *gin.Engine) {
|
||||
ctx := context.Background()
|
||||
initialize.Gorm(ctx)
|
||||
|
||||
@@ -20,6 +20,10 @@ type RouterGroup struct {
|
||||
SysExportTemplateRouter
|
||||
SysParamsRouter
|
||||
SysVersionRouter
|
||||
SysErrorRouter
|
||||
LoginLogRouter
|
||||
ApiTokenRouter
|
||||
SkillsRouter
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -43,4 +47,6 @@ var (
|
||||
autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi
|
||||
exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi
|
||||
sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi
|
||||
sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi
|
||||
skillsApi = api.ApiGroupApp.SystemApiGroup.SkillsApi
|
||||
)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/api/v1"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ApiTokenRouter struct{}
|
||||
|
||||
func (s *ApiTokenRouter) InitApiTokenRouter(Router *gin.RouterGroup) {
|
||||
apiTokenRouter := Router.Group("sysApiToken").Use(middleware.OperationRecord())
|
||||
apiTokenApi := v1.ApiGroupApp.SystemApiGroup.ApiTokenApi
|
||||
{
|
||||
apiTokenRouter.POST("createApiToken", apiTokenApi.CreateApiToken) // 签发Token
|
||||
apiTokenRouter.POST("getApiTokenList", apiTokenApi.GetApiTokenList) // 获取列表
|
||||
apiTokenRouter.POST("deleteApiToken", apiTokenApi.DeleteApiToken) // 作废Token
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,13 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli
|
||||
{
|
||||
autoCodeRouter.POST("pubPlug", autoCodePluginApi.Packaged) // 打包插件
|
||||
autoCodeRouter.POST("installPlugin", autoCodePluginApi.Install) // 自动安装插件
|
||||
|
||||
autoCodeRouter.POST("removePlugin", autoCodePluginApi.Remove) // 自动删除插件
|
||||
autoCodeRouter.GET("getPluginList", autoCodePluginApi.GetPluginList) // 获取插件列表
|
||||
}
|
||||
{
|
||||
publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto)
|
||||
publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单
|
||||
publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API
|
||||
publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单
|
||||
publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API
|
||||
publicAutoCodeRouter.POST("initDictionary", autoCodePluginApi.InitDictionary) // 同步插件字典
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) {
|
||||
sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary
|
||||
sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary
|
||||
sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary
|
||||
sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary
|
||||
sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary
|
||||
}
|
||||
{
|
||||
sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SysErrorRouter struct{}
|
||||
|
||||
// InitSysErrorRouter 初始化 错误日志 路由信息
|
||||
func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
|
||||
sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord())
|
||||
sysErrorRouterWithoutRecord := Router.Group("sysError")
|
||||
sysErrorRouterWithoutAuth := PublicRouter.Group("sysError")
|
||||
{
|
||||
sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志
|
||||
sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志
|
||||
sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志
|
||||
sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理
|
||||
}
|
||||
{
|
||||
sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志
|
||||
sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表
|
||||
}
|
||||
{
|
||||
sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.Router
|
||||
sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表
|
||||
sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token
|
||||
sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板
|
||||
sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL
|
||||
}
|
||||
{
|
||||
sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/api/v1"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LoginLogRouter struct{}
|
||||
|
||||
func (s *LoginLogRouter) InitLoginLogRouter(Router *gin.RouterGroup) {
|
||||
loginLogRouter := Router.Group("sysLoginLog").Use(middleware.OperationRecord())
|
||||
loginLogRouterWithoutRecord := Router.Group("sysLoginLog")
|
||||
sysLoginLogApi := v1.ApiGroupApp.SystemApiGroup.LoginLogApi
|
||||
{
|
||||
loginLogRouter.DELETE("deleteLoginLog", sysLoginLogApi.DeleteLoginLog) // 删除登录日志
|
||||
loginLogRouter.DELETE("deleteLoginLogByIds", sysLoginLogApi.DeleteLoginLogByIds) // 批量删除登录日志
|
||||
}
|
||||
{
|
||||
loginLogRouterWithoutRecord.GET("findLoginLog", sysLoginLogApi.FindLoginLog) // 根据ID获取登录日志(详情)
|
||||
loginLogRouterWithoutRecord.GET("getLoginLogList", sysLoginLogApi.GetLoginLogList) // 获取登录日志列表
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package system
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type SkillsRouter struct{}
|
||||
|
||||
func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) {
|
||||
skillsRouter := Router.Group("skills")
|
||||
{
|
||||
skillsRouter.GET("getTools", skillsApi.GetTools)
|
||||
skillsRouter.POST("getSkillList", skillsApi.GetSkillList)
|
||||
skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail)
|
||||
skillsRouter.POST("saveSkill", skillsApi.SaveSkill)
|
||||
skillsRouter.POST("createScript", skillsApi.CreateScript)
|
||||
skillsRouter.POST("getScript", skillsApi.GetScript)
|
||||
skillsRouter.POST("saveScript", skillsApi.SaveScript)
|
||||
skillsRouter.POST("createResource", skillsApi.CreateResource)
|
||||
skillsRouter.POST("getResource", skillsApi.GetResource)
|
||||
skillsRouter.POST("saveResource", skillsApi.SaveResource)
|
||||
skillsRouter.POST("createReference", skillsApi.CreateReference)
|
||||
skillsRouter.POST("getReference", skillsApi.GetReference)
|
||||
skillsRouter.POST("saveReference", skillsApi.SaveReference)
|
||||
skillsRouter.POST("createTemplate", skillsApi.CreateTemplate)
|
||||
skillsRouter.POST("getTemplate", skillsApi.GetTemplate)
|
||||
skillsRouter.POST("saveTemplate", skillsApi.SaveTemplate)
|
||||
skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint)
|
||||
skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName
|
||||
cfile.FileName = fileName
|
||||
cfile.ChunkTotal = chunkTotal
|
||||
|
||||
if errors.Is(global.GVA_DB.Where("file_md5 = ? AND is_finish = ?", fileMd5, true).First(&file).Error, gorm.ErrRecordNotFound) {
|
||||
if errors.Is(global.GVA_DB.Where("file_md5 = ? AND file_name = ? AND is_finish = ?", fileMd5, fileName, true).First(&file).Error, gorm.ErrRecordNotFound) {
|
||||
err = global.GVA_DB.Where("file_md5 = ? AND file_name = ?", fileMd5, fileName).Preload("ExaFileChunk").FirstOrCreate(&file, cfile).Error
|
||||
return file, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/example"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/example/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/upload"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
//@author: [piexlmax](https://github.com/piexlmax)
|
||||
@@ -107,7 +108,13 @@ func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader,
|
||||
Key: key,
|
||||
}
|
||||
if noSave == "0" {
|
||||
return f, e.Upload(f)
|
||||
// 检查是否已存在相同key的记录
|
||||
var existingFile example.ExaFileUploadAndDownload
|
||||
err = global.GVA_DB.Where(&example.ExaFileUploadAndDownload{Key: key}).First(&existingFile).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return f, e.Upload(f)
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
|
||||
commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
|
||||
"github.com/goccy/go-json"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LLMAuto 调用大模型服务,返回生成结果数据
|
||||
// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload
|
||||
func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) {
|
||||
if global.GVA_CONFIG.AutoCode.AiPath == "" {
|
||||
return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中")
|
||||
}
|
||||
|
||||
// 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换
|
||||
mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常
|
||||
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode)
|
||||
|
||||
res, err := request.HttpRequest(
|
||||
path,
|
||||
"POST",
|
||||
nil,
|
||||
nil,
|
||||
llm,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("大模型生成失败: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var resStruct commonResp.Response
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取大模型响应失败: %w", err)
|
||||
}
|
||||
if err = json.Unmarshal(b, &resStruct); err != nil {
|
||||
return nil, fmt.Errorf("解析大模型响应失败: %w", err)
|
||||
}
|
||||
if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败
|
||||
return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg)
|
||||
}
|
||||
return resStruct.Data, nil
|
||||
}
|
||||
@@ -3,6 +3,12 @@ package system
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
@@ -11,12 +17,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/autocode"
|
||||
"github.com/pkg/errors"
|
||||
"go/token"
|
||||
"gorm.io/gorm"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var AutoCodePackage = new(autoCodePackage)
|
||||
@@ -162,7 +163,6 @@ func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCode
|
||||
"api": true,
|
||||
"config": true,
|
||||
"initialize": true,
|
||||
"model": true,
|
||||
"plugin": true,
|
||||
"router": true,
|
||||
"service": true,
|
||||
@@ -179,14 +179,25 @@ func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCode
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dirNameMap) != 0 {
|
||||
continue
|
||||
|
||||
var desc string
|
||||
if len(dirNameMap) == 0 {
|
||||
// 完全符合标准结构
|
||||
desc = "系统自动读取" + pluginDir[i].Name() + "插件,使用前请确认是否为v2版本插件"
|
||||
} else {
|
||||
// 缺少某些结构,生成警告描述
|
||||
var missingDirs []string
|
||||
for dirName := range dirNameMap {
|
||||
missingDirs = append(missingDirs, dirName)
|
||||
}
|
||||
desc = fmt.Sprintf("系统自动读取,但是缺少 %s 结构,不建议自动化和mcp使用", strings.Join(missingDirs, "、"))
|
||||
}
|
||||
|
||||
pluginPackage := model.SysAutoCodePackage{
|
||||
PackageName: pluginDir[i].Name(),
|
||||
Template: "plugin",
|
||||
Label: pluginDir[i].Name() + "插件",
|
||||
Desc: "系统自动读取" + pluginDir[i].Name() + "插件,使用前请确认是否为v2版本插件",
|
||||
Desc: desc,
|
||||
Module: global.GVA_CONFIG.AutoCode.Module,
|
||||
}
|
||||
plugin = append(plugin, pluginPackage)
|
||||
@@ -326,7 +337,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod
|
||||
pluginInitialize := &ast.PluginInitializeV2{
|
||||
Type: ast.TypePluginInitializeV2,
|
||||
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, name),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
|
||||
ImportPath: fmt.Sprintf(`"%s/plugin/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName),
|
||||
PackageName: entity.PackageName,
|
||||
}
|
||||
@@ -536,10 +547,11 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod
|
||||
router := strings.Index(threeDirs[k].Name(), "router")
|
||||
hasGorm := strings.Index(threeDirs[k].Name(), "gorm")
|
||||
response := strings.Index(threeDirs[k].Name(), "response")
|
||||
if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 {
|
||||
dictionary := strings.Index(threeDirs[k].Name(), "dictionary")
|
||||
if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 && dictionary != -1 {
|
||||
return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four)
|
||||
}
|
||||
if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 {
|
||||
if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 || dictionary != -1 {
|
||||
creates[four] = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext))
|
||||
}
|
||||
if gen != -1 {
|
||||
|
||||
@@ -4,15 +4,7 @@ import (
|
||||
"bytes"
|
||||
"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/model/system/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
|
||||
"github.com/mholt/archives"
|
||||
cp "github.com/otiai10/copy"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
goast "go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
@@ -21,6 +13,17 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
pluginUtils "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
ast "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
|
||||
"github.com/mholt/archives"
|
||||
cp "github.com/otiai10/copy"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var AutoCodePlugin = new(autoCodePlugin)
|
||||
@@ -42,13 +45,27 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 在临时目录创建目标文件
|
||||
// 使用完整路径拼接的好处:明确文件位置,避免路径混乱
|
||||
out, err := os.Create(GVAPLUGPINATH + file.Filename)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 将上传的文件内容复制到临时文件
|
||||
// 使用io.Copy的好处:高效处理大文件,自动管理缓冲区,避免内存溢出
|
||||
_, err = io.Copy(out, src)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
// 立即关闭文件,确保数据写入磁盘并释放文件句柄
|
||||
// 必须在解压前关闭,否则在Windows系统上会导致文件被占用无法解压
|
||||
err = out.Close()
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH)
|
||||
paths = filterFile(paths)
|
||||
@@ -56,6 +73,8 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e
|
||||
var serverIndex = -1
|
||||
webPlugin := ""
|
||||
serverPlugin := ""
|
||||
serverPackage := ""
|
||||
serverRootName := ""
|
||||
|
||||
for i := range paths {
|
||||
paths[i] = filepath.ToSlash(paths[i])
|
||||
@@ -65,8 +84,16 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e
|
||||
if ln < 4 {
|
||||
continue
|
||||
}
|
||||
if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 {
|
||||
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
|
||||
if pathArr[2]+"/"+pathArr[3] == `server/plugin` {
|
||||
if len(serverPlugin) == 0 {
|
||||
serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
|
||||
}
|
||||
if serverRootName == "" && ln > 1 && pathArr[1] != "" {
|
||||
serverRootName = pathArr[1]
|
||||
}
|
||||
if ln > 4 && serverPackage == "" && pathArr[4] != "" {
|
||||
serverPackage = pathArr[4]
|
||||
}
|
||||
}
|
||||
if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
|
||||
webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
|
||||
@@ -78,10 +105,17 @@ func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, e
|
||||
}
|
||||
|
||||
if len(serverPlugin) != 0 {
|
||||
if serverPackage == "" {
|
||||
serverPackage = serverRootName
|
||||
}
|
||||
err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server)
|
||||
if err != nil {
|
||||
return webIndex, serverIndex, err
|
||||
}
|
||||
err = ensurePluginRegisterImport(serverPackage)
|
||||
if err != nil {
|
||||
return webIndex, serverIndex, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(webPlugin) != 0 {
|
||||
@@ -112,6 +146,64 @@ func installation(path string, formPath string, toPath string) error {
|
||||
return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument})
|
||||
}
|
||||
|
||||
func ensurePluginRegisterImport(packageName string) error {
|
||||
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
|
||||
if module == "" {
|
||||
return errors.New("autocode module is empty")
|
||||
}
|
||||
if packageName == "" {
|
||||
return errors.New("plugin package is empty")
|
||||
}
|
||||
|
||||
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
|
||||
src, err := os.ReadFile(registerPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
|
||||
if ast.CheckImport(astFile, importPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
importSpec := &goast.ImportSpec{
|
||||
Name: goast.NewIdent("_"),
|
||||
Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)},
|
||||
}
|
||||
var importDecl *goast.GenDecl
|
||||
for _, decl := range astFile.Decls {
|
||||
genDecl, ok := decl.(*goast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
importDecl = genDecl
|
||||
break
|
||||
}
|
||||
}
|
||||
if importDecl == nil {
|
||||
astFile.Decls = append([]goast.Decl{
|
||||
&goast.GenDecl{
|
||||
Tok: token.IMPORT,
|
||||
Specs: []goast.Spec{importSpec},
|
||||
},
|
||||
}, astFile.Decls...)
|
||||
} else {
|
||||
importDecl.Specs = append(importDecl.Specs, importSpec)
|
||||
}
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
return os.WriteFile(registerPath, bf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func filterFile(paths []string) []string {
|
||||
np := make([]string, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
@@ -249,3 +341,172 @@ func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) {
|
||||
os.WriteFile(apiPath, bf.Bytes(), 0666)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) {
|
||||
dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go")
|
||||
src, err := os.ReadFile(dictPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, "", src, 0)
|
||||
arrayAst := ast.FindArray(astFile, "model", "SysDictionary")
|
||||
var dictionaries []system.SysDictionary
|
||||
err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dictExpr := ast.CreateDictionaryStructAst(dictionaries)
|
||||
arrayAst.Elts = *dictExpr
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
os.WriteFile(dictPath, bf.Bytes(), 0666)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) {
|
||||
// 1. 删除前端代码
|
||||
if pluginType == "web" || pluginType == "full" {
|
||||
webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName)
|
||||
err = os.RemoveAll(webDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "删除前端插件目录失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 删除后端代码
|
||||
if pluginType == "server" || pluginType == "full" {
|
||||
serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName)
|
||||
err = os.RemoveAll(serverDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "删除后端插件目录失败")
|
||||
}
|
||||
|
||||
// 移除注册
|
||||
removePluginRegisterImport(pluginName)
|
||||
}
|
||||
|
||||
// 通过utils 获取 api 菜单 字典
|
||||
apis, menus, dicts := pluginUtils.GetPluginData(pluginName)
|
||||
|
||||
// 3. 删除菜单 (递归删除)
|
||||
if len(menus) > 0 {
|
||||
for _, menu := range menus {
|
||||
var dbMenu system.SysBaseMenu
|
||||
if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil {
|
||||
// 获取该菜单及其所有子菜单的ID
|
||||
var menuIds []int
|
||||
GetMenuIds(dbMenu, &menuIds)
|
||||
// 逆序删除,先删除子菜单
|
||||
for i := len(menuIds) - 1; i >= 0; i-- {
|
||||
err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i])
|
||||
if err != nil {
|
||||
zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 删除API
|
||||
if len(apis) > 0 {
|
||||
for _, api := range apis {
|
||||
var dbApi system.SysApi
|
||||
if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil {
|
||||
err := ApiServiceApp.DeleteApi(dbApi)
|
||||
if err != nil {
|
||||
zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 删除字典
|
||||
if len(dicts) > 0 {
|
||||
for _, dict := range dicts {
|
||||
var dbDict system.SysDictionary
|
||||
if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil {
|
||||
err := DictionaryServiceApp.DeleteSysDictionary(dbDict)
|
||||
if err != nil {
|
||||
zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMenuIds(menu system.SysBaseMenu, ids *[]int) {
|
||||
*ids = append(*ids, int(menu.ID))
|
||||
var children []system.SysBaseMenu
|
||||
global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children)
|
||||
for _, child := range children {
|
||||
// 先递归收集子菜单
|
||||
GetMenuIds(child, ids)
|
||||
}
|
||||
}
|
||||
|
||||
func removePluginRegisterImport(packageName string) error {
|
||||
module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module)
|
||||
if module == "" {
|
||||
return errors.New("autocode module is empty")
|
||||
}
|
||||
if packageName == "" {
|
||||
return errors.New("plugin package is empty")
|
||||
}
|
||||
|
||||
registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go")
|
||||
src, err := os.ReadFile(registerPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileSet := token.NewFileSet()
|
||||
astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
importPath := fmt.Sprintf("%s/plugin/%s", module, packageName)
|
||||
importLit := fmt.Sprintf("%q", importPath)
|
||||
|
||||
// 移除 import
|
||||
var newDecls []goast.Decl
|
||||
for _, decl := range astFile.Decls {
|
||||
genDecl, ok := decl.(*goast.GenDecl)
|
||||
if !ok {
|
||||
newDecls = append(newDecls, decl)
|
||||
continue
|
||||
}
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
var newSpecs []goast.Spec
|
||||
for _, spec := range genDecl.Specs {
|
||||
importSpec, ok := spec.(*goast.ImportSpec)
|
||||
if !ok {
|
||||
newSpecs = append(newSpecs, spec)
|
||||
continue
|
||||
}
|
||||
if importSpec.Path.Value != importLit {
|
||||
newSpecs = append(newSpecs, spec)
|
||||
}
|
||||
}
|
||||
// 如果还有其他import,保留该 decl
|
||||
if len(newSpecs) > 0 {
|
||||
genDecl.Specs = newSpecs
|
||||
newDecls = append(newDecls, genDecl)
|
||||
}
|
||||
} else {
|
||||
newDecls = append(newDecls, decl)
|
||||
}
|
||||
}
|
||||
astFile.Decls = newDecls
|
||||
|
||||
var out []byte
|
||||
bf := bytes.NewBuffer(out)
|
||||
printer.Fprint(bf, fileSet, astFile)
|
||||
|
||||
return os.WriteFile(registerPath, bf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ type ServiceGroup struct {
|
||||
SysExportTemplateService
|
||||
SysParamsService
|
||||
SysVersionService
|
||||
SkillsService
|
||||
AutoCodePlugin autoCodePlugin
|
||||
AutoCodePackage autoCodePackage
|
||||
AutoCodeHistory autoCodeHistory
|
||||
AutoCodeTemplate autoCodeTemplate
|
||||
SysErrorService
|
||||
LoginLogService
|
||||
ApiTokenService
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
sysReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApiTokenService struct{}
|
||||
|
||||
func (apiVersion *ApiTokenService) CreateApiToken(apiToken system.SysApiToken, days int) (string, error) {
|
||||
var user system.SysUser
|
||||
if err := global.GVA_DB.Where("id = ?", apiToken.UserID).First(&user).Error; err != nil {
|
||||
return "", errors.New("用户不存在")
|
||||
}
|
||||
|
||||
hasAuth := false
|
||||
for _, auth := range user.Authorities {
|
||||
if auth.AuthorityId == apiToken.AuthorityID {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAuth && user.AuthorityId != apiToken.AuthorityID {
|
||||
return "", errors.New("用户不具备该角色权限")
|
||||
}
|
||||
|
||||
j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一不同的部分是过期时间
|
||||
|
||||
expireTime := time.Duration(days) * 24 * time.Hour
|
||||
if days == -1 {
|
||||
expireTime = 100 * 365 * 24 * time.Hour
|
||||
}
|
||||
|
||||
bf, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime)
|
||||
|
||||
claims := sysReq.CustomClaims{
|
||||
BaseClaims: sysReq.BaseClaims{
|
||||
UUID: user.UUID,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
NickName: user.NickName,
|
||||
AuthorityId: apiToken.AuthorityID,
|
||||
},
|
||||
BufferTime: int64(bf / time.Second), // 缓冲时间
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{"GVA"},
|
||||
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)),
|
||||
Issuer: global.GVA_CONFIG.JWT.Issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token, err := j.CreateToken(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
apiToken.Token = token
|
||||
apiToken.Status = true
|
||||
apiToken.ExpiresAt = time.Now().Add(expireTime)
|
||||
err = global.GVA_DB.Create(&apiToken).Error
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (apiVersion *ApiTokenService) GetApiTokenList(info sysReq.SysApiTokenSearch) (list []system.SysApiToken, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
db := global.GVA_DB.Model(&system.SysApiToken{})
|
||||
|
||||
db = db.Preload("User")
|
||||
|
||||
if info.UserID != 0 {
|
||||
db = db.Where("user_id = ?", info.UserID)
|
||||
}
|
||||
if info.Status != nil {
|
||||
db = db.Where("status = ?", *info.Status)
|
||||
}
|
||||
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&list).Error
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
func (apiVersion *ApiTokenService) DeleteApiToken(id uint) error {
|
||||
var apiToken system.SysApiToken
|
||||
err := global.GVA_DB.First(&apiToken, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jwtService := JwtService{}
|
||||
err = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: apiToken.Token})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return global.GVA_DB.Model(&apiToken).Update("status", false).Error
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
@@ -152,3 +153,145 @@ func (dictionaryService *DictionaryService) checkCircularReference(currentID uin
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//@author: [pixelMax]
|
||||
//@function: ExportSysDictionary
|
||||
//@description: 导出字典JSON(包含字典详情)
|
||||
//@param: id uint
|
||||
//@return: exportData map[string]interface{}, err error
|
||||
|
||||
func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) {
|
||||
var dictionary system.SysDictionary
|
||||
// 查询字典及其所有详情
|
||||
err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort")
|
||||
}).First(&dictionary).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清空字典详情中的ID、创建时间、更新时间等字段
|
||||
var cleanDetails []map[string]interface{}
|
||||
for _, detail := range dictionary.SysDictionaryDetails {
|
||||
cleanDetail := map[string]interface{}{
|
||||
"label": detail.Label,
|
||||
"value": detail.Value,
|
||||
"extend": detail.Extend,
|
||||
"status": detail.Status,
|
||||
"sort": detail.Sort,
|
||||
"level": detail.Level,
|
||||
"path": detail.Path,
|
||||
}
|
||||
cleanDetails = append(cleanDetails, cleanDetail)
|
||||
}
|
||||
|
||||
// 构造导出数据
|
||||
exportData = map[string]interface{}{
|
||||
"name": dictionary.Name,
|
||||
"type": dictionary.Type,
|
||||
"status": dictionary.Status,
|
||||
"desc": dictionary.Desc,
|
||||
"sysDictionaryDetails": cleanDetails,
|
||||
}
|
||||
|
||||
return exportData, nil
|
||||
}
|
||||
|
||||
//@author: [pixelMax]
|
||||
//@function: ImportSysDictionary
|
||||
//@description: 导入字典JSON(包含字典详情)
|
||||
//@param: jsonStr string
|
||||
//@return: err error
|
||||
|
||||
func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error {
|
||||
// 直接解析到 SysDictionary 结构体
|
||||
var importData system.SysDictionary
|
||||
if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil {
|
||||
return errors.New("JSON 格式错误: " + err.Error())
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if importData.Name == "" {
|
||||
return errors.New("字典名称不能为空")
|
||||
}
|
||||
if importData.Type == "" {
|
||||
return errors.New("字典类型不能为空")
|
||||
}
|
||||
|
||||
// 检查字典类型是否已存在
|
||||
if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) {
|
||||
return errors.New("存在相同的type,不允许导入")
|
||||
}
|
||||
|
||||
// 创建字典(清空导入数据的ID和时间戳)
|
||||
dictionary := system.SysDictionary{
|
||||
Name: importData.Name,
|
||||
Type: importData.Type,
|
||||
Status: importData.Status,
|
||||
Desc: importData.Desc,
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 创建字典
|
||||
if err := tx.Create(&dictionary).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理字典详情
|
||||
if len(importData.SysDictionaryDetails) > 0 {
|
||||
// 创建一个映射来跟踪旧ID到新ID的对应关系
|
||||
idMap := make(map[uint]uint)
|
||||
|
||||
// 第一遍:创建所有详情记录
|
||||
for _, detail := range importData.SysDictionaryDetails {
|
||||
// 验证必填字段
|
||||
if detail.Label == "" || detail.Value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 记录旧ID
|
||||
oldID := detail.ID
|
||||
|
||||
// 创建新的详情记录(ID会被GORM自动设置)
|
||||
detailRecord := system.SysDictionaryDetail{
|
||||
Label: detail.Label,
|
||||
Value: detail.Value,
|
||||
Extend: detail.Extend,
|
||||
Status: detail.Status,
|
||||
Sort: detail.Sort,
|
||||
Level: detail.Level,
|
||||
Path: detail.Path,
|
||||
SysDictionaryID: int(dictionary.ID),
|
||||
}
|
||||
|
||||
// 创建详情记录
|
||||
if err := tx.Create(&detailRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录旧ID到新ID的映射
|
||||
if oldID > 0 {
|
||||
idMap[oldID] = detailRecord.ID
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:更新parent_id关系
|
||||
for _, detail := range importData.SysDictionaryDetails {
|
||||
if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 {
|
||||
if newID, exists := idMap[detail.ID]; exists {
|
||||
if newParentID, parentExists := idMap[*detail.ParentID]; parentExists {
|
||||
if err := tx.Model(&system.SysDictionaryDetail{}).
|
||||
Where("id = ?", newID).
|
||||
Update("parent_id", newParentID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
)
|
||||
|
||||
type SysErrorService struct{}
|
||||
|
||||
// CreateSysError 创建错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) {
|
||||
if global.GVA_DB == nil {
|
||||
return nil
|
||||
}
|
||||
err = global.GVA_DB.Create(sysError).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSysError 删除错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) DeleteSysError(ctx context.Context, ID string) (err error) {
|
||||
err = global.GVA_DB.Delete(&system.SysError{}, "id = ?", ID).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSysErrorByIds 批量删除错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) DeleteSysErrorByIds(ctx context.Context, IDs []string) (err error) {
|
||||
err = global.GVA_DB.Delete(&[]system.SysError{}, "id in ?", IDs).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSysError 更新错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) UpdateSysError(ctx context.Context, sysError system.SysError) (err error) {
|
||||
err = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", sysError.ID).Updates(&sysError).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSysError 根据ID获取错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) GetSysError(ctx context.Context, ID string) (sysError system.SysError, err error) {
|
||||
err = global.GVA_DB.Where("id = ?", ID).First(&sysError).Error
|
||||
return
|
||||
}
|
||||
|
||||
// GetSysErrorInfoList 分页获取错误日志记录
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, info systemReq.SysErrorSearch) (list []system.SysError, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
// 创建db
|
||||
db := global.GVA_DB.Model(&system.SysError{}).Order("created_at desc")
|
||||
var sysErrors []system.SysError
|
||||
// 如果有条件搜索 下方会自动创建搜索语句
|
||||
if len(info.CreatedAtRange) == 2 {
|
||||
db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1])
|
||||
}
|
||||
|
||||
if info.Form != nil && *info.Form != "" {
|
||||
db = db.Where("form = ?", *info.Form)
|
||||
}
|
||||
if info.Info != nil && *info.Info != "" {
|
||||
db = db.Where("info LIKE ?", "%"+*info.Info+"%")
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
db = db.Limit(limit).Offset(offset)
|
||||
}
|
||||
|
||||
err = db.Find(&sysErrors).Error
|
||||
return sysErrors, total, err
|
||||
}
|
||||
|
||||
// GetSysErrorSolution 异步处理错误
|
||||
// Author [yourname](https://github.com/yourname)
|
||||
func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) {
|
||||
// 立即更新为处理中
|
||||
err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步协程在一分钟后更新为处理完成
|
||||
go func(id string) {
|
||||
// 查询当前错误信息用于生成方案
|
||||
var se system.SysError
|
||||
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error
|
||||
|
||||
// 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案
|
||||
var form, info string
|
||||
if se.Form != nil {
|
||||
form = *se.Form
|
||||
}
|
||||
if se.Info != nil {
|
||||
info = *se.Info
|
||||
}
|
||||
|
||||
llmReq := common.JSONMap{
|
||||
"mode": "solution",
|
||||
"info": info,
|
||||
"form": form,
|
||||
}
|
||||
|
||||
// 调用服务层 LLMAuto,忽略错误但尽量写入方案
|
||||
var solution string
|
||||
if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil {
|
||||
solution = fmt.Sprintf("%v", data.(map[string]interface{})["text"])
|
||||
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error
|
||||
} else {
|
||||
// 即使生成失败也标记为完成,避免任务卡住
|
||||
_ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error
|
||||
}
|
||||
}(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -173,119 +173,147 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
|
||||
db = global.MustGetGlobalDBByDBName(template.DBName)
|
||||
}
|
||||
|
||||
if len(template.JoinTemplate) > 0 {
|
||||
for _, join := range template.JoinTemplate {
|
||||
db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON)
|
||||
// 如果有自定义SQL,则优先使用自定义SQL
|
||||
if template.SQL != "" {
|
||||
// 将 url.Values 转换为 map[string]interface{} 以支持 GORM 的命名参数
|
||||
sqlParams := make(map[string]interface{})
|
||||
for k, v := range paramsValues {
|
||||
if len(v) > 0 {
|
||||
sqlParams[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db = db.Select(selects).Table(template.TableName)
|
||||
|
||||
filterDeleted := false
|
||||
|
||||
filterParam := paramsValues.Get("filterDeleted")
|
||||
if filterParam == "true" {
|
||||
filterDeleted = true
|
||||
}
|
||||
|
||||
if filterDeleted {
|
||||
// 自动过滤主表的软删除
|
||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
|
||||
|
||||
// 过滤关联表的软删除(如果有)
|
||||
// 执行原生 SQL,支持 @key 命名参数
|
||||
err = db.Raw(template.SQL, sqlParams).Scan(&tableMap).Error
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
} else {
|
||||
if len(template.JoinTemplate) > 0 {
|
||||
for _, join := range template.JoinTemplate {
|
||||
// 检查关联表是否有deleted_at字段
|
||||
hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table)
|
||||
if hasDeletedAt {
|
||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
||||
db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON)
|
||||
}
|
||||
}
|
||||
|
||||
db = db.Select(selects).Table(template.TableName)
|
||||
|
||||
filterDeleted := false
|
||||
|
||||
filterParam := paramsValues.Get("filterDeleted")
|
||||
if filterParam == "true" {
|
||||
filterDeleted = true
|
||||
}
|
||||
|
||||
if filterDeleted {
|
||||
// 自动过滤主表的软删除
|
||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
|
||||
|
||||
// 过滤关联表的软删除(如果有)
|
||||
if len(template.JoinTemplate) > 0 {
|
||||
for _, join := range template.JoinTemplate {
|
||||
// 检查关联表是否有deleted_at字段
|
||||
hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table)
|
||||
if hasDeletedAt {
|
||||
db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(template.Conditions) > 0 {
|
||||
for _, condition := range template.Conditions {
|
||||
sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator)
|
||||
value := paramsValues.Get(condition.From)
|
||||
if len(template.Conditions) > 0 {
|
||||
for _, condition := range template.Conditions {
|
||||
sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator)
|
||||
value := paramsValues.Get(condition.From)
|
||||
|
||||
if condition.Operator == "IN" || condition.Operator == "NOT IN" {
|
||||
sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator)
|
||||
}
|
||||
if condition.Operator == "IN" || condition.Operator == "NOT IN" {
|
||||
sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator)
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
if condition.Operator == "LIKE" {
|
||||
value = "%" + value + "%"
|
||||
if condition.Operator == "BETWEEN" {
|
||||
sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column)
|
||||
startValue := paramsValues.Get("start" + condition.From)
|
||||
endValue := paramsValues.Get("end" + condition.From)
|
||||
if startValue != "" && endValue != "" {
|
||||
db = db.Where(sql, startValue, endValue)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
if condition.Operator == "LIKE" {
|
||||
value = "%" + value + "%"
|
||||
}
|
||||
db = db.Where(sql, value)
|
||||
}
|
||||
db = db.Where(sql, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 通过参数传入limit
|
||||
limit := paramsValues.Get("limit")
|
||||
if limit != "" {
|
||||
l, e := strconv.Atoi(limit)
|
||||
if e == nil {
|
||||
db = db.Limit(l)
|
||||
}
|
||||
}
|
||||
// 模板的默认limit
|
||||
if limit == "" && template.Limit != nil && *template.Limit != 0 {
|
||||
db = db.Limit(*template.Limit)
|
||||
}
|
||||
|
||||
// 通过参数传入offset
|
||||
offset := paramsValues.Get("offset")
|
||||
if offset != "" {
|
||||
o, e := strconv.Atoi(offset)
|
||||
if e == nil {
|
||||
db = db.Offset(o)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前表的所有字段
|
||||
table := template.TableName
|
||||
orderColumns, err := db.Migrator().ColumnTypes(table)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 创建一个 map 来存储字段名
|
||||
fields := make(map[string]bool)
|
||||
|
||||
for _, column := range orderColumns {
|
||||
fields[column.Name()] = true
|
||||
}
|
||||
|
||||
// 通过参数传入order
|
||||
order := paramsValues.Get("order")
|
||||
|
||||
if order == "" && template.Order != "" {
|
||||
// 如果没有order入参,这里会使用模板的默认排序
|
||||
order = template.Order
|
||||
}
|
||||
|
||||
if order != "" {
|
||||
checkOrderArr := strings.Split(order, " ")
|
||||
orderStr := ""
|
||||
// 检查请求的排序字段是否在字段列表中
|
||||
if _, ok := fields[checkOrderArr[0]]; !ok {
|
||||
return nil, "", fmt.Errorf("order by %s is not in the fields", order)
|
||||
}
|
||||
orderStr = checkOrderArr[0]
|
||||
if len(checkOrderArr) > 1 {
|
||||
if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" {
|
||||
return nil, "", fmt.Errorf("order by %s is not secure", order)
|
||||
// 通过参数传入limit
|
||||
limit := paramsValues.Get("limit")
|
||||
if limit != "" {
|
||||
l, e := strconv.Atoi(limit)
|
||||
if e == nil {
|
||||
db = db.Limit(l)
|
||||
}
|
||||
orderStr = orderStr + " " + checkOrderArr[1]
|
||||
}
|
||||
db = db.Order(orderStr)
|
||||
// 模板的默认limit
|
||||
if limit == "" && template.Limit != nil && *template.Limit != 0 {
|
||||
db = db.Limit(*template.Limit)
|
||||
}
|
||||
|
||||
// 通过参数传入offset
|
||||
offset := paramsValues.Get("offset")
|
||||
if offset != "" {
|
||||
o, e := strconv.Atoi(offset)
|
||||
if e == nil {
|
||||
db = db.Offset(o)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前表的所有字段
|
||||
table := template.TableName
|
||||
orderColumns, err := db.Migrator().ColumnTypes(table)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 创建一个 map 来存储字段名
|
||||
fields := make(map[string]bool)
|
||||
|
||||
for _, column := range orderColumns {
|
||||
fields[column.Name()] = true
|
||||
}
|
||||
|
||||
// 通过参数传入order
|
||||
order := paramsValues.Get("order")
|
||||
|
||||
if order == "" && template.Order != "" {
|
||||
// 如果没有order入参,这里会使用模板的默认排序
|
||||
order = template.Order
|
||||
}
|
||||
|
||||
if order != "" {
|
||||
checkOrderArr := strings.Split(order, " ")
|
||||
orderStr := ""
|
||||
// 检查请求的排序字段是否在字段列表中
|
||||
if _, ok := fields[checkOrderArr[0]]; !ok {
|
||||
return nil, "", fmt.Errorf("order by %s is not in the fields", order)
|
||||
}
|
||||
orderStr = checkOrderArr[0]
|
||||
if len(checkOrderArr) > 1 {
|
||||
if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" {
|
||||
return nil, "", fmt.Errorf("order by %s is not secure", order)
|
||||
}
|
||||
orderStr = orderStr + " " + checkOrderArr[1]
|
||||
}
|
||||
db = db.Order(orderStr)
|
||||
}
|
||||
|
||||
err = db.Debug().Find(&tableMap).Error
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
err = db.Debug().Find(&tableMap).Error
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
var rows [][]string
|
||||
rows = append(rows, tableTitle)
|
||||
for _, exTable := range tableMap {
|
||||
@@ -317,14 +345,14 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
|
||||
for j, colCell := range row {
|
||||
cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1)
|
||||
|
||||
var sErr error
|
||||
if v, err := strconv.ParseFloat(colCell, 64); err == nil {
|
||||
sErr = f.SetCellValue("Sheet1", cell, v)
|
||||
} else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil {
|
||||
sErr = f.SetCellValue("Sheet1", cell, v)
|
||||
} else {
|
||||
sErr = f.SetCellValue("Sheet1", cell, colCell)
|
||||
}
|
||||
var sErr error
|
||||
if v, err := strconv.ParseFloat(colCell, 64); err == nil {
|
||||
sErr = f.SetCellValue("Sheet1", cell, v)
|
||||
} else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil {
|
||||
sErr = f.SetCellValue("Sheet1", cell, v)
|
||||
} else {
|
||||
sErr = f.SetCellValue("Sheet1", cell, colCell)
|
||||
}
|
||||
|
||||
if sErr != nil {
|
||||
return nil, "", sErr
|
||||
@@ -340,6 +368,191 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID
|
||||
return file, template.Name, nil
|
||||
}
|
||||
|
||||
// PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串)
|
||||
// Author [piexlmax](https://github.com/piexlmax) & [trae-ai]
|
||||
func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) {
|
||||
// 解析 params(与导出逻辑保持一致)
|
||||
var params = values.Get("params")
|
||||
paramsValues, _ := url.ParseQuery(params)
|
||||
|
||||
// 加载模板
|
||||
var template system.SysExportTemplate
|
||||
err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 解析模板列
|
||||
var templateInfoMap = make(map[string]string)
|
||||
columns, err := utils.GetJSONKeys(template.TemplateInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var selectKeyFmt []string
|
||||
for _, key := range columns {
|
||||
selectKeyFmt = append(selectKeyFmt, key)
|
||||
}
|
||||
selects := strings.Join(selectKeyFmt, ", ")
|
||||
|
||||
// 生成 FROM 与 JOIN 片段
|
||||
var sb strings.Builder
|
||||
sb.WriteString("SELECT ")
|
||||
sb.WriteString(selects)
|
||||
sb.WriteString(" FROM ")
|
||||
sb.WriteString(template.TableName)
|
||||
|
||||
if len(template.JoinTemplate) > 0 {
|
||||
for _, join := range template.JoinTemplate {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(join.JOINS)
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(join.Table)
|
||||
sb.WriteString(" ON ")
|
||||
sb.WriteString(join.ON)
|
||||
}
|
||||
}
|
||||
|
||||
// WHERE 条件
|
||||
var wheres []string
|
||||
|
||||
// 软删除过滤
|
||||
filterDeleted := false
|
||||
if paramsValues != nil {
|
||||
filterParam := paramsValues.Get("filterDeleted")
|
||||
if filterParam == "true" {
|
||||
filterDeleted = true
|
||||
}
|
||||
}
|
||||
if filterDeleted {
|
||||
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName))
|
||||
if len(template.JoinTemplate) > 0 {
|
||||
for _, join := range template.JoinTemplate {
|
||||
if sysExportTemplateService.hasDeletedAtColumn(join.Table) {
|
||||
wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模板条件(保留与 ExportExcel 同步的解析规则)
|
||||
if len(template.Conditions) > 0 {
|
||||
for _, condition := range template.Conditions {
|
||||
op := strings.ToUpper(strings.TrimSpace(condition.Operator))
|
||||
col := strings.TrimSpace(condition.Column)
|
||||
|
||||
// 预览优先展示传入值,没有则展示占位符
|
||||
val := ""
|
||||
if paramsValues != nil {
|
||||
val = paramsValues.Get(condition.From)
|
||||
}
|
||||
|
||||
switch op {
|
||||
case "BETWEEN":
|
||||
startValue := ""
|
||||
endValue := ""
|
||||
if paramsValues != nil {
|
||||
startValue = paramsValues.Get("start" + condition.From)
|
||||
endValue = paramsValues.Get("end" + condition.From)
|
||||
}
|
||||
if startValue != "" && endValue != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From))
|
||||
}
|
||||
case "IN", "NOT IN":
|
||||
if val != "" {
|
||||
// 逗号分隔值做简单展示
|
||||
parts := strings.Split(val, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','")))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From))
|
||||
}
|
||||
case "LIKE":
|
||||
if val != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From))
|
||||
}
|
||||
default:
|
||||
if val != "" {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val))
|
||||
} else {
|
||||
wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(wheres) > 0 {
|
||||
sb.WriteString(" WHERE ")
|
||||
sb.WriteString(strings.Join(wheres, " AND "))
|
||||
}
|
||||
|
||||
// 排序
|
||||
order := ""
|
||||
if paramsValues != nil {
|
||||
order = paramsValues.Get("order")
|
||||
}
|
||||
if order == "" && template.Order != "" {
|
||||
order = template.Order
|
||||
}
|
||||
if order != "" {
|
||||
sb.WriteString(" ORDER BY ")
|
||||
sb.WriteString(order)
|
||||
}
|
||||
|
||||
// limit/offset(如果传入或默认值为0,则不生成)
|
||||
limitStr := ""
|
||||
offsetStr := ""
|
||||
if paramsValues != nil {
|
||||
limitStr = paramsValues.Get("limit")
|
||||
offsetStr = paramsValues.Get("offset")
|
||||
}
|
||||
|
||||
// 处理模板默认limit(仅当非0时)
|
||||
if limitStr == "" && template.Limit != nil && *template.Limit != 0 {
|
||||
limitStr = strconv.Itoa(*template.Limit)
|
||||
}
|
||||
|
||||
// 解析为数值,用于判断是否生成
|
||||
limitInt := 0
|
||||
offsetInt := 0
|
||||
if limitStr != "" {
|
||||
if v, e := strconv.Atoi(limitStr); e == nil {
|
||||
limitInt = v
|
||||
}
|
||||
}
|
||||
if offsetStr != "" {
|
||||
if v, e := strconv.Atoi(offsetStr); e == nil {
|
||||
offsetInt = v
|
||||
}
|
||||
}
|
||||
|
||||
if limitInt > 0 {
|
||||
sb.WriteString(" LIMIT ")
|
||||
sb.WriteString(strconv.Itoa(limitInt))
|
||||
if offsetInt > 0 {
|
||||
sb.WriteString(" OFFSET ")
|
||||
sb.WriteString(strconv.Itoa(offsetInt))
|
||||
}
|
||||
} else {
|
||||
// 当limit未设置或为0时,仅当offset>0才生成OFFSET
|
||||
if offsetInt > 0 {
|
||||
sb.WriteString(" OFFSET ")
|
||||
sb.WriteString(strconv.Itoa(offsetInt))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// ExportTemplate 导出Excel模板
|
||||
// Author [piexlmax](https://github.com/piexlmax)
|
||||
func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) {
|
||||
@@ -429,50 +642,77 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID
|
||||
return err
|
||||
}
|
||||
|
||||
var titleKeyMap = make(map[string]string)
|
||||
for key, title := range templateInfoMap {
|
||||
titleKeyMap[title] = key
|
||||
}
|
||||
|
||||
db := global.GVA_DB
|
||||
if template.DBName != "" {
|
||||
db = global.MustGetGlobalDBByDBName(template.DBName)
|
||||
}
|
||||
|
||||
items, err := sysExportTemplateService.parseExcelToMap(rows, templateInfoMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
excelTitle := rows[0]
|
||||
for i, str := range excelTitle {
|
||||
excelTitle[i] = strings.TrimSpace(str)
|
||||
if template.ImportSQL != "" {
|
||||
return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items)
|
||||
}
|
||||
values := rows[1:]
|
||||
items := make([]map[string]interface{}, 0, len(values))
|
||||
for _, row := range values {
|
||||
var item = make(map[string]interface{})
|
||||
for ii, value := range row {
|
||||
if _, ok := titleKeyMap[excelTitle[ii]]; !ok {
|
||||
continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过
|
||||
}
|
||||
key := titleKeyMap[excelTitle[ii]]
|
||||
item[key] = value
|
||||
}
|
||||
|
||||
needCreated := tx.Migrator().HasColumn(template.TableName, "created_at")
|
||||
needUpdated := tx.Migrator().HasColumn(template.TableName, "updated_at")
|
||||
|
||||
if item["created_at"] == nil && needCreated {
|
||||
item["created_at"] = time.Now()
|
||||
}
|
||||
if item["updated_at"] == nil && needUpdated {
|
||||
item["updated_at"] = time.Now()
|
||||
}
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
cErr := tx.Table(template.TableName).CreateInBatches(&items, 1000).Error
|
||||
return cErr
|
||||
return sysExportTemplateService.importByGORM(tx, template.TableName, items)
|
||||
})
|
||||
}
|
||||
|
||||
func (sysExportTemplateService *SysExportTemplateService) parseExcelToMap(rows [][]string, templateInfoMap map[string]string) ([]map[string]interface{}, error) {
|
||||
var titleKeyMap = make(map[string]string)
|
||||
for key, title := range templateInfoMap {
|
||||
titleKeyMap[title] = key
|
||||
}
|
||||
|
||||
excelTitle := rows[0]
|
||||
for i, str := range excelTitle {
|
||||
excelTitle[i] = strings.TrimSpace(str)
|
||||
}
|
||||
values := rows[1:]
|
||||
items := make([]map[string]interface{}, 0, len(values))
|
||||
for _, row := range values {
|
||||
var item = make(map[string]interface{})
|
||||
for ii, value := range row {
|
||||
if ii >= len(excelTitle) {
|
||||
continue
|
||||
}
|
||||
if _, ok := titleKeyMap[excelTitle[ii]]; !ok {
|
||||
continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过
|
||||
}
|
||||
key := titleKeyMap[excelTitle[ii]]
|
||||
item[key] = value
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (sysExportTemplateService *SysExportTemplateService) importBySQL(tx *gorm.DB, sql string, items []map[string]interface{}) error {
|
||||
for _, item := range items {
|
||||
if err := tx.Exec(sql, item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sysExportTemplateService *SysExportTemplateService) importByGORM(tx *gorm.DB, tableName string, items []map[string]interface{}) error {
|
||||
needCreated := tx.Migrator().HasColumn(tableName, "created_at")
|
||||
needUpdated := tx.Migrator().HasColumn(tableName, "updated_at")
|
||||
|
||||
for _, item := range items {
|
||||
if item["created_at"] == nil && needCreated {
|
||||
item["created_at"] = time.Now()
|
||||
}
|
||||
if item["updated_at"] == nil && needUpdated {
|
||||
item["updated_at"] = time.Now()
|
||||
}
|
||||
}
|
||||
return tx.Table(tableName).CreateInBatches(&items, 1000).Error
|
||||
}
|
||||
|
||||
func getColumnName(n int) string {
|
||||
columnName := ""
|
||||
for n > 0 {
|
||||
|
||||
@@ -160,7 +160,7 @@ func createDatabase(dsn string, driver string, createSql string) error {
|
||||
// createTables 创建表(默认 dbInitHandler.initTables 行为)
|
||||
func createTables(ctx context.Context, inits initSlice) error {
|
||||
next, cancel := context.WithCancel(ctx)
|
||||
defer func(c func()) { c() }(cancel)
|
||||
defer cancel()
|
||||
for _, init := range inits {
|
||||
if init.TableCreated(next) {
|
||||
continue
|
||||
|
||||
@@ -73,7 +73,7 @@ func (h MssqlInitHandler) InitTables(ctx context.Context, inits initSlice) error
|
||||
|
||||
func (h MssqlInitHandler) InitData(ctx context.Context, inits initSlice) error {
|
||||
next, cancel := context.WithCancel(ctx)
|
||||
defer func(c func()) { c() }(cancel)
|
||||
defer cancel()
|
||||
for _, init := range inits {
|
||||
if init.DataInserted(next) {
|
||||
color.Info.Printf(InitDataExist, Mssql, init.InitializerName())
|
||||
|
||||
@@ -78,7 +78,7 @@ func (h MysqlInitHandler) InitTables(ctx context.Context, inits initSlice) error
|
||||
|
||||
func (h MysqlInitHandler) InitData(ctx context.Context, inits initSlice) error {
|
||||
next, cancel := context.WithCancel(ctx)
|
||||
defer func(c func()) { c() }(cancel)
|
||||
defer cancel()
|
||||
for _, init := range inits {
|
||||
if init.DataInserted(next) {
|
||||
color.Info.Printf(InitDataExist, Mysql, init.InitializerName())
|
||||
|
||||
@@ -82,7 +82,7 @@ func (h PgsqlInitHandler) InitTables(ctx context.Context, inits initSlice) error
|
||||
|
||||
func (h PgsqlInitHandler) InitData(ctx context.Context, inits initSlice) error {
|
||||
next, cancel := context.WithCancel(ctx)
|
||||
defer func(c func()) { c() }(cancel)
|
||||
defer cancel()
|
||||
for i := 0; i < len(inits); i++ {
|
||||
if inits[i].DataInserted(next) {
|
||||
color.Info.Printf(InitDataExist, Pgsql, inits[i].InitializerName())
|
||||
|
||||
@@ -69,7 +69,7 @@ func (h SqliteInitHandler) InitTables(ctx context.Context, inits initSlice) erro
|
||||
|
||||
func (h SqliteInitHandler) InitData(ctx context.Context, inits initSlice) error {
|
||||
next, cancel := context.WithCancel(ctx)
|
||||
defer func(c func()) { c() }(cancel)
|
||||
defer cancel()
|
||||
for _, init := range inits {
|
||||
if init.DataInserted(next) {
|
||||
color.Info.Printf(InitDataExist, Sqlite, init.InitializerName())
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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/system"
|
||||
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
)
|
||||
|
||||
type LoginLogService struct{}
|
||||
|
||||
var LoginLogServiceApp = new(LoginLogService)
|
||||
|
||||
func (loginLogService *LoginLogService) CreateLoginLog(loginLog system.SysLoginLog) (err error) {
|
||||
err = global.GVA_DB.Create(&loginLog).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (loginLogService *LoginLogService) DeleteLoginLogByIds(ids request.IdsReq) (err error) {
|
||||
err = global.GVA_DB.Delete(&[]system.SysLoginLog{}, "id in (?)", ids.Ids).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (loginLogService *LoginLogService) DeleteLoginLog(loginLog system.SysLoginLog) (err error) {
|
||||
err = global.GVA_DB.Delete(&loginLog).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (loginLogService *LoginLogService) GetLoginLog(id uint) (loginLog system.SysLoginLog, err error) {
|
||||
err = global.GVA_DB.Where("id = ?", id).First(&loginLog).Error
|
||||
return
|
||||
}
|
||||
|
||||
func (loginLogService *LoginLogService) GetLoginLogInfoList(info systemReq.SysLoginLogSearch) (list interface{}, total int64, err error) {
|
||||
limit := info.PageSize
|
||||
offset := info.PageSize * (info.Page - 1)
|
||||
// 创建db
|
||||
db := global.GVA_DB.Model(&system.SysLoginLog{})
|
||||
var loginLogs []system.SysLoginLog
|
||||
// 如果有条件搜索 下方会自动创建搜索语句
|
||||
if info.Username != "" {
|
||||
db = db.Where("username LIKE ?", "%"+info.Username+"%")
|
||||
}
|
||||
if info.Status != false {
|
||||
db = db.Where("status = ?", info.Status)
|
||||
}
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.Limit(limit).Offset(offset).Order("id desc").Preload("User").Find(&loginLogs).Error
|
||||
return loginLogs, total, err
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"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"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
skillFileName = "SKILL.md"
|
||||
globalConstraintFileName = "README.md"
|
||||
)
|
||||
|
||||
var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"}
|
||||
|
||||
var skillToolDirs = map[string]string{
|
||||
"copilot": ".aone_copilot",
|
||||
"claude": ".claude",
|
||||
"trae": ".trae",
|
||||
"codex": ".codex",
|
||||
"cursor": ".cursor",
|
||||
}
|
||||
|
||||
var skillToolLabels = map[string]string{
|
||||
"copilot": "Copilot",
|
||||
"claude": "Claude",
|
||||
"trae": "Trae",
|
||||
"codex": "Codex",
|
||||
"cursor": "Cursor",
|
||||
}
|
||||
|
||||
const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n"
|
||||
|
||||
const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n"
|
||||
|
||||
const defaultReferenceMarkdown = "# 参考资料\n请在这里补充参考资料内容。\n"
|
||||
|
||||
const defaultTemplateMarkdown = "# 模板\n请在这里补充模板内容。\n"
|
||||
|
||||
const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n"
|
||||
|
||||
type SkillsService struct{}
|
||||
|
||||
func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) {
|
||||
tools := make([]system.SkillTool, 0, len(skillToolOrder))
|
||||
for _, key := range skillToolOrder {
|
||||
if _, err := s.toolSkillsDir(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]})
|
||||
}
|
||||
return tools, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(skillsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var skills []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
skills = append(skills, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(skills)
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) {
|
||||
var detail system.SkillDetail
|
||||
if !isSafeName(skill) {
|
||||
return detail, errors.New("技能名称不合法")
|
||||
}
|
||||
detail.Tool = tool
|
||||
detail.Skill = skill
|
||||
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return detail, err
|
||||
}
|
||||
|
||||
skillFilePath := filepath.Join(skillDir, skillFileName)
|
||||
content, err := os.ReadFile(skillFilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return detail, err
|
||||
}
|
||||
detail.Meta = system.SkillMeta{Name: skill}
|
||||
detail.Markdown = defaultSkillMarkdown
|
||||
} else {
|
||||
meta, body, parseErr := parseSkillContent(string(content))
|
||||
if parseErr != nil {
|
||||
meta = system.SkillMeta{Name: skill}
|
||||
body = string(content)
|
||||
}
|
||||
if meta.Name == "" {
|
||||
meta.Name = skill
|
||||
}
|
||||
detail.Meta = meta
|
||||
detail.Markdown = body
|
||||
}
|
||||
|
||||
detail.Scripts = listFiles(filepath.Join(skillDir, "scripts"))
|
||||
detail.Resources = listFiles(filepath.Join(skillDir, "resources"))
|
||||
detail.References = listFiles(filepath.Join(skillDir, "references"))
|
||||
detail.Templates = listFiles(filepath.Join(skillDir, "templates"))
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error {
|
||||
if !isSafeName(req.Skill) {
|
||||
return errors.New("技能名称不合法")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Meta.Name == "" {
|
||||
req.Meta.Name = req.Skill
|
||||
}
|
||||
content, err := buildSkillContent(req.Meta, req.Markdown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.SyncTools) > 0 {
|
||||
for _, tool := range req.SyncTools {
|
||||
if tool == req.Tool {
|
||||
continue
|
||||
}
|
||||
targetDir, err := s.ensureSkillDir(tool, req.Skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copySkillDir(skillDir, targetDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) {
|
||||
if !isSafeName(req.Skill) {
|
||||
return "", "", errors.New("技能名称不合法")
|
||||
}
|
||||
fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if lang == "" {
|
||||
return "", "", errors.New("脚本类型不支持")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(req.Tool, req.Skill)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, "scripts", fileName)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
return "", "", errors.New("脚本已存在")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
content := scriptTemplate(lang)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return fileName, content, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "resources", req.FileName, defaultResourceMarkdown, "资源")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateReference(_ context.Context, req request.SkillReferenceCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "references", req.FileName, defaultReferenceMarkdown, "参考")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetReference(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "references", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveReference(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "references", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) CreateTemplate(_ context.Context, req request.SkillTemplateCreateRequest) (string, string, error) {
|
||||
return s.createMarkdownFile(req.Tool, req.Skill, "templates", req.FileName, defaultTemplateMarkdown, "模板")
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetTemplate(_ context.Context, req request.SkillFileRequest) (string, error) {
|
||||
return s.readSkillFile(req.Tool, req.Skill, "templates", req.FileName)
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveTemplate(_ context.Context, req request.SkillFileSaveRequest) error {
|
||||
return s.writeSkillFile(req.Tool, req.Skill, "templates", req.FileName, req.Content)
|
||||
}
|
||||
|
||||
func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
filePath := filepath.Join(skillsDir, globalConstraintFileName)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return defaultGlobalConstraintMarkdown, false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return string(content), true, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error {
|
||||
if strings.TrimSpace(req.Tool) == "" {
|
||||
return errors.New("工具类型不能为空")
|
||||
}
|
||||
writeConstraint := func(tool, content string) error {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(skillsDir, globalConstraintFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
if err := writeConstraint(req.Tool, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(req.SyncTools) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, tool := range req.SyncTools {
|
||||
if tool == "" || tool == req.Tool {
|
||||
continue
|
||||
}
|
||||
if err := writeConstraint(tool, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) toolSkillsDir(tool string) (string, error) {
|
||||
toolDir, ok := skillToolDirs[tool]
|
||||
if !ok {
|
||||
return "", errors.New("工具类型不支持")
|
||||
}
|
||||
root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root)
|
||||
if root == "" {
|
||||
root = "."
|
||||
}
|
||||
skillsDir := filepath.Join(root, toolDir, "skills")
|
||||
if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return skillsDir, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) skillDir(tool, skill string) (string, error) {
|
||||
skillsDir, err := s.toolSkillsDir(tool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(skillsDir, skill), nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", errors.New("技能名称不合法")
|
||||
}
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(skillDir, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return skillDir, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) createMarkdownFile(tool, skill, subDir, fileName, defaultContent, label string) (string, string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", "", errors.New("技能名称不合法")
|
||||
}
|
||||
cleanName, err := buildResourceFileName(fileName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, cleanName)
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if label == "" {
|
||||
label = "文件"
|
||||
}
|
||||
return "", "", fmt.Errorf("%s已存在", label)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
content := defaultContent
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return cleanName, content, nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) {
|
||||
if !isSafeName(skill) {
|
||||
return "", errors.New("技能名称不合法")
|
||||
}
|
||||
if !isSafeFileName(fileName) {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
skillDir, err := s.skillDir(tool, skill)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, fileName)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error {
|
||||
if !isSafeName(skill) {
|
||||
return errors.New("技能名称不合法")
|
||||
}
|
||||
if !isSafeFileName(fileName) {
|
||||
return errors.New("文件名不合法")
|
||||
}
|
||||
skillDir, err := s.ensureSkillDir(tool, skill)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(skillDir, subDir, fileName)
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func parseSkillContent(content string) (system.SkillMeta, string, error) {
|
||||
clean := strings.TrimPrefix(content, "\ufeff")
|
||||
lines := strings.Split(clean, "\n")
|
||||
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
||||
return system.SkillMeta{}, clean, nil
|
||||
}
|
||||
end := -1
|
||||
for i := 1; i < len(lines); i++ {
|
||||
if strings.TrimSpace(lines[i]) == "---" {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if end == -1 {
|
||||
return system.SkillMeta{}, clean, nil
|
||||
}
|
||||
yamlText := strings.Join(lines[1:end], "\n")
|
||||
body := strings.Join(lines[end+1:], "\n")
|
||||
var meta system.SkillMeta
|
||||
if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil {
|
||||
return system.SkillMeta{}, body, err
|
||||
}
|
||||
return meta, body, nil
|
||||
}
|
||||
|
||||
func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) {
|
||||
if meta.Name == "" {
|
||||
return "", errors.New("name不能为空")
|
||||
}
|
||||
data, err := yaml.Marshal(meta)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
yamlText := strings.TrimRight(string(data), "\n")
|
||||
body := strings.TrimLeft(markdown, "\n")
|
||||
if body != "" {
|
||||
body = body + "\n"
|
||||
}
|
||||
return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil
|
||||
}
|
||||
|
||||
func listFiles(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
files := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Type().IsRegular() {
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
return files
|
||||
}
|
||||
|
||||
func isSafeName(name string) bool {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Base(name)
|
||||
}
|
||||
|
||||
func isSafeFileName(name string) bool {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(name, "..") {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Base(name)
|
||||
}
|
||||
|
||||
func buildScriptFileName(fileName, scriptType string) (string, string, error) {
|
||||
clean := strings.TrimSpace(fileName)
|
||||
if clean == "" {
|
||||
return "", "", errors.New("文件名不能为空")
|
||||
}
|
||||
if !isSafeFileName(clean) {
|
||||
return "", "", errors.New("文件名不合法")
|
||||
}
|
||||
base := strings.TrimSuffix(clean, filepath.Ext(clean))
|
||||
if base == "" {
|
||||
return "", "", errors.New("文件名不合法")
|
||||
}
|
||||
|
||||
switch strings.ToLower(scriptType) {
|
||||
case "py", "python":
|
||||
return base + ".py", "python", nil
|
||||
case "js", "javascript", "script":
|
||||
return base + ".js", "javascript", nil
|
||||
case "sh", "shell", "bash":
|
||||
return base + ".sh", "sh", nil
|
||||
default:
|
||||
return "", "", errors.New("脚本类型不支持")
|
||||
}
|
||||
}
|
||||
|
||||
func buildResourceFileName(fileName string) (string, error) {
|
||||
clean := strings.TrimSpace(fileName)
|
||||
if clean == "" {
|
||||
return "", errors.New("文件名不能为空")
|
||||
}
|
||||
if !isSafeFileName(clean) {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
base := strings.TrimSuffix(clean, filepath.Ext(clean))
|
||||
if base == "" {
|
||||
return "", errors.New("文件名不合法")
|
||||
}
|
||||
return base + ".md", nil
|
||||
}
|
||||
|
||||
func scriptTemplate(lang string) string {
|
||||
switch lang {
|
||||
case "python":
|
||||
return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n"
|
||||
case "javascript":
|
||||
return "// TODO: 在这里实现脚本逻辑\n"
|
||||
case "sh":
|
||||
return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func copySkillDir(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, os.ModePerm)
|
||||
}
|
||||
if !d.Type().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, 0644)
|
||||
})
|
||||
}
|
||||
@@ -46,6 +46,15 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
entities := []sysModel.SysApi{
|
||||
{ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"},
|
||||
|
||||
{ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLog", Description: "删除登录日志"},
|
||||
{ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLogByIds", Description: "批量删除登录日志"},
|
||||
{ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/findLoginLog", Description: "根据ID获取登录日志"},
|
||||
{ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/getLoginLogList", Description: "获取登录日志列表"},
|
||||
|
||||
{ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/createApiToken", Description: "签发API Token"},
|
||||
{ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/getApiTokenList", Description: "获取API Token列表"},
|
||||
{ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/deleteApiToken", Description: "作废API Token"},
|
||||
|
||||
{ApiGroup: "系统用户", Method: "DELETE", Path: "/user/deleteUser", Description: "删除用户"},
|
||||
{ApiGroup: "系统用户", Method: "POST", Path: "/user/admin_register", Description: "用户注册"},
|
||||
{ApiGroup: "系统用户", Method: "POST", Path: "/user/getUserList", Description: "获取用户列表"},
|
||||
@@ -105,6 +114,25 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "系统服务", Method: "POST", Path: "/system/getSystemConfig", Description: "获取配置文件内容"},
|
||||
{ApiGroup: "系统服务", Method: "POST", Path: "/system/setSystemConfig", Description: "设置配置文件内容"},
|
||||
|
||||
{ApiGroup: "skills", Method: "GET", Path: "/skills/getTools", Description: "获取技能工具列表"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillList", Description: "获取技能列表"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillDetail", Description: "获取技能详情"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveSkill", Description: "保存技能定义"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/createScript", Description: "创建技能脚本"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getScript", Description: "读取技能脚本"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveScript", Description: "保存技能脚本"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/createResource", Description: "创建技能资源"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getResource", Description: "读取技能资源"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveResource", Description: "保存技能资源"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/createReference", Description: "创建技能参考"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getReference", Description: "读取技能参考"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveReference", Description: "保存技能参考"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/createTemplate", Description: "创建技能模板"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getTemplate", Description: "读取技能模板"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveTemplate", Description: "保存技能模板"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/getGlobalConstraint", Description: "读取全局约束"},
|
||||
{ApiGroup: "skills", Method: "POST", Path: "/skills/saveGlobalConstraint", Description: "保存全局约束"},
|
||||
|
||||
{ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"},
|
||||
{ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"},
|
||||
{ApiGroup: "客户", Method: "DELETE", Path: "/customer/customer", Description: "删除客户"},
|
||||
@@ -118,6 +146,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/removePlugin", Description: "卸载插件"},
|
||||
{ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getPluginList", Description: "获取已安装插件"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"},
|
||||
{ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"},
|
||||
@@ -149,6 +179,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"},
|
||||
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典(建议选择)"},
|
||||
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"},
|
||||
{ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"},
|
||||
{ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"},
|
||||
|
||||
{ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"},
|
||||
{ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"},
|
||||
@@ -175,8 +207,17 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) {
|
||||
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"},
|
||||
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"},
|
||||
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"},
|
||||
{ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"},
|
||||
{ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"},
|
||||
|
||||
{ApiGroup: "错误日志", Method: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"},
|
||||
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"},
|
||||
{ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"},
|
||||
{ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"},
|
||||
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"},
|
||||
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"},
|
||||
{ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"},
|
||||
|
||||
{ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"},
|
||||
{ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"},
|
||||
{ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfoByIds", Description: "批量删除公告"},
|
||||
|
||||
@@ -47,6 +47,15 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
entities := []adapter.CasbinRule{
|
||||
{Ptype: "p", V0: "888", V1: "/user/admin_register", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLog", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLogByIds", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysLoginLog/findLoginLog", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysLoginLog/getLoginLogList", V2: "GET"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/sysApiToken/createApiToken", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysApiToken/getApiTokenList", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysApiToken/deleteApiToken", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/api/createApi", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/api/getApiList", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/api/getApiById", V2: "POST"},
|
||||
@@ -107,6 +116,25 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/system/setSystemConfig", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/system/getServerInfo", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getTools", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getSkillList", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getSkillDetail", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveSkill", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/createScript", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getScript", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveScript", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/createResource", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getResource", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveResource", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/createReference", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getReference", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveReference", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/createTemplate", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getTemplate", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveTemplate", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/getGlobalConstraint", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/skills/saveGlobalConstraint", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/customer/customer", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/customer/customer", V2: "PUT"},
|
||||
{Ptype: "p", V0: "888", V1: "/customer/customer", V2: "POST"},
|
||||
@@ -129,6 +157,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/createPlug", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/removePlugin", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/getPluginList", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"},
|
||||
@@ -149,6 +179,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysDictionary/importSysDictionary", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"},
|
||||
@@ -176,8 +208,17 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error
|
||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/previewSQL", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/createSysError", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"},
|
||||
{Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"},
|
||||
|
||||
{Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"},
|
||||
{Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"},
|
||||
{Ptype: "p", V0: "888", V1: "/info/deleteInfoByIds", V2: "DELETE"},
|
||||
|
||||
@@ -96,13 +96,17 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 5, Meta: Meta{Title: "登录日志", Icon: "monitor"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 6, Meta: Meta{Title: "API Token", Icon: "key"}},
|
||||
{MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "skills", Name: "skills", Component: "view/systemTools/skills/index.vue", Sort: 6, Meta: Meta{Title: "Skills管理", Icon: "document"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 9, Meta: Meta{Title: "错误日志", Icon: "warn"}},
|
||||
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}},
|
||||
{MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}},
|
||||
|
||||
@@ -304,3 +304,106 @@ func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool {
|
||||
})
|
||||
return exists
|
||||
}
|
||||
|
||||
func CreateDictionaryStructAst(dictionaries []system.SysDictionary) *[]ast.Expr {
|
||||
var dictElts []ast.Expr
|
||||
for i := range dictionaries {
|
||||
statusStr := "true"
|
||||
if dictionaries[i].Status != nil && !*dictionaries[i].Status {
|
||||
statusStr = "false"
|
||||
}
|
||||
|
||||
elts := []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Name"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Name)},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Type"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Type)},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Status"},
|
||||
Value: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "utils"},
|
||||
Sel: &ast.Ident{Name: "Pointer"},
|
||||
},
|
||||
Args: []ast.Expr{
|
||||
&ast.Ident{Name: statusStr},
|
||||
},
|
||||
},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Desc"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Desc)},
|
||||
},
|
||||
}
|
||||
|
||||
if len(dictionaries[i].SysDictionaryDetails) > 0 {
|
||||
var detailElts []ast.Expr
|
||||
for _, detail := range dictionaries[i].SysDictionaryDetails {
|
||||
detailStatusStr := "true"
|
||||
if detail.Status != nil && !*detail.Status {
|
||||
detailStatusStr = "false"
|
||||
}
|
||||
|
||||
detailElts = append(detailElts, &ast.CompositeLit{
|
||||
Type: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "model"},
|
||||
Sel: &ast.Ident{Name: "SysDictionaryDetail"},
|
||||
},
|
||||
Elts: []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Label"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Label)},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Value"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Value)},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Extend"},
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Extend)},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Status"},
|
||||
Value: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "utils"},
|
||||
Sel: &ast.Ident{Name: "Pointer"},
|
||||
},
|
||||
Args: []ast.Expr{
|
||||
&ast.Ident{Name: detailStatusStr},
|
||||
},
|
||||
},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "Sort"},
|
||||
Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", detail.Sort)},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
elts = append(elts, &ast.KeyValueExpr{
|
||||
Key: &ast.Ident{Name: "SysDictionaryDetails"},
|
||||
Value: &ast.CompositeLit{
|
||||
Type: &ast.ArrayType{Elt: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "model"},
|
||||
Sel: &ast.Ident{Name: "SysDictionaryDetail"},
|
||||
}},
|
||||
Elts: detailElts,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
dictElts = append(dictElts, &ast.CompositeLit{
|
||||
Type: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "model"},
|
||||
Sel: &ast.Ident{Name: "SysDictionary"},
|
||||
},
|
||||
Elts: elts,
|
||||
})
|
||||
}
|
||||
return &dictElts
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const (
|
||||
TypePluginGen = "PluginGen" // server/plugin/{package}/gen/main.go
|
||||
TypePluginApiEnter = "PluginApiEnter" // server/plugin/{package}/enter.go
|
||||
TypePluginInitializeV1 = "PluginInitializeV1" // server/initialize/plugin_biz_v1.go
|
||||
TypePluginInitializeV2 = "PluginInitializeV2" // server/initialize/plugin_biz_v2.go
|
||||
TypePluginInitializeV2 = "PluginInitializeV2" // server/plugin/register.go
|
||||
TypePluginRouterEnter = "PluginRouterEnter" // server/plugin/{package}/enter.go
|
||||
TypePluginServiceEnter = "PluginServiceEnter" // server/plugin/{package}/enter.go
|
||||
TypePluginInitializeApi = "PluginInitializeApi" // server/plugin/{package}/initialize/api.go
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码
|
||||
// 返回:方法名、完整源码、起止行号
|
||||
func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) {
|
||||
// 读取源文件
|
||||
src, readErr := os.ReadFile(filePath)
|
||||
if readErr != nil {
|
||||
err = fmt.Errorf("read file failed: %w", readErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 AST
|
||||
fset := token.NewFileSet()
|
||||
file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments)
|
||||
if parseErr != nil {
|
||||
err = fmt.Errorf("parse file failed: %w", parseErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 在 AST 中定位包含指定行号的函数声明
|
||||
var target *ast.FuncDecl
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
fd, ok := n.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
s := fset.Position(fd.Pos()).Line
|
||||
e := fset.Position(fd.End()).Line
|
||||
if line >= s && line <= e {
|
||||
target = fd
|
||||
startLine = s
|
||||
endLine = e
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if target == nil {
|
||||
err = fmt.Errorf("no function encloses line %d in %s", line, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用字节偏移精确提取源码片段(包含注释与原始格式)
|
||||
start := fset.Position(target.Pos()).Offset
|
||||
end := fset.Position(target.End()).Offset
|
||||
if start < 0 || end > len(src) || start >= end {
|
||||
err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src))
|
||||
return
|
||||
}
|
||||
source = string(src[start:end])
|
||||
name = target.Name.Name
|
||||
return
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PluginInitializeV2 struct {
|
||||
@@ -31,12 +33,40 @@ func (a *PluginInitializeV2) Parse(filename string, writer io.Writer) (file *ast
|
||||
}
|
||||
|
||||
func (a *PluginInitializeV2) Injection(file *ast.File) error {
|
||||
if !CheckImport(file, a.ImportPath) {
|
||||
NewImport(a.ImportPath).Injection(file)
|
||||
funcDecl := FindFunction(file, "bizPluginV2")
|
||||
stmt := CreateStmt(fmt.Sprintf("PluginInitV2(engine, %s.Plugin)", a.PackageName))
|
||||
funcDecl.Body.List = append(funcDecl.Body.List, stmt)
|
||||
importPath := strings.TrimSpace(a.ImportPath)
|
||||
if importPath == "" {
|
||||
return nil
|
||||
}
|
||||
importPath = strings.Trim(importPath, "\"")
|
||||
if importPath == "" || CheckImport(file, importPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
importSpec := &ast.ImportSpec{
|
||||
Name: ast.NewIdent("_"),
|
||||
Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(importPath)},
|
||||
}
|
||||
var importDecl *ast.GenDecl
|
||||
for _, decl := range file.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
importDecl = genDecl
|
||||
break
|
||||
}
|
||||
}
|
||||
if importDecl == nil {
|
||||
file.Decls = append([]ast.Decl{
|
||||
&ast.GenDecl{
|
||||
Tok: token.IMPORT,
|
||||
Specs: []ast.Spec{importSpec},
|
||||
},
|
||||
}, file.Decls...)
|
||||
return nil
|
||||
}
|
||||
importDecl.Specs = append(importDecl.Specs, importSpec)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ func TestPluginInitialize_Injection(t *testing.T) {
|
||||
name: "测试 Gva插件 注册注入",
|
||||
fields: fields{
|
||||
Type: TypePluginInitializeV2,
|
||||
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"),
|
||||
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
|
||||
ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`,
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -37,12 +37,12 @@ func TestPluginInitialize_Injection(t *testing.T) {
|
||||
PluginPath: tt.fields.PluginPath,
|
||||
ImportPath: tt.fields.ImportPath,
|
||||
}
|
||||
file, err := a.Parse(a.Path, nil)
|
||||
file, err := a.Parse("", nil)
|
||||
if err != nil {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
a.Injection(file)
|
||||
err = a.Format(a.Path, nil, file)
|
||||
err = a.Format("", nil, file)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@@ -69,8 +69,8 @@ func TestPluginInitialize_Rollback(t *testing.T) {
|
||||
name: "测试 Gva插件 回滚",
|
||||
fields: fields{
|
||||
Type: TypePluginInitializeV2,
|
||||
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "plugin_biz_v2.go"),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"),
|
||||
Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"),
|
||||
PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"),
|
||||
ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva"`,
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -86,12 +86,12 @@ func TestPluginInitialize_Rollback(t *testing.T) {
|
||||
StructName: "Plugin",
|
||||
PackageName: "gva",
|
||||
}
|
||||
file, err := a.Parse(a.Path, nil)
|
||||
file, err := a.Parse("", nil)
|
||||
if err != nil {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
a.Rollback(file)
|
||||
err = a.Format(a.Path, nil, file)
|
||||
err = a.Format("", nil, file)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string {
|
||||
if field.FieldType == "array" {
|
||||
multipleAttr = "multiple "
|
||||
}
|
||||
result += fmt.Sprintf(` <el-tree-select v-model="formData.%s" placeholder="请选择%s" :data="%sOptions" style="width:100%%" filterable :clearable="%v" check-strictly %s></el-tree-select>
|
||||
result += fmt.Sprintf(` <el-tree-select v-model="searchInfo.%s" placeholder="请选择%s" :data="%sOptions" style="width:100%%" filterable :clearable="%v" check-strictly %s></el-tree-select>
|
||||
`,
|
||||
field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr)
|
||||
} else if field.CheckDataSource {
|
||||
|
||||
@@ -24,6 +24,9 @@ const (
|
||||
//@return: error, string
|
||||
|
||||
func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) {
|
||||
if strings.Contains(fileName, "..") || strings.Contains(fileMd5, "..") {
|
||||
return "", errors.New("文件名或路径不合法")
|
||||
}
|
||||
path := breakpointDir + fileMd5 + "/"
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
@@ -62,13 +65,13 @@ func makeFileContent(content []byte, fileName string, FileDir string, contentNum
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return path, err
|
||||
} else {
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
@@ -79,6 +82,9 @@ func makeFileContent(content []byte, fileName string, FileDir string, contentNum
|
||||
//@return: error, string
|
||||
|
||||
func MakeFile(fileName string, FileMd5 string) (string, error) {
|
||||
if strings.Contains(fileName, "..") || strings.Contains(FileMd5, "..") {
|
||||
return "", errors.New("文件名或路径不合法")
|
||||
}
|
||||
rd, err := os.ReadDir(breakpointDir + FileMd5)
|
||||
if err != nil {
|
||||
return finishDir + fileName, err
|
||||
@@ -107,6 +113,9 @@ func MakeFile(fileName string, FileMd5 string) (string, error) {
|
||||
//@return: error
|
||||
|
||||
func RemoveChunk(FileMd5 string) error {
|
||||
if strings.Contains(FileMd5, "..") {
|
||||
return errors.New("路径不合法")
|
||||
}
|
||||
err := os.RemoveAll(breakpointDir + FileMd5)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func GetToken(c *gin.Context) string {
|
||||
global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在x-token且claims是否为规定结构")
|
||||
return token
|
||||
}
|
||||
SetToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60))
|
||||
SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix()))
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package plugin
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry []Plugin
|
||||
)
|
||||
|
||||
// Register records a plugin for auto initialization.
|
||||
func Register(p Plugin) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
registryMu.Lock()
|
||||
registry = append(registry, p)
|
||||
registryMu.Unlock()
|
||||
}
|
||||
|
||||
// Registered returns a snapshot of all registered plugins.
|
||||
func Registered() []Plugin {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
out := make([]Plugin, len(registry))
|
||||
copy(out, registry)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stacktrace
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame 表示一次栈帧解析结果
|
||||
type Frame struct {
|
||||
File string
|
||||
Line int
|
||||
Func string
|
||||
}
|
||||
|
||||
var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`)
|
||||
|
||||
// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号
|
||||
// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件
|
||||
func FindFinalCaller(stack string) (Frame, bool) {
|
||||
if stack == "" {
|
||||
return Frame{}, false
|
||||
}
|
||||
lines := strings.Split(stack, "\n")
|
||||
var currFunc string
|
||||
for i := 0; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if m := fileLineRe.FindStringSubmatch(line); m != nil {
|
||||
file := m[1]
|
||||
ln, _ := strconv.Atoi(m[2])
|
||||
if shouldSkip(file) {
|
||||
// 跳过此帧,同时重置函数名以避免错误配对
|
||||
currFunc = ""
|
||||
continue
|
||||
}
|
||||
return Frame{File: file, Line: ln, Func: currFunc}, true
|
||||
}
|
||||
// 记录函数名行,下一行通常是文件:行
|
||||
currFunc = line
|
||||
}
|
||||
return Frame{}, false
|
||||
}
|
||||
|
||||
func shouldSkip(file string) bool {
|
||||
// 第三方库与 Go 模块缓存
|
||||
if strings.Contains(file, "/go/pkg/mod/") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/go.uber.org/") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/gorm.io/") {
|
||||
return true
|
||||
}
|
||||
// 标准库
|
||||
if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go
|
||||
return true
|
||||
}
|
||||
// 框架内不需要作为最终调用方的路径
|
||||
if strings.Contains(file, "/server/core/zap.go") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/server/core/") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/server/utils/errorhook/") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/server/middleware/") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(file, "/server/router/") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -42,6 +42,7 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
|
||||
Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket),
|
||||
Key: aws.String(filename),
|
||||
Body: f,
|
||||
ContentType: aws.String(file.Header.Get("Content-Type")),
|
||||
})
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error()))
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gin-vue-admin",
|
||||
"version": "2.8.6",
|
||||
"version": "2.8.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node openDocument.js && vite --host --mode development",
|
||||
@@ -45,7 +45,7 @@
|
||||
"spark-md5": "^3.0.2",
|
||||
"universal-cookie": "^7",
|
||||
"vform3-builds": "^3.0.10",
|
||||
"vite-auto-import-svg": "^1.9.0",
|
||||
"vite-auto-import-svg": "^2.1.0",
|
||||
"vue": "^3.5.7",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-echarts": "^7.0.3",
|
||||
@@ -79,6 +79,7 @@
|
||||
"sass": "^1.78.0",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^6.2.3",
|
||||
"vite-check-multiple-dom": "0.2.1",
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"vite-plugin-importer": "^0.2.5",
|
||||
"vite-plugin-vue-devtools": "^7.0.16"
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
id="app"
|
||||
class="bg-gray-50 text-slate-700 !dark:text-slate-500 dark:bg-slate-800"
|
||||
>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<el-config-provider :locale="zhCn" :size="appStore.config.global_size">
|
||||
<router-view />
|
||||
<Application />
|
||||
</el-config-provider>
|
||||
@@ -15,7 +15,7 @@
|
||||
import Application from '@/components/application/index.vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
useAppStore()
|
||||
const appStore = useAppStore()
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
|
||||
+39
-32
@@ -143,7 +143,7 @@ export const llmAuto = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/llmAuto',
|
||||
method: 'post',
|
||||
data: { ...data, mode: 'ai' },
|
||||
data: { ...data },
|
||||
timeout: 1000 * 60 * 10,
|
||||
loadingOption: {
|
||||
lock: true,
|
||||
@@ -153,37 +153,6 @@ export const llmAuto = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const butler = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/llmAuto',
|
||||
method: 'post',
|
||||
data: { ...data, mode: 'butler' },
|
||||
timeout: 1000 * 60 * 10
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const eye = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/llmAuto',
|
||||
method: 'post',
|
||||
data: { ...data, mode: 'eye' },
|
||||
timeout: 1000 * 60 * 10
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const createWeb = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/llmAuto',
|
||||
method: 'post',
|
||||
data: { ...data, mode: 'painter' },
|
||||
timeout: 1000 * 60 * 10
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const addFunc = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/addFunc',
|
||||
@@ -208,6 +177,14 @@ export const initAPI = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const initDictionary = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/initDictionary',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const mcp = (data) => {
|
||||
return service({
|
||||
url: '/autoCode/mcp',
|
||||
@@ -233,3 +210,33 @@ export const mcpTest = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags SysApi
|
||||
// @Summary 获取插件列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
|
||||
// @Router /autoCode/getPluginList [get]
|
||||
export const getPluginList = (params) => {
|
||||
return service({
|
||||
url: '/autoCode/getPluginList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags SysApi
|
||||
// @Summary 删除插件
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||
// @Router /autoCode/removePlugin [post]
|
||||
export const removePlugin = (params) => {
|
||||
return service({
|
||||
url: '/autoCode/removePlugin',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user