Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andpixelmaxQm 7e272a9f8c feat: replace process.env manual loading with Vite's native loadEnv
Co-authored-by: pixelmaxQm <165128580+pixelmaxQm@users.noreply.github.com>
2026-03-03 05:59:02 +00:00
copilot-swe-agent[bot] 7602673ed3 Initial plan 2026-03-03 05:56:11 +00:00
piexlMax(奇淼 556b4431a2 fix: 修复获取x-token的方式,确保正确获取用户token 2026-02-10 16:15:40 +08:00
piexlMax(奇淼 9fea067931 feat: 更新富文本编辑器样式和结构,增强可读性 2026-02-10 11:44:10 +08:00
piexlMax(奇淼 fa9957c43c feat: 调整自动化插件的注册逻辑 2026-02-05 14:08:13 +08:00
piexlMax(奇淼 00224efd24 fix: 字典搜索bug 2026-02-05 13:50:33 +08:00
piexlMax(奇淼 506c9ea7a6 feat: 自动生成插件不再添加到注册功能 2026-02-05 13:44:26 +08:00
piexlMax(奇淼 05872354b8 feat: 补充丢失的自动迁移结构 2026-02-04 13:49:24 +08:00
piexlMax(奇淼 b71cc443c0 refactor: 调整ai工具rules 2026-02-03 10:18:34 +08:00
piexlMax(奇淼 1317e9504e feat: 增加技能配置界面的填写指引和模板功能 2026-02-02 18:06:19 +08:00
piexlMax(奇淼 670aff79cc feat: 增加技能参考和模板的创建、获取及保存功能 2026-02-02 18:06:11 +08:00
pixelmaxQM 52cd5e72ab feat: 更新版本号至v2.8.9 2026-02-01 23:42:20 +08:00
pixelmaxQM 68d6d3660b feat: 更新版本号至2.8.9 2026-02-01 22:26:03 +08:00
876017115c feat:发布2.8.9版本 (#2175)
* feat: ai相关调用调整

* feat: ai请求模式变更

* feat: ai自动化完善

* feat: 添加环境变量支持,条件渲染开发环境特定元素

* feat: 2.5版本插件,不再冗余进主项目,可以做到完全自动安装。

* feat: 添加插件管理功能,包括获取插件列表和删除插件的API接口

* feat: 同步测试插件的id调整

* feat: 增加插件相关初始化api

* feat: 增加登录日志

* feat: 增加签发token功能

* feat: 更安全的目录清理功能,确保仅在无Go文件时删除目录

* feat: 增加skills定义能力

* feat: 优化技能管理界面,增强用户体验和视觉效果

* feat: 更新授权链接,确保用户获取最新的授权信息

* feat: 修改技能定义器名称为“Skills管理”,并重命名文件为sys_skills.go

* feat: 增加全局约束的获取和保存功能,更新相关API和前端实现

* feat: 增加skills全局约束管理

* feat: 添加 vite-check-multiple-dom 插件,支持路由多根检测 (#2173)

---------

Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
Co-authored-by: Azir-11 <2075125282@qq.com>
Co-authored-by: 青菜白玉汤 <79054161+Azir-11@users.noreply.github.com>
2026-01-30 14:33:57 +08:00
piexlMax(奇淼 755373468c feat: 为mcp增加字段示例 2026-01-16 16:19:22 +08:00
piexlMax(奇淼 d2e64e618e fix: 重新实现组件缓存处理逻辑 2026-01-12 15:27:41 +08:00
piexlMax(奇淼 fd2a6f45fd fixed: 增加错误设置首页的补救措施 2026-01-12 15:11:59 +08:00
2242f5d6e1 public: 发布v2.8.8 (#2167)
* refactor: 移除冗余的工具名称和描述,简化代码生成器的文档

* Add Content-Type to S3 upload input

Add Content-Type to S3 upload input

* style: 优化仪表盘组件样式,调整各个子组件的布局和样式,提升视觉效果

* fix: 更新插件链接

* 优化角色配置-首页配置相关内容

* feat: 默认首页只允许选择已选中的菜单

* feat: 更新样式和优化组件,调整背景色及文本颜色

* feat: 清理无用的右侧边线

* feat: 增加了导入导出模板的自定义sql能力,方便灵活化扩展导入导出

* feat: 优化日志输出,增加插件字典定义

* feat: 增加插件字典定义前端相关

* feat: 增加插件自定义字典方法

* feat: 增加文件名和路径合法性检查

* feat: 修复暗黑模式和亮色模式logo路径定义

* feat: 移除未使用的defineEmits导入

* feat: 更新授权链接至新地址

* feat: 移除菜单组件背景色

* feat: 更新导出和导入SQL语句的示例占位符

* feat: 错误日志必须有数据库链接才会存储

* feat: 移除仪表板中公告卡片的高度限制

* feat: 添加插件列表接口,更新插件表格组件,移除不必要的注释

* feat: 更新版本号至v2.8.8,调整插件表格组件的请求参数

---------

Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
Co-authored-by: Yexk_M <yexk@yexk.cn>
Co-authored-by: Azir-11 <2075125282@qq.com>
2026-01-11 14:41:19 +08:00
PiexlMax(奇淼 567a8eb9bb fixed: 文件句柄泄露问题
文件句柄泄露问题
2026-01-11 14:34:21 +08:00
PiexlMax(奇淼 2ebeec8765 fixed: MD5 碰撞可能导致不同内容被误判为同一文件
MD5 碰撞可能导致不同内容被误判为同一文件
2026-01-11 14:33:47 +08:00
wike2019 70ee13e10c 文件句柄泄露问题 2026-01-11 13:13:59 +08:00
wike2019 ed65e31cd8 MD5 碰撞可能导致不同内容被误判为同一文件 2026-01-11 13:11:38 +08:00
PiexlMax(奇淼 f693f29326 feat: 优化unzip逻辑
io.Copy 后立即调用 Unzip,但 out 由 defer 关闭,可能在解压时尚未关闭
2026-01-11 12:40:34 +08:00
wike2019 2fda961643 io.Copy 后立即调用 Unzip,但 out 由 defer 关闭,可能在解压时尚未关闭 2026-01-10 16:00:10 +08:00
PiexlMax(奇淼 fd9883e035 Merge pull request #2158 from wike2019/pr/fix-cutter
fix bug 当日志文件删除失败时不阻塞日志写入
2026-01-06 11:18:58 +08:00
wike2019 f41f068b8b fix bug 当日志文件删除失败时不阻塞日志写入 2026-01-05 22:18:41 +08:00
PiexlMax(奇淼 799f411c73 Merge pull request #2150 from zayn-code/user-dialog
用户管理的侧边栏优化体验
2026-01-04 11:45:10 +08:00
PiexlMax(奇淼 97e7be9d3c Merge pull request #2157 from morya/main
FIX: code too complicated
2026-01-04 11:43:37 +08:00
morya 5cfe04e5f5 FIX: code too complicated 2026-01-03 23:56:05 +08:00
zayn 5f0d59a02b 其他页面都可以通过点击外层关闭侧边栏,用户界面不可以,养成习惯后体验不太好,故允许点击外层关闭侧边栏 2025-12-09 15:27:22 +08:00
pixelmaxQM 805f319578 fix: 优化文件上传时的数据库查询条件 2025-12-06 15:46:15 +08:00
piexlMax(奇淼 f23df92c18 feat: 调整获取logo的方式 2025-12-05 17:51:18 +08:00
piexlMax(奇淼 41d9453110 fix: 修复初始化没有创建SysError表的问题 2025-12-01 17:29:30 +08:00
piexlMax(奇淼 48d0cae58b Merge remote-tracking branch 'origin/main' 2025-12-01 17:26:55 +08:00
PiexlMax(奇淼 49af9439dc update: 2.8.7-beta发布
update: 2.8.7-beta发布
2025-12-01 17:21:02 +08:00
piexlMax(奇淼 1994497ad4 fix: 调整 gva-search-box 的内边距 2025-12-01 17:07:31 +08:00
piexlMax(奇淼 4ce191cdfa fix: 修复分片上传任意文件删除问题 2025-12-01 16:56:31 +08:00
piexlMax(奇淼 ee8d8d7e04 fix: 添加路径穿越拦截,禁止非法路径删除切片 2025-12-01 16:44:32 +08:00
PiexlMax(奇淼 cf022f492c Merge pull request #2146 from Azir-11/dev-287
添加全局组件大小配置和相关主题配置项,优化主题配置栏
2025-12-01 16:17:51 +08:00
piexlMax(奇淼 96019e8a9b feat: 版本号调整 2025-11-28 17:34:37 +08:00
piexlMax(奇淼 9ff1d4ca77 fix: 修复 vueDevTools 插件的缩进问题 2025-11-28 16:45:00 +08:00
Azir-11 8bc631ece9 feat: 重构优化系统配置相关 css 代码,优化了部分 UI 样式 2025-11-27 19:12:41 +08:00
Azir-11 0edb567ded feat: 添加全局组件大小配置和相关主题配置项 2025-11-27 18:40:22 +08:00
piexlMax(奇淼 d404fcf746 feat: 优化字典管理界面,增加按钮提示和调整按钮样式 2025-11-27 15:15:24 +08:00
piexlMax(奇淼 8af78e3af7 feat: 增加图片上传和粘贴识别功能,支持通过图片生成字典描述 2025-11-27 15:11:14 +08:00
piexlMax(奇淼 3722a8e7f3 feat: 增加AI生成字典功能,支持用户输入描述生成字典 2025-11-27 14:43:50 +08:00
piexlMax(奇淼 b86f7412d1 feat: 添加项目规则文档,详细描述功能、架构及开发规范 2025-11-27 14:04:13 +08:00
piexlMax(奇淼 ad52418f99 feat: 优化前端页面展示 2025-11-26 17:24:41 +08:00
piexlMax(奇淼 08ae44158f feat: 增加字典的json导出和导入 2025-11-26 16:40:48 +08:00
piexlMax(奇淼 a1f18e1386 feat: 增加字典的json导出和导入 2025-11-26 15:54:18 +08:00
PiexlMax(奇淼 c028ea3d15 Merge pull request #2145 from bypanghu/main
fix: 更新优化插件注册 api 和注册 menus 逻辑。
2025-11-24 18:34:54 +08:00
bypanghu f357c676bb fix: 更新优化插件注册 api 和注册 menus 逻辑。 2025-11-24 09:13:57 +08:00
piexlMax(奇淼 8022c662e0 fix(layout): 将Iframe路由名称更新为gvaLayoutIframe 2025-11-18 14:06:05 +08:00
piexlMax(奇淼 aa84605c4d fix(deps): 修复自动划代码字典查询条件失效问题 2025-11-17 17:49:50 +08:00
piexlMax(奇淼 287cb6ec9c feat: 优化error样式 2025-11-14 14:41:11 +08:00
piexlMax(奇淼 b9788e9b2a feat(错误日志): 添加异步处理错误日志功能 2025-11-12 18:02:50 +08:00
piexlMax(奇淼 a469ea23f4 refactor(zap): 重构日志信息格式以提高可读性 2025-11-11 18:21:14 +08:00
piexlMax(奇淼 c5f81d64c8 fix(错误处理): 统一前端错误类型显示为中文并优化错误详情布局 2025-11-11 18:18:23 +08:00
piexlMax(奇淼 78d135c6bf feat(日志): 增强日志功能以显示调用方源码 2025-11-11 18:11:32 +08:00
PiexlMax(奇淼 ff06aca146 Merge pull request #2138 from feitianbubu/pr/fix-cookie-duration
fix: correct cookie maxAge using seconds instead of minutes
2025-11-11 17:12:44 +08:00
PiexlMax(奇淼 849d5760da Merge pull request #2139 from feitianbubu/pr/fix-repeat-migrate-alter
fix: repeat alter gorm autoMigrate due to incorrect comments
2025-11-11 17:12:18 +08:00
piexlMax(奇淼 8cce9edbde feat(错误日志): 实现错误日志记录功能并修复拼写错误
添加错误日志记录功能,包括API、路由、服务和前端接口
修复sysError相关变量名的拼写错误
更新依赖版本至go 1.24
使用自定义Recovery中间件记录panic信息
添加zap日志钩子将错误日志写入数据库
2025-11-11 16:33:26 +08:00
piexlMax(奇淼 6bea7bc524 feat(导出模板): 添加SQL预览功能并优化模板展示界面 2025-11-10 11:05:03 +08:00
PiexlMax(奇淼 b6c0138dd2 feat: 统一配置命名 2025-11-09 16:15:22 +08:00
PiexlMax(奇淼 7658ff46c3 Update config.js 2025-11-09 16:10:40 +08:00
PiexlMax(奇淼 bd1aa1f559 Merge pull request #2140 from xue-ding-e/patch-7
bugfix:用户管理界面无法分配 / 查看角色
2025-11-09 16:09:29 +08:00
Qing Liang 27fb85f630 bugfix:用户管理界面无法分配 / 查看角色 2025-11-07 22:50:01 +08:00
feitianbubu 0be88bb442 fix: repeat alter gorm autoMigrate due to incorrect comments 2025-11-07 14:44:06 +08:00
feitianbubuandClaude a8642f4582 fix: correct cookie maxAge calculation by using seconds instead of minutes
The SetToken function expects maxAge in seconds (standard for HTTP cookies), but the code was incorrectly dividing by 60, causing cookies to expire 60 times faster than intended.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 10:54:11 +08:00
piexlMax(奇淼 5d67fc95d2 Merge branch 'main' into dev-287 2025-11-06 17:59:24 +08:00
piexlMax(奇淼 057ad2eeeb fix: 添加占位符字段防止json schema校验失败 2025-11-06 17:50:23 +08:00
piexlMax(奇淼 ca0a1250cd refactor(mcp): 修改execute定义 2025-11-06 17:50:23 +08:00
piexlMax(奇淼 6c82b34e8b fix: 添加占位符字段防止json schema校验失败 2025-11-06 17:43:09 +08:00
piexlMax(奇淼 fdc5e3cbf6 Merge branch 'dev-287' of github.com:flipped-aurora/gin-vue-admin into dev-287 2025-11-06 17:28:30 +08:00
piexlMax(奇淼 a0643d8ded refactor(mcp): 修改execute定义 2025-11-06 17:25:36 +08:00
PiexlMax(奇淼 83fb5d5921 Merge pull request #2107 from feitianbubu/pr/fix--dup-key-upload
feat: add check for existing file key before upload
2025-11-05 11:49:57 +08:00
PiexlMax(奇淼 a1070a64e7 Merge pull request #2132 from andrewbytecoder/main
fix(jwt):修正令牌过期时间计算错误
2025-11-03 13:28:47 +08:00
PiexlMax(奇淼 709cdef050 Merge pull request #2134 from frank-515/main
doc: 翻译了README-en.md 中的中文
2025-11-03 13:27:24 +08:00
PiexlMax(奇淼 c95ece3fe2 Merge pull request #2136 from Azir-11/dev-287
style(版本管理): 优化导出和导入版本抽屉的样式,支持暗色模式
2025-11-03 13:05:15 +08:00
Azir-11 d5befa1900 style(版本管理): 优化导出和导入版本抽屉的样式,支持暗色模式 2025-11-03 02:13:58 +08:00
frank515 89ea284cc6 doc: 翻译了README-en.md 中的中文 2025-10-30 21:46:10 +08:00
piexlMax(奇淼 ffe1844ba6 fix(mcp): 递归检查包文件夹中是否存在.go文件 2025-10-28 17:47:29 +08:00
piexlMax(奇淼 d36eeb5134 feat(错误日志): 实现前后端错误日志收集与管理功能 2025-10-28 16:19:25 +08:00
wangyazhou b9009bbd48 fix(jwt):修正令牌过期时间计算错误
- 调整了SetToken函数中的过期时间单位,从秒转换为分钟
- 确保新令牌的过期时间与配置保持一致- 修复了因时间单位不匹配导致的令牌提前失效问题
2025-10-28 15:14:26 +08:00
pixelmaxQM 5be32930f8 Merge branch 'main' of github.com:flipped-aurora/gin-vue-admin 2025-10-25 12:05:34 +08:00
pixelmaxQM 296c71290c fix(autocode): 修复插件包检测逻辑并清理无效数据库记录
改进插件包目录结构检测逻辑,当缺少必要目录时生成警告描述
添加清理数据库中不存在对应实体文件的包记录功能
2025-10-25 12:05:25 +08:00
PiexlMax(奇淼 2378971c8c Merge pull request #2129 from Azir-11/main
typo: 优化注释说明
2025-10-25 11:43:21 +08:00
piexlMax(奇淼 5a30df558c fix(登录): 修正验证码长度计算逻辑 2025-10-24 15:50:53 +08:00
青菜白玉汤 2c21f3cd75 typo: 优化注释说明 2025-10-24 10:52:16 +08:00
feitianbubu 32f21d8555 feat: add check for existing file key before upload 2025-09-30 12:59:26 +08:00
169 changed files with 9810 additions and 1577 deletions
+773
View File
@@ -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. **重视安全性**:实现完善的权限控制和数据验证机制
+12
View File
@@ -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):
+773
View File
@@ -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. **重视安全性**:实现完善的权限控制和数据验证机制
+41 -29
View File
@@ -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. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
+41 -29
View File
@@ -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
View File
@@ -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
````
+2 -2
View File
@@ -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))
+99
View File
@@ -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)
}
+8
View File
@@ -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
)
+81
View File
@@ -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)
}
+3 -41
View File
@@ -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)
}
+54
View File
@@ -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)
}
+199
View File
@@ -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 创建导出模板
+82
View File
@@ -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)
}
+219
View File
@@ -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)
}
+35 -1
View File
@@ -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
View File
@@ -87,7 +87,7 @@ system:
router-prefix: ""
# 严格角色模式 打开后权限将会存在上下级关系
use-strict-auth: false
# 自动迁移数据库表结构,生产环境建议设为false,手动迁移
# 禁用自动迁移数据库表结构,生产环境建议设为true,手动迁移
disable-auto-migrate: false
# captcha configuration
+8 -4
View File
@@ -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
+66 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+3
View File
@@ -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{},
+3
View File
@@ -62,6 +62,9 @@ func RegisterTables() {
system.JoinTemplate{},
system.SysParams{},
system.SysVersion{},
system.SysError{},
system.SysApiToken{},
system.SysLoginLog{},
example.ExaFile{},
example.ExaCustomer{},
+2 -2
View File
@@ -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()...)
}
+6 -1
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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校验失败"),
),
)
}
-87
View File
@@ -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) {
// 解析请求参数
+32 -5
View File
@@ -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
View File
@@ -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("原始需求描述(可选,用于日志记录)"),
),
+3
View File
@@ -46,6 +46,9 @@ func (m *MenuLister) New() mcp.Tool {
- 菜单权限管理:了解系统中所有可用的菜单项
- 导航组件开发:构建动态导航菜单
- 系统架构分析:了解系统的菜单结构和页面组织`),
mcp.WithString("_placeholder",
mcp.Description("占位符,防止json schema校验失败"),
),
)
}
+20 -1
View File
@@ -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)),
+4 -3
View File
@@ -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字符串
}
+14
View File
@@ -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
}
+64
View File
@@ -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"`
}
+17
View File
@@ -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:备注"`
}
+1 -1
View File
@@ -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"`
+21
View File
@@ -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"
}
+7 -5
View File
@@ -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:条件"`
+16
View File
@@ -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"`
}
+25
View File
@@ -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,
+7 -1
View File
@@ -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)
}
+117 -29
View File
@@ -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]
}
+5
View File
@@ -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)
+6
View File
@@ -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
)
+19
View File
@@ -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
}
}
+5 -3
View File
@@ -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) // 同步插件字典
}
}
+2
View File
@@ -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
+28
View File
@@ -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导出表格
+23
View File
@@ -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) // 获取登录日志列表
}
}
+29
View File
@@ -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
}
+51
View File
@@ -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
}
+24 -12
View File
@@ -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 {
+273 -12
View File
@@ -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)
}
+4
View File
@@ -18,8 +18,12 @@ type ServiceGroup struct {
SysExportTemplateService
SysParamsService
SysVersionService
SkillsService
AutoCodePlugin autoCodePlugin
AutoCodePackage autoCodePackage
AutoCodeHistory autoCodeHistory
AutoCodeTemplate autoCodeTemplate
SysErrorService
LoginLogService
ApiTokenService
}
+106
View File
@@ -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
}
+143
View File
@@ -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
})
}
+126
View File
@@ -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
}
+378 -138
View File
@@ -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 {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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())
+53
View File
@@ -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
}
+549
View File
@@ -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)
})
}
+41
View File
@@ -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: "批量删除公告"},
+41
View File
@@ -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"},
+4
View File
@@ -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"}},
+103
View File
@@ -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
}
+1 -1
View File
@@ -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
+62
View File
@@ -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
}
+36 -6
View File
@@ -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)
}
+1 -1
View File
@@ -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 {
+14 -5
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+27
View File
@@ -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
}
+79
View File
@@ -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
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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