commit c9ffb52b7fda6cc75f4882691fe3d32776c0adea Author: Eg <1711788888@qq.com> Date: Tue Apr 7 09:03:48 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..613bcd4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# ---> Vue +# gitignore template for Vue.js projects +# +# Recommended template: Node.gitignore + +# TODO: where does this rule come from? +docs/_book + +# TODO: where does this rule come from? +test/ + +# ---> macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +.idea + +# ---> Server Files +logs +log +uploads +upload +server/config.yaml + +# ---> AI +.claude +.codex +.cursor +.gpt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9135c9a --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +SHELL = /bin/bash + +#SCRIPT_DIR = $(shell pwd)/etc/script +#请选择golang版本 +BUILD_IMAGE_SERVER = golang:1.22 +#请选择node版本 +BUILD_IMAGE_WEB = node:20 +#项目名称 +PROJECT_NAME = github.com/flipped-aurora/gin-vue-admin/server +#配置文件目录 +CONFIG_FILE = config.yaml +#镜像仓库命名空间 +IMAGE_NAME = gva +#镜像地址 +REPOSITORY = registry.cn-hangzhou.aliyuncs.com/${IMAGE_NAME} +#镜像版本 +TAGS_OPT ?= latest +PLUGIN ?= email + +#容器环境前后端共同打包 +build: build-web build-server + docker run --name build-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-local + +#容器环境打包前端 +build-web: + docker run --name build-web-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_WEB} make build-web-local + +#容器环境打包后端 +build-server: + docker run --name build-server-local --rm -v $(shell pwd):/go/src/${PROJECT_NAME} -w /go/src/${PROJECT_NAME} ${BUILD_IMAGE_SERVER} make build-server-local + +#构建web镜像 +build-image-web: + @cd web/ && docker build -t ${REPOSITORY}/web:${TAGS_OPT} . + +#构建server镜像 +build-image-server: + @cd server/ && docker build -t ${REPOSITORY}/server:${TAGS_OPT} . + +#本地环境打包前后端 +build-local: + if [ -d "build" ];then rm -rf build; else echo "OK!"; fi \ + && if [ -f "/.dockerenv" ];then echo "OK!"; else make build-web-local && make build-server-local; fi \ + && mkdir build && cp -r web/dist build/ && cp server/server build/ && cp -r server/resource build/resource + +#本地环境打包前端 +build-web-local: + @cd web/ && if [ -d "dist" ];then rm -rf dist; else echo "OK!"; fi \ + && yarn config set registry http://mirrors.cloud.tencent.com/npm/ && yarn install && yarn build + +#本地环境打包后端 +build-server-local: + @cd server/ && if [ -f "server" ];then rm -rf server; else echo "OK!"; fi \ + && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ + && go env -w CGO_ENABLED=0 && go env && go mod tidy \ + && go build -ldflags "-B 0x$(shell head -c20 /dev/urandom|od -An -tx1|tr -d ' \n') -X main.Version=${TAGS_OPT}" -v + +#打包前后端二合一镜像 +image: build + docker build -t ${REPOSITORY}/gin-vue-admin:${TAGS_OPT} -f deploy/docker/Dockerfile . + +#尝鲜版 +images: build build-image-web build-image-server + docker build -t ${REPOSITORY}/all:${TAGS_OPT} -f deploy/docker/Dockerfile . + +#swagger 文档生成 +doc: + @cd server && swag init + +#插件快捷打包: make plugin PLUGIN="这里是插件文件夹名称,默认为email" +plugin: + if [ -d ".plugin" ];then rm -rf .plugin ; else echo "OK!"; fi && mkdir -p .plugin/${PLUGIN}/{server/plugin,web/plugin} \ + && if [ -d "server/plugin/${PLUGIN}" ];then cp -r server/plugin/${PLUGIN} .plugin/${PLUGIN}/server/plugin/ ; else echo "OK!"; fi \ + && if [ -d "web/src/plugin/${PLUGIN}" ];then cp -r web/src/plugin/${PLUGIN} .plugin/${PLUGIN}/web/plugin/ ; else echo "OK!"; fi \ + && cd .plugin && zip -r ${PLUGIN}.zip ${PLUGIN} && mv ${PLUGIN}.zip ../ && cd .. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfae15a --- /dev/null +++ b/README.md @@ -0,0 +1,389 @@ + +
+ +
+ +
+ + + + + + +
+ +
+Calcium-Ion%2Fnew-api | Trendshift +
+ +[English](./README-en.md) | 简体中文 + +## 支持claw生态 + +[🦞GvaClaw](https://plugin.gin-vue-admin.com/details/159) + +## ✨一分钟生成前后端基础代码 + + + + + + +
+

⭐️ 高度适配AI编辑器的MCP

+

📄 创建基础模板

+

🤖 AI生成结构

+

⏰ 生成代码

+

🏷️ 分配权限

+

🎉 基础CURD生成完成

+
+ +
+ + +# 项目文档 +[在线文档](https://www.gin-vue-admin.com) : https://www.gin-vue-admin.com + +[初始化](https://www.gin-vue-admin.com/guide/start-quickly/initialization.html) + +[从环境到部署教学视频](https://www.bilibili.com/video/BV1Rg411u7xH) + +[开发教学](https://www.gin-vue-admin.com/guide/start-quickly/env.html) (贡献者: LLemonGreen And Fann) + +[交流社区](https://support.qq.com/products/371961) + +[插件市场](https://plugin.gin-vue-admin.com/) + +[软件著作权证书](https://www.gin-vue-admin.com/copyright.pdf) + +# 重要提示 + +1.本项目从起步到开发到部署均有文档和详细视频教程 + +2.本项目需要您有一定的golang和vue基础 + +3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行[付费支持](https://www.gin-vue-admin.com/coffee/payment.html) + +4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,以及日志和代码中所包含的版权声明信息。所需保留信息均为文案性质,不会影响任何业务内容,如决定商用【产生收益的商业行为均在商用行列】或者必须剔除请[购买授权](https://plugin.gin-vue-admin.com/licenseindex.html) +\ + + + + +## 1. 基本介绍 + +### 1.1 项目介绍 + +> Gin-vue-admin是一个基于 [vue](https://vuejs.org) 和 [gin](https://gin-gonic.com) 开发的全栈前后端分离的开发基础平台,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。 + +[在线预览](http://demo.gin-vue-admin.com): http://demo.gin-vue-admin.com + +测试用户名:admin + +测试密码:123456 + +### 1.2 贡献指南 +Hi! 首先感谢你使用 gin-vue-admin。 + +Gin-vue-admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。 + +Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-admin 贡献代码或提供建议,请阅读以下内容。 + +#### 1.2.1 Issue 规范 +- issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。 + +- 在提交 issue 之前,请搜索相关内容是否已被提出。 + +#### 1.2.2 Pull Request 规范 +- 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。 + +- commit 信息要以`[文件名]: 描述信息` 的形式填写,例如 `README.md: fix xxx bug`。 + +- 如果是修复 bug,请在 PR 中给出描述信息。 + +- 合并代码需要两名维护人员参与:一人进行 review 后 approve,另一人再次 review,通过后即可合并。 + +## 2. 使用说明 + +``` +- node版本 > v18.16.0 +- golang版本 >= v1.22 +- IDE推荐:Goland +``` + +### 2.1 server项目 + +使用 `Goland` 等编辑工具,打开server目录,不可以打开 gin-vue-admin 根目录 + +```bash + +# 克隆项目 +git clone https://github.com/flipped-aurora/gin-vue-admin.git +# 进入server文件夹 +cd server + +# 使用 go mod 并安装go依赖包 +go generate + +# 运行 +go run . + +``` + +### 2.2 web项目 + +```bash +# 进入web文件夹 +cd web + +# 安装依赖 +npm install + +# 启动web项目 +npm run serve +``` + +### 2.3 swagger自动化API文档 + +#### 2.3.1 安装 swagger + +``` shell +go install github.com/swaggo/swag/cmd/swag@latest +``` + +#### 2.3.2 生成API文档 + +```` shell +cd server +swag init +```` + +> 执行上面的命令后,server目录下会出现docs文件夹里的 `docs.go`, `swagger.json`, `swagger.yaml` 三个文件更新,启动go服务之后, 在浏览器输入 [http://localhost:8888/swagger/index.html](http://localhost:8888/swagger/index.html) 即可查看swagger文档 + +### 2.4 VSCode工作区 + +#### 2.4.1 开发 + +使用`VSCode`打开根目录下的工作区文件`gin-vue-admin.code-workspace`,在边栏可以看到三个虚拟目录:`backend`、`frontend`、`root`。 + +#### 2.4.2 运行/调试 + +在运行和调试中也可以看到三个task:`Backend`、`Frontend`、`Both (Backend & Frontend)`。运行`Both (Backend & Frontend)`可以同时启动前后端项目。 + +#### 2.4.3 settings + +在工作区配置文件中有`go.toolsEnvVars`字段,是用于`VSCode`自身的go工具环境变量。此外在多go版本的系统中,可以通过`gopath`、`go.goroot`指定运行版本。 + +```json + "go.gopath": null, + "go.goroot": null, +``` + +## 3. 技术选型 + +- 前端:用基于 [Vue](https://vuejs.org) 的 [Element](https://github.com/ElemeFE/element) 构建基础页面。 +- 后端:用 [Gin](https://gin-gonic.com/) 快速搭建基础restful风格API,[Gin](https://gin-gonic.com/) 是一个go语言编写的Web框架。 +- 数据库:采用`MySql` > (5.7) 版本 数据库引擎 InnoDB,使用 [gorm](http://gorm.cn) 实现对数据库的基本操作。 +- 缓存:使用`Redis`实现记录当前活跃用户的`jwt`令牌并实现多点登录限制。 +- API文档:使用`Swagger`构建自动化文档。 +- 配置文件:使用 [fsnotify](https://github.com/fsnotify/fsnotify) 和 [viper](https://github.com/spf13/viper) 实现`yaml`格式的配置文件。 +- 日志:使用 [zap](https://github.com/uber-go/zap) 实现日志记录。 + +## 4. 项目架构 + +### 4.1 系统架构图 + +![系统架构图](http://qmplusimg.henrongyi.top/gva/gin-vue-admin.png) + +### 4.2 前端详细设计图 (提供者:baobeisuper) + +![前端详细设计图](http://qmplusimg.henrongyi.top/naotu.png) + +### 4.3 目录结构 + +``` + ├── server + ├── api (api层) + │ └── v1 (v1版本接口) + ├── config (配置包) + ├── core (核心文件) + ├── docs (swagger文档目录) + ├── global (全局对象) + ├── initialize (初始化) + │ └── internal (初始化内部函数) + ├── middleware (中间件层) + ├── model (模型层) + │ ├── request (入参结构体) + │ └── response (出参结构体) + ├── packfile (静态文件打包) + ├── resource (静态资源文件夹) + │ ├── excel (excel导入导出默认路径) + │ ├── page (表单生成器) + │ └── template (模板) + ├── router (路由层) + ├── service (service层) + ├── source (source层) + └── utils (工具包) + ├── timer (定时器接口封装) + └── upload (oss接口封装) + + web + ├── babel.config.js + ├── Dockerfile + ├── favicon.ico + ├── index.html -- 主页面 + ├── limit.js -- 助手代码 + ├── package.json -- 包管理器代码 + ├── src -- 源代码 + │ ├── api -- api 组 + │ ├── App.vue -- 主页面 + │ ├── assets -- 静态资源 + │ ├── components -- 全局组件 + │ ├── core -- gva 组件包 + │ │ ├── config.js -- gva网站配置文件 + │ │ ├── gin-vue-admin.js -- 注册欢迎文件 + │ │ └── global.js -- 统一导入文件 + │ ├── directive -- v-auth 注册文件 + │ ├── main.js -- 主文件 + │ ├── permission.js -- 路由中间件 + │ ├── pinia -- pinia 状态管理器,取代vuex + │ │ ├── index.js -- 入口文件 + │ │ └── modules -- modules + │ │ ├── dictionary.js + │ │ ├── router.js + │ │ └── user.js + │ ├── router -- 路由声明文件 + │ │ └── index.js + │ ├── style -- 全局样式 + │ │ ├── base.scss + │ │ ├── basics.scss + │ │ ├── element_visiable.scss -- 此处可以全局覆盖 element-plus 样式 + │ │ ├── iconfont.css -- 顶部几个icon的样式文件 + │ │ ├── main.scss + │ │ ├── mobile.scss + │ │ └── newLogin.scss + │ ├── utils -- 方法包库 + │ │ ├── asyncRouter.js -- 动态路由相关 + │ │ ├── btnAuth.js -- 动态权限按钮相关 + │ │ ├── bus.js -- 全局mitt声明文件 + │ │ ├── date.js -- 日期相关 + │ │ ├── dictionary.js -- 获取字典方法 + │ │ ├── downloadImg.js -- 下载图片方法 + │ │ ├── format.js -- 格式整理相关 + │ │ ├── image.js -- 图片相关方法 + │ │ ├── page.js -- 设置页面标题 + │ │ ├── request.js -- 请求 + │ │ └── stringFun.js -- 字符串文件 + | ├── view -- 主要view代码 + | | ├── about -- 关于我们 + | | ├── dashboard -- 面板 + | | ├── error -- 错误 + | | ├── example --上传案例 + | | ├── iconList -- icon列表 + | | ├── init -- 初始化数据 + | | | ├── index -- 新版本 + | | | ├── init -- 旧版本 + | | ├── layout -- layout约束页面 + | | | ├── aside + | | | ├── bottomInfo -- bottomInfo + | | | ├── screenfull -- 全屏设置 + | | | ├── setting -- 系统设置 + | | | └── index.vue -- base 约束 + | | ├── login --登录 + | | ├── person --个人中心 + | | ├── superAdmin -- 超级管理员操作 + | | ├── system -- 系统检测页面 + | | ├── systemTools -- 系统配置相关页面 + | | └── routerHolder.vue -- page 入口页面 + ├── vite.config.js -- vite 配置文件 + └── yarn.lock + +``` + +## 5. 主要功能 + +- 权限管理:基于`jwt`和`casbin`实现的权限管理。 +- 文件上传下载:实现基于`七牛云`, `阿里云`, `腾讯云` 的文件上传操作(请开发自己去各个平台的申请对应 `token` 或者对应`key`)。 +- 分页封装:前端使用 `mixins` 封装分页,分页方法调用 `mixins` 即可。 +- 用户管理:系统管理员分配用户角色和角色权限。 +- 角色管理:创建权限控制的主要对象,可以给角色分配不同api权限和菜单权限。 +- 菜单管理:实现用户动态菜单配置,实现不同角色不同菜单。 +- api管理:不同用户可调用的api接口的权限不同。 +- 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。 +- 条件搜索:增加条件搜索示例。 +- restful示例:可以参考用户管理模块中的示例API。 + - 前端文件参考: [web/src/view/superAdmin/api/api.vue](https://github.com/flipped-aurora/gin-vue-admin/blob/master/web/src/view/superAdmin/api/api.vue) + - 后台文件参考: [server/router/sys_api.go](https://github.com/flipped-aurora/gin-vue-admin/blob/master/server/router/sys_api.go) +- 多点登录限制:需要在`config.yaml`中把`system`中的`use-multipoint`修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。 +- 分片上传:提供文件分片上传和大文件分片上传功能示例。 +- 表单生成器:表单生成器借助 [@Variant Form](https://github.com/vform666/variant-form) 。 +- 代码生成器:后台基础逻辑以及简单curd的代码生成器。 + +## 6. 知识库 + +## 6.1 团队博客 + +> https://www.yuque.com/flipped-aurora +> +>内有前端框架教学视频。如果觉得项目对您有所帮助可以添加我的个人微信:shouzi_1994,欢迎您提出宝贵的需求。 + +## 6.2 教学视频 + +(1)手把手教学视频 + +> https://www.bilibili.com/video/BV1Rg411u7xH/ + +(2)后端目录结构调整介绍以及使用方法 + +> https://www.bilibili.com/video/BV1x44y117TT/ + +(3)golang基础教学视频 + +> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=108884 + +(4)gin框架基础教学 + +> bilibili:https://space.bilibili.com/322210472/channel/detail?cid=126418&ctype=0 + +(5)gin-vue-admin 版本更新介绍视频 + +> bilibili:https://www.bilibili.com/video/BV1kv4y1g7nT + +## 7. 联系方式 + +### 7.1 技术群 + +### QQ交流群:971857775 + +### 微信交流群 +| 微信 | +| :---: | +| + +防止广告进群,添加微信,输入以下代码执行结果(请勿转码为string) + +``` +str := "5Yqg5YWlR1ZB5Lqk5rWB576k" +decodeBytes, err := base64.StdEncoding.DecodeString(str) +fmt.Println(decodeBytes, err) +``` + +### [关于我们](https://www.gin-vue-admin.com/about/join.html) + +## 8. 贡献者 + +感谢您对gin-vue-admin的贡献! + + + Contribution Leaderboard + + +## 9. 捐赠 + +如果你觉得这个项目对你有帮助,你可以请作者喝饮料 :tropical_drink: [点我](https://www.gin-vue-admin.com/coffee/index.html) + +## 10. 注意事项 + +请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://plugin.gin-vue-admin.com/license) +未授权去除版权信息将依法追究法律责任 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..db76aea --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to qimiaojiangjizhao@gmail.com diff --git a/deploy/docker-compose/docker-compose.yaml b/deploy/docker-compose/docker-compose.yaml new file mode 100644 index 0000000..1e304c2 --- /dev/null +++ b/deploy/docker-compose/docker-compose.yaml @@ -0,0 +1,90 @@ +version: "3" + +# 声明一个名为network的networks,subnet为network的子网地址,默认网关是177.7.0.1 +networks: + network: + ipam: + driver: default + config: + - subnet: '177.7.0.0/16' + +# 设置mysql,redis持久化保存 +volumes: + mysql: + redis: + +services: + web: + build: + context: ../../web + dockerfile: ./Dockerfile + container_name: gva-web + restart: always + ports: + - '8080:8080' + depends_on: + - server + command: [ 'nginx-debug', '-g', 'daemon off;' ] + networks: + network: + ipv4_address: 177.7.0.11 + + server: + build: + context: ../../server + dockerfile: ./Dockerfile + container_name: gva-server + restart: always + ports: + - '8888:8888' + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + links: + - mysql + - redis + networks: + network: + ipv4_address: 177.7.0.12 + + mysql: + image: mysql:8.0.21 # 如果您是 arm64 架构:如 MacOS 的 M1,请修改镜像为 image: mysql/mysql-server:8.0.21 + container_name: gva-mysql + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集 + restart: always + ports: + - "13306:3306" # host物理直接映射端口为13306 + environment: + #MYSQL_ROOT_PASSWORD: 'Aa@6447985' # root管理员用户密码 + MYSQL_DATABASE: 'qmPlus' # 初始化启动时要创建的数据库的名称 + MYSQL_USER: 'gva' + MYSQL_PASSWORD: 'Aa@6447985' + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "gva", "-pAa@6447985"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - mysql:/var/lib/mysql + networks: + network: + ipv4_address: 177.7.0.13 + + redis: + image: redis:6.0.6 + container_name: gva-redis # 容器名 + restart: always + ports: + - '16379:6379' + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + volumes: + - redis:/data + networks: + network: + ipv4_address: 177.7.0.14 diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 0000000..da23805 --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,18 @@ +FROM centos:7 +WORKDIR /opt +ENV LANG=en_US.utf8 +COPY deploy/docker/entrypoint.sh . +COPY build/ /usr/share/nginx/html/ +COPY server/config.yaml /usr/share/nginx/html/config.yaml +COPY web/.docker-compose/nginx/conf.d/nginx.conf /etc/nginx/conf.d/nginx.conf +RUN set -ex \ + && echo "LANG=en_US.utf8" > /etc/locale.conf \ + && echo "net.core.somaxconn = 1024" >> /etc/sysctl.conf \ + && echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf \ + && yum -y install epel-release \ + && yum -y localinstall http://mirrors.ustc.edu.cn/mysql-repo/mysql57-community-release-el7.rpm \ + && yum -y install mysql-community-server git redis nginx go npm --nogpgcheck && chmod +x ./entrypoint.sh \ + && npm install -g yarn && go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct \ + && echo "start" > /dev/null +EXPOSE 80 +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh new file mode 100644 index 0000000..0f6dd13 --- /dev/null +++ b/deploy/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash +if [ ! -d "/var/lib/mysql/gva" ]; then + mysqld --initialize-insecure --user=mysql --datadir=/var/lib/mysql + mysqld --daemonize --user=mysql + sleep 5s + mysql -uroot -e "create database gva default charset 'utf8' collate 'utf8_bin'; grant all on gva.* to 'root'@'127.0.0.1' identified by '123456'; flush privileges;" +else + mysqld --daemonize --user=mysql +fi +redis-server & +if [ "$1" = "actions" ]; then + cd /opt/gva/server && go run main.go & + cd /opt/gva/web/ && yarn serve & +else + /usr/sbin/nginx & + cd /usr/share/nginx/html/ && ./server & +fi +echo "gva ALL start!!!" +tail -f /dev/null \ No newline at end of file diff --git a/deploy/kubernetes/server/gva-server-configmap.yaml b/deploy/kubernetes/server/gva-server-configmap.yaml new file mode 100644 index 0000000..fb889fb --- /dev/null +++ b/deploy/kubernetes/server/gva-server-configmap.yaml @@ -0,0 +1,148 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: config.yaml + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +data: + config.yaml: | + # github.com/flipped-aurora/gin-vue-admin/server Global Configuration + + # jwt configuration + jwt: + signing-key: 'qmPlus' + expires-time: 604800 + buffer-time: 86400 + + # zap logger configuration + zap: + level: 'info' + format: 'console' + prefix: '[github.com/flipped-aurora/gin-vue-admin/server]' + director: 'log' + link-name: 'latest_log' + show-line: true + encode-level: 'LowercaseColorLevelEncoder' + stacktrace-key: 'stacktrace' + log-in-console: true + + # redis configuration + redis: + db: 0 + addr: '127.0.0.1:6379' + password: '' + + # email configuration + email: + to: 'xxx@qq.com' + port: 465 + from: 'xxx@163.com' + host: 'smtp.163.com' + is-ssl: true + secret: 'xxx' + nickname: 'test' + + # casbin configuration + casbin: + model-path: './resource/rbac_model.conf' + + # system configuration + system: + env: 'develop' # Change to "develop" to skip authentication for development mode + addr: 8888 + db-type: 'mysql' + oss-type: 'local' # 控制oss选择走本期还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-multipoint: false + + # captcha configuration + captcha: + key-long: 6 + img-width: 240 + img-height: 80 + + # mysql connect configuration + # 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://www.github.com/flipped-aurora/gin-vue-admin/server.com/docs/first) + mysql: + path: '' + config: '' + db-name: '' + username: '' + password: '' + max-idle-conns: 10 + max-open-conns: 100 + log-mode: false + log-zap: "" + + # local configuration + local: + path: 'uploads/file' + + # autocode configuration + autocode: + transfer-restart: true + root: "" + server: /server + server-api: /api/v1/autocode + server-initialize: /initialize + server-model: /model/autocode + server-request: /model/autocode/request/ + server-router: /router/autocode + server-service: /service/autocode + web: /web/src + web-api: /api + web-flow: /view + web-form: /view + web-table: /view + + # qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) + qiniu: + zone: 'ZoneHuaDong' + bucket: '' + img-path: '' + use-https: false + access-key: '' + secret-key: '' + use-cdn-domains: false + + + # aliyun oss configuration + aliyun-oss: + endpoint: 'yourEndpoint' + access-key-id: 'yourAccessKeyId' + access-key-secret: 'yourAccessKeySecret' + bucket-name: 'yourBucketName' + bucket-url: 'yourBucketUrl' + base-path: 'yourBasePath' + + # tencent cos configuration + tencent-cos: + bucket: 'xxxxx-10005608' + region: 'ap-shanghai' + secret-id: 'xxxxxxxx' + secret-key: 'xxxxxxxx' + base-url: 'https://gin.vue.admin' + path-prefix: 'github.com/flipped-aurora/gin-vue-admin/server' + + # excel configuration + excel: + dir: './resource/excel/' + + + # timer task db clear table + Timer: + start: true + spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3 + detail: [ + # tableName: 需要清理的表名 + # compareField: 需要比较时间的字段 + # interval: 时间间隔, 具体配置详看 time.ParseDuration() 中字符串表示 且不能为负数 + # 2160h = 24 * 30 * 3 -> 三个月 + { tableName: "sys_operation_records" , compareField: "created_at", interval: "2160h" }, + { tableName: "jwt_blacklists" , compareField: "created_at", interval: "168h" } + #{ tableName: "log2" , compareField: "created_at", interval: "2160h" } + ] diff --git a/deploy/kubernetes/server/gva-server-deployment.yaml b/deploy/kubernetes/server/gva-server-deployment.yaml new file mode 100644 index 0000000..7da9d20 --- /dev/null +++ b/deploy/kubernetes/server/gva-server-deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gva-server + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +spec: + replicas: 1 + selector: + matchLabels: + app: gva-server + version: gva-vue3 + template: + metadata: + labels: + app: gva-server + version: gva-vue3 + spec: + containers: + - name: gin-vue-admin-container + image: registry.cn-hangzhou.aliyuncs.com/gva/server:latest + imagePullPolicy: Always + ports: + - containerPort: 8888 + name: http + volumeMounts: + - mountPath: /go/src/github.com/flipped-aurora/gin-vue-admin/server/config.docker.yaml + name: config + subPath: config.yaml + - mountPath: /etc/localtime + name: localtime + resources: + limits: + cpu: 1000m + memory: 2000Mi + requests: + cpu: 100m + memory: 200Mi + livenessProbe: + failureThreshold: 1 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + startupProbe: + failureThreshold: 40 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: 8888 + timeoutSeconds: 1 + #imagePullSecrets: + # - name: docker-registry + volumes: + - name: localtime + hostPath: + path: /etc/localtime + - name: config + configMap: + name: config.yaml \ No newline at end of file diff --git a/deploy/kubernetes/server/gva-server-service.yaml b/deploy/kubernetes/server/gva-server-service.yaml new file mode 100644 index 0000000..17aaef2 --- /dev/null +++ b/deploy/kubernetes/server/gva-server-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: gva-server + annotations: + flipped-aurora/gin-vue-admin: backend + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-server + version: gva-vue3 +spec: + selector: + app: gva-server + version: gva-vue3 + ports: + - port: 8888 + name: http + targetPort: 8888 + type: ClusterIP +# type: NodePort diff --git a/deploy/kubernetes/web/gva-web-configmap.yaml b/deploy/kubernetes/web/gva-web-configmap.yaml new file mode 100644 index 0000000..189b861 --- /dev/null +++ b/deploy/kubernetes/web/gva-web-configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my.conf +data: + my.conf: | + server { + listen 8080; + server_name localhost; + + #charset koi8-r; + #access_log logs/host.access.log main; + + location / { + root /usr/share/nginx/html; + add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + rewrite ^/api/(.*)$ /$1 break; #重写 + proxy_pass http://gva-server:8888; # 设置代理服务器的协议和地址 + } + + location /api/swagger/index.html { + proxy_pass http://gva-server:8888/swagger/index.html; + } + } \ No newline at end of file diff --git a/deploy/kubernetes/web/gva-web-deploymemt.yaml b/deploy/kubernetes/web/gva-web-deploymemt.yaml new file mode 100644 index 0000000..e6d15bc --- /dev/null +++ b/deploy/kubernetes/web/gva-web-deploymemt.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gva-web + annotations: + flipped-aurora/gin-vue-admin: ui + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-web + version: gva-vue3 +spec: + replicas: 1 + selector: + matchLabels: + app: gva-web + version: gva-vue3 + template: + metadata: + labels: + app: gva-web + version: gva-vue3 + spec: + containers: + - name: gin-vue-admin-nginx-container + image: registry.cn-hangzhou.aliyuncs.com/gva/web:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: http + readinessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + resources: + limits: + cpu: 500m + memory: 1000Mi + requests: + cpu: 100m + memory: 100Mi + volumeMounts: + - mountPath: /etc/nginx/conf.d/ + name: nginx-config + volumes: + - name: nginx-config + configMap: + name: my.conf diff --git a/deploy/kubernetes/web/gva-web-ingress.yaml b/deploy/kubernetes/web/gva-web-ingress.yaml new file mode 100644 index 0000000..81922f1 --- /dev/null +++ b/deploy/kubernetes/web/gva-web-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gva-ingress + annotations: + kubernetes.io/ingress.class: "nginx" +spec: + rules: + - host: demo.gin-vue-admin.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gva-web + port: + number: 8080 \ No newline at end of file diff --git a/deploy/kubernetes/web/gva-web-service.yaml b/deploy/kubernetes/web/gva-web-service.yaml new file mode 100644 index 0000000..c374bdb --- /dev/null +++ b/deploy/kubernetes/web/gva-web-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: gva-web + annotations: + flipped-aurora/gin-vue-admin: ui + github: "https://github.com/flipped-aurora/gin-vue-admin.git" + app.kubernetes.io/version: 0.0.1 + labels: + app: gva-web + version: gva-vue3 +spec: +# type: NodePort + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: gva-web + version: gva-vue3 diff --git a/docs/gin-vue-admin.png b/docs/gin-vue-admin.png new file mode 100644 index 0000000..bbe0549 Binary files /dev/null and b/docs/gin-vue-admin.png differ diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..69cb44d --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:alpine as builder + +WORKDIR /go/src/github.com/flipped-aurora/gin-vue-admin/server +COPY . . + +RUN go env -w GO111MODULE=on \ + && go env -w GOPROXY=https://goproxy.cn,direct \ + && go env -w CGO_ENABLED=0 \ + && go env \ + && go mod tidy \ + && go build -o server . + +FROM alpine:latest + +LABEL MAINTAINER="SliverHorn@sliver_horn@qq.com" +# 设置时区 +ENV TZ=Asia/Shanghai +RUN apk update && apk add --no-cache tzdata openntpd \ + && ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /go/src/github.com/flipped-aurora/gin-vue-admin/server + +COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/server ./ +COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/resource ./resource/ +COPY --from=0 /go/src/github.com/flipped-aurora/gin-vue-admin/server/config.docker.yaml ./ + +# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/github.com/flipped-aurora/gin-vue-admin/server/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0 +# VOLUME ["/go/src/github.com/flipped-aurora/gin-vue-admin/server"] + +EXPOSE 8888 +ENTRYPOINT ./server -c config.docker.yaml diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..9a34870 --- /dev/null +++ b/server/README.md @@ -0,0 +1,54 @@ +## server项目结构 + +```shell +├── api +│   └── v1 +├── config +├── core +├── docs +├── global +├── initialize +│   └── internal +├── middleware +├── model +│   ├── request +│   └── response +├── packfile +├── resource +│   ├── excel +│   ├── page +│   └── template +├── router +├── service +├── source +└── utils + ├── timer + └── upload +``` + +| 文件夹 | 说明 | 描述 | +| ------------ | ----------------------- | --------------------------- | +| `api` | api层 | api层 | +| `--v1` | v1版本接口 | v1版本接口 | +| `config` | 配置包 | config.yaml对应的配置结构体 | +| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 | +| `docs` | swagger文档目录 | swagger文档目录 | +| `global` | 全局对象 | 全局对象 | +| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 | +| `--internal` | 初始化内部函数 | gorm 的 longger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 | +| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 | +| `model` | 模型层 | 模型对应数据表 | +| `--request` | 入参结构体 | 接收前端发送到后端的数据。 | +| `--response` | 出参结构体 | 返回给前端的数据结构体 | +| `packfile` | 静态文件打包 | 静态文件打包 | +| `resource` | 静态资源文件夹 | 负责存放静态文件 | +| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 | +| `--page` | 表单生成器 | 表单生成器 打包后的dist | +| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 | +| `router` | 路由层 | 路由层 | +| `service` | service层 | 存放业务逻辑问题 | +| `source` | source层 | 存放初始化数据的函数 | +| `utils` | 工具包 | 工具函数封装 | +| `--timer` | timer | 定时器接口封装 | +| `--upload` | oss | oss接口封装 | + diff --git a/server/api/v1/enter.go b/server/api/v1/enter.go new file mode 100644 index 0000000..5c1dff4 --- /dev/null +++ b/server/api/v1/enter.go @@ -0,0 +1,13 @@ +package v1 + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/example" + "github.com/flipped-aurora/gin-vue-admin/server/api/v1/system" +) + +var ApiGroupApp = new(ApiGroup) + +type ApiGroup struct { + SystemApiGroup system.ApiGroup + ExampleApiGroup example.ApiGroup +} diff --git a/server/api/v1/example/enter.go b/server/api/v1/example/enter.go new file mode 100644 index 0000000..4d753ec --- /dev/null +++ b/server/api/v1/example/enter.go @@ -0,0 +1,17 @@ +package example + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { + CustomerApi + + AttachmentCategoryApi + FileUploadAndDownloadApi +} + +var ( + customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService + + attachmentCategoryService = service.ServiceGroupApp.ExampleServiceGroup.AttachmentCategoryService + fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService +) diff --git a/server/api/v1/example/exa_attachment_category.go b/server/api/v1/example/exa_attachment_category.go new file mode 100644 index 0000000..0436f78 --- /dev/null +++ b/server/api/v1/example/exa_attachment_category.go @@ -0,0 +1,82 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "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/example" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AttachmentCategoryApi struct{} + +// GetCategoryList +// @Tags GetCategoryList +// @Summary 媒体库分类列表 +// @Security AttachmentCategory +// @Produce application/json +// @Success 200 {object} response.Response{data=example.ExaAttachmentCategory,msg=string} "媒体库分类列表" +// @Router /attachmentCategory/getCategoryList [get] +func (a *AttachmentCategoryApi) GetCategoryList(c *gin.Context) { + res, err := attachmentCategoryService.GetCategoryList() + if err != nil { + global.GVA_LOG.Error("获取分类列表失败!", zap.Error(err)) + response.FailWithMessage("获取分类列表失败", c) + return + } + response.OkWithData(res, c) +} + +// AddCategory +// @Tags AddCategory +// @Summary 添加媒体库分类 +// @Security AttachmentCategory +// @accept application/json +// @Produce application/json +// @Param data body example.ExaAttachmentCategory true "媒体库分类数据"// @Success 200 {object} response.Response{msg=string} "添加媒体库分类" +// @Router /attachmentCategory/addCategory [post] +func (a *AttachmentCategoryApi) AddCategory(c *gin.Context) { + var req example.ExaAttachmentCategory + if err := c.ShouldBindJSON(&req); err != nil { + global.GVA_LOG.Error("参数错误!", zap.Error(err)) + response.FailWithMessage("参数错误", c) + return + } + + if err := attachmentCategoryService.AddCategory(&req); err != nil { + global.GVA_LOG.Error("创建/更新失败!", zap.Error(err)) + response.FailWithMessage("创建/更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建/更新成功", c) +} + +// DeleteCategory +// @Tags DeleteCategory +// @Summary 删除分类 +// @Security AttachmentCategory +// @accept application/json +// @Produce application/json +// @Param data body common.GetById true "分类id" +// @Success 200 {object} response.Response{msg=string} "删除分类" +// @Router /attachmentCategory/deleteCategory [post] +func (a *AttachmentCategoryApi) DeleteCategory(c *gin.Context) { + var req common.GetById + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("参数错误", c) + return + } + + if req.ID == 0 { + response.FailWithMessage("参数错误", c) + return + } + + if err := attachmentCategoryService.DeleteCategory(&req.ID); err != nil { + response.FailWithMessage("删除失败", c) + return + } + + response.OkWithMessage("删除成功", c) +} diff --git a/server/api/v1/example/exa_breakpoint_continue.go b/server/api/v1/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..565ffe6 --- /dev/null +++ b/server/api/v1/example/exa_breakpoint_continue.go @@ -0,0 +1,156 @@ +package example + +import ( + "fmt" + "io" + "mime/multipart" + "strconv" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// BreakpointContinue +// @Tags ExaFileUploadAndDownload +// @Summary 断点续传到服务器 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "an example for breakpoint resume, 断点续传示例" +// @Success 200 {object} response.Response{msg=string} "断点续传到服务器" +// @Router /fileUploadAndDownload/breakpointContinue [post] +func (b *FileUploadAndDownloadApi) BreakpointContinue(c *gin.Context) { + fileMd5 := c.Request.FormValue("fileMd5") + fileName := c.Request.FormValue("fileName") + chunkMd5 := c.Request.FormValue("chunkMd5") + chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber")) + chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal")) + _, FileHeader, err := c.Request.FormFile("file") + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + f, err := FileHeader.Open() + if err != nil { + global.GVA_LOG.Error("文件读取失败!", zap.Error(err)) + response.FailWithMessage("文件读取失败", c) + return + } + defer func(f multipart.File) { + err := f.Close() + if err != nil { + fmt.Println(err) + } + }(f) + cen, _ := io.ReadAll(f) + if !utils.CheckMd5(cen, chunkMd5) { + global.GVA_LOG.Error("检查md5失败!", zap.Error(err)) + response.FailWithMessage("检查md5失败", c) + return + } + file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) + if err != nil { + global.GVA_LOG.Error("查找或创建记录失败!", zap.Error(err)) + response.FailWithMessage("查找或创建记录失败", c) + return + } + pathC, err := utils.BreakPointContinue(cen, fileName, chunkNumber, chunkTotal, fileMd5) + if err != nil { + global.GVA_LOG.Error("断点续传失败!", zap.Error(err)) + response.FailWithMessage("断点续传失败", c) + return + } + + if err = fileUploadAndDownloadService.CreateFileChunk(file.ID, pathC, chunkNumber); err != nil { + global.GVA_LOG.Error("创建文件记录失败!", zap.Error(err)) + response.FailWithMessage("创建文件记录失败", c) + return + } + response.OkWithMessage("切片创建成功", c) +} + +// FindFile +// @Tags ExaFileUploadAndDownload +// @Summary 查找文件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "Find the file, 查找文件" +// @Success 200 {object} response.Response{data=exampleRes.FileResponse,msg=string} "查找文件,返回包括文件详情" +// @Router /fileUploadAndDownload/findFile [get] +func (b *FileUploadAndDownloadApi) FindFile(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal")) + file, err := fileUploadAndDownloadService.FindOrCreateFile(fileMd5, fileName, chunkTotal) + if err != nil { + global.GVA_LOG.Error("查找失败!", zap.Error(err)) + response.FailWithMessage("查找失败", c) + } else { + response.OkWithDetailed(exampleRes.FileResponse{File: file}, "查找成功", c) + } +} + +// BreakpointContinueFinish +// @Tags ExaFileUploadAndDownload +// @Summary 创建文件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件完成" +// @Success 200 {object} response.Response{data=exampleRes.FilePathResponse,msg=string} "创建文件,返回包括文件路径" +// @Router /fileUploadAndDownload/findFile [post] +func (b *FileUploadAndDownloadApi) BreakpointContinueFinish(c *gin.Context) { + fileMd5 := c.Query("fileMd5") + fileName := c.Query("fileName") + filePath, err := utils.MakeFile(fileName, fileMd5) + if err != nil { + global.GVA_LOG.Error("文件创建失败!", zap.Error(err)) + response.FailWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建失败", c) + } else { + response.OkWithDetailed(exampleRes.FilePathResponse{FilePath: filePath}, "文件创建成功", c) + } +} + +// RemoveChunk +// @Tags ExaFileUploadAndDownload +// @Summary 删除切片 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "删除缓存切片" +// @Success 200 {object} response.Response{msg=string} "删除切片" +// @Router /fileUploadAndDownload/removeChunk [post] +func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) { + var file example.ExaFile + err := c.ShouldBindJSON(&file) + if err != nil { + 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)) + return + } + err = fileUploadAndDownloadService.DeleteFileChunk(file.FileMd5, file.FilePath) + if err != nil { + global.GVA_LOG.Error(err.Error(), zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("缓存切片删除成功", c) +} diff --git a/server/api/v1/example/exa_customer.go b/server/api/v1/example/exa_customer.go new file mode 100644 index 0000000..5d9ef1c --- /dev/null +++ b/server/api/v1/example/exa_customer.go @@ -0,0 +1,176 @@ +package example + +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/example" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CustomerApi struct{} + +// CreateExaCustomer +// @Tags ExaCustomer +// @Summary 创建客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户用户名, 客户手机号码" +// @Success 200 {object} response.Response{msg=string} "创建客户" +// @Router /customer/customer [post] +func (e *CustomerApi) CreateExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer, utils.CustomerVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + customer.SysUserID = utils.GetUserID(c) + customer.SysUserAuthorityID = utils.GetUserAuthorityId(c) + err = customerService.CreateExaCustomer(customer) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteExaCustomer +// @Tags ExaCustomer +// @Summary 删除客户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户ID" +// @Success 200 {object} response.Response{msg=string} "删除客户" +// @Router /customer/customer [delete] +func (e *CustomerApi) DeleteExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = customerService.DeleteExaCustomer(customer) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateExaCustomer +// @Tags ExaCustomer +// @Summary 更新客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body example.ExaCustomer true "客户ID, 客户信息" +// @Success 200 {object} response.Response{msg=string} "更新客户信息" +// @Router /customer/customer [put] +func (e *CustomerApi) UpdateExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindJSON(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer, utils.CustomerVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = customerService.UpdateExaCustomer(&customer) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetExaCustomer +// @Tags ExaCustomer +// @Summary 获取单一客户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query example.ExaCustomer true "客户ID" +// @Success 200 {object} response.Response{data=exampleRes.ExaCustomerResponse,msg=string} "获取单一客户信息,返回包括客户详情" +// @Router /customer/customer [get] +func (e *CustomerApi) GetExaCustomer(c *gin.Context) { + var customer example.ExaCustomer + err := c.ShouldBindQuery(&customer) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(customer.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + data, err := customerService.GetExaCustomer(customer.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(exampleRes.ExaCustomerResponse{Customer: data}, "获取成功", c) +} + +// GetExaCustomerList +// @Tags ExaCustomer +// @Summary 分页获取权限客户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取权限客户列表,返回包括列表,总数,页码,每页数量" +// @Router /customer/customerList [get] +func (e *CustomerApi) GetExaCustomerList(c *gin.Context) { + var pageInfo request.PageInfo + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + customerList, total, err := customerService.GetCustomerInfoList(utils.GetUserAuthorityId(c), pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: customerList, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} diff --git a/server/api/v1/example/exa_file_upload_download.go b/server/api/v1/example/exa_file_upload_download.go new file mode 100644 index 0000000..9497c6d --- /dev/null +++ b/server/api/v1/example/exa_file_upload_download.go @@ -0,0 +1,135 @@ +package example + +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/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/example/request" + exampleRes "github.com/flipped-aurora/gin-vue-admin/server/model/example/response" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strconv" +) + +type FileUploadAndDownloadApi struct{} + +// UploadFile +// @Tags ExaFileUploadAndDownload +// @Summary 上传文件示例 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件示例" +// @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情" +// @Router /fileUploadAndDownload/upload [post] +func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + noSave := c.DefaultQuery("noSave", "0") + _, header, err := c.Request.FormFile("file") + classId, _ := strconv.Atoi(c.DefaultPostForm("classId", "0")) + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径 + if err != nil { + global.GVA_LOG.Error("上传文件失败!", zap.Error(err)) + response.FailWithMessage("上传文件失败", c) + return + } + response.OkWithDetailed(exampleRes.ExaFileResponse{File: file}, "上传成功", c) +} + +// EditFileName 编辑文件名或者备注 +func (b *FileUploadAndDownloadApi) EditFileName(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = fileUploadAndDownloadService.EditFileName(file) + if err != nil { + global.GVA_LOG.Error("编辑失败!", zap.Error(err)) + response.FailWithMessage("编辑失败", c) + return + } + response.OkWithMessage("编辑成功", c) +} + +// DeleteFile +// @Tags ExaFileUploadAndDownload +// @Summary 删除文件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @Router /fileUploadAndDownload/deleteFile [post] +func (b *FileUploadAndDownloadApi) DeleteFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.DeleteFile(file); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetFileList +// @Tags ExaFileUploadAndDownload +// @Summary 分页文件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.ExaAttachmentCategorySearch true "页码, 每页大小, 分类id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /fileUploadAndDownload/getFileList [post] +func (b *FileUploadAndDownloadApi) GetFileList(c *gin.Context) { + var pageInfo request.ExaAttachmentCategorySearch + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := fileUploadAndDownloadService.GetFileRecordInfoList(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) +} + +// ImportURL +// @Tags ExaFileUploadAndDownload +// @Summary 导入URL +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "对象" +// @Success 200 {object} response.Response{msg=string} "导入URL" +// @Router /fileUploadAndDownload/importURL [post] +func (b *FileUploadAndDownloadApi) ImportURL(c *gin.Context) { + var file []example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.ImportURL(&file); err != nil { + global.GVA_LOG.Error("导入URL失败!", zap.Error(err)) + response.FailWithMessage("导入URL失败", c) + return + } + response.OkWithMessage("导入URL成功", c) +} diff --git a/server/api/v1/system/ai_workflow_session.go b/server/api/v1/system/ai_workflow_session.go new file mode 100644 index 0000000..037fa8c --- /dev/null +++ b/server/api/v1/system/ai_workflow_session.go @@ -0,0 +1,102 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AIWorkflowSessionApi struct{} + +func (a *AIWorkflowSessionApi) Save(c *gin.Context) { + var info systemReq.SysAIWorkflowSessionUpsert + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + session, err := aiWorkflowSessionService.Save(c.Request.Context(), utils.GetUserID(c), info) + if err != nil { + global.GVA_LOG.Error("保存 AI 工作流会话失败", zap.Error(err)) + response.FailWithMessage("保存会话失败", c) + return + } + + response.OkWithDetailed(gin.H{"session": session}, "保存成功", c) +} + +func (a *AIWorkflowSessionApi) GetList(c *gin.Context) { + var info systemReq.SysAIWorkflowSessionSearch + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + list, total, err := aiWorkflowSessionService.GetList(c.Request.Context(), utils.GetUserID(c), info) + if err != nil { + global.GVA_LOG.Error("获取 AI 工作流会话列表失败", zap.Error(err)) + response.FailWithMessage("获取会话列表失败", c) + return + } + + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: info.Page, + PageSize: info.PageSize, + }, "获取成功", c) +} + +func (a *AIWorkflowSessionApi) GetDetail(c *gin.Context) { + var info commonReq.GetById + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + session, err := aiWorkflowSessionService.GetDetail(c.Request.Context(), utils.GetUserID(c), info.Uint()) + if err != nil { + global.GVA_LOG.Error("获取 AI 工作流会话详情失败", zap.Error(err)) + response.FailWithMessage("获取会话详情失败", c) + return + } + + response.OkWithDetailed(gin.H{"session": session}, "获取成功", c) +} + +func (a *AIWorkflowSessionApi) Delete(c *gin.Context) { + var info commonReq.GetById + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if err := aiWorkflowSessionService.Delete(c.Request.Context(), utils.GetUserID(c), info.Uint()); err != nil { + global.GVA_LOG.Error("删除 AI 工作流会话失败", zap.Error(err)) + response.FailWithMessage("删除会话失败", c) + return + } + + response.OkWithMessage("删除成功", c) +} + +func (a *AIWorkflowSessionApi) DumpMarkdown(c *gin.Context) { + var info systemReq.SysAIWorkflowMarkdownDump + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + result, err := aiWorkflowSessionService.DumpMarkdown(c.Request.Context(), utils.GetUserID(c), info) + if err != nil { + global.GVA_LOG.Error("AI 工作流 Markdown 落盘失败", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + + response.OkWithDetailed(gin.H{"result": result}, "落盘成功", c) +} diff --git a/server/api/v1/system/auto_code_history.go b/server/api/v1/system/auto_code_history.go new file mode 100644 index 0000000..065ddd8 --- /dev/null +++ b/server/api/v1/system/auto_code_history.go @@ -0,0 +1,115 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodeHistoryApi struct{} + +// First +// @Tags AutoCode +// @Summary 获取meta信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "请求参数" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取meta信息" +// @Router /autoCode/getMeta [post] +func (a *AutoCodeHistoryApi) First(c *gin.Context) { + var info common.GetById + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + data, err := autoCodeHistoryService.First(c.Request.Context(), info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithDetailed(gin.H{"meta": data}, "获取成功", c) +} + +// Delete +// @Tags AutoCode +// @Summary 删除回滚记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "请求参数" +// @Success 200 {object} response.Response{msg=string} "删除回滚记录" +// @Router /autoCode/delSysHistory [post] +func (a *AutoCodeHistoryApi) Delete(c *gin.Context) { + var info common.GetById + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeHistoryService.Delete(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// RollBack +// @Tags AutoCode +// @Summary 回滚自动生成代码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAutoHistoryRollBack true "请求参数" +// @Success 200 {object} response.Response{msg=string} "回滚自动生成代码" +// @Router /autoCode/rollback [post] +func (a *AutoCodeHistoryApi) RollBack(c *gin.Context) { + var info request.SysAutoHistoryRollBack + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeHistoryService.RollBack(c.Request.Context(), info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("回滚成功", c) +} + +// GetList +// @Tags AutoCode +// @Summary 查询回滚记录 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body common.PageInfo true "请求参数" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "查询回滚记录,返回包括列表,总数,页码,每页数量" +// @Router /autoCode/getSysHistory [post] +func (a *AutoCodeHistoryApi) GetList(c *gin.Context) { + var info common.PageInfo + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := autoCodeHistoryService.GetList(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: info.Page, + PageSize: info.PageSize, + }, "获取成功", c) +} diff --git a/server/api/v1/system/auto_code_mcp.go b/server/api/v1/system/auto_code_mcp.go new file mode 100644 index 0000000..671b8d5 --- /dev/null +++ b/server/api/v1/system/auto_code_mcp.go @@ -0,0 +1,173 @@ +package system + +import ( + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp" + "github.com/flipped-aurora/gin-vue-admin/server/mcp/client" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/mark3labs/mcp-go/mcp" +) + +func (a *AutoCodeTemplateApi) MCP(c *gin.Context) { + var info request.AutoMcpTool + if err := c.ShouldBindJSON(&info); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + toolFilePath, err := autoCodeTemplateService.CreateMcp(c.Request.Context(), info) + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c) +} + +func (a *AutoCodeTemplateApi) MCPStatus(c *gin.Context) { + response.OkWithData(gin.H{ + "status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()), + "mcpServerConfig": buildMCPServerConfig(), + }, c) +} + +func (a *AutoCodeTemplateApi) MCPStart(c *gin.Context) { + status, err := mcpTool.StartManagedStandalone(c.Request.Context()) + if err != nil { + response.FailWithDetailed(gin.H{ + "status": status, + "mcpServerConfig": buildMCPServerConfig(), + }, err.Error(), c) + return + } + + response.OkWithDetailed(gin.H{ + "status": status, + "mcpServerConfig": buildMCPServerConfig(), + }, "MCP独立服务已启动", c) +} + +func (a *AutoCodeTemplateApi) MCPStop(c *gin.Context) { + status, err := mcpTool.StopManagedStandalone(c.Request.Context()) + if err != nil { + response.FailWithDetailed(gin.H{ + "status": status, + "mcpServerConfig": buildMCPServerConfig(), + }, err.Error(), c) + return + } + + response.OkWithDetailed(gin.H{ + "status": status, + "mcpServerConfig": buildMCPServerConfig(), + }, "MCP独立服务已停用", c) +} + +func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) { + baseURL := mcpTool.ResolveMCPServiceURL() + testClient, err := client.NewClient(baseURL, "testClient", "v1.0.0", mcpServerName(), incomingMCPHeaders(c)) + if err != nil { + response.FailWithDetailed(gin.H{ + "status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()), + "mcpServerConfig": buildMCPServerConfig(), + }, "连接MCP服务失败:"+err.Error(), c) + return + } + defer testClient.Close() + + list, err := testClient.ListTools(c.Request.Context(), mcp.ListToolsRequest{}) + if err != nil { + response.FailWithDetailed(gin.H{ + "status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()), + "mcpServerConfig": buildMCPServerConfig(), + }, "获取工具列表失败:"+err.Error(), c) + return + } + + response.OkWithData(gin.H{ + "status": mcpTool.GetManagedStandaloneStatus(c.Request.Context()), + "mcpServerConfig": buildMCPServerConfig(), + "list": list, + }, c) +} + +func (a *AutoCodeTemplateApi) MCPRoutes(c *gin.Context) { + response.OkWithData(gin.H{ + "routes": global.GVA_ROUTERS, + }, c) +} + +func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) { + var testRequest struct { + Name string `json:"name" binding:"required"` + Arguments map[string]interface{} `json:"arguments" binding:"required"` + } + if err := c.ShouldBindJSON(&testRequest); err != nil { + response.FailWithMessage("参数解析失败:"+err.Error(), c) + return + } + + baseURL := mcpTool.ResolveMCPServiceURL() + testClient, err := client.NewClient(baseURL, "testClient", "v1.0.0", mcpServerName(), incomingMCPHeaders(c)) + if err != nil { + response.FailWithMessage("连接MCP服务失败:"+err.Error(), c) + return + } + defer testClient.Close() + + callRequest := mcp.CallToolRequest{} + callRequest.Params.Name = testRequest.Name + callRequest.Params.Arguments = testRequest.Arguments + + result, err := testClient.CallTool(c.Request.Context(), callRequest) + if err != nil { + response.FailWithMessage("工具调用失败:"+err.Error(), c) + return + } + if len(result.Content) == 0 { + response.FailWithMessage("工具未返回任何内容", c) + return + } + + response.OkWithData(result.Content, c) +} + +func incomingMCPHeaders(c *gin.Context) map[string]string { + headerName := mcpTool.ConfiguredAuthHeader() + headerValue := c.GetHeader(headerName) + if headerValue == "" { + return nil + } + + return map[string]string{ + headerName: headerValue, + } +} + +func buildMCPServerConfig() map[string]interface{} { + baseURL := mcpTool.ResolveMCPServiceURL() + authHeader := mcpTool.ConfiguredAuthHeader() + serverName := mcpServerName() + + return map[string]interface{}{ + "mcpServers": map[string]interface{}{ + serverName: map[string]interface{}{ + "url": baseURL, + "headers": map[string]string{ + authHeader: "${YOUR_GVA_TOKEN}", + }, + }, + }, + } +} + +func mcpServerName() string { + if name := strings.TrimSpace(global.GVA_CONFIG.MCP.Name); name != "" { + return name + } + return "gva" +} diff --git a/server/api/v1/system/auto_code_package.go b/server/api/v1/system/auto_code_package.go new file mode 100644 index 0000000..655f29a --- /dev/null +++ b/server/api/v1/system/auto_code_package.go @@ -0,0 +1,100 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "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/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "strings" +) + +type AutoCodePackageApi struct{} + +// Create +// @Tags AutoCodePackage +// @Summary 创建package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAutoCodePackageCreate true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/createPackage [post] +func (a *AutoCodePackageApi) Create(c *gin.Context) { + var info request.SysAutoCodePackageCreate + _ = c.ShouldBindJSON(&info) + if err := utils.Verify(info, utils.AutoPackageVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if strings.Contains(info.PackageName, "\\") || strings.Contains(info.PackageName, "/") || strings.Contains(info.PackageName, "..") { + response.FailWithMessage("包名不合法", c) + return + } // PackageName可能导致路径穿越的问题 / 和 \ 都要防止 + err := autoCodePackageService.Create(c.Request.Context(), &info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete +// @Tags AutoCode +// @Summary 删除package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body common.GetById true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "删除package成功" +// @Router /autoCode/delPackage [post] +func (a *AutoCodePackageApi) Delete(c *gin.Context) { + var info common.GetById + _ = c.ShouldBindJSON(&info) + err := autoCodePackageService.Delete(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// All +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getPackage [post] +func (a *AutoCodePackageApi) All(c *gin.Context) { + data, err := autoCodePackageService.All(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"pkgs": data}, "获取成功", c) +} + +// Templates +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getTemplates [get] +func (a *AutoCodePackageApi) Templates(c *gin.Context) { + data, err := autoCodePackageService.Templates(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(data, "获取成功", c) +} diff --git a/server/api/v1/system/auto_code_plugin.go b/server/api/v1/system/auto_code_plugin.go new file mode 100644 index 0000000..c751b8b --- /dev/null +++ b/server/api/v1/system/auto_code_plugin.go @@ -0,0 +1,218 @@ +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" +) + +type AutoCodePluginApi struct{} + +// Install +// @Tags AutoCodePlugin +// @Summary 安装插件 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param plug formData file true "this is a test file" +// @Success 200 {object} response.Response{data=[]interface{},msg=string} "安装插件成功" +// @Router /autoCode/installPlugin [post] +func (a *AutoCodePluginApi) Install(c *gin.Context) { + header, err := c.FormFile("plug") + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + web, server, err := autoCodePluginService.Install(header) + webStr := "web插件安装成功" + serverStr := "server插件安装成功" + if web == -1 { + webStr = "web端插件未成功安装,请按照文档自行解压安装,如果为纯后端插件请忽略此条提示" + } + if server == -1 { + serverStr = "server端插件未成功安装,请按照文档自行解压安装,如果为纯前端插件请忽略此条提示" + } + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithData([]interface{}{ + gin.H{ + "code": web, + "msg": webStr, + }, + gin.H{ + "code": server, + "msg": serverStr, + }}, c) +} + +// Packaged +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param plugName query string true "插件名称" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/pubPlug [post] +func (a *AutoCodePluginApi) Packaged(c *gin.Context) { + plugName := c.Query("plugName") + zipPath, err := autoCodePluginService.PubPlug(plugName) + if err != nil { + global.GVA_LOG.Error("打包失败!", zap.Error(err)) + response.FailWithMessage("打包失败"+err.Error(), c) + return + } + response.OkWithMessage(fmt.Sprintf("打包成功,文件路径为:%s", zipPath), c) +} + +// InitMenu +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/initMenu [post] +func (a *AutoCodePluginApi) InitMenu(c *gin.Context) { + var menuInfo request.InitMenu + err := c.ShouldBindJSON(&menuInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodePluginService.InitMenu(menuInfo) + if err != nil { + global.GVA_LOG.Error("创建初始化Menu失败!", zap.Error(err)) + response.FailWithMessage("创建初始化Menu失败"+err.Error(), c) + return + } + response.OkWithMessage("文件变更成功", c) +} + +// InitAPI +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/initAPI [post] +func (a *AutoCodePluginApi) InitAPI(c *gin.Context) { + var apiInfo request.InitApi + err := c.ShouldBindJSON(&apiInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodePluginService.InitAPI(apiInfo) + if err != nil { + global.GVA_LOG.Error("创建初始化API失败!", zap.Error(err)) + response.FailWithMessage("创建初始化API失败"+err.Error(), c) + return + } + 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) +} diff --git a/server/api/v1/system/auto_code_template.go b/server/api/v1/system/auto_code_template.go new file mode 100644 index 0000000..0f6041c --- /dev/null +++ b/server/api/v1/system/auto_code_template.go @@ -0,0 +1,121 @@ +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/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AutoCodeTemplateApi struct{} + +// Preview +// @Tags AutoCodeTemplate +// @Summary 预览创建后的代码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "预览创建代码" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "预览创建后的代码" +// @Router /autoCode/preview [post] +func (a *AutoCodeTemplateApi) Preview(c *gin.Context) { + var info request.AutoCode + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(info, utils.AutoCodeVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = info.Pretreatment() + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + info.PackageT = utils.FirstUpper(info.Package) + autoCode, err := autoCodeTemplateService.Preview(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error(err.Error(), zap.Error(err)) + response.FailWithMessage("预览失败:"+err.Error(), c) + } else { + response.OkWithDetailed(gin.H{"autoCode": autoCode}, "预览成功", c) + } +} + +// Create +// @Tags AutoCodeTemplate +// @Summary 自动代码模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/createTemp [post] +func (a *AutoCodeTemplateApi) Create(c *gin.Context) { + var info request.AutoCode + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(info, utils.AutoCodeVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = info.Pretreatment() + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodeTemplateService.Create(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + } else { + response.OkWithMessage("创建成功", c) + } +} + +// AddFunc +// @Tags AddFunc +// @Summary 增加方法 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoCode true "增加方法" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/addFunc [post] +func (a *AutoCodeTemplateApi) AddFunc(c *gin.Context) { + var info request.AutoFunc + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + var tempMap map[string]string + if info.IsPreview { + info.Router = "填充router" + info.FuncName = "填充funcName" + info.Method = "填充method" + info.Description = "填充description" + tempMap, err = autoCodeTemplateService.GetApiAndServer(info) + } else { + err = autoCodeTemplateService.AddFunc(info) + } + if err != nil { + global.GVA_LOG.Error("注入失败!", zap.Error(err)) + response.FailWithMessage("注入失败", c) + } else { + if info.IsPreview { + response.OkWithDetailed(tempMap, "注入成功", c) + return + } + response.OkWithMessage("注入成功", c) + } +} diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go new file mode 100644 index 0000000..aceac88 --- /dev/null +++ b/server/api/v1/system/enter.go @@ -0,0 +1,59 @@ +package system + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { + DBApi + JwtApi + BaseApi + SystemApi + CasbinApi + AutoCodeApi + SystemApiApi + AuthorityApi + DictionaryApi + AuthorityMenuApi + OperationRecordApi + DictionaryDetailApi + AuthorityBtnApi + SysExportTemplateApi + AutoCodePluginApi + AutoCodePackageApi + AutoCodeHistoryApi + AutoCodeTemplateApi + SysParamsApi + SysVersionApi + SysErrorApi + LoginLogApi + ApiTokenApi + SkillsApi + AIWorkflowSessionApi +} + +var ( + apiService = service.ServiceGroupApp.SystemServiceGroup.ApiService + jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService + menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService + userService = service.ServiceGroupApp.SystemServiceGroup.UserService + initDBService = service.ServiceGroupApp.SystemServiceGroup.InitDBService + casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService + baseMenuService = service.ServiceGroupApp.SystemServiceGroup.BaseMenuService + authorityService = service.ServiceGroupApp.SystemServiceGroup.AuthorityService + dictionaryService = service.ServiceGroupApp.SystemServiceGroup.DictionaryService + authorityBtnService = service.ServiceGroupApp.SystemServiceGroup.AuthorityBtnService + systemConfigService = service.ServiceGroupApp.SystemServiceGroup.SystemConfigService + sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService + operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService + dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + autoCodeService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeService + aiWorkflowSessionService = service.ServiceGroupApp.SystemServiceGroup.AIWorkflowSession + autoCodePluginService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePlugin + autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory + autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService + sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService + loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService + apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService + skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService +) diff --git a/server/api/v1/system/sys_api.go b/server/api/v1/system/sys_api.go new file mode 100644 index 0000000..97a07b0 --- /dev/null +++ b/server/api/v1/system/sys_api.go @@ -0,0 +1,381 @@ +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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SystemApiApi struct{} + +// CreateApi +// @Tags SysApi +// @Summary 创建基础api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" +// @Success 200 {object} response.Response{msg=string} "创建基础api" +// @Router /api/createApi [post] +func (s *SystemApiApi) CreateApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api, utils.ApiVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.CreateApi(api) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// SyncApi +// @Tags SysApi +// @Summary 同步API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "同步API" +// @Router /api/syncApi [get] +func (s *SystemApiApi) SyncApi(c *gin.Context) { + newApis, deleteApis, ignoreApis, err := apiService.SyncApi() + if err != nil { + global.GVA_LOG.Error("同步失败!", zap.Error(err)) + response.FailWithMessage("同步失败", c) + return + } + response.OkWithData(gin.H{ + "newApis": newApis, + "deleteApis": deleteApis, + "ignoreApis": ignoreApis, + }, c) +} + +// GetApiGroups +// @Tags SysApi +// @Summary 获取API分组 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "获取API分组" +// @Router /api/getApiGroups [get] +func (s *SystemApiApi) GetApiGroups(c *gin.Context) { + groups, apiGroupMap, err := apiService.GetApiGroups() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithData(gin.H{ + "groups": groups, + "apiGroupMap": apiGroupMap, + }, c) +} + +// IgnoreApi +// @Tags IgnoreApi +// @Summary 忽略API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "同步API" +// @Router /api/ignoreApi [post] +func (s *SystemApiApi) IgnoreApi(c *gin.Context) { + var ignoreApi system.SysIgnoreApi + err := c.ShouldBindJSON(&ignoreApi) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.IgnoreApi(ignoreApi) + if err != nil { + global.GVA_LOG.Error("忽略失败!", zap.Error(err)) + response.FailWithMessage("忽略失败", c) + return + } + response.Ok(c) +} + +// EnterSyncApi +// @Tags SysApi +// @Summary 确认同步API +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "确认同步API" +// @Router /api/enterSyncApi [post] +func (s *SystemApiApi) EnterSyncApi(c *gin.Context) { + var syncApi systemRes.SysSyncApis + err := c.ShouldBindJSON(&syncApi) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.EnterSyncApi(syncApi) + if err != nil { + global.GVA_LOG.Error("忽略失败!", zap.Error(err)) + response.FailWithMessage("忽略失败", c) + return + } + response.Ok(c) +} + +// DeleteApi +// @Tags SysApi +// @Summary 删除api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "ID" +// @Success 200 {object} response.Response{msg=string} "删除api" +// @Router /api/deleteApi [post] +func (s *SystemApiApi) DeleteApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api.GVA_MODEL, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.DeleteApi(api) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetApiList +// @Tags SysApi +// @Summary 分页获取API列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SearchApiParams true "分页获取API列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取API列表,返回包括列表,总数,页码,每页数量" +// @Router /api/getApiList [post] +func (s *SystemApiApi) GetApiList(c *gin.Context) { + var pageInfo systemReq.SearchApiParams + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo.PageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := apiService.GetAPIInfoList(pageInfo.SysApi, pageInfo.PageInfo, pageInfo.OrderKey, pageInfo.Desc) + 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) +} + +// GetApiById +// @Tags SysApi +// @Summary 根据id获取api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "根据id获取api" +// @Success 200 {object} response.Response{data=systemRes.SysAPIResponse} "根据id获取api,返回包括api详情" +// @Router /api/getApiById [post] +func (s *SystemApiApi) GetApiById(c *gin.Context) { + var idInfo request.GetById + err := c.ShouldBindJSON(&idInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(idInfo, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + api, err := apiService.GetApiById(idInfo.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysAPIResponse{Api: api}, "获取成功", c) +} + +// UpdateApi +// @Tags SysApi +// @Summary 修改基础api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysApi true "api路径, api中文描述, api组, 方法" +// @Success 200 {object} response.Response{msg=string} "修改基础api" +// @Router /api/updateApi [post] +func (s *SystemApiApi) UpdateApi(c *gin.Context) { + var api system.SysApi + err := c.ShouldBindJSON(&api) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(api, utils.ApiVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.UpdateApi(api) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// GetAllApis +// @Tags SysApi +// @Summary 获取所有的Api 不分页 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysAPIListResponse,msg=string} "获取所有的Api 不分页,返回包括api列表" +// @Router /api/getAllApis [post] +func (s *SystemApiApi) GetAllApis(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + apis, err := apiService.GetAllApis(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysAPIListResponse{Apis: apis}, "获取成功", c) +} + +// DeleteApisByIds +// @Tags SysApi +// @Summary 删除选中Api +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "ID" +// @Success 200 {object} response.Response{msg=string} "删除选中Api" +// @Router /api/deleteApisByIds [delete] +func (s *SystemApiApi) DeleteApisByIds(c *gin.Context) { + var ids request.IdsReq + err := c.ShouldBindJSON(&ids) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = apiService.DeleteApisByIds(ids) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// FreshCasbin +// @Tags SysApi +// @Summary 刷新casbin缓存 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "刷新成功" +// @Router /api/freshCasbin [get] +func (s *SystemApiApi) FreshCasbin(c *gin.Context) { + err := casbinService.FreshCasbin() + if err != nil { + global.GVA_LOG.Error("刷新失败!", zap.Error(err)) + response.FailWithMessage("刷新失败", c) + return + } + response.OkWithMessage("刷新成功", c) +} + +// GetApiRoles +// @Tags SysApi +// @Summary 获取拥有指定API权限的角色ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param path query string true "API路径" +// @Param method query string true "请求方法" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" +// @Router /api/getApiRoles [get] +func (s *SystemApiApi) GetApiRoles(c *gin.Context) { + path := c.Query("path") + method := c.Query("method") + if path == "" || method == "" { + response.FailWithMessage("API路径和请求方法不能为空", c) + return + } + authorityIds, err := casbinService.GetAuthoritiesByApi(path, method) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if authorityIds == nil { + authorityIds = []uint{} + } + response.OkWithDetailed(authorityIds, "获取成功", c) +} + +// SetApiRoles +// @Tags SysApi +// @Summary 全量覆盖某API关联的角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /api/setApiRoles [post] +func (s *SystemApiApi) SetApiRoles(c *gin.Context) { + var req systemReq.SetApiAuthorities + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.Path == "" || req.Method == "" { + response.FailWithMessage("API路径和请求方法不能为空", c) + return + } + if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + // 刷新casbin缓存使策略立即生效 + _ = casbinService.FreshCasbin() + response.OkWithMessage("设置成功", c) +} diff --git a/server/api/v1/system/sys_api_token.go b/server/api/v1/system/sys_api_token.go new file mode 100644 index 0000000..14582d8 --- /dev/null +++ b/server/api/v1/system/sys_api_token.go @@ -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) +} diff --git a/server/api/v1/system/sys_authority.go b/server/api/v1/system/sys_authority.go new file mode 100644 index 0000000..1a4c101 --- /dev/null +++ b/server/api/v1/system/sys_authority.go @@ -0,0 +1,257 @@ +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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthorityApi struct{} + +// CreateAuthority +// @Tags Authority +// @Summary 创建角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "创建角色,返回包括系统角色详情" +// @Router /authority/createAuthority [post] +func (a *AuthorityApi) CreateAuthority(c *gin.Context) { + var authority, authBack system.SysAuthority + var err error + + if err = c.ShouldBindJSON(&authority); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if err = utils.Verify(authority, utils.AuthorityVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if *authority.ParentId == 0 && global.GVA_CONFIG.System.UseStrictAuth { + authority.ParentId = utils.Pointer(utils.GetUserAuthorityId(c)) + } + + if authBack, err = authorityService.CreateAuthority(authority); err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败"+err.Error(), c) + return + } + err = casbinService.FreshCasbin() + if err != nil { + global.GVA_LOG.Error("创建成功,权限刷新失败。", zap.Error(err)) + response.FailWithMessage("创建成功,权限刷新失败。"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "创建成功", c) +} + +// CopyAuthority +// @Tags Authority +// @Summary 拷贝角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body response.SysAuthorityCopyResponse true "旧角色id, 新权限id, 新权限名, 新父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "拷贝角色,返回包括系统角色详情" +// @Router /authority/copyAuthority [post] +func (a *AuthorityApi) CopyAuthority(c *gin.Context) { + var copyInfo systemRes.SysAuthorityCopyResponse + err := c.ShouldBindJSON(©Info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo, utils.OldAuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo.Authority, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + authBack, err := authorityService.CopyAuthority(adminAuthorityID, copyInfo) + if err != nil { + global.GVA_LOG.Error("拷贝失败!", zap.Error(err)) + response.FailWithMessage("拷贝失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "拷贝成功", c) +} + +// DeleteAuthority +// @Tags Authority +// @Summary 删除角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "删除角色" +// @Success 200 {object} response.Response{msg=string} "删除角色" +// @Router /authority/deleteAuthority [post] +func (a *AuthorityApi) DeleteAuthority(c *gin.Context) { + var authority system.SysAuthority + var err error + if err = c.ShouldBindJSON(&authority); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err = utils.Verify(authority, utils.AuthorityIdVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // 删除角色之前需要判断是否有用户正在使用此角色 + if err = authorityService.DeleteAuthority(&authority); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败"+err.Error(), c) + return + } + _ = casbinService.FreshCasbin() + response.OkWithMessage("删除成功", c) +} + +// UpdateAuthority +// @Tags Authority +// @Summary 更新角色信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "更新角色信息,返回包括系统角色详情" +// @Router /authority/updateAuthority [put] +func (a *AuthorityApi) UpdateAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + authority, err := authorityService.UpdateAuthority(auth) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authority}, "更新成功", c) +} + +// GetAuthorityList +// @Tags Authority +// @Summary 分页获取角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取角色列表,返回包括列表,总数,页码,每页数量" +// @Router /authority/getAuthorityList [post] +func (a *AuthorityApi) GetAuthorityList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + list, err := authorityService.GetAuthorityInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} + +// SetDataAuthority +// @Tags Authority +// @Summary 设置角色资源权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "设置角色资源权限" +// @Success 200 {object} response.Response{msg=string} "设置角色资源权限" +// @Router /authority/setDataAuthority [post] +func (a *AuthorityApi) SetDataAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + err = authorityService.SetDataAuthority(adminAuthorityID, auth) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} + +// GetUsersByAuthority +// @Tags Authority +// @Summary 获取拥有指定角色的用户ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param authorityId query uint true "角色ID" +// @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功" +// @Router /authority/getUsersByAuthority [get] +func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) { + var req systemReq.SetRoleUsers + if err := c.ShouldBindQuery(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if userIds == nil { + userIds = []uint{} + } + response.OkWithDetailed(userIds, "获取成功", c) +} + +// SetRoleUsers +// @Tags Authority +// @Summary 全量覆盖某角色关联的用户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /authority/setRoleUsers [post] +func (a *AuthorityApi) SetRoleUsers(c *gin.Context) { + var req systemReq.SetRoleUsers + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.AuthorityId == 0 { + response.FailWithMessage("角色ID不能为空", c) + return + } + if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} diff --git a/server/api/v1/system/sys_authority_btn.go b/server/api/v1/system/sys_authority_btn.go new file mode 100644 index 0000000..94f02a0 --- /dev/null +++ b/server/api/v1/system/sys_authority_btn.go @@ -0,0 +1,80 @@ +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 AuthorityBtnApi struct{} + +// GetAuthorityBtn +// @Tags AuthorityBtn +// @Summary 获取权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" +// @Success 200 {object} response.Response{data=response.SysAuthorityBtnRes,msg=string} "返回列表成功" +// @Router /authorityBtn/getAuthorityBtn [post] +func (a *AuthorityBtnApi) GetAuthorityBtn(c *gin.Context) { + var req request.SysAuthorityBtnReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + res, err := authorityBtnService.GetAuthorityBtn(req) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(res, "查询成功", c) +} + +// SetAuthorityBtn +// @Tags AuthorityBtn +// @Summary 设置权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAuthorityBtnReq true "菜单id, 角色id, 选中的按钮id" +// @Success 200 {object} response.Response{msg=string} "返回列表成功" +// @Router /authorityBtn/setAuthorityBtn [post] +func (a *AuthorityBtnApi) SetAuthorityBtn(c *gin.Context) { + var req request.SysAuthorityBtnReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = authorityBtnService.SetAuthorityBtn(req) + if err != nil { + global.GVA_LOG.Error("分配失败!", zap.Error(err)) + response.FailWithMessage("分配失败", c) + return + } + response.OkWithMessage("分配成功", c) +} + +// CanRemoveAuthorityBtn +// @Tags AuthorityBtn +// @Summary 设置权限按钮 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /authorityBtn/canRemoveAuthorityBtn [post] +func (a *AuthorityBtnApi) CanRemoveAuthorityBtn(c *gin.Context) { + id := c.Query("id") + err := authorityBtnService.CanRemoveAuthorityBtn(id) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} diff --git a/server/api/v1/system/sys_auto_code.go b/server/api/v1/system/sys_auto_code.go new file mode 100644 index 0000000..e0505d5 --- /dev/null +++ b/server/api/v1/system/sys_auto_code.go @@ -0,0 +1,219 @@ +package system + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-gonic/gin" + "github.com/goccy/go-json" + "go.uber.org/zap" +) + +type AutoCodeApi struct{} + +func (autoApi *AutoCodeApi) GetDB(c *gin.Context) { + businessDB := c.Query("businessDB") + dbs, err := autoCodeService.Database(businessDB).GetDB(businessDB) + var dbList []map[string]interface{} + for _, db := range global.GVA_CONFIG.DBList { + item := map[string]interface{}{ + "aliasName": db.AliasName, + "dbName": db.Dbname, + "disable": db.Disable, + "dbtype": db.Type, + } + dbList = append(dbList, item) + } + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"dbs": dbs, "dbList": dbList}, "获取成功", c) +} + +func (autoApi *AutoCodeApi) GetTables(c *gin.Context) { + dbName := c.Query("dbName") + businessDB := c.Query("businessDB") + if dbName == "" { + dbName = *global.GVA_ACTIVE_DBNAME + if businessDB != "" { + for _, db := range global.GVA_CONFIG.DBList { + if db.AliasName == businessDB { + dbName = db.Dbname + } + } + } + } + + tables, err := autoCodeService.Database(businessDB).GetTables(businessDB, dbName) + if err != nil { + global.GVA_LOG.Error("查询table失败!", zap.Error(err)) + response.FailWithMessage("查询table失败", c) + return + } + response.OkWithDetailed(gin.H{"tables": tables}, "获取成功", c) +} + +func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) { + businessDB := c.Query("businessDB") + dbName := c.Query("dbName") + if dbName == "" { + dbName = *global.GVA_ACTIVE_DBNAME + if businessDB != "" { + for _, db := range global.GVA_CONFIG.DBList { + if db.AliasName == businessDB { + dbName = db.Dbname + } + } + } + } + tableName := c.Query("tableName") + columns, err := autoCodeService.Database(businessDB).GetColumn(businessDB, tableName, dbName) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"columns": columns}, "获取成功", c) +} + +func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) { + var llm common.JSONMap + if err := c.ShouldBindJSON(&llm); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + if shouldStreamLLM(c, llm) { + if err := autoApi.proxyLLMStream(c, llm); err != nil { + global.GVA_LOG.Error("大模型流式代理失败!", zap.Error(err)) + if c.Writer.Written() { + writeLLMStreamError(c, err) + return + } + response.FailWithMessage(err.Error(), c) + } + return + } + + data, err := autoCodeService.LLMAuto(c.Request.Context(), llm) + if err != nil { + global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + response.OkWithData(data, c) +} + +func shouldStreamLLM(c *gin.Context, llm common.JSONMap) bool { + responseMode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", llm["response_mode"]))) + if responseMode == "streaming" || responseMode == "sse" { + return true + } + if stream, ok := llm["stream"].(bool); ok && stream { + return true + } + return strings.Contains(strings.ToLower(c.GetHeader("Accept")), "text/event-stream") +} + +func (autoApi *AutoCodeApi) proxyLLMStream(c *gin.Context, llm common.JSONMap) error { + res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr) + } + return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body)) + } + + flusher, ok := c.Writer.(http.Flusher) + if !ok { + return errors.New("当前响应不支持流式输出") + } + + copyLLMStreamHeaders(c.Writer.Header(), res.Header) + if c.Writer.Header().Get("Content-Type") == "" { + c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8") + } + if c.Writer.Header().Get("Cache-Control") == "" { + c.Writer.Header().Set("Cache-Control", "no-cache") + } + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("X-Accel-Buffering", "no") + c.Status(res.StatusCode) + flusher.Flush() + + buf := make([]byte, 32*1024) + for { + n, readErr := res.Body.Read(buf) + if n > 0 { + if _, writeErr := c.Writer.Write(buf[:n]); writeErr != nil { + return fmt.Errorf("向客户端写入流式响应失败: %w", writeErr) + } + flusher.Flush() + } + if readErr != nil { + if errors.Is(readErr, io.EOF) { + return nil + } + return fmt.Errorf("读取上游流式响应失败: %w", readErr) + } + } +} + +func copyLLMStreamHeaders(dst, src http.Header) { + for _, key := range []string{ + "Content-Type", + "Cache-Control", + "Content-Encoding", + "Content-Language", + "X-Accel-Buffering", + } { + if value := src.Get(key); value != "" { + dst.Set(key, value) + } + } +} + +func writeLLMStreamError(c *gin.Context, err error) { + payload, marshalErr := json.Marshal(gin.H{ + "message": err.Error(), + }) + if marshalErr != nil { + payload = []byte(`{"message":"流式代理失败"}`) + } + _, _ = c.Writer.WriteString("event: error\n") + _, _ = c.Writer.WriteString("data: ") + _, _ = c.Writer.Write(payload) + _, _ = c.Writer.WriteString("\n\n") + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } +} + +func previewResponseBody(body []byte) string { + text := strings.TrimSpace(string(body)) + text = strings.ReplaceAll(text, "\r", " ") + text = strings.ReplaceAll(text, "\n", " ") + text = strings.Join(strings.Fields(text), " ") + if text == "" { + return "" + } + runes := []rune(text) + if len(runes) > 300 { + return string(runes[:300]) + "..." + } + return text +} diff --git a/server/api/v1/system/sys_auto_code_sse.go b/server/api/v1/system/sys_auto_code_sse.go new file mode 100644 index 0000000..1acb96d --- /dev/null +++ b/server/api/v1/system/sys_auto_code_sse.go @@ -0,0 +1,210 @@ +package system + +import ( + "bufio" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-contrib/sse" + "github.com/gin-gonic/gin" + "github.com/goccy/go-json" + "go.uber.org/zap" +) + +func (autoApi *AutoCodeApi) LLMAutoSSE(c *gin.Context) { + var llm common.JSONMap + if err := c.ShouldBindJSON(&llm); err != nil { + global.GVA_LOG.Error("LLMAutoSSE 参数绑定失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + + if llm == nil { + llm = common.JSONMap{} + } + llm["response_mode"] = "streaming" + global.GVA_LOG.Info("LLMAutoSSE 收到请求", zap.Any("mode", llm["mode"])) + + if err := autoApi.streamLLMAsSSE(c, llm); err != nil { + global.GVA_LOG.Error("大模型 SSE 代理失败!", zap.Error(err)) + if c.Writer.Written() { + writeLLMStreamError(c, err) + return + } + response.FailWithMessage(err.Error(), c) + } +} + +func (autoApi *AutoCodeApi) streamLLMAsSSE(c *gin.Context, llm common.JSONMap) error { + res, err := autoCodeService.LLMAutoStream(c.Request.Context(), llm) + if err != nil { + return fmt.Errorf("调用上游大模型失败: %w", err) + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s read-body-err=%w", res.StatusCode, res.Header.Get("Content-Type"), readErr) + } + return fmt.Errorf("上游大模型流式服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, res.Header.Get("Content-Type"), previewResponseBody(body)) + } + + ct := res.Header.Get("Content-Type") + global.GVA_LOG.Info("LLMAutoSSE 上游返回成功,开始 SSE 流式转发", + zap.Int("status", res.StatusCode), + zap.String("content-type", ct)) + + // 如果上游返回的不是 SSE 流(可能是 blocking 模式返回的 JSON),直接读取并转发 + if !strings.Contains(ct, "text/event-stream") && !strings.Contains(ct, "text/plain") { + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("读取上游非流式响应失败: %w", readErr) + } + global.GVA_LOG.Warn("LLMAutoSSE 上游返回非 SSE 流,Content-Type: "+ct+", 将以单次事件转发", + zap.String("body_preview", previewResponseBody(body))) + + flusher, ok := c.Writer.(http.Flusher) + if !ok { + return errors.New("当前响应不支持流式输出") + } + prepareSSEHeaders(c) + c.Status(http.StatusOK) + + var payload any + if err := json.Unmarshal(body, &payload); err != nil { + payload = string(body) + } + if err := renderSSE(c, sse.Event{Event: "message", Data: payload}); err != nil { + return err + } + if err := renderSSE(c, sse.Event{Event: "done", Data: gin.H{"done": true}}); err != nil { + return err + } + flusher.Flush() + return nil + } + + flusher, ok := c.Writer.(http.Flusher) + if !ok { + return errors.New("当前响应不支持流式输出") + } + + prepareSSEHeaders(c) + c.Status(http.StatusOK) + flusher.Flush() + + reader := bufio.NewReader(res.Body) + lines := make([]string, 0, 8) + blockCount := 0 + + global.GVA_LOG.Info("LLMAutoSSE 开始读取上游流数据...") + + for { + global.GVA_LOG.Debug("LLMAutoSSE 等待读取下一行...") + line, readErr := reader.ReadString('\n') + if readErr != nil && !errors.Is(readErr, io.EOF) { + global.GVA_LOG.Error("LLMAutoSSE 读取上游流失败", zap.Int("已转发块数", blockCount), zap.Error(readErr)) + return fmt.Errorf("读取上游流式响应失败: %w", readErr) + } + + line = strings.TrimRight(line, "\r\n") + if line == "" { + if len(lines) > 0 { + blockCount++ + if blockCount <= 3 { + global.GVA_LOG.Debug("LLMAutoSSE 转发 SSE 块", zap.Int("block", blockCount), zap.Strings("lines", lines)) + } + } + if err := emitSSEBlock(c, lines); err != nil { + return err + } + lines = lines[:0] + } else { + lines = append(lines, line) + } + + if errors.Is(readErr, io.EOF) { + if err := emitSSEBlock(c, lines); err != nil { + return err + } + if err := renderSSE(c, sse.Event{ + Event: "done", + Data: gin.H{"done": true}, + }); err != nil { + return err + } + flusher.Flush() + global.GVA_LOG.Info("LLMAutoSSE 流式转发完成", zap.Int("总块数", blockCount)) + return nil + } + } +} + +func prepareSSEHeaders(c *gin.Context) { + header := c.Writer.Header() + header.Set("Content-Type", "text/event-stream; charset=utf-8") + header.Set("Cache-Control", "no-cache, no-transform") + header.Set("Connection", "keep-alive") + header.Set("X-Accel-Buffering", "no") +} + +func emitSSEBlock(c *gin.Context, lines []string) error { + if len(lines) == 0 { + return nil + } + + eventName := "message" + eventID := "" + dataLines := make([]string, 0, len(lines)) + + for _, line := range lines { + switch { + case strings.HasPrefix(line, "event:"): + eventName = strings.TrimSpace(strings.TrimPrefix(line, "event:")) + case strings.HasPrefix(line, "id:"): + eventID = strings.TrimSpace(strings.TrimPrefix(line, "id:")) + case strings.HasPrefix(line, "data:"): + dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:"))) + } + } + + rawData := strings.TrimSpace(strings.Join(dataLines, "\n")) + if rawData == "" { + return nil + } + if rawData == "[DONE]" { + return renderSSE(c, sse.Event{ + Id: eventID, + Event: "done", + Data: gin.H{"done": true}, + }) + } + + var payload interface{} + if err := json.Unmarshal([]byte(rawData), &payload); err != nil { + payload = rawData + } + + return renderSSE(c, sse.Event{ + Id: eventID, + Event: eventName, + Data: payload, + }) +} + +func renderSSE(c *gin.Context, event sse.Event) error { + if err := event.Render(c.Writer); err != nil { + return fmt.Errorf("写入 SSE 事件失败: %w", err) + } + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } + return nil +} diff --git a/server/api/v1/system/sys_captcha.go b/server/api/v1/system/sys_captcha.go new file mode 100644 index 0000000..b9f2110 --- /dev/null +++ b/server/api/v1/system/sys_captcha.go @@ -0,0 +1,70 @@ +package system + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/gin-gonic/gin" + "github.com/mojocn/base64Captcha" + "go.uber.org/zap" +) + +// 当开启多服务器部署时,替换下面的配置,使用redis共享存储验证码 +// var store = captcha.NewDefaultRedisStore() +var store = base64Captcha.DefaultMemStore + +type BaseApi struct{} + +// Captcha +// @Tags Base +// @Summary 生成验证码 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysCaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码" +// @Router /base/captcha [post] +func (b *BaseApi) Captcha(c *gin.Context) { + // 判断验证码是否开启 + openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 + openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 + key := c.ClientIP() + v, ok := global.BlackCache.Get(key) + if !ok { + global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) + } + + var oc bool + if openCaptcha == 0 || openCaptcha < interfaceToInt(v) { + oc = true + } + // 字符,公式,验证码配置 + // 生成默认数字的driver + driver := base64Captcha.NewDriverDigit(global.GVA_CONFIG.Captcha.ImgHeight, global.GVA_CONFIG.Captcha.ImgWidth, global.GVA_CONFIG.Captcha.KeyLong, 0.7, 80) + // cp := base64Captcha.NewCaptcha(driver, store.UseWithCtx(c)) // v8下使用redis + cp := base64Captcha.NewCaptcha(driver, store) + id, b64s, _, err := cp.Generate() + if err != nil { + global.GVA_LOG.Error("验证码获取失败!", zap.Error(err)) + response.FailWithMessage("验证码获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysCaptchaResponse{ + CaptchaId: id, + PicPath: b64s, + CaptchaLength: global.GVA_CONFIG.Captcha.KeyLong, + OpenCaptcha: oc, + }, "验证码获取成功", c) +} + +// 类型转换 +func interfaceToInt(v interface{}) (i int) { + switch v := v.(type) { + case int: + i = v + default: + i = 0 + } + return +} diff --git a/server/api/v1/system/sys_casbin.go b/server/api/v1/system/sys_casbin.go new file mode 100644 index 0000000..c1bf548 --- /dev/null +++ b/server/api/v1/system/sys_casbin.go @@ -0,0 +1,69 @@ +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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CasbinApi struct{} + +// UpdateCasbin +// @Tags Casbin +// @Summary 更新角色api权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.CasbinInReceive true "权限id, 权限模型列表" +// @Success 200 {object} response.Response{msg=string} "更新角色api权限" +// @Router /casbin/UpdateCasbin [post] +func (cas *CasbinApi) UpdateCasbin(c *gin.Context) { + var cmr request.CasbinInReceive + err := c.ShouldBindJSON(&cmr) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(cmr, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + err = casbinService.UpdateCasbin(adminAuthorityID, cmr.AuthorityId, cmr.CasbinInfos) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetPolicyPathByAuthorityId +// @Tags Casbin +// @Summary 获取权限列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.CasbinInReceive true "权限id, 权限模型列表" +// @Success 200 {object} response.Response{data=systemRes.PolicyPathResponse,msg=string} "获取权限列表,返回包括casbin详情列表" +// @Router /casbin/getPolicyPathByAuthorityId [post] +func (cas *CasbinApi) GetPolicyPathByAuthorityId(c *gin.Context) { + var casbin request.CasbinInReceive + err := c.ShouldBindJSON(&casbin) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(casbin, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + paths := casbinService.GetPolicyPathByAuthorityId(casbin.AuthorityId) + response.OkWithDetailed(systemRes.PolicyPathResponse{Paths: paths}, "获取成功", c) +} diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go new file mode 100644 index 0000000..9673e7f --- /dev/null +++ b/server/api/v1/system/sys_dictionary.go @@ -0,0 +1,191 @@ +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" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type DictionaryApi struct{} + +// CreateSysDictionary +// @Tags SysDictionary +// @Summary 创建SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "创建SysDictionary" +// @Router /sysDictionary/createSysDictionary [post] +func (s *DictionaryApi) CreateSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysDictionary +// @Tags SysDictionary +// @Summary 删除SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "删除SysDictionary" +// @Router /sysDictionary/deleteSysDictionary [delete] +func (s *DictionaryApi) DeleteSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.DeleteSysDictionary(dictionary) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateSysDictionary +// @Tags SysDictionary +// @Summary 更新SysDictionary +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionary true "SysDictionary模型" +// @Success 200 {object} response.Response{msg=string} "更新SysDictionary" +// @Router /sysDictionary/updateSysDictionary [put] +func (s *DictionaryApi) UpdateSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindJSON(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.UpdateSysDictionary(&dictionary) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysDictionary +// @Tags SysDictionary +// @Summary 用id查询SysDictionary +// @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} "用id查询SysDictionary" +// @Router /sysDictionary/findSysDictionary [get] +func (s *DictionaryApi) FindSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindQuery(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + sysDictionary, err := dictionaryService.GetSysDictionary(dictionary.Type, dictionary.ID, dictionary.Status) + if err != nil { + global.GVA_LOG.Error("字典未创建或未开启!", zap.Error(err)) + response.FailWithMessage("字典未创建或未开启", c) + return + } + response.OkWithDetailed(gin.H{"resysDictionary": sysDictionary}, "查询成功", c) +} + +// GetSysDictionaryList +// @Tags SysDictionary +// @Summary 分页获取SysDictionary列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.SysDictionarySearch true "字典 name 或者 type" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量" +// @Router /sysDictionary/getSysDictionaryList [get] +func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) { + var dictionary request.SysDictionarySearch + err := c.ShouldBindQuery(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + 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) +} diff --git a/server/api/v1/system/sys_dictionary_detail.go b/server/api/v1/system/sys_dictionary_detail.go new file mode 100644 index 0000000..6be069f --- /dev/null +++ b/server/api/v1/system/sys_dictionary_detail.go @@ -0,0 +1,267 @@ +package system + +import ( + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type DictionaryDetailApi struct{} + +// CreateSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 创建SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" +// @Success 200 {object} response.Response{msg=string} "创建SysDictionaryDetail" +// @Router /sysDictionaryDetail/createSysDictionaryDetail [post] +func (s *DictionaryDetailApi) CreateSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.CreateSysDictionaryDetail(detail) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 删除SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "SysDictionaryDetail模型" +// @Success 200 {object} response.Response{msg=string} "删除SysDictionaryDetail" +// @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete] +func (s *DictionaryDetailApi) DeleteSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.DeleteSysDictionaryDetail(detail) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 更新SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysDictionaryDetail true "更新SysDictionaryDetail" +// @Success 200 {object} response.Response{msg=string} "更新SysDictionaryDetail" +// @Router /sysDictionaryDetail/updateSysDictionaryDetail [put] +func (s *DictionaryDetailApi) UpdateSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindJSON(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryDetailService.UpdateSysDictionaryDetail(&detail) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysDictionaryDetail +// @Tags SysDictionaryDetail +// @Summary 用id查询SysDictionaryDetail +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysDictionaryDetail true "用id查询SysDictionaryDetail" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysDictionaryDetail" +// @Router /sysDictionaryDetail/findSysDictionaryDetail [get] +func (s *DictionaryDetailApi) FindSysDictionaryDetail(c *gin.Context) { + var detail system.SysDictionaryDetail + err := c.ShouldBindQuery(&detail) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(detail, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + reSysDictionaryDetail, err := dictionaryDetailService.GetSysDictionaryDetail(detail.ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(gin.H{"reSysDictionaryDetail": reSysDictionaryDetail}, "查询成功", c) +} + +// GetSysDictionaryDetailList +// @Tags SysDictionaryDetail +// @Summary 分页获取SysDictionaryDetail列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.SysDictionaryDetailSearch true "页码, 每页大小, 搜索条件" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量" +// @Router /sysDictionaryDetail/getSysDictionaryDetailList [get] +func (s *DictionaryDetailApi) GetSysDictionaryDetailList(c *gin.Context) { + var pageInfo request.SysDictionaryDetailSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := dictionaryDetailService.GetSysDictionaryDetailInfoList(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) +} + +// GetDictionaryTreeList +// @Tags SysDictionaryDetail +// @Summary 获取字典详情树形结构 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param sysDictionaryID query int true "字典ID" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构" +// @Router /sysDictionaryDetail/getDictionaryTreeList [get] +func (s *DictionaryDetailApi) GetDictionaryTreeList(c *gin.Context) { + sysDictionaryID := c.Query("sysDictionaryID") + if sysDictionaryID == "" { + response.FailWithMessage("字典ID不能为空", c) + return + } + + var id uint + if idUint64, err := strconv.ParseUint(sysDictionaryID, 10, 32); err != nil { + response.FailWithMessage("字典ID格式错误", c) + return + } else { + id = uint(idUint64) + } + + list, err := dictionaryDetailService.GetDictionaryTreeList(id) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryTreeListByType +// @Tags SysDictionaryDetail +// @Summary 根据字典类型获取字典详情树形结构 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param type query string true "字典类型" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情树形结构" +// @Router /sysDictionaryDetail/getDictionaryTreeListByType [get] +func (s *DictionaryDetailApi) GetDictionaryTreeListByType(c *gin.Context) { + dictType := c.Query("type") + if dictType == "" { + response.FailWithMessage("字典类型不能为空", c) + return + } + + list, err := dictionaryDetailService.GetDictionaryTreeListByType(dictType) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryDetailsByParent +// @Tags SysDictionaryDetail +// @Summary 根据父级ID获取字典详情 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.GetDictionaryDetailsByParentRequest true "查询参数" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情列表" +// @Router /sysDictionaryDetail/getDictionaryDetailsByParent [get] +func (s *DictionaryDetailApi) GetDictionaryDetailsByParent(c *gin.Context) { + var req request.GetDictionaryDetailsByParentRequest + err := c.ShouldBindQuery(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + list, err := dictionaryDetailService.GetDictionaryDetailsByParent(req) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"list": list}, "获取成功", c) +} + +// GetDictionaryPath +// @Tags SysDictionaryDetail +// @Summary 获取字典详情的完整路径 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param id query uint true "字典详情ID" +// @Success 200 {object} response.Response{data=[]system.SysDictionaryDetail,msg=string} "获取字典详情路径" +// @Router /sysDictionaryDetail/getDictionaryPath [get] +func (s *DictionaryDetailApi) GetDictionaryPath(c *gin.Context) { + idStr := c.Query("id") + if idStr == "" { + response.FailWithMessage("字典详情ID不能为空", c) + return + } + + var id uint + if idUint64, err := strconv.ParseUint(idStr, 10, 32); err != nil { + response.FailWithMessage("字典详情ID格式错误", c) + return + } else { + id = uint(idUint64) + } + + path, err := dictionaryDetailService.GetDictionaryPath(id) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"path": path}, "获取成功", c) +} diff --git a/server/api/v1/system/sys_error.go b/server/api/v1/system/sys_error.go new file mode 100644 index 0000000..966cd50 --- /dev/null +++ b/server/api/v1/system/sys_error.go @@ -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) +} diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go new file mode 100644 index 0000000..604309d --- /dev/null +++ b/server/api/v1/system/sys_export_template.go @@ -0,0 +1,456 @@ +package system + +import ( + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "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/flipped-aurora/gin-vue-admin/server/service" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// 用于token一次性存储 +var ( + exportTokenCache = make(map[string]interface{}) + exportTokenExpiration = make(map[string]time.Time) + tokenMutex sync.RWMutex +) + +// 五分钟检测窗口过期 +func cleanupExpiredTokens() { + for { + time.Sleep(5 * time.Minute) + tokenMutex.Lock() + now := time.Now() + for token, expiry := range exportTokenExpiration { + if now.After(expiry) { + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + } + } + tokenMutex.Unlock() + } +} + +func init() { + go cleanupExpiredTokens() +} + +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 创建导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "创建导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /sysExportTemplate/createSysExportTemplate [post] +func (sysExportTemplateApi *SysExportTemplateApi) CreateSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + verify := utils.Rules{ + "Name": {utils.NotEmpty()}, + } + if err := utils.Verify(sysExportTemplate, verify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.CreateSysExportTemplate(&sysExportTemplate); err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + } else { + response.OkWithMessage("创建成功", c) + } +} + +// DeleteSysExportTemplate 删除导出模板 +// @Tags SysExportTemplate +// @Summary 删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplate [delete] +func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.DeleteSysExportTemplate(sysExportTemplate); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + } else { + response.OkWithMessage("删除成功", c) + } +} + +// DeleteSysExportTemplateByIds 批量删除导出模板 +// @Tags SysExportTemplate +// @Summary 批量删除导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"批量删除成功"}" +// @Router /sysExportTemplate/deleteSysExportTemplateByIds [delete] +func (sysExportTemplateApi *SysExportTemplateApi) DeleteSysExportTemplateByIds(c *gin.Context) { + var IDS request.IdsReq + err := c.ShouldBindJSON(&IDS) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.DeleteSysExportTemplateByIds(IDS); err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + } else { + response.OkWithMessage("批量删除成功", c) + } +} + +// UpdateSysExportTemplate 更新导出模板 +// @Tags SysExportTemplate +// @Summary 更新导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysExportTemplate true "更新导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysExportTemplate/updateSysExportTemplate [put] +func (sysExportTemplateApi *SysExportTemplateApi) UpdateSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindJSON(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + verify := utils.Rules{ + "Name": {utils.NotEmpty()}, + } + if err := utils.Verify(sysExportTemplate, verify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := sysExportTemplateService.UpdateSysExportTemplate(sysExportTemplate); err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + } else { + response.OkWithMessage("更新成功", c) + } +} + +// FindSysExportTemplate 用id查询导出模板 +// @Tags SysExportTemplate +// @Summary 用id查询导出模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysExportTemplate true "用id查询导出模板" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysExportTemplate/findSysExportTemplate [get] +func (sysExportTemplateApi *SysExportTemplateApi) FindSysExportTemplate(c *gin.Context) { + var sysExportTemplate system.SysExportTemplate + err := c.ShouldBindQuery(&sysExportTemplate) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if resysExportTemplate, err := sysExportTemplateService.GetSysExportTemplate(sysExportTemplate.ID); err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + } else { + response.OkWithData(gin.H{"resysExportTemplate": resysExportTemplate}, c) + } +} + +// GetSysExportTemplateList 分页获取导出模板列表 +// @Tags SysExportTemplate +// @Summary 分页获取导出模板列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query systemReq.SysExportTemplateSearch true "分页获取导出模板列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysExportTemplate/getSysExportTemplateList [get] +func (sysExportTemplateApi *SysExportTemplateApi) GetSysExportTemplateList(c *gin.Context) { + var pageInfo systemReq.SysExportTemplateSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if list, total, err := sysExportTemplateService.GetSysExportTemplateInfoList(pageInfo); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) + } +} + +// ExportExcel 导出表格token +// @Tags SysExportTemplate +// @Summary 导出表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcel [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportExcel(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + queryParams := c.Request.URL.Query() + + //创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "queryParams": queryParams, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportExcelByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportExcelByToken 导出表格 +// @Tags ExportExcelByToken +// @Summary 导出表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcelByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportExcelByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + queryParams := exportParams["queryParams"].(url.Values) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出 + if file, name, err := sysExportTemplateService.ExportExcel(templateID, queryParams); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+utils.RandomString(6)+".xlsx")) + c.Header("success", "true") + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) + } +} + +// ExportTemplate 导出表格模板 +// @Tags SysExportTemplate +// @Summary 导出表格模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportTemplate [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplate(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + // 创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "isTemplate": true, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportTemplateByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportTemplateByToken 通过token导出表格模板 +// @Tags ExportTemplateByToken +// @Summary 通过token导出表格模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportTemplateByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplateByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 检查是否为模板导出 + isTemplate, _ := exportParams["isTemplate"].(bool) + if !isTemplate { + global.GVA_LOG.Error("token类型错误!") + response.FailWithMessage("token类型错误", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出模板 + if file, name, err := sysExportTemplateService.ExportTemplate(templateID); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+"模板.xlsx")) + c.Header("success", "true") + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) + } +} + +// ImportExcel 导入表格 +// @Tags SysImportTemplate +// @Summary 导入表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/importExcel [post] +func (sysExportTemplateApi *SysExportTemplateApi) ImportExcel(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + file, err := c.FormFile("file") + if err != nil { + global.GVA_LOG.Error("文件获取失败!", zap.Error(err)) + response.FailWithMessage("文件获取失败", c) + return + } + if err := sysExportTemplateService.ImportExcel(templateID, file); err != nil { + global.GVA_LOG.Error(err.Error(), zap.Error(err)) + response.FailWithMessage(err.Error(), c) + } else { + response.OkWithMessage("导入成功", c) + } +} diff --git a/server/api/v1/system/sys_initdb.go b/server/api/v1/system/sys_initdb.go new file mode 100644 index 0000000..5f740c6 --- /dev/null +++ b/server/api/v1/system/sys_initdb.go @@ -0,0 +1,59 @@ +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" + "go.uber.org/zap" + + "github.com/gin-gonic/gin" +) + +type DBApi struct{} + +// InitDB +// @Tags InitDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Param data body request.InitDB true "初始化数据库参数" +// @Success 200 {object} response.Response{data=string} "初始化用户数据库" +// @Router /init/initdb [post] +func (i *DBApi) InitDB(c *gin.Context) { + if global.GVA_DB != nil { + global.GVA_LOG.Error("已存在数据库配置!") + response.FailWithMessage("已存在数据库配置", c) + return + } + var dbInfo request.InitDB + if err := c.ShouldBindJSON(&dbInfo); err != nil { + global.GVA_LOG.Error("参数校验不通过!", zap.Error(err)) + response.FailWithMessage("参数校验不通过", c) + return + } + if err := initDBService.InitDB(dbInfo); err != nil { + global.GVA_LOG.Error("自动创建数据库失败!", zap.Error(err)) + response.FailWithMessage("自动创建数据库失败,请查看后台日志,检查后在进行初始化", c) + return + } + response.OkWithMessage("自动创建数据库成功", c) +} + +// CheckDB +// @Tags CheckDB +// @Summary 初始化用户数据库 +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "初始化用户数据库" +// @Router /init/checkdb [post] +func (i *DBApi) CheckDB(c *gin.Context) { + var ( + message = "前往初始化数据库" + needInit = true + ) + + if global.GVA_DB != nil { + message = "数据库无需初始化" + needInit = false + } + global.GVA_LOG.Info(message) + response.OkWithDetailed(gin.H{"needInit": needInit}, message, c) +} diff --git a/server/api/v1/system/sys_jwt_blacklist.go b/server/api/v1/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..f66c155 --- /dev/null +++ b/server/api/v1/system/sys_jwt_blacklist.go @@ -0,0 +1,33 @@ +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" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type JwtApi struct{} + +// JsonInBlacklist +// @Tags Jwt +// @Summary jwt加入黑名单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "jwt加入黑名单" +// @Router /jwt/jsonInBlacklist [post] +func (j *JwtApi) JsonInBlacklist(c *gin.Context) { + token := utils.GetToken(c) + jwt := system.JwtBlacklist{Jwt: token} + err := jwtService.JsonInBlacklist(jwt) + if err != nil { + global.GVA_LOG.Error("jwt作废失败!", zap.Error(err)) + response.FailWithMessage("jwt作废失败", c) + return + } + utils.ClearToken(c) + response.OkWithMessage("jwt作废成功", c) +} diff --git a/server/api/v1/system/sys_login_log.go b/server/api/v1/system/sys_login_log.go new file mode 100644 index 0000000..6a6b391 --- /dev/null +++ b/server/api/v1/system/sys_login_log.go @@ -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) +} diff --git a/server/api/v1/system/sys_menu.go b/server/api/v1/system/sys_menu.go new file mode 100644 index 0000000..0802708 --- /dev/null +++ b/server/api/v1/system/sys_menu.go @@ -0,0 +1,335 @@ +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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthorityMenuApi struct{} + +// GetMenu +// @Tags AuthorityMenu +// @Summary 获取用户动态路由 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body request.Empty true "空" +// @Success 200 {object} response.Response{data=systemRes.SysMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单详情列表" +// @Router /menu/getMenu [post] +func (a *AuthorityMenuApi) GetMenu(c *gin.Context) { + menus, err := menuService.GetMenuTree(utils.GetUserAuthorityId(c)) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + if menus == nil { + menus = []system.SysMenu{} + } + response.OkWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取成功", c) +} + +// GetBaseMenuTree +// @Tags AuthorityMenu +// @Summary 获取用户动态路由 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body request.Empty true "空" +// @Success 200 {object} response.Response{data=systemRes.SysBaseMenusResponse,msg=string} "获取用户动态路由,返回包括系统菜单列表" +// @Router /menu/getBaseMenuTree [post] +func (a *AuthorityMenuApi) GetBaseMenuTree(c *gin.Context) { + authority := utils.GetUserAuthorityId(c) + menus, err := menuService.GetBaseMenuTree(authority) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysBaseMenusResponse{Menus: menus}, "获取成功", c) +} + +// AddMenuAuthority +// @Tags AuthorityMenu +// @Summary 增加menu和角色关联关系 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.AddMenuAuthorityInfo true "角色ID" +// @Success 200 {object} response.Response{msg=string} "增加menu和角色关联关系" +// @Router /menu/addMenuAuthority [post] +func (a *AuthorityMenuApi) AddMenuAuthority(c *gin.Context) { + var authorityMenu systemReq.AddMenuAuthorityInfo + err := c.ShouldBindJSON(&authorityMenu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := utils.Verify(authorityMenu, utils.AuthorityIdVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + if err := menuService.AddMenuAuthority(authorityMenu.Menus, adminAuthorityID, authorityMenu.AuthorityId); err != nil { + global.GVA_LOG.Error("添加失败!", zap.Error(err)) + response.FailWithMessage("添加失败", c) + } else { + response.OkWithMessage("添加成功", c) + } +} + +// GetMenuAuthority +// @Tags AuthorityMenu +// @Summary 获取指定角色menu +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetAuthorityId true "角色ID" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取指定角色menu" +// @Router /menu/getMenuAuthority [post] +func (a *AuthorityMenuApi) GetMenuAuthority(c *gin.Context) { + var param request.GetAuthorityId + err := c.ShouldBindJSON(¶m) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(param, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menus, err := menuService.GetMenuAuthority(¶m) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取失败", c) + return + } + response.OkWithDetailed(gin.H{"menus": menus}, "获取成功", c) +} + +// AddBaseMenu +// @Tags Menu +// @Summary 新增菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "新增菜单" +// @Router /menu/addBaseMenu [post] +func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = menuService.AddBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("添加失败!", zap.Error(err)) + response.FailWithMessage("添加失败:"+err.Error(), c) + return + } + response.OkWithMessage("添加成功", c) +} + +// DeleteBaseMenu +// @Tags Menu +// @Summary 删除菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{msg=string} "删除菜单" +// @Router /menu/deleteBaseMenu [post] +func (a *AuthorityMenuApi) DeleteBaseMenu(c *gin.Context) { + var menu request.GetById + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.DeleteBaseMenu(menu.ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateBaseMenu +// @Tags Menu +// @Summary 更新菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "更新菜单" +// @Router /menu/updateBaseMenu [post] +func (a *AuthorityMenuApi) UpdateBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.UpdateBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetBaseMenuById +// @Tags Menu +// @Summary 根据id获取菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{data=systemRes.SysBaseMenuResponse,msg=string} "根据id获取菜单,返回包括系统菜单列表" +// @Router /menu/getBaseMenuById [post] +func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) { + var idInfo request.GetById + err := c.ShouldBindJSON(&idInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(idInfo, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menu, err := baseMenuService.GetBaseMenuById(idInfo.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c) +} + +// GetMenuRoles +// @Tags AuthorityMenu +// @Summary 获取拥有指定菜单的角色ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param menuId query uint true "菜单ID" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" +// @Router /menu/getMenuRoles [get] +func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) { + var req systemReq.SetMenuAuthorities + if err := c.ShouldBindQuery(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.MenuId == 0 { + response.FailWithMessage("菜单ID不能为空", c) + return + } + authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if authorityIds == nil { + authorityIds = []uint{} + } + defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId) + if err != nil { + global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if defaultRouterAuthorityIds == nil { + defaultRouterAuthorityIds = []uint{} + } + response.OkWithDetailed(gin.H{ + "authorityIds": authorityIds, + "defaultRouterAuthorityIds": defaultRouterAuthorityIds, + }, "获取成功", c) +} + +// SetMenuRoles +// @Tags AuthorityMenu +// @Summary 全量覆盖某菜单关联的角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /menu/setMenuRoles [post] +func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) { + var req systemReq.SetMenuAuthorities + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.MenuId == 0 { + response.FailWithMessage("菜单ID不能为空", c) + return + } + if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} + +// GetMenuList +// @Tags Menu +// @Summary 分页获取基础menu列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取基础menu列表,返回包括列表,总数,页码,每页数量" +// @Router /menu/getMenuList [post] +func (a *AuthorityMenuApi) GetMenuList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + menuList, err := menuService.GetInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(menuList, "获取成功", c) +} diff --git a/server/api/v1/system/sys_operation_record.go b/server/api/v1/system/sys_operation_record.go new file mode 100644 index 0000000..c88511f --- /dev/null +++ b/server/api/v1/system/sys_operation_record.go @@ -0,0 +1,124 @@ +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/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type OperationRecordApi struct{} + +// DeleteSysOperationRecord +// @Tags SysOperationRecord +// @Summary 删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysOperationRecord true "SysOperationRecord模型" +// @Success 200 {object} response.Response{msg=string} "删除SysOperationRecord" +// @Router /sysOperationRecord/deleteSysOperationRecord [delete] +func (s *OperationRecordApi) DeleteSysOperationRecord(c *gin.Context) { + var sysOperationRecord system.SysOperationRecord + err := c.ShouldBindJSON(&sysOperationRecord) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = operationRecordService.DeleteSysOperationRecord(sysOperationRecord) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysOperationRecordByIds +// @Tags SysOperationRecord +// @Summary 批量删除SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除SysOperationRecord" +// @Success 200 {object} response.Response{msg=string} "批量删除SysOperationRecord" +// @Router /sysOperationRecord/deleteSysOperationRecordByIds [delete] +func (s *OperationRecordApi) DeleteSysOperationRecordByIds(c *gin.Context) { + var IDS request.IdsReq + err := c.ShouldBindJSON(&IDS) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = operationRecordService.DeleteSysOperationRecordByIds(IDS) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// FindSysOperationRecord +// @Tags SysOperationRecord +// @Summary 用id查询SysOperationRecord +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysOperationRecord true "Id" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "用id查询SysOperationRecord" +// @Router /sysOperationRecord/findSysOperationRecord [get] +func (s *OperationRecordApi) FindSysOperationRecord(c *gin.Context) { + var sysOperationRecord system.SysOperationRecord + err := c.ShouldBindQuery(&sysOperationRecord) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(sysOperationRecord, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + reSysOperationRecord, err := operationRecordService.GetSysOperationRecord(sysOperationRecord.ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithDetailed(gin.H{"reSysOperationRecord": reSysOperationRecord}, "查询成功", c) +} + +// GetSysOperationRecordList +// @Tags SysOperationRecord +// @Summary 分页获取SysOperationRecord列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.SysOperationRecordSearch true "页码, 每页大小, 搜索条件" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量" +// @Router /sysOperationRecord/getSysOperationRecordList [get] +func (s *OperationRecordApi) GetSysOperationRecordList(c *gin.Context) { + var pageInfo systemReq.SysOperationRecordSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := operationRecordService.GetSysOperationRecordInfoList(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) +} diff --git a/server/api/v1/system/sys_params.go b/server/api/v1/system/sys_params.go new file mode 100644 index 0000000..45fb1df --- /dev/null +++ b/server/api/v1/system/sys_params.go @@ -0,0 +1,171 @@ +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 SysParamsApi struct{} + +// CreateSysParams 创建参数 +// @Tags SysParams +// @Summary 创建参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "创建参数" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysParams/createSysParams [post] +func (sysParamsApi *SysParamsApi) CreateSysParams(c *gin.Context) { + var sysParams system.SysParams + err := c.ShouldBindJSON(&sysParams) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysParamsService.CreateSysParams(&sysParams) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysParams 删除参数 +// @Tags SysParams +// @Summary 删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "删除参数" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysParams/deleteSysParams [delete] +func (sysParamsApi *SysParamsApi) DeleteSysParams(c *gin.Context) { + ID := c.Query("ID") + err := sysParamsService.DeleteSysParams(ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysParamsByIds 批量删除参数 +// @Tags SysParams +// @Summary 批量删除参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysParams/deleteSysParamsByIds [delete] +func (sysParamsApi *SysParamsApi) DeleteSysParamsByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + err := sysParamsService.DeleteSysParamsByIds(IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateSysParams 更新参数 +// @Tags SysParams +// @Summary 更新参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysParams true "更新参数" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /sysParams/updateSysParams [put] +func (sysParamsApi *SysParamsApi) UpdateSysParams(c *gin.Context) { + var sysParams system.SysParams + err := c.ShouldBindJSON(&sysParams) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysParamsService.UpdateSysParams(sysParams) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysParams 用id查询参数 +// @Tags SysParams +// @Summary 用id查询参数 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysParams true "用id查询参数" +// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "查询成功" +// @Router /sysParams/findSysParams [get] +func (sysParamsApi *SysParamsApi) FindSysParams(c *gin.Context) { + ID := c.Query("ID") + resysParams, err := sysParamsService.GetSysParams(ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysParams, c) +} + +// GetSysParamsList 分页获取参数列表 +// @Tags SysParams +// @Summary 分页获取参数列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query systemReq.SysParamsSearch true "分页获取参数列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysParams/getSysParamsList [get] +func (sysParamsApi *SysParamsApi) GetSysParamsList(c *gin.Context) { + var pageInfo systemReq.SysParamsSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysParamsService.GetSysParamsInfoList(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) +} + +// GetSysParam 根据key获取参数value +// @Tags SysParams +// @Summary 根据key获取参数value +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param key query string true "key" +// @Success 200 {object} response.Response{data=system.SysParams,msg=string} "获取成功" +// @Router /sysParams/getSysParam [get] +func (sysParamsApi *SysParamsApi) GetSysParam(c *gin.Context) { + k := c.Query("key") + params, err := sysParamsService.GetSysParam(k) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(params, "获取成功", c) +} diff --git a/server/api/v1/system/sys_skills.go b/server/api/v1/system/sys_skills.go new file mode 100644 index 0000000..b0f24f5 --- /dev/null +++ b/server/api/v1/system/sys_skills.go @@ -0,0 +1,263 @@ +package system + +import ( + "net/http" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SkillsApi struct{} + +func (s *SkillsApi) GetTools(c *gin.Context) { + data, err := skillsService.Tools(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取工具列表失败", zap.Error(err)) + response.FailWithMessage("获取工具列表失败", c) + return + } + response.OkWithDetailed(gin.H{"tools": data}, "获取成功", c) +} + +func (s *SkillsApi) GetSkillList(c *gin.Context) { + var req request.SkillToolRequest + _ = c.ShouldBindJSON(&req) + data, err := skillsService.List(c.Request.Context(), req.Tool) + if err != nil { + global.GVA_LOG.Error("获取技能列表失败", zap.Error(err)) + response.FailWithMessage("获取技能列表失败", c) + return + } + response.OkWithDetailed(gin.H{"skills": data}, "获取成功", c) +} + +func (s *SkillsApi) GetSkillDetail(c *gin.Context) { + var req request.SkillDetailRequest + _ = c.ShouldBindJSON(&req) + data, err := skillsService.Detail(c.Request.Context(), req.Tool, req.Skill) + if err != nil { + global.GVA_LOG.Error("获取技能详情失败", zap.Error(err)) + response.FailWithMessage("获取技能详情失败", c) + return + } + response.OkWithDetailed(gin.H{"detail": data}, "获取成功", c) +} + +func (s *SkillsApi) SaveSkill(c *gin.Context) { + var req request.SkillSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.Save(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存技能失败", zap.Error(err)) + response.FailWithMessage("保存技能失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) DeleteSkill(c *gin.Context) { + var req request.SkillDeleteRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.Delete(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("删除技能失败", zap.Error(err)) + response.FailWithMessage("删除技能失败: "+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +func (s *SkillsApi) CreateScript(c *gin.Context) { + var req request.SkillScriptCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateScript(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建脚本失败", zap.Error(err)) + response.FailWithMessage("创建脚本失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetScript(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetScript(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取脚本失败", zap.Error(err)) + response.FailWithMessage("读取脚本失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveScript(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveScript(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存脚本失败", zap.Error(err)) + response.FailWithMessage("保存脚本失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) CreateResource(c *gin.Context) { + var req request.SkillResourceCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateResource(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建资源失败", zap.Error(err)) + response.FailWithMessage("创建资源失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetResource(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetResource(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取资源失败", zap.Error(err)) + response.FailWithMessage("读取资源失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveResource(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveResource(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存资源失败", zap.Error(err)) + response.FailWithMessage("保存资源失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) CreateReference(c *gin.Context) { + var req request.SkillReferenceCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateReference(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建参考失败", zap.Error(err)) + response.FailWithMessage("创建参考失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetReference(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetReference(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取参考失败", zap.Error(err)) + response.FailWithMessage("读取参考失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveReference(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveReference(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存参考失败", zap.Error(err)) + response.FailWithMessage("保存参考失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) CreateTemplate(c *gin.Context) { + var req request.SkillTemplateCreateRequest + _ = c.ShouldBindJSON(&req) + fileName, content, err := skillsService.CreateTemplate(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("创建模板失败", zap.Error(err)) + response.FailWithMessage("创建模板失败", c) + return + } + response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c) +} + +func (s *SkillsApi) GetTemplate(c *gin.Context) { + var req request.SkillFileRequest + _ = c.ShouldBindJSON(&req) + content, err := skillsService.GetTemplate(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("读取模板失败", zap.Error(err)) + response.FailWithMessage("读取模板失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content}, "获取成功", c) +} + +func (s *SkillsApi) SaveTemplate(c *gin.Context) { + var req request.SkillFileSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveTemplate(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存模板失败", zap.Error(err)) + response.FailWithMessage("保存模板失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) GetGlobalConstraint(c *gin.Context) { + var req request.SkillToolRequest + _ = c.ShouldBindJSON(&req) + content, exists, err := skillsService.GetGlobalConstraint(c.Request.Context(), req.Tool) + if err != nil { + global.GVA_LOG.Error("读取全局约束失败", zap.Error(err)) + response.FailWithMessage("读取全局约束失败", c) + return + } + response.OkWithDetailed(gin.H{"content": content, "exists": exists}, "获取成功", c) +} + +func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) { + var req request.SkillGlobalConstraintSaveRequest + _ = c.ShouldBindJSON(&req) + if err := skillsService.SaveGlobalConstraint(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("保存全局约束失败", zap.Error(err)) + response.FailWithMessage("保存全局约束失败", c) + return + } + response.OkWithMessage("保存成功", c) +} + +func (s *SkillsApi) PackageSkill(c *gin.Context) { + var req request.SkillPackageRequest + _ = c.ShouldBindJSON(&req) + + fileName, data, err := skillsService.Package(c.Request.Context(), req) + if err != nil { + global.GVA_LOG.Error("打包技能失败", zap.Error(err)) + response.FailWithMessage("打包技能失败: "+err.Error(), c) + return + } + + c.Header("Content-Type", "application/zip") + c.Header("Content-Disposition", "attachment; filename=\""+fileName+"\"") + c.Data(http.StatusOK, "application/zip", data) +} + +func (s *SkillsApi) DownloadOnlineSkill(c *gin.Context) { + var req request.DownloadOnlineSkillReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("参数错误", c) + return + } + + if err := skillsService.DownloadOnlineSkill(c.Request.Context(), req); err != nil { + global.GVA_LOG.Error("下载在线技能失败", zap.Error(err)) + response.FailWithMessage("下载在线技能失败: "+err.Error(), c) + return + } + response.OkWithMessage("下载成功", c) +} diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go new file mode 100644 index 0000000..6dabb0a --- /dev/null +++ b/server/api/v1/system/sys_system.go @@ -0,0 +1,89 @@ +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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SystemApi struct{} + +// GetSystemConfig +// @Tags System +// @Summary 获取配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=systemRes.SysConfigResponse,msg=string} "获取配置文件内容,返回包括系统配置" +// @Router /system/getSystemConfig [post] +func (s *SystemApi) GetSystemConfig(c *gin.Context) { + config, err := systemConfigService.GetSystemConfig() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysConfigResponse{Config: config}, "获取成功", c) +} + +// SetSystemConfig +// @Tags System +// @Summary 设置配置文件内容 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body system.System true "设置配置文件内容" +// @Success 200 {object} response.Response{data=string} "设置配置文件内容" +// @Router /system/setSystemConfig [post] +func (s *SystemApi) SetSystemConfig(c *gin.Context) { + var sys system.System + err := c.ShouldBindJSON(&sys) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = systemConfigService.SetSystemConfig(sys) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// ReloadSystem +// @Tags System +// @Summary 重载系统 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "重载系统" +// @Router /system/reloadSystem [post] +func (s *SystemApi) ReloadSystem(c *gin.Context) { + // 触发系统重载事件 + err := utils.GlobalSystemEvents.TriggerReload() + if err != nil { + global.GVA_LOG.Error("重载系统失败!", zap.Error(err)) + response.FailWithMessage("重载系统失败:"+err.Error(), c) + return + } + response.OkWithMessage("重载系统成功", c) +} + +// GetServerInfo +// @Tags System +// @Summary 获取服务器信息 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取服务器信息" +// @Router /system/getServerInfo [post] +func (s *SystemApi) GetServerInfo(c *gin.Context) { + server, err := systemConfigService.GetServerInfo() + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"server": server}, "获取成功", c) +} diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go new file mode 100644 index 0000000..12b2849 --- /dev/null +++ b/server/api/v1/system/sys_user.go @@ -0,0 +1,516 @@ +package system + +import ( + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +// Login +// @Tags Base +// @Summary 用户登录 +// @Produce application/json +// @Param data body systemReq.Login true "用户名, 密码, 验证码" +// @Success 200 {object} response.Response{data=systemRes.LoginResponse,msg=string} "返回包括用户信息,token,过期时间" +// @Router /base/login [post] +func (b *BaseApi) Login(c *gin.Context) { + var l systemReq.Login + err := c.ShouldBindJSON(&l) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(l, utils.LoginVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + key := c.ClientIP() + // 判断验证码是否开启 + openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 + openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 + v, ok := global.BlackCache.Get(key) + if !ok { + global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) + } + + var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v) + if oc && (l.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) { + // 验证码次数+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 + } + + u := &system.SysUser{Username: l.Username, Password: l.Password} + user, err := userService.Login(u) + if err != nil { + global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err)) + // 验证码次数+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 { + global.GVA_LOG.Error("登陆失败! 用户被禁止登录!") + // 验证码次数+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) +} + +// TokenNext 登录以后签发jwt +func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { + token, claims, err := utils.LoginToken(&user) + if err != nil { + global.GVA_LOG.Error("获取token失败!", zap.Error(err)) + 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{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + return + } + + if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil { + if err := utils.SetRedisJWT(token, user.Username); err != nil { + global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) + response.FailWithMessage("设置登录状态失败", c) + return + } + utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) + response.OkWithDetailed(systemRes.LoginResponse{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + } else if err != nil { + global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) + response.FailWithMessage("设置登录状态失败", c) + } else { + var blackJWT system.JwtBlacklist + blackJWT.Jwt = jwtStr + if err := jwtService.JsonInBlacklist(blackJWT); err != nil { + response.FailWithMessage("jwt作废失败", c) + return + } + if err := utils.SetRedisJWT(token, user.GetUsername()); err != nil { + response.FailWithMessage("设置登录状态失败", c) + return + } + utils.SetToken(c, token, int(claims.RegisteredClaims.ExpiresAt.Unix()-time.Now().Unix())) + response.OkWithDetailed(systemRes.LoginResponse{ + User: user, + Token: token, + ExpiresAt: claims.RegisteredClaims.ExpiresAt.Unix() * 1000, + }, "登录成功", c) + } +} + +// Register +// @Tags SysUser +// @Summary 用户注册账号 +// @Produce application/json +// @Param data body systemReq.Register true "用户名, 昵称, 密码, 角色ID" +// @Success 200 {object} response.Response{data=systemRes.SysUserResponse,msg=string} "用户注册账号,返回包括用户信息" +// @Router /user/admin_register [post] +func (b *BaseApi) Register(c *gin.Context) { + var r systemReq.Register + err := c.ShouldBindJSON(&r) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(r, utils.RegisterVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + var authorities []system.SysAuthority + for _, v := range r.AuthorityIds { + authorities = append(authorities, system.SysAuthority{ + AuthorityId: v, + }) + } + user := &system.SysUser{Username: r.Username, NickName: r.NickName, Password: r.Password, HeaderImg: r.HeaderImg, AuthorityId: r.AuthorityId, Authorities: authorities, Enable: r.Enable, Phone: r.Phone, Email: r.Email} + userReturn, err := userService.Register(*user) + if err != nil { + global.GVA_LOG.Error("注册失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册失败", c) + return + } + response.OkWithDetailed(systemRes.SysUserResponse{User: userReturn}, "注册成功", c) +} + +// ChangePassword +// @Tags SysUser +// @Summary 用户修改密码 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body systemReq.ChangePasswordReq true "用户名, 原密码, 新密码" +// @Success 200 {object} response.Response{msg=string} "用户修改密码" +// @Router /user/changePassword [post] +func (b *BaseApi) ChangePassword(c *gin.Context) { + var req systemReq.ChangePasswordReq + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(req, utils.ChangePasswordVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + uid := utils.GetUserID(c) + u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password} + err = userService.ChangePassword(u, req.NewPassword) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败,原密码与当前账户不符", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// GetUserList +// @Tags SysUser +// @Summary 分页获取用户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.GetUserList true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取用户列表,返回包括列表,总数,页码,每页数量" +// @Router /user/getUserList [post] +func (b *BaseApi) GetUserList(c *gin.Context) { + var pageInfo systemReq.GetUserList + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(pageInfo, utils.PageInfoVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := userService.GetUserInfoList(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) +} + +// SetUserAuthority +// @Tags SysUser +// @Summary 更改用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetUserAuth true "用户UUID, 角色ID" +// @Success 200 {object} response.Response{msg=string} "设置用户权限" +// @Router /user/setUserAuthority [post] +func (b *BaseApi) SetUserAuthority(c *gin.Context) { + var sua systemReq.SetUserAuth + err := c.ShouldBindJSON(&sua) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if UserVerifyErr := utils.Verify(sua, utils.SetUserAuthorityVerify); UserVerifyErr != nil { + response.FailWithMessage(UserVerifyErr.Error(), c) + return + } + userID := utils.GetUserID(c) + err = userService.SetUserAuthority(userID, sua.AuthorityId) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + claims := utils.GetUserInfo(c) + claims.AuthorityId = sua.AuthorityId + token, err := utils.NewJWT().CreateToken(*claims) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage(err.Error(), c) + return + } + 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())) + response.OkWithMessage("修改成功", c) +} + +// SetUserAuthorities +// @Tags SysUser +// @Summary 设置用户权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetUserAuthorities true "用户UUID, 角色ID" +// @Success 200 {object} response.Response{msg=string} "设置用户权限" +// @Router /user/setUserAuthorities [post] +func (b *BaseApi) SetUserAuthorities(c *gin.Context) { + var sua systemReq.SetUserAuthorities + err := c.ShouldBindJSON(&sua) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + authorityID := utils.GetUserAuthorityId(c) + err = userService.SetUserAuthorities(authorityID, sua.ID, sua.AuthorityIds) + if err != nil { + global.GVA_LOG.Error("修改失败!", zap.Error(err)) + response.FailWithMessage("修改失败", c) + return + } + response.OkWithMessage("修改成功", c) +} + +// DeleteUser +// @Tags SysUser +// @Summary 删除用户 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "用户ID" +// @Success 200 {object} response.Response{msg=string} "删除用户" +// @Router /user/deleteUser [delete] +func (b *BaseApi) DeleteUser(c *gin.Context) { + var reqId request.GetById + err := c.ShouldBindJSON(&reqId) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(reqId, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + jwtId := utils.GetUserID(c) + if jwtId == uint(reqId.ID) { + response.FailWithMessage("删除失败, 无法删除自己。", c) + return + } + err = userService.DeleteUser(reqId.ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// SetUserInfo +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysUser true "ID, 用户名, 昵称, 头像链接" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户信息" +// @Router /user/setUserInfo [put] +func (b *BaseApi) SetUserInfo(c *gin.Context) { + var user systemReq.ChangeUserInfo + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(user, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if len(user.AuthorityIds) != 0 { + authorityID := utils.GetUserAuthorityId(c) + err = userService.SetUserAuthorities(authorityID, user.ID, user.AuthorityIds) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + } + err = userService.SetUserInfo(system.SysUser{ + GVA_MODEL: global.GVA_MODEL{ + ID: user.ID, + }, + NickName: user.NickName, + HeaderImg: user.HeaderImg, + Phone: user.Phone, + Email: user.Email, + Enable: user.Enable, + }) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// SetSelfInfo +// @Tags SysUser +// @Summary 设置用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysUser true "ID, 用户名, 昵称, 头像链接" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户信息" +// @Router /user/SetSelfInfo [put] +func (b *BaseApi) SetSelfInfo(c *gin.Context) { + var user systemReq.ChangeUserInfo + err := c.ShouldBindJSON(&user) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + user.ID = utils.GetUserID(c) + err = userService.SetSelfInfo(system.SysUser{ + GVA_MODEL: global.GVA_MODEL{ + ID: user.ID, + }, + NickName: user.NickName, + HeaderImg: user.HeaderImg, + Phone: user.Phone, + Email: user.Email, + Enable: user.Enable, + }) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// SetSelfSetting +// @Tags SysUser +// @Summary 设置用户配置 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body map[string]interface{} true "用户配置数据" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "设置用户配置" +// @Router /user/SetSelfSetting [put] +func (b *BaseApi) SetSelfSetting(c *gin.Context) { + var req common.JSONMap + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + err = userService.SetSelfSetting(req, utils.GetUserID(c)) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败", c) + return + } + response.OkWithMessage("设置成功", c) +} + +// GetUserInfo +// @Tags SysUser +// @Summary 获取用户信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取用户信息" +// @Router /user/getUserInfo [get] +func (b *BaseApi) GetUserInfo(c *gin.Context) { + uuid := utils.GetUserUuid(c) + ReqUser, err := userService.GetUserInfo(uuid) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"userInfo": ReqUser}, "获取成功", c) +} + +// ResetPassword +// @Tags SysUser +// @Summary 重置用户密码 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body system.SysUser true "ID" +// @Success 200 {object} response.Response{msg=string} "重置用户密码" +// @Router /user/resetPassword [post] +func (b *BaseApi) ResetPassword(c *gin.Context) { + var rps systemReq.ResetPassword + err := c.ShouldBindJSON(&rps) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = userService.ResetPassword(rps.ID, rps.Password) + if err != nil { + global.GVA_LOG.Error("重置失败!", zap.Error(err)) + response.FailWithMessage("重置失败"+err.Error(), c) + return + } + response.OkWithMessage("重置成功", c) +} diff --git a/server/api/v1/system/sys_version.go b/server/api/v1/system/sys_version.go new file mode 100644 index 0000000..1e22808 --- /dev/null +++ b/server/api/v1/system/sys_version.go @@ -0,0 +1,486 @@ +package system + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysVersionApi struct{} + +// buildMenuTree 构建菜单树结构 +func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu { + // 创建菜单映射 + menuMap := make(map[uint]*system.SysBaseMenu) + for i := range menus { + menuMap[menus[i].ID] = &menus[i] + } + + // 构建树结构 + var rootMenus []system.SysBaseMenu + for _, menu := range menus { + if menu.ParentId == 0 { + // 根菜单 + menuData := convertMenuToStruct(menu, menuMap) + rootMenus = append(rootMenus, menuData) + } + } + + // 按sort排序根菜单 + sort.Slice(rootMenus, func(i, j int) bool { + return rootMenus[i].Sort < rootMenus[j].Sort + }) + + return rootMenus +} + +// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单 +func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu { + result := system.SysBaseMenu{ + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + // 清理并复制参数数据 + if len(menu.Parameters) > 0 { + cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters)) + for _, param := range menu.Parameters { + cleanParam := system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanParameters = append(cleanParameters, cleanParam) + } + result.Parameters = cleanParameters + } + + // 清理并复制菜单按钮数据 + if len(menu.MenuBtn) > 0 { + cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn)) + for _, btn := range menu.MenuBtn { + cleanBtn := system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanMenuBtns = append(cleanMenuBtns, cleanBtn) + } + result.MenuBtn = cleanMenuBtns + } + + // 查找并处理子菜单 + var children []system.SysBaseMenu + for _, childMenu := range menuMap { + if childMenu.ParentId == menu.ID { + childData := convertMenuToStruct(*childMenu, menuMap) + children = append(children, childData) + } + } + + // 按sort排序子菜单 + if len(children) > 0 { + sort.Slice(children, func(i, j int) bool { + return children[i].Sort < children[j].Sort + }) + result.Children = children + } + + return result +} + +// DeleteSysVersion 删除版本管理 +// @Tags SysVersion +// @Summary 删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysVersion true "删除版本管理" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysVersion/deleteSysVersion [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := sysVersionService.DeleteSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysVersionByIds 批量删除版本管理 +// @Tags SysVersion +// @Summary 批量删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysVersion/deleteSysVersionByIds [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := sysVersionService.DeleteSysVersionByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// FindSysVersion 用id查询版本管理 +// @Tags SysVersion +// @Summary 用id查询版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询版本管理" +// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功" +// @Router /sysVersion/findSysVersion [get] +func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + resysVersion, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysVersion, c) +} + +// GetSysVersionList 分页获取版本管理列表 +// @Tags SysVersion +// @Summary 分页获取版本管理列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionList [get] +func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo systemReq.SysVersionSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysVersionService.GetSysVersionInfoList(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) +} + +// GetSysVersionPublic 不需要鉴权的版本管理接口 +// @Tags SysVersion +// @Summary 不需要鉴权的版本管理接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionPublic [get] +func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + sysVersionService.GetSysVersionPublic(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的版本管理接口信息", + }, "获取成功", c) +} + +// ExportVersion 创建发版数据 +// @Tags SysVersion +// @Summary 创建发版数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ExportVersionRequest true "创建发版数据" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysVersion/exportVersion [post] +func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) { + ctx := c.Request.Context() + + var req systemReq.ExportVersionRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 获取选中的菜单数据 + var menuData []system.SysBaseMenu + if len(req.MenuIds) > 0 { + menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds) + if err != nil { + global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err)) + response.FailWithMessage("获取菜单数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的API数据 + var apiData []system.SysApi + if len(req.ApiIds) > 0 { + apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds) + if err != nil { + global.GVA_LOG.Error("获取API数据失败!", zap.Error(err)) + response.FailWithMessage("获取API数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的字典数据 + var dictData []system.SysDictionary + if len(req.DictIds) > 0 { + dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds) + if err != nil { + global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err)) + response.FailWithMessage("获取字典数据失败:"+err.Error(), c) + return + } + } + + // 处理菜单数据,构建递归的children结构 + processedMenus := buildMenuTree(menuData) + + // 处理API数据,清除ID和时间戳字段 + processedApis := make([]system.SysApi, 0, len(apiData)) + for _, api := range apiData { + cleanApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + processedApis = append(processedApis, cleanApi) + } + + // 处理字典数据,清除ID和时间戳字段,包含字典详情 + processedDicts := make([]system.SysDictionary, 0, len(dictData)) + for _, dict := range dictData { + cleanDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + } + + // 处理字典详情数据,清除ID和时间戳字段 + cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails)) + for _, detail := range dict.SysDictionaryDetails { + cleanDetail := system.SysDictionaryDetail{ + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + // 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID + } + cleanDetails = append(cleanDetails, cleanDetail) + } + cleanDict.SysDictionaryDetails = cleanDetails + + processedDicts = append(processedDicts, cleanDict) + } + + // 构建导出数据 + exportData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: req.VersionName, + Code: req.VersionCode, + Description: req.Description, + ExportTime: time.Now().Format("2006-01-02 15:04:05"), + }, + Menus: processedMenus, + Apis: processedApis, + Dictionaries: processedDicts, + } + + // 转换为JSON + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err)) + response.FailWithMessage("JSON序列化失败:"+err.Error(), c) + return + } + + // 保存版本记录 + version := system.SysVersion{ + VersionName: utils.Pointer(req.VersionName), + VersionCode: utils.Pointer(req.VersionCode), + Description: utils.Pointer(req.Description), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err)) + response.FailWithMessage("保存版本记录失败:"+err.Error(), c) + return + } + + response.OkWithMessage("创建发版成功", c) +} + +// DownloadVersionJson 下载版本JSON数据 +// @Tags SysVersion +// @Summary 下载版本JSON数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query string true "版本ID" +// @Success 200 {object} response.Response{data=object,msg=string} "下载成功" +// @Router /sysVersion/downloadVersionJson [get] +func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) { + ctx := c.Request.Context() + + ID := c.Query("ID") + if ID == "" { + response.FailWithMessage("版本ID不能为空", c) + return + } + + // 获取版本记录 + version, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err)) + response.FailWithMessage("获取版本记录失败:"+err.Error(), c) + return + } + + // 构建JSON数据 + var jsonData []byte + if version.VersionData != nil && *version.VersionData != "" { + jsonData = []byte(*version.VersionData) + } else { + // 如果没有存储的JSON数据,构建一个基本的结构 + basicData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: *version.VersionName, + Code: *version.VersionCode, + Description: *version.Description, + ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"), + }, + Menus: []system.SysBaseMenu{}, + Apis: []system.SysApi{}, + } + jsonData, _ = json.MarshalIndent(basicData, "", " ") + } + + // 设置下载响应头 + filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405")) + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + c.Header("Content-Length", strconv.Itoa(len(jsonData))) + + c.Data(http.StatusOK, "application/json", jsonData) +} + +// ImportVersion 导入版本数据 +// @Tags SysVersion +// @Summary 导入版本数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ImportVersionRequest true "版本JSON数据" +// @Success 200 {object} response.Response{msg=string} "导入成功" +// @Router /sysVersion/importVersion [post] +func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) { + ctx := c.Request.Context() + + // 获取JSON数据 + var importData systemReq.ImportVersionRequest + err := c.ShouldBindJSON(&importData) + if err != nil { + response.FailWithMessage("解析JSON数据失败:"+err.Error(), c) + return + } + + // 验证数据格式 + if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" { + response.FailWithMessage("版本信息格式错误", c) + return + } + + // 导入菜单数据 + if len(importData.ExportMenu) > 0 { + if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil { + global.GVA_LOG.Error("导入菜单失败!", zap.Error(err)) + response.FailWithMessage("导入菜单失败: "+err.Error(), c) + return + } + } + + // 导入API数据 + if len(importData.ExportApi) > 0 { + if err := sysVersionService.ImportApis(importData.ExportApi); err != nil { + global.GVA_LOG.Error("导入API失败!", zap.Error(err)) + response.FailWithMessage("导入API失败: "+err.Error(), c) + return + } + } + + // 导入字典数据 + if len(importData.ExportDictionary) > 0 { + if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil { + global.GVA_LOG.Error("导入字典失败!", zap.Error(err)) + response.FailWithMessage("导入字典失败: "+err.Error(), c) + return + } + } + + // 创建导入记录 + jsonData, _ := json.Marshal(importData) + version := system.SysVersion{ + VersionName: utils.Pointer(importData.VersionInfo.Name), + VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))), + Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err)) + // 这里不返回错误,因为数据已经导入成功 + } + + response.OkWithMessage("导入成功", c) +} diff --git a/server/cmd/mcp/config.go b/server/cmd/mcp/config.go new file mode 100644 index 0000000..cc1d906 --- /dev/null +++ b/server/cmd/mcp/config.go @@ -0,0 +1,192 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gopkg.in/yaml.v3" +) + +type standaloneConfig struct { + MCP config.MCP `yaml:"mcp"` + AutoCode config.Autocode `yaml:"autocode"` +} + +func loadStandaloneConfig() (string, error) { + configPath, err := resolveConfigPath() + if err != nil { + return "", err + } + + content, err := os.ReadFile(configPath) + if err != nil { + return "", fmt.Errorf("读取 MCP 配置失败: %w", err) + } + + var cfg standaloneConfig + if err := yaml.Unmarshal(content, &cfg); err != nil { + return "", fmt.Errorf("解析 MCP 配置失败: %w", err) + } + + applyStandaloneDefaults(configPath, &cfg) + + global.GVA_CONFIG.MCP = cfg.MCP + global.GVA_CONFIG.AutoCode = cfg.AutoCode + + return configPath, nil +} + +func resolveConfigPath() (string, error) { + explicit, err := parseConfigFlag(os.Args[1:]) + if err != nil { + return "", err + } + if explicit != "" { + return filepath.Abs(explicit) + } + + if envPath := strings.TrimSpace(os.Getenv("GVA_MCP_CONFIG")); envPath != "" { + return filepath.Abs(envPath) + } + + wd, _ := os.Getwd() + exe, _ := os.Executable() + exeDir := filepath.Dir(exe) + + candidates := []string{ + filepath.Join(wd, "config.yaml"), + filepath.Join(wd, "mcp.yaml"), + filepath.Join(wd, "cmd", "mcp", "config.yaml"), + filepath.Join(wd, "server", "cmd", "mcp", "config.yaml"), + filepath.Join(exeDir, "config.yaml"), + filepath.Join(exeDir, "mcp.yaml"), + } + + for _, candidate := range candidates { + if candidate == "" { + continue + } + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return filepath.Abs(candidate) + } + } + + return "", errors.New("未找到 MCP 独立配置文件,请在当前目录、cmd/mcp 目录或通过 -config / GVA_MCP_CONFIG 指定 config.yaml") +} + +func parseConfigFlag(args []string) (string, error) { + fs := flag.NewFlagSet("gva-mcp", flag.ContinueOnError) + fs.SetOutput(io.Discard) + + var configPath string + fs.StringVar(&configPath, "c", "", "MCP config file path") + fs.StringVar(&configPath, "config", "", "MCP config file path") + if err := fs.Parse(args); err != nil { + return "", err + } + + return strings.TrimSpace(configPath), nil +} + +func applyStandaloneDefaults(configPath string, cfg *standaloneConfig) { + if cfg.MCP.Name == "" { + cfg.MCP.Name = "GVA_MCP" + } + if cfg.MCP.Version == "" { + cfg.MCP.Version = "v1.0.0" + } + if cfg.MCP.Path == "" { + cfg.MCP.Path = "/mcp" + } + if cfg.MCP.Addr == 0 { + cfg.MCP.Addr = 8889 + } + if cfg.MCP.AuthHeader == "" { + cfg.MCP.AuthHeader = "x-token" + } + if cfg.MCP.RequestTimeout <= 0 { + cfg.MCP.RequestTimeout = 15 + } + if cfg.MCP.UpstreamBaseURL == "" { + cfg.MCP.UpstreamBaseURL = "http://127.0.0.1:8888" + } + if cfg.MCP.BaseURL == "" { + cfg.MCP.BaseURL = fmt.Sprintf("http://127.0.0.1:%d%s", cfg.MCP.Addr, cfg.MCP.Path) + } + + configDir := filepath.Dir(configPath) + if cfg.AutoCode.Server == "" { + cfg.AutoCode.Server = "server" + } + if cfg.AutoCode.Web == "" { + cfg.AutoCode.Web = "web/src" + } + if cfg.AutoCode.Root == "" { + if root, err := detectProjectRoot(configDir); err == nil { + cfg.AutoCode.Root = root + } + } else if !filepath.IsAbs(cfg.AutoCode.Root) { + cfg.AutoCode.Root = filepath.Clean(filepath.Join(configDir, cfg.AutoCode.Root)) + } + + if cfg.AutoCode.Module == "" && cfg.AutoCode.Root != "" { + goModPath := filepath.Join(cfg.AutoCode.Root, cfg.AutoCode.Server, "go.mod") + if module, err := detectGoModule(goModPath); err == nil { + cfg.AutoCode.Module = module + } + } +} + +func detectProjectRoot(startDir string) (string, error) { + dir := startDir + for { + serverDir := filepath.Join(dir, "server") + webDir := filepath.Join(dir, "web") + if isDir(serverDir) && isDir(webDir) { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + return "", errors.New("未能自动识别项目根目录,请在 MCP 配置中设置 autocode.root") +} + +func detectGoModule(goModPath string) (string, error) { + file, err := os.Open(goModPath) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "module ") { + return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil + } + } + + if err := scanner.Err(); err != nil { + return "", err + } + + return "", errors.New("go.mod 中未找到 module 定义") +} + +func isDir(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} diff --git a/server/cmd/mcp/config.yaml b/server/cmd/mcp/config.yaml new file mode 100644 index 0000000..84fd082 --- /dev/null +++ b/server/cmd/mcp/config.yaml @@ -0,0 +1,14 @@ +mcp: + name: GVA_MCP + version: v1.0.0 + path: /mcp + addr: 8889 + base_url: http://127.0.0.1:8889/mcp + upstream_base_url: http://127.0.0.1:8888 + auth_header: x-token + request_timeout: 15 + +autocode: + root: ../../.. + server: server + web: web/src diff --git a/server/cmd/mcp/logger.go b/server/cmd/mcp/logger.go new file mode 100644 index 0000000..ebeaf7c --- /dev/null +++ b/server/cmd/mcp/logger.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +func initializeStandaloneLogger() error { + logger, err := zap.NewDevelopment() + if err != nil { + return err + } + + global.GVA_LOG = logger + zap.ReplaceGlobals(logger) + return nil +} diff --git a/server/cmd/mcp/main.go b/server/cmd/mcp/main.go new file mode 100644 index 0000000..ef7c353 --- /dev/null +++ b/server/cmd/mcp/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp" + _ "go.uber.org/automaxprocs" + "go.uber.org/zap" +) + +func main() { + configPath, err := loadStandaloneConfig() + if err != nil { + panic(err) + } + + if err := initializeStandaloneLogger(); err != nil { + panic(err) + } + + addr := fmt.Sprintf(":%d", global.GVA_CONFIG.MCP.Addr) + server := mcpTool.NewStreamableHTTPServer() + + global.GVA_LOG.Info("mcp独立服务启动", + zap.String("config", configPath), + zap.String("addr", addr), + zap.String("path", global.GVA_CONFIG.MCP.Path), + zap.String("upstream", global.GVA_CONFIG.MCP.UpstreamBaseURL), + ) + + if err := server.Start(addr); err != nil { + global.GVA_LOG.Fatal("mcp独立服务启动失败", zap.Error(err)) + } +} diff --git a/server/config.docker.yaml b/server/config.docker.yaml new file mode 100644 index 0000000..42d1124 --- /dev/null +++ b/server/config.docker.yaml @@ -0,0 +1,282 @@ +# github.com/flipped-aurora/gin-vue-admin/server Global Configuration + +# jwt configuration +jwt: + signing-key: qmPlus + expires-time: 7d + buffer-time: 1d + issuer: qmPlus +# zap logger configuration +zap: + level: info + format: console + prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + retention-day: -1 + +# redis configuration +redis: + #是否使用redis集群模式 + useCluster: false + #使用集群模式addr和db默认无效 + addr: 177.7.0.14:6379 + password: "" + db: 0 + clusterAddrs: + - "177.7.0.14:7000" + - "177.7.0.15:7001" + - "177.7.0.13:7002" + +# redis-list configuration +redis-list: + - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 + useCluster: false # 是否使用redis集群模式 + addr: 177.7.0.14:6379 # 使用集群模式addr和db默认无效 + password: "" + db: 0 + clusterAddrs: + - "177.7.0.14:7000" + - "177.7.0.15:7001" + - "177.7.0.13:7002" + +# mongo configuration +mongo: + coll: '' + options: '' + database: '' + username: '' + password: '' + auth-source: '' + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: '' + port: '' + +# email configuration +email: + to: xxx@qq.com + port: 465 + from: xxx@163.com + host: smtp.163.com + is-ssl: true + secret: xxx + nickname: test + +# system configuration +system: + env: local # 修改为public可以关闭路由日志输出 + addr: 8888 + db-type: mysql + oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-redis: false # 使用redis + use-mongo: false # 使用mongo + use-multipoint: false + # IP限制次数 一个小时15000次 + iplimit-count: 15000 + # IP限制一个小时 + iplimit-time: 3600 + # 路由全局前缀 + router-prefix: "" + # 严格角色模式 打开后权限将会存在上下级关系 + use-strict-auth: false + +# captcha configuration +captcha: + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 # 0代表一直开启,大于0代表限制次数 + open-captcha-timeout: 3600 # open-captcha大于0时才生效 + +# mysql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +mysql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +# pgsql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +pgsql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +oracle: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +mssql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +sqlite: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +db-list: + - disable: true # 是否禁用 + type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle + alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +# local configuration +local: + path: uploads/file + store-path: uploads/file + +# autocode configuration +autocode: + web: web/src + root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径 + server: server + module: 'github.com/flipped-aurora/gin-vue-admin/server' + ai-path: "" # AI服务路径 + +# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) +qiniu: + zone: ZoneHuaDong + bucket: "" + img-path: "" + use-https: false + access-key: "" + secret-key: "" + use-cdn-domains: false + +# minio oss configuration +minio: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + use-ssl: false + base-path: "" + bucket-url: "http://host:9000/yourBucketName" + +# aliyun oss configuration +aliyun-oss: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + bucket-url: yourBucketUrl + base-path: yourBasePath + +# tencent cos configuration +tencent-cos: + bucket: xxxxx-10005608 + region: ap-shanghai + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server + +# aws s3 configuration (minio compatible) +aws-s3: + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + s3-force-path-style: false + disable-ssl: false + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server + +# cloudflare r2 configuration +cloudflare-r2: + bucket: xxxx0bucket + base-url: https://gin.vue.admin.com + path: uploads + account-id: xxx_account_id + access-key-id: xxx_key_id + secret-access-key: xxx_secret_key + +# huawei obs configuration +hua-wei-obs: + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key + +# excel configuration +excel: + dir: ./resource/excel/ + +# disk usage configuration +disk-list: + - mount-point: "/" + +# 跨域配置 +# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 +cors: + mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + whitelist: + - allow-origin: example1.com + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + allow-methods: POST, GET + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + + allow-credentials: true # 布尔值 + - allow-origin: example2.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 +mcp: + name: GVA_MCP + version: v1.0.0 + addr: 8889 + separate: false diff --git a/server/config/auto_code.go b/server/config/auto_code.go new file mode 100644 index 0000000..ade79a0 --- /dev/null +++ b/server/config/auto_code.go @@ -0,0 +1,22 @@ +package config + +import ( + "path/filepath" + "strings" +) + +type Autocode struct { + Web string `mapstructure:"web" json:"web" yaml:"web"` + Root string `mapstructure:"root" json:"root" yaml:"root"` + Server string `mapstructure:"server" json:"server" yaml:"server"` + Module string `mapstructure:"module" json:"module" yaml:"module"` + AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"` +} + +func (a *Autocode) WebRoot() string { + webs := strings.Split(a.Web, "/") + if len(webs) == 0 { + webs = strings.Split(a.Web, "\\") + } + return filepath.Join(webs...) +} diff --git a/server/config/captcha.go b/server/config/captcha.go new file mode 100644 index 0000000..d678a41 --- /dev/null +++ b/server/config/captcha.go @@ -0,0 +1,9 @@ +package config + +type Captcha struct { + KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 + ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 + ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 + OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码 + OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒) +} diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..3abac5a --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,40 @@ +package config + +type Server struct { + JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` + Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` + Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` + RedisList []Redis `mapstructure:"redis-list" json:"redis-list" yaml:"redis-list"` + Mongo Mongo `mapstructure:"mongo" json:"mongo" yaml:"mongo"` + Email Email `mapstructure:"email" json:"email" yaml:"email"` + System System `mapstructure:"system" json:"system" yaml:"system"` + Captcha Captcha `mapstructure:"captcha" json:"captcha" yaml:"captcha"` + // auto + AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"` + // gorm + Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` + Mssql Mssql `mapstructure:"mssql" json:"mssql" yaml:"mssql"` + Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"` + Oracle Oracle `mapstructure:"oracle" json:"oracle" yaml:"oracle"` + Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"` + DBList []SpecializedDB `mapstructure:"db-list" json:"db-list" yaml:"db-list"` + // oss + Local Local `mapstructure:"local" json:"local" yaml:"local"` + Qiniu Qiniu `mapstructure:"qiniu" json:"qiniu" yaml:"qiniu"` + AliyunOSS AliyunOSS `mapstructure:"aliyun-oss" json:"aliyun-oss" yaml:"aliyun-oss"` + HuaWeiObs HuaWeiObs `mapstructure:"hua-wei-obs" json:"hua-wei-obs" yaml:"hua-wei-obs"` + TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"` + AwsS3 AwsS3 `mapstructure:"aws-s3" json:"aws-s3" yaml:"aws-s3"` + CloudflareR2 CloudflareR2 `mapstructure:"cloudflare-r2" json:"cloudflare-r2" yaml:"cloudflare-r2"` + Minio Minio `mapstructure:"minio" json:"minio" yaml:"minio"` + + Excel Excel `mapstructure:"excel" json:"excel" yaml:"excel"` + + DiskList []DiskList `mapstructure:"disk-list" json:"disk-list" yaml:"disk-list"` + + // 跨域配置 + Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` + + // MCP配置 + MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"` +} diff --git a/server/config/cors.go b/server/config/cors.go new file mode 100644 index 0000000..7fba993 --- /dev/null +++ b/server/config/cors.go @@ -0,0 +1,14 @@ +package config + +type CORS struct { + Mode string `mapstructure:"mode" json:"mode" yaml:"mode"` + Whitelist []CORSWhitelist `mapstructure:"whitelist" json:"whitelist" yaml:"whitelist"` +} + +type CORSWhitelist struct { + AllowOrigin string `mapstructure:"allow-origin" json:"allow-origin" yaml:"allow-origin"` + AllowMethods string `mapstructure:"allow-methods" json:"allow-methods" yaml:"allow-methods"` + AllowHeaders string `mapstructure:"allow-headers" json:"allow-headers" yaml:"allow-headers"` + ExposeHeaders string `mapstructure:"expose-headers" json:"expose-headers" yaml:"expose-headers"` + AllowCredentials bool `mapstructure:"allow-credentials" json:"allow-credentials" yaml:"allow-credentials"` +} diff --git a/server/config/db_list.go b/server/config/db_list.go new file mode 100644 index 0000000..17674b7 --- /dev/null +++ b/server/config/db_list.go @@ -0,0 +1,53 @@ +package config + +import ( + "strings" + + "gorm.io/gorm/logger" +) + +type DsnProvider interface { + Dsn() string +} + +// Embeded 结构体可以压平到上一层,从而保持 config 文件的结构和原来一样 +// 见 playground: https://go.dev/play/p/KIcuhqEoxmY + +// GeneralDB 也被 Pgsql 和 Mysql 原样使用 +type GeneralDB struct { + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀 + Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口 + Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置 + Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名 + Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库账号 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码 + Path string `mapstructure:"path" json:"path" yaml:"path"` // 数据库地址 + Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB + LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志 + MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 + MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 + Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启 + LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件 +} + +func (c GeneralDB) LogLevel() logger.LogLevel { + switch strings.ToLower(c.LogMode) { + case "silent": + return logger.Silent + case "error": + return logger.Error + case "warn": + return logger.Warn + case "info": + return logger.Info + default: + return logger.Info + } +} + +type SpecializedDB struct { + Type string `mapstructure:"type" json:"type" yaml:"type"` + AliasName string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name"` + GeneralDB `yaml:",inline" mapstructure:",squash"` + Disable bool `mapstructure:"disable" json:"disable" yaml:"disable"` +} diff --git a/server/config/disk.go b/server/config/disk.go new file mode 100644 index 0000000..59a6332 --- /dev/null +++ b/server/config/disk.go @@ -0,0 +1,9 @@ +package config + +type Disk struct { + MountPoint string `mapstructure:"mount-point" json:"mount-point" yaml:"mount-point"` +} + +type DiskList struct { + Disk `yaml:",inline" mapstructure:",squash"` +} diff --git a/server/config/email.go b/server/config/email.go new file mode 100644 index 0000000..9fd7642 --- /dev/null +++ b/server/config/email.go @@ -0,0 +1,12 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) +} diff --git a/server/config/excel.go b/server/config/excel.go new file mode 100644 index 0000000..13caab7 --- /dev/null +++ b/server/config/excel.go @@ -0,0 +1,5 @@ +package config + +type Excel struct { + Dir string `mapstructure:"dir" json:"dir" yaml:"dir"` +} diff --git a/server/config/gorm_mssql.go b/server/config/gorm_mssql.go new file mode 100644 index 0000000..d187119 --- /dev/null +++ b/server/config/gorm_mssql.go @@ -0,0 +1,10 @@ +package config + +type Mssql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" +func (m *Mssql) Dsn() string { + return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable" +} diff --git a/server/config/gorm_mysql.go b/server/config/gorm_mysql.go new file mode 100644 index 0000000..77e0245 --- /dev/null +++ b/server/config/gorm_mysql.go @@ -0,0 +1,9 @@ +package config + +type Mysql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Mysql) Dsn() string { + return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config +} diff --git a/server/config/gorm_oracle.go b/server/config/gorm_oracle.go new file mode 100644 index 0000000..52cf21c --- /dev/null +++ b/server/config/gorm_oracle.go @@ -0,0 +1,18 @@ +package config + +import ( + "fmt" + "net" + "net/url" +) + +type Oracle struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Oracle) Dsn() string { + dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password), + net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config) + return dsn + +} diff --git a/server/config/gorm_pgsql.go b/server/config/gorm_pgsql.go new file mode 100644 index 0000000..29fe03f --- /dev/null +++ b/server/config/gorm_pgsql.go @@ -0,0 +1,17 @@ +package config + +type Pgsql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn 基于配置文件获取 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) Dsn() string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config +} + +// LinkDsn 根据 dbname 生成 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) LinkDsn(dbname string) string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config +} diff --git a/server/config/gorm_sqlite.go b/server/config/gorm_sqlite.go new file mode 100644 index 0000000..46f2e19 --- /dev/null +++ b/server/config/gorm_sqlite.go @@ -0,0 +1,13 @@ +package config + +import ( + "path/filepath" +) + +type Sqlite struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (s *Sqlite) Dsn() string { + return filepath.Join(s.Path, s.Dbname+".db") +} diff --git a/server/config/jwt.go b/server/config/jwt.go new file mode 100644 index 0000000..c95d30d --- /dev/null +++ b/server/config/jwt.go @@ -0,0 +1,8 @@ +package config + +type JWT struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} diff --git a/server/config/mcp.go b/server/config/mcp.go new file mode 100644 index 0000000..87028f3 --- /dev/null +++ b/server/config/mcp.go @@ -0,0 +1,18 @@ +package config + +type MCP struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` + Version string `mapstructure:"version" json:"version" yaml:"version"` + Path string `mapstructure:"path" json:"path" yaml:"path"` + Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` + BaseURL string `mapstructure:"base_url" json:"base_url" yaml:"base_url"` + UpstreamBaseURL string `mapstructure:"upstream_base_url" json:"upstream_base_url" yaml:"upstream_base_url"` + AuthHeader string `mapstructure:"auth_header" json:"auth_header" yaml:"auth_header"` + RequestTimeout int `mapstructure:"request_timeout" json:"request_timeout" yaml:"request_timeout"` + + // Deprecated fields kept for backward compatibility with older configs. + SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` + MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` + UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` + Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` +} diff --git a/server/config/mongo.go b/server/config/mongo.go new file mode 100644 index 0000000..2034a3f --- /dev/null +++ b/server/config/mongo.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "strings" +) + +type Mongo struct { + Coll string `json:"coll" yaml:"coll" mapstructure:"coll"` // collection name + Options string `json:"options" yaml:"options" mapstructure:"options"` // mongodb options + Database string `json:"database" yaml:"database" mapstructure:"database"` // database name + Username string `json:"username" yaml:"username" mapstructure:"username"` // 用户名 + Password string `json:"password" yaml:"password" mapstructure:"password"` // 密码 + AuthSource string `json:"auth-source" yaml:"auth-source" mapstructure:"auth-source"` // 验证数据库 + MinPoolSize uint64 `json:"min-pool-size" yaml:"min-pool-size" mapstructure:"min-pool-size"` // 最小连接池 + MaxPoolSize uint64 `json:"max-pool-size" yaml:"max-pool-size" mapstructure:"max-pool-size"` // 最大连接池 + SocketTimeoutMs int64 `json:"socket-timeout-ms" yaml:"socket-timeout-ms" mapstructure:"socket-timeout-ms"` // socket超时时间 + ConnectTimeoutMs int64 `json:"connect-timeout-ms" yaml:"connect-timeout-ms" mapstructure:"connect-timeout-ms"` // 连接超时时间 + IsZap bool `json:"is-zap" yaml:"is-zap" mapstructure:"is-zap"` // 是否开启zap日志 + Hosts []*MongoHost `json:"hosts" yaml:"hosts" mapstructure:"hosts"` // 主机列表 +} + +type MongoHost struct { + Host string `json:"host" yaml:"host" mapstructure:"host"` // ip地址 + Port string `json:"port" yaml:"port" mapstructure:"port"` // 端口 +} + +// Uri . +func (x *Mongo) Uri() string { + length := len(x.Hosts) + hosts := make([]string, 0, length) + for i := 0; i < length; i++ { + if x.Hosts[i].Host != "" && x.Hosts[i].Port != "" { + hosts = append(hosts, x.Hosts[i].Host+":"+x.Hosts[i].Port) + } + } + if x.Options != "" { + return fmt.Sprintf("mongodb://%s/%s?%s", strings.Join(hosts, ","), x.Database, x.Options) + } + return fmt.Sprintf("mongodb://%s/%s", strings.Join(hosts, ","), x.Database) +} diff --git a/server/config/oss_aliyun.go b/server/config/oss_aliyun.go new file mode 100644 index 0000000..934bd78 --- /dev/null +++ b/server/config/oss_aliyun.go @@ -0,0 +1,10 @@ +package config + +type AliyunOSS struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` +} diff --git a/server/config/oss_aws.go b/server/config/oss_aws.go new file mode 100644 index 0000000..7ec6acc --- /dev/null +++ b/server/config/oss_aws.go @@ -0,0 +1,13 @@ +package config + +type AwsS3 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` + S3ForcePathStyle bool `mapstructure:"s3-force-path-style" json:"s3-force-path-style" yaml:"s3-force-path-style"` + DisableSSL bool `mapstructure:"disable-ssl" json:"disable-ssl" yaml:"disable-ssl"` +} diff --git a/server/config/oss_cloudflare.go b/server/config/oss_cloudflare.go new file mode 100644 index 0000000..ab7a393 --- /dev/null +++ b/server/config/oss_cloudflare.go @@ -0,0 +1,10 @@ +package config + +type CloudflareR2 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + Path string `mapstructure:"path" json:"path" yaml:"path"` + AccountID string `mapstructure:"account-id" json:"account-id" yaml:"account-id"` + AccessKeyID string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + SecretAccessKey string `mapstructure:"secret-access-key" json:"secret-access-key" yaml:"secret-access-key"` +} diff --git a/server/config/oss_huawei.go b/server/config/oss_huawei.go new file mode 100644 index 0000000..45dfbcd --- /dev/null +++ b/server/config/oss_huawei.go @@ -0,0 +1,9 @@ +package config + +type HuaWeiObs struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` +} diff --git a/server/config/oss_local.go b/server/config/oss_local.go new file mode 100644 index 0000000..7038d4a --- /dev/null +++ b/server/config/oss_local.go @@ -0,0 +1,6 @@ +package config + +type Local struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径 + StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径 +} diff --git a/server/config/oss_minio.go b/server/config/oss_minio.go new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/server/config/oss_minio.go @@ -0,0 +1,11 @@ +package config + +type Minio struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` +} diff --git a/server/config/oss_qiniu.go b/server/config/oss_qiniu.go new file mode 100644 index 0000000..298fe2d --- /dev/null +++ b/server/config/oss_qiniu.go @@ -0,0 +1,11 @@ +package config + +type Qiniu struct { + Zone string `mapstructure:"zone" json:"zone" yaml:"zone"` // 存储区域 + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` // 空间名称 + ImgPath string `mapstructure:"img-path" json:"img-path" yaml:"img-path"` // CDN加速域名 + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` // 秘钥AK + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` // 秘钥SK + UseHTTPS bool `mapstructure:"use-https" json:"use-https" yaml:"use-https"` // 是否使用https + UseCdnDomains bool `mapstructure:"use-cdn-domains" json:"use-cdn-domains" yaml:"use-cdn-domains"` // 上传是否使用CDN上传加速 +} diff --git a/server/config/oss_tencent.go b/server/config/oss_tencent.go new file mode 100644 index 0000000..39a29d1 --- /dev/null +++ b/server/config/oss_tencent.go @@ -0,0 +1,10 @@ +package config + +type TencentCOS struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` +} diff --git a/server/config/redis.go b/server/config/redis.go new file mode 100644 index 0000000..94b5bf6 --- /dev/null +++ b/server/config/redis.go @@ -0,0 +1,10 @@ +package config + +type Redis struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字 + Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码 + DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库 + UseCluster bool `mapstructure:"useCluster" json:"useCluster" yaml:"useCluster"` // 是否使用集群模式 + ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表 +} diff --git a/server/config/system.go b/server/config/system.go new file mode 100644 index 0000000..3036143 --- /dev/null +++ b/server/config/system.go @@ -0,0 +1,15 @@ +package config + +type System struct { + DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"` // 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql + OssType string `mapstructure:"oss-type" json:"oss-type" yaml:"oss-type"` // Oss类型 + RouterPrefix string `mapstructure:"router-prefix" json:"router-prefix" yaml:"router-prefix"` + Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 端口值 + LimitCountIP int `mapstructure:"iplimit-count" json:"iplimit-count" yaml:"iplimit-count"` + LimitTimeIP int `mapstructure:"iplimit-time" json:"iplimit-time" yaml:"iplimit-time"` + UseMultipoint bool `mapstructure:"use-multipoint" json:"use-multipoint" yaml:"use-multipoint"` // 多点登录拦截 + UseRedis bool `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis"` // 使用redis + UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo + UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式 + DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构,生产环境建议设为false,手动迁移 +} diff --git a/server/config/zap.go b/server/config/zap.go new file mode 100644 index 0000000..0e8ae2b --- /dev/null +++ b/server/config/zap.go @@ -0,0 +1,71 @@ +package config + +import ( + "go.uber.org/zap/zapcore" + "time" +) + +type Zap struct { + Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别 + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 + Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出 + Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹 + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级 + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 + RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数 +} + +// Levels 根据字符串转化为 zapcore.Levels +func (c *Zap) Levels() []zapcore.Level { + levels := make([]zapcore.Level, 0, 7) + level, err := zapcore.ParseLevel(c.Level) + if err != nil { + level = zapcore.DebugLevel + } + for ; level <= zapcore.FatalLevel; level++ { + levels = append(levels, level) + } + return levels +} + +func (c *Zap) Encoder() zapcore.Encoder { + config := zapcore.EncoderConfig{ + TimeKey: "time", + NameKey: "name", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "message", + StacktraceKey: c.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000")) + }, + EncodeLevel: c.LevelEncoder(), + EncodeCaller: zapcore.FullCallerEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + } + if c.Format == "json" { + return zapcore.NewJSONEncoder(config) + } + return zapcore.NewConsoleEncoder(config) + +} + +// LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder +// Author [SliverHorn](https://github.com/SliverHorn) +func (c *Zap) LevelEncoder() zapcore.LevelEncoder { + switch { + case c.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) + return zapcore.LowercaseLevelEncoder + case c.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 + return zapcore.LowercaseColorLevelEncoder + case c.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 + return zapcore.CapitalLevelEncoder + case c.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 + return zapcore.CapitalColorLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} diff --git a/server/core/internal/constant.go b/server/core/internal/constant.go new file mode 100644 index 0000000..b22362c --- /dev/null +++ b/server/core/internal/constant.go @@ -0,0 +1,9 @@ +package internal + +const ( + ConfigEnv = "GVA_CONFIG" + ConfigDefaultFile = "config.yaml" + ConfigTestFile = "config.test.yaml" + ConfigDebugFile = "config.debug.yaml" + ConfigReleaseFile = "config.release.yaml" +) diff --git a/server/core/internal/cutter.go b/server/core/internal/cutter.go new file mode 100644 index 0000000..2873b7c --- /dev/null +++ b/server/core/internal/cutter.go @@ -0,0 +1,125 @@ +package internal + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +// Cutter 实现 io.Writer 接口 +// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator) +type Cutter struct { + level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal) + layout string // 时间格式 2006-01-02 15:04:05 + formats []string // 自定义参数([]string{Director,"2006-01-02", "business"(此参数可不写), level+".log"} + director string // 日志文件夹 + retentionDay int //日志保留天数 + file *os.File // 文件句柄 + mutex *sync.RWMutex // 读写锁 +} + +type CutterOption func(*Cutter) + +// CutterWithLayout 时间格式 +func CutterWithLayout(layout string) CutterOption { + return func(c *Cutter) { + c.layout = layout + } +} + +// CutterWithFormats 格式化参数 +func CutterWithFormats(format ...string) CutterOption { + return func(c *Cutter) { + if len(format) > 0 { + c.formats = format + } + } +} + +func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter { + rotate := &Cutter{ + level: level, + director: director, + retentionDay: retentionDay, + mutex: new(sync.RWMutex), + } + for i := 0; i < len(options); i++ { + options[i](rotate) + } + return rotate +} + +// Write satisfies the io.Writer interface. It writes to the +// appropriate file handle that is currently being used. +// If we have reached rotation time, the target file gets +// automatically rotated, and also purged if necessary. +func (c *Cutter) Write(bytes []byte) (n int, err error) { + c.mutex.Lock() + defer func() { + if c.file != nil { + _ = c.file.Close() + c.file = nil + } + c.mutex.Unlock() + }() + length := len(c.formats) + values := make([]string, 0, 3+length) + values = append(values, c.director) + if c.layout != "" { + values = append(values, time.Now().Format(c.layout)) + } + for i := 0; i < length; i++ { + values = append(values, c.formats[i]) + } + values = append(values, c.level+".log") + filename := filepath.Join(values...) + director := filepath.Dir(filename) + err = os.MkdirAll(director, os.ModePerm) + 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 + } + return c.file.Write(bytes) +} + +func (c *Cutter) Sync() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.file != nil { + return c.file.Sync() + } + return nil +} + +// 增加日志目录文件清理 小于等于零的值默认忽略不再处理 +func removeNDaysFolders(dir string, days int) error { + if days <= 0 { + return nil + } + cutoff := time.Now().AddDate(0, 0, -days) + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.ModTime().Before(cutoff) && path != dir { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + return nil + }) +} diff --git a/server/core/internal/zap_core.go b/server/core/internal/zap_core.go new file mode 100644 index 0000000..3a95dac --- /dev/null +++ b/server/core/internal/zap_core.go @@ -0,0 +1,133 @@ +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" +) + +type ZapCore struct { + level zapcore.Level + zapcore.Core +} + +func NewZapCore(level zapcore.Level) *ZapCore { + entity := &ZapCore{level: level} + syncer := entity.WriteSyncer() + levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { + return l == level + }) + entity.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, levelEnabler) + return entity +} + +func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer { + cutter := NewCutter( + global.GVA_CONFIG.Zap.Director, + z.level.String(), + global.GVA_CONFIG.Zap.RetentionDay, + CutterWithLayout(time.DateOnly), + CutterWithFormats(formats...), + ) + if global.GVA_CONFIG.Zap.LogInConsole { + multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter) + return zapcore.AddSync(multiSyncer) + } + return zapcore.AddSync(cutter) +} + +func (z *ZapCore) Enabled(level zapcore.Level) bool { + return z.level == level +} + +func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core { + return z.Core.With(fields) +} + +func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if z.Enabled(entry.Level) { + return check.AddCore(entry, z) + } + return check +} + +func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) + } + } + // 先写入原日志目标 + 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 { + return z.Core.Sync() +} diff --git a/server/core/server.go b/server/core/server.go new file mode 100644 index 0000000..6c36f33 --- /dev/null +++ b/server/core/server.go @@ -0,0 +1,48 @@ +package core + +import ( + "fmt" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize" + mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "go.uber.org/zap" +) + +func RunServer() { + if global.GVA_CONFIG.System.UseRedis { + initialize.Redis() + if global.GVA_CONFIG.System.UseMultipoint { + initialize.RedisList() + } + } + + if global.GVA_CONFIG.System.UseMongo { + if err := initialize.Mongo.Initialization(); err != nil { + zap.L().Error(fmt.Sprintf("%+v", err)) + } + } + + if global.GVA_DB != nil { + system.LoadAll() + } + + Router := initialize.Routers() + address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) + mcpBaseURL := mcpTool.ResolveMCPServiceURL() + + fmt.Printf(` + 欢迎使用 gin-vue-admin + 当前版本:%s + 项目地址:https://github.com/flipped-aurora/gin-vue-admin + 插件市场:https://plugin.gin-vue-admin.com + 默认自动化文档地址:http://127.0.0.1%s/swagger/index.html + MCP 独立服务请手动启动: go run ./cmd/mcp -config ./cmd/mcp/config.yaml + 默认MCP StreamHTTP地址:%s + 默认前端文件运行地址:http://127.0.0.1:8080 +`, global.Version, address, mcpBaseURL) + + initServer(address, Router, 10*time.Minute, 10*time.Minute) +} diff --git a/server/core/server_run.go b/server/core/server_run.go new file mode 100644 index 0000000..067ce6b --- /dev/null +++ b/server/core/server_run.go @@ -0,0 +1,60 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.L().Info("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") +} diff --git a/server/core/viper.go b/server/core/viper.go new file mode 100644 index 0000000..d846c90 --- /dev/null +++ b/server/core/viper.go @@ -0,0 +1,76 @@ +package core + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/fsnotify/fsnotify" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// Viper 配置 +func Viper() *viper.Viper { + config := getConfigPath() + + v := viper.New() + v.SetConfigFile(config) + v.SetConfigType("yaml") + err := v.ReadInConfig() + if err != nil { + panic(fmt.Errorf("fatal error config file: %w", err)) + } + v.WatchConfig() + + v.OnConfigChange(func(e fsnotify.Event) { + fmt.Println("config file changed:", e.Name) + if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { + fmt.Println(err) + } + }) + if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { + panic(fmt.Errorf("fatal error unmarshal config: %w", err)) + } + + // root 适配性 根据root位置去找到对应迁移位置,保证root路径有效 + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + return v +} + +// getConfigPath 获取配置文件路径, 优先级: 命令行 > 环境变量 > 默认值 +func getConfigPath() (config string) { + // `-c` flag parse + flag.StringVar(&config, "c", "", "choose config file.") + flag.Parse() + if config != "" { // 命令行参数不为空 将值赋值于config + fmt.Printf("您正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config) + return + } + if env := os.Getenv(internal.ConfigEnv); env != "" { // 判断环境变量 GVA_CONFIG + config = env + fmt.Printf("您正在使用 %s 环境变量, config 的路径为 %s\n", internal.ConfigEnv, config) + return + } + + switch gin.Mode() { // 根据 gin 模式文件名 + case gin.DebugMode: + config = internal.ConfigDebugFile + case gin.ReleaseMode: + config = internal.ConfigReleaseFile + case gin.TestMode: + config = internal.ConfigTestFile + } + fmt.Printf("您正在使用 gin 的 %s 模式运行, config 的路径为 %s\n", gin.Mode(), config) + + _, err := os.Stat(config) + if err != nil || os.IsNotExist(err) { + config = internal.ConfigDefaultFile + fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config) + } + + return +} diff --git a/server/core/zap.go b/server/core/zap.go new file mode 100644 index 0000000..09aa165 --- /dev/null +++ b/server/core/zap.go @@ -0,0 +1,36 @@ +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" +) + +// Zap 获取 zap.Logger +// Author [SliverHorn](https://github.com/SliverHorn) +func Zap() (logger *zap.Logger) { + if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹 + fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director) + _ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm) + } + levels := global.GVA_CONFIG.Zap.Levels() + length := len(levels) + cores := make([]zapcore.Core, 0, length) + for i := 0; i < length; i++ { + core := internal.NewZapCore(levels[i]) + cores = append(cores, core) + } + // 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理) + logger = zap.New(zapcore.NewTee(cores...)) + // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用 + opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)} + if global.GVA_CONFIG.Zap.ShowLine { + opts = append(opts, zap.AddCaller()) + } + logger = logger.WithOptions(opts...) + return logger +} diff --git a/server/docs/docs.go b/server/docs/docs.go new file mode 100644 index 0000000..e5fc176 --- /dev/null +++ b/server/docs/docs.go @@ -0,0 +1,9314 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package docs + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/swaggo/swag" +) + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/createApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "创建基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "创建基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "删除api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApisByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除选中Api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除选中Api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/enterSyncApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "确认同步API", + "responses": { + "200": { + "description": "确认同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/freshCasbin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "刷新casbin缓存", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getAllApis": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取所有的Api 不分页", + "responses": { + "200": { + "description": "获取所有的Api 不分页,返回包括api列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIListResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "根据id获取api", + "parameters": [ + { + "description": "根据id获取api", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取api,返回包括api详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIResponse" + } + } + } + ] + } + } + } + } + }, + "/api/getApiGroups": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取API分组", + "responses": { + "200": { + "description": "获取API分组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "分页获取API列表", + "parameters": [ + { + "description": "分页获取API列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchApiParams" + } + } + ], + "responses": { + "200": { + "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/ignoreApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "IgnoreApi" + ], + "summary": "忽略API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/syncApi": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "同步API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/updateApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "修改基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "修改基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/addCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddCategory" + ], + "summary": "添加媒体库分类", + "parameters": [ + { + "description": "媒体库分类数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + } + ], + "responses": {} + } + }, + "/attachmentCategory/deleteCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DeleteCategory" + ], + "summary": "删除分类", + "parameters": [ + { + "description": "分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除分类", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/getCategoryList": { + "get": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "GetCategoryList" + ], + "summary": "媒体库分类列表", + "responses": { + "200": { + "description": "媒体库分类列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/copyAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "拷贝角色", + "parameters": [ + { + "description": "旧角色id, 新权限id, 新权限名, 新父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.SysAuthorityCopyResponse" + } + } + ], + "responses": { + "200": { + "description": "拷贝角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/createAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "创建角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/deleteAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/getAuthorityList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "分页获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/setDataAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "设置角色资源权限", + "parameters": [ + { + "description": "设置角色资源权限", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "设置角色资源权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/updateAuthority": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "更新角色信息", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "更新角色信息,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/canRemoveAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/getAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "获取权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityBtnRes" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/setAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/addFunc": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddFunc" + ], + "summary": "增加方法", + "parameters": [ + { + "description": "增加方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/createPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "创建package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoCodePackageCreate" + } + } + ], + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/createTemp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "自动代码模板", + "parameters": [ + { + "description": "创建自动代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/delPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/delSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除回滚记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getColumn": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前表所有字段", + "responses": { + "200": { + "description": "获取当前表所有字段", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getDB": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前所有数据库", + "responses": { + "200": { + "description": "获取当前所有数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getMeta": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取meta信息", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "获取meta信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "查询回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTables": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前数据库所有表", + "responses": { + "200": { + "description": "获取当前数据库所有表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTemplates": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initAPI": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/installPlugin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "plug", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "安装插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/preview": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "预览创建后的代码", + "parameters": [ + { + "description": "预览创建代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "预览创建后的代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/pubPlug": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "plugName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "回滚自动生成代码", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoHistoryRollBack" + } + } + ], + "responses": { + "200": { + "description": "回滚自动生成代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/captcha": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysCaptchaResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "返回包括用户信息,token,过期时间", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/UpdateCasbin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "更新角色api权限", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "更新角色api权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/getPolicyPathByAuthorityId": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "获取权限列表", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "获取权限列表,返回包括casbin详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PolicyPathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customer": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "获取单一客户信息", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "客户名", + "name": "customerName", + "in": "query" + }, + { + "type": "string", + "description": "客户手机号", + "name": "customerPhoneData", + "in": "query" + }, + { + "type": "integer", + "description": "管理角色ID", + "name": "sysUserAuthorityID", + "in": "query" + }, + { + "type": "integer", + "description": "管理ID", + "name": "sysUserId", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取单一客户信息,返回包括客户详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaCustomerResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "更新客户信息", + "parameters": [ + { + "description": "客户ID, 客户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "更新客户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "创建客户", + "parameters": [ + { + "description": "客户用户名, 客户手机号码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "创建客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "删除客户", + "parameters": [ + { + "description": "客户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "删除客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customerList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "分页获取权限客户列表", + "parameters": [ + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/email/emailTest": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送测试邮件", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/email/sendEmail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送邮件", + "parameters": [ + { + "description": "发送邮件必须的参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.Email" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/fileUploadAndDownload/breakpointContinue": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "断点续传到服务器", + "parameters": [ + { + "type": "file", + "description": "an example for breakpoint resume, 断点续传示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "断点续传到服务器", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/deleteFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "传入文件里面id即可", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/findFile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "查找文件", + "parameters": [ + { + "type": "file", + "description": "Find the file, 查找文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "查找文件,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "创建文件", + "parameters": [ + { + "type": "file", + "description": "上传文件完成", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "创建文件,返回包括文件路径", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FilePathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "分页文件列表", + "parameters": [ + { + "description": "页码, 每页大小, 分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ExaAttachmentCategorySearch" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/importURL": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "导入URL", + "parameters": [ + { + "description": "对象", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "导入URL", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/removeChunk": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除切片", + "parameters": [ + { + "type": "file", + "description": "删除缓存切片", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "删除切片", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "上传文件示例", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件示例,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaFileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/createInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "创建公告", + "parameters": [ + { + "description": "创建公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfo": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "删除公告", + "parameters": [ + { + "description": "删除公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfoByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "批量删除公告", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/findInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "用id查询公告", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "内容", + "name": "content", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "标题", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "作者", + "name": "userID", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Info" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoDataSource": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "获取Info的数据源", + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "分页获取公告列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoPublic": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "不需要鉴权的公告接口", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/updateInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "更新公告", + "parameters": [ + { + "description": "更新公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/checkdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CheckDB" + ], + "summary": "初始化用户数据库", + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/initdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InitDB" + ], + "summary": "初始化用户数据库", + "parameters": [ + { + "description": "初始化数据库参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.InitDB" + } + } + ], + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/jwt/jsonInBlacklist": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Jwt" + ], + "summary": "jwt加入黑名单", + "responses": { + "200": { + "description": "jwt加入黑名单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "新增菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "增加menu和角色关联关系", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AddMenuAuthorityInfo" + } + } + ], + "responses": { + "200": { + "description": "增加menu和角色关联关系", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/deleteBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "删除菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "根据id获取菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取菜单,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenuResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取指定角色menu", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetAuthorityId" + } + } + ], + "responses": { + "200": { + "description": "获取指定角色menu", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "分页获取基础menu列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/updateBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/createSysDictionary": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "创建SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/deleteSysDictionary": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "删除SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/findSysDictionary": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "用id查询SysDictionary", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "描述", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "字典名(中)", + "name": "name", + "in": "query" + }, + { + "type": "boolean", + "description": "状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "字典名(英)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/getSysDictionaryList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "分页获取SysDictionary列表", + "responses": { + "200": { + "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/updateSysDictionary": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "更新SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/createSysDictionaryDetail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "创建SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/deleteSysDictionaryDetail": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "删除SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/findSysDictionaryDetail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "用id查询SysDictionaryDetail", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/getSysDictionaryDetailList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "分页获取SysDictionaryDetail列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/updateSysDictionaryDetail": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "更新SysDictionaryDetail", + "parameters": [ + { + "description": "更新SysDictionaryDetail", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysExportTemplate/ExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/sysExportTemplate/createSysExportTemplate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "创建导出模板", + "parameters": [ + { + "description": "创建导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplate": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "删除导出模板", + "parameters": [ + { + "description": "删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplateByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "批量删除导出模板", + "parameters": [ + { + "description": "批量删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/exportExcel": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格", + "responses": {} + } + }, + "/sysExportTemplate/findSysExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "用id查询导出模板", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/getSysExportTemplateList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "分页获取导出模板列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/importExcel": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysImportTemplate" + ], + "summary": "导入表格", + "responses": {} + } + }, + "/sysExportTemplate/updateSysExportTemplate": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "更新导出模板", + "parameters": [ + { + "description": "更新导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysOperationRecord/createSysOperationRecord": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "创建SysOperationRecord", + "parameters": [ + { + "description": "创建SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "创建SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecord": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "删除SysOperationRecord", + "parameters": [ + { + "description": "SysOperationRecord模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecordByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "批量删除SysOperationRecord", + "parameters": [ + { + "description": "批量删除SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "批量删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/findSysOperationRecord": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "用id查询SysOperationRecord", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/getSysOperationRecordList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "分页获取SysOperationRecord列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/createSysParams": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "创建参数", + "parameters": [ + { + "description": "创建参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParams": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "删除参数", + "parameters": [ + { + "description": "删除参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParamsByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "批量删除参数", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/findSysParams": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "用id查询参数", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "参数说明", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "参数键", + "name": "key", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "参数名称", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "参数值", + "name": "value", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParam": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "根据key获取参数value", + "parameters": [ + { + "type": "string", + "description": "key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParamsList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "分页获取参数列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/updateSysParams": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "更新参数", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getServerInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取服务器信息", + "responses": { + "200": { + "description": "获取服务器信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取配置文件内容", + "responses": { + "200": { + "description": "获取配置文件内容,返回包括系统配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysConfigResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/reloadSystem": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "重启系统", + "responses": { + "200": { + "description": "重启系统", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/setSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "设置配置文件内容", + "parameters": [ + { + "description": "设置配置文件内容", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.System" + } + } + ], + "responses": { + "200": { + "description": "设置配置文件内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfSetting": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户配置", + "parameters": [ + { + "description": "用户配置数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "设置用户配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/admin_register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户注册账号", + "parameters": [ + { + "description": "用户名, 昵称, 密码, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Register" + } + } + ], + "responses": { + "200": { + "description": "用户注册账号,返回包括用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysUserResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户修改密码", + "parameters": [ + { + "description": "用户名, 原密码, 新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePasswordReq" + } + } + ], + "responses": { + "200": { + "description": "用户修改密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/deleteUser": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "用户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "获取用户信息", + "responses": { + "200": { + "description": "获取用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "分页获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetUserList" + } + } + ], + "responses": { + "200": { + "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/resetPassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "重置用户密码", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "重置用户密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthorities": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuthorities" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "更改用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuth" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "common.JSONMap": { + "type": "object", + "additionalProperties": true + }, + "config.AliyunOSS": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + } + }, + "config.Autocode": { + "type": "object", + "properties": { + "ai-path": { + "type": "string" + }, + "module": { + "type": "string" + }, + "root": { + "type": "string" + }, + "server": { + "type": "string" + }, + "web": { + "type": "string" + } + } + }, + "config.AwsS3": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "disable-ssl": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "s3-force-path-style": { + "type": "boolean" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.CORS": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "whitelist": { + "type": "array", + "items": { + "$ref": "#/definitions/config.CORSWhitelist" + } + } + } + }, + "config.CORSWhitelist": { + "type": "object", + "properties": { + "allow-credentials": { + "type": "boolean" + }, + "allow-headers": { + "type": "string" + }, + "allow-methods": { + "type": "string" + }, + "allow-origin": { + "type": "string" + }, + "expose-headers": { + "type": "string" + } + } + }, + "config.Captcha": { + "type": "object", + "properties": { + "img-height": { + "description": "验证码高度", + "type": "integer" + }, + "img-width": { + "description": "验证码宽度", + "type": "integer" + }, + "key-long": { + "description": "验证码长度", + "type": "integer" + }, + "open-captcha": { + "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", + "type": "integer" + }, + "open-captcha-timeout": { + "description": "防爆破验证码超时时间,单位:s(秒)", + "type": "integer" + } + } + }, + "config.CloudflareR2": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "account-id": { + "type": "string" + }, + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-access-key": { + "type": "string" + } + } + }, + "config.DiskList": { + "type": "object", + "properties": { + "mount-point": { + "type": "string" + } + } + }, + "config.Excel": { + "type": "object", + "properties": { + "dir": { + "type": "string" + } + } + }, + "config.HuaWeiObs": { + "type": "object", + "properties": { + "access-key": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.JWT": { + "type": "object", + "properties": { + "buffer-time": { + "description": "缓冲时间", + "type": "string" + }, + "expires-time": { + "description": "过期时间", + "type": "string" + }, + "issuer": { + "description": "签发者", + "type": "string" + }, + "signing-key": { + "description": "jwt签名", + "type": "string" + } + } + }, + "config.Local": { + "type": "object", + "properties": { + "path": { + "description": "本地文件访问路径", + "type": "string" + }, + "store-path": { + "description": "本地文件存储路径", + "type": "string" + } + } + }, + "config.Minio": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "use-ssl": { + "type": "boolean" + } + } + }, + "config.Mongo": { + "type": "object", + "properties": { + "auth-source": { + "description": "验证数据库", + "type": "string" + }, + "coll": { + "description": "collection name", + "type": "string" + }, + "connect-timeout-ms": { + "description": "连接超时时间", + "type": "integer" + }, + "database": { + "description": "database name", + "type": "string" + }, + "hosts": { + "description": "主机列表", + "type": "array", + "items": { + "$ref": "#/definitions/config.MongoHost" + } + }, + "is-zap": { + "description": "是否开启zap日志", + "type": "boolean" + }, + "max-pool-size": { + "description": "最大连接池", + "type": "integer" + }, + "min-pool-size": { + "description": "最小连接池", + "type": "integer" + }, + "options": { + "description": "mongodb options", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "socket-timeout-ms": { + "description": "socket超时时间", + "type": "integer" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "config.MongoHost": { + "type": "object", + "properties": { + "host": { + "description": "ip地址", + "type": "string" + }, + "port": { + "description": "端口", + "type": "string" + } + } + }, + "config.Mssql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Mysql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Oracle": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Pgsql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Qiniu": { + "type": "object", + "properties": { + "access-key": { + "description": "秘钥AK", + "type": "string" + }, + "bucket": { + "description": "空间名称", + "type": "string" + }, + "img-path": { + "description": "CDN加速域名", + "type": "string" + }, + "secret-key": { + "description": "秘钥SK", + "type": "string" + }, + "use-cdn-domains": { + "description": "上传是否使用CDN上传加速", + "type": "boolean" + }, + "use-https": { + "description": "是否使用https", + "type": "boolean" + }, + "zone": { + "description": "存储区域", + "type": "string" + } + } + }, + "config.Redis": { + "type": "object", + "properties": { + "addr": { + "description": "服务器地址:端口", + "type": "string" + }, + "clusterAddrs": { + "description": "集群模式下的节点地址列表", + "type": "array", + "items": { + "type": "string" + } + }, + "db": { + "description": "单实例模式下redis的哪个数据库", + "type": "integer" + }, + "name": { + "description": "代表当前实例的名字", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "useCluster": { + "description": "是否使用集群模式", + "type": "boolean" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "aliyun-oss": { + "$ref": "#/definitions/config.AliyunOSS" + }, + "autocode": { + "description": "auto", + "allOf": [ + { + "$ref": "#/definitions/config.Autocode" + } + ] + }, + "aws-s3": { + "$ref": "#/definitions/config.AwsS3" + }, + "captcha": { + "$ref": "#/definitions/config.Captcha" + }, + "cloudflare-r2": { + "$ref": "#/definitions/config.CloudflareR2" + }, + "cors": { + "description": "跨域配置", + "allOf": [ + { + "$ref": "#/definitions/config.CORS" + } + ] + }, + "db-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SpecializedDB" + } + }, + "disk-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.DiskList" + } + }, + "email": { + "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" + }, + "excel": { + "$ref": "#/definitions/config.Excel" + }, + "hua-wei-obs": { + "$ref": "#/definitions/config.HuaWeiObs" + }, + "jwt": { + "$ref": "#/definitions/config.JWT" + }, + "local": { + "description": "oss", + "allOf": [ + { + "$ref": "#/definitions/config.Local" + } + ] + }, + "minio": { + "$ref": "#/definitions/config.Minio" + }, + "mongo": { + "$ref": "#/definitions/config.Mongo" + }, + "mssql": { + "$ref": "#/definitions/config.Mssql" + }, + "mysql": { + "description": "gorm", + "allOf": [ + { + "$ref": "#/definitions/config.Mysql" + } + ] + }, + "oracle": { + "$ref": "#/definitions/config.Oracle" + }, + "pgsql": { + "$ref": "#/definitions/config.Pgsql" + }, + "qiniu": { + "$ref": "#/definitions/config.Qiniu" + }, + "redis": { + "$ref": "#/definitions/config.Redis" + }, + "redis-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.Redis" + } + }, + "sqlite": { + "$ref": "#/definitions/config.Sqlite" + }, + "system": { + "$ref": "#/definitions/config.System" + }, + "tencent-cos": { + "$ref": "#/definitions/config.TencentCOS" + }, + "zap": { + "$ref": "#/definitions/config.Zap" + } + } + }, + "config.SpecializedDB": { + "type": "object", + "properties": { + "alias-name": { + "type": "string" + }, + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Sqlite": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.System": { + "type": "object", + "properties": { + "addr": { + "description": "端口值", + "type": "integer" + }, + "db-type": { + "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", + "type": "string" + }, + "iplimit-count": { + "type": "integer" + }, + "iplimit-time": { + "type": "integer" + }, + "oss-type": { + "description": "Oss类型", + "type": "string" + }, + "router-prefix": { + "type": "string" + }, + "use-mongo": { + "description": "使用mongo", + "type": "boolean" + }, + "use-multipoint": { + "description": "多点登录拦截", + "type": "boolean" + }, + "use-redis": { + "description": "使用redis", + "type": "boolean" + }, + "use-strict-auth": { + "description": "使用树形角色分配模式", + "type": "boolean" + } + } + }, + "config.TencentCOS": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.Zap": { + "type": "object", + "properties": { + "director": { + "description": "日志文件夹", + "type": "string" + }, + "encode-level": { + "description": "编码级", + "type": "string" + }, + "format": { + "description": "输出", + "type": "string" + }, + "level": { + "description": "级别", + "type": "string" + }, + "log-in-console": { + "description": "输出控制台", + "type": "boolean" + }, + "prefix": { + "description": "日志前缀", + "type": "string" + }, + "retention-day": { + "description": "日志保留天数", + "type": "integer" + }, + "show-line": { + "description": "显示行", + "type": "boolean" + }, + "stacktrace-key": { + "description": "栈名", + "type": "string" + } + } + }, + "example.ExaAttachmentCategory": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "name": { + "type": "string" + }, + "pid": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaCustomer": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "customerName": { + "description": "客户名", + "type": "string" + }, + "customerPhoneData": { + "description": "客户手机号", + "type": "string" + }, + "sysUser": { + "description": "管理详情", + "allOf": [ + { + "$ref": "#/definitions/system.SysUser" + } + ] + }, + "sysUserAuthorityID": { + "description": "管理角色ID", + "type": "integer" + }, + "sysUserId": { + "description": "管理ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFile": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "chunkTotal": { + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileChunk": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaFileChunk" + } + }, + "fileMd5": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "isFinish": { + "type": "boolean" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileChunk": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileID": { + "type": "integer" + }, + "fileChunkNumber": { + "type": "integer" + }, + "fileChunkPath": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileUploadAndDownload": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "classId": { + "description": "分类id", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "编号", + "type": "string" + }, + "name": { + "description": "文件名", + "type": "string" + }, + "tag": { + "description": "文件标签", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "url": { + "description": "文件地址", + "type": "string" + } + } + }, + "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { + "type": "object", + "properties": { + "from": { + "description": "发件人 你自己要发邮件的邮箱", + "type": "string" + }, + "host": { + "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", + "type": "string" + }, + "is-ssl": { + "description": "是否SSL 是否开启SSL", + "type": "boolean" + }, + "nickname": { + "description": "昵称 发件人昵称 通常为自己的邮箱", + "type": "string" + }, + "port": { + "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", + "type": "integer" + }, + "secret": { + "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", + "type": "string" + }, + "to": { + "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", + "type": "string" + } + } + }, + "model.Info": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "attachments": { + "description": "附件", + "type": "array", + "items": { + "type": "object" + } + }, + "content": { + "description": "内容", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "title": { + "description": "标题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userID": { + "description": "作者", + "type": "integer" + } + } + }, + "request.AddMenuAuthorityInfo": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "request.AutoCode": { + "type": "object", + "properties": { + "abbreviation": { + "description": "Struct简称", + "type": "string", + "example": "Struct简称" + }, + "autoCreateApiToSql": { + "description": "是否自动创建api", + "type": "boolean", + "example": false + }, + "autoCreateBtnAuth": { + "description": "是否自动创建按钮权限", + "type": "boolean", + "example": false + }, + "autoCreateMenuToSql": { + "description": "是否自动创建menu", + "type": "boolean", + "example": false + }, + "autoCreateResource": { + "description": "是否自动创建资源标识", + "type": "boolean", + "example": false + }, + "autoMigrate": { + "description": "是否自动迁移表结构", + "type": "boolean", + "example": false + }, + "businessDB": { + "description": "业务数据库", + "type": "string", + "example": "业务数据库" + }, + "description": { + "description": "Struct中文名称", + "type": "string", + "example": "Struct中文名称" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/request.AutoCodeField" + } + }, + "generateServer": { + "description": "是否生成server", + "type": "boolean", + "example": true + }, + "generateWeb": { + "description": "是否生成web", + "type": "boolean", + "example": true + }, + "gvaModel": { + "description": "是否使用gva默认Model", + "type": "boolean", + "example": false + }, + "humpPackageName": { + "description": "go文件名称", + "type": "string", + "example": "go文件名称" + }, + "isAdd": { + "description": "是否新增", + "type": "boolean", + "example": false + }, + "isTree": { + "description": "是否树形结构", + "type": "boolean", + "example": false + }, + "onlyTemplate": { + "description": "是否只生成模板", + "type": "boolean", + "example": false + }, + "package": { + "type": "string" + }, + "packageName": { + "description": "文件名称", + "type": "string", + "example": "文件名称" + }, + "primaryField": { + "$ref": "#/definitions/request.AutoCodeField" + }, + "structName": { + "description": "Struct名称", + "type": "string", + "example": "Struct名称" + }, + "tableName": { + "description": "表名", + "type": "string", + "example": "表名" + }, + "treeJson": { + "description": "展示的树json字段", + "type": "string", + "example": "展示的树json字段" + } + } + }, + "request.AutoCodeField": { + "type": "object", + "properties": { + "checkDataSource": { + "description": "是否检查数据源", + "type": "boolean" + }, + "clearable": { + "description": "是否可清空", + "type": "boolean" + }, + "columnName": { + "description": "数据库字段", + "type": "string" + }, + "comment": { + "description": "数据库字段描述", + "type": "string" + }, + "dataSource": { + "description": "数据源", + "allOf": [ + { + "$ref": "#/definitions/request.DataSource" + } + ] + }, + "dataTypeLong": { + "description": "数据库字段长度", + "type": "string" + }, + "defaultValue": { + "description": "是否必填", + "type": "string" + }, + "desc": { + "description": "是否前端详情", + "type": "boolean" + }, + "dictType": { + "description": "字典", + "type": "string" + }, + "errorText": { + "description": "校验失败文字", + "type": "string" + }, + "excel": { + "description": "是否导入/导出", + "type": "boolean" + }, + "fieldDesc": { + "description": "中文名", + "type": "string" + }, + "fieldIndexType": { + "description": "索引类型", + "type": "string" + }, + "fieldJson": { + "description": "FieldJson", + "type": "string" + }, + "fieldName": { + "description": "Field名", + "type": "string" + }, + "fieldSearchHide": { + "description": "是否隐藏查询条件", + "type": "boolean" + }, + "fieldSearchType": { + "description": "搜索条件", + "type": "string" + }, + "fieldType": { + "description": "Field数据类型", + "type": "string" + }, + "form": { + "description": "Front bool ` + "`" + `json:\"front\"` + "`" + ` // 是否前端可见", + "type": "boolean" + }, + "primaryKey": { + "description": "是否主键", + "type": "boolean" + }, + "require": { + "description": "是否必填", + "type": "boolean" + }, + "sort": { + "description": "是否增加排序", + "type": "boolean" + }, + "table": { + "description": "是否前端表格列", + "type": "boolean" + } + } + }, + "request.CasbinInReceive": { + "type": "object", + "properties": { + "authorityId": { + "description": "权限id", + "type": "integer" + }, + "casbinInfos": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "request.CasbinInfo": { + "type": "object", + "properties": { + "method": { + "description": "方法", + "type": "string" + }, + "path": { + "description": "路径", + "type": "string" + } + } + }, + "request.ChangePasswordReq": { + "type": "object", + "properties": { + "newPassword": { + "description": "新密码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + } + } + }, + "request.DataSource": { + "type": "object", + "properties": { + "association": { + "description": "关联关系 1 一对一 2 一对多", + "type": "integer" + }, + "dbName": { + "type": "string" + }, + "hasDeletedAt": { + "type": "boolean" + }, + "label": { + "type": "string" + }, + "table": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.ExaAttachmentCategorySearch": { + "type": "object", + "properties": { + "classId": { + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "nickName": { + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.InitDB": { + "type": "object", + "required": [ + "adminPassword", + "dbName" + ], + "properties": { + "adminPassword": { + "type": "string" + }, + "dbName": { + "description": "数据库名", + "type": "string" + }, + "dbPath": { + "description": "sqlite数据库文件路径", + "type": "string" + }, + "dbType": { + "description": "数据库类型", + "type": "string" + }, + "host": { + "description": "服务器地址", + "type": "string" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "port": { + "description": "数据库连接端口", + "type": "string" + }, + "template": { + "description": "postgresql指定template", + "type": "string" + }, + "userName": { + "description": "数据库用户名", + "type": "string" + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "captcha": { + "description": "验证码", + "type": "string" + }, + "captchaId": { + "description": "验证码ID", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.Register": { + "type": "object", + "properties": { + "authorityId": { + "type": "string", + "example": "int 角色id" + }, + "authorityIds": { + "type": "string", + "example": "[]uint 角色id" + }, + "email": { + "type": "string", + "example": "电子邮箱" + }, + "enable": { + "type": "string", + "example": "int 是否启用" + }, + "headerImg": { + "type": "string", + "example": "头像链接" + }, + "nickName": { + "type": "string", + "example": "昵称" + }, + "passWord": { + "type": "string", + "example": "密码" + }, + "phone": { + "type": "string", + "example": "电话号码" + }, + "userName": { + "type": "string", + "example": "用户名" + } + } + }, + "request.SearchApiParams": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "排序方式:升序false(默认)|降序true", + "type": "boolean" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "orderKey": { + "description": "排序", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "request.SetUserAuth": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.SetUserAuthorities": { + "type": "object", + "properties": { + "authorityIds": { + "description": "角色ID", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + } + } + }, + "request.SysAuthorityBtnReq": { + "type": "object", + "properties": { + "authorityId": { + "type": "integer" + }, + "menuID": { + "type": "integer" + }, + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.SysAutoCodePackageCreate": { + "type": "object", + "properties": { + "desc": { + "type": "string", + "example": "描述" + }, + "label": { + "type": "string", + "example": "展示名" + }, + "packageName": { + "type": "string", + "example": "包名" + }, + "template": { + "type": "string", + "example": "模版" + } + } + }, + "request.SysAutoHistoryRollBack": { + "type": "object", + "properties": { + "deleteApi": { + "description": "是否删除接口", + "type": "boolean" + }, + "deleteMenu": { + "description": "是否删除菜单", + "type": "boolean" + }, + "deleteTable": { + "description": "是否删除表", + "type": "boolean" + }, + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "response.Email": { + "type": "object", + "properties": { + "body": { + "description": "邮件内容", + "type": "string" + }, + "subject": { + "description": "邮件标题", + "type": "string" + }, + "to": { + "description": "邮件发送给谁", + "type": "string" + } + } + }, + "response.ExaCustomerResponse": { + "type": "object", + "properties": { + "customer": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + }, + "response.ExaFileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + }, + "response.FilePathResponse": { + "type": "object", + "properties": { + "filePath": { + "type": "string" + } + } + }, + "response.FileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFile" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.PolicyPathResponse": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "response.SysAPIListResponse": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysApi" + } + } + } + }, + "response.SysAPIResponse": { + "type": "object", + "properties": { + "api": { + "$ref": "#/definitions/system.SysApi" + } + } + }, + "response.SysAuthorityBtnRes": { + "type": "object", + "properties": { + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "response.SysAuthorityCopyResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "oldAuthorityId": { + "description": "旧角色ID", + "type": "integer" + } + } + }, + "response.SysAuthorityResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + } + } + }, + "response.SysBaseMenuResponse": { + "type": "object", + "properties": { + "menu": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + }, + "response.SysBaseMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "response.SysCaptchaResponse": { + "type": "object", + "properties": { + "captchaId": { + "type": "string" + }, + "captchaLength": { + "type": "integer" + }, + "openCaptcha": { + "type": "boolean" + }, + "picPath": { + "type": "string" + } + } + }, + "response.SysConfigResponse": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + }, + "response.SysMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + } + } + }, + "response.SysUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "system.Condition": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "column": { + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "from": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.JoinTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "joins": { + "type": "string" + }, + "on": { + "type": "string" + }, + "table": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.Meta": { + "type": "object", + "properties": { + "activeName": { + "type": "string" + }, + "closeTab": { + "description": "自动关闭tab", + "type": "boolean" + }, + "defaultMenu": { + "description": "是否是基础路由(开发中)", + "type": "boolean" + }, + "icon": { + "description": "菜单图标", + "type": "string" + }, + "keepAlive": { + "description": "是否缓存", + "type": "boolean" + }, + "title": { + "description": "菜单名", + "type": "string" + } + } + }, + "system.SysApi": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysAuthority": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "authorityName": { + "description": "角色名", + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dataAuthorityId": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "defaultRouter": { + "description": "默认菜单(默认dashboard)", + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "parentId": { + "description": "父角色ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuBtn": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuParameter": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "地址栏携带参数的key", + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "type": { + "description": "地址栏携带参数为params还是query", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "地址栏携带参数的值", + "type": "string" + } + } + }, + "system.SysDictionary": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "描述", + "type": "string" + }, + "name": { + "description": "字典名(中)", + "type": "string" + }, + "status": { + "description": "状态", + "type": "boolean" + }, + "sysDictionaryDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + }, + "type": { + "description": "字典名(英)", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysDictionaryDetail": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "extend": { + "description": "扩展值", + "type": "string" + }, + "label": { + "description": "展示值", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "status": { + "description": "启用状态", + "type": "boolean" + }, + "sysDictionaryID": { + "description": "关联标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "字典值", + "type": "string" + } + } + }, + "system.SysExportTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Condition" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dbName": { + "description": "数据库名称", + "type": "string" + }, + "joinTemplate": { + "type": "array", + "items": { + "$ref": "#/definitions/system.JoinTemplate" + } + }, + "limit": { + "type": "integer" + }, + "name": { + "description": "模板名称", + "type": "string" + }, + "order": { + "type": "string" + }, + "tableName": { + "description": "表名称", + "type": "string" + }, + "templateID": { + "description": "模板标识", + "type": "string" + }, + "templateInfo": { + "description": "模板信息", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "btns": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "menuId": { + "type": "integer" + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysOperationRecord": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "agent": { + "description": "代理", + "type": "string" + }, + "body": { + "description": "请求Body", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "error_message": { + "description": "错误信息", + "type": "string" + }, + "ip": { + "description": "请求ip", + "type": "string" + }, + "latency": { + "description": "延迟", + "type": "string" + }, + "method": { + "description": "请求方法", + "type": "string" + }, + "path": { + "description": "请求路径", + "type": "string" + }, + "resp": { + "description": "响应Body", + "type": "string" + }, + "status": { + "description": "请求状态", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + }, + "user_id": { + "description": "用户id", + "type": "integer" + } + } + }, + "system.SysParams": { + "type": "object", + "required": [ + "key", + "name", + "value" + ], + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "参数说明", + "type": "string" + }, + "key": { + "description": "参数键", + "type": "string" + }, + "name": { + "description": "参数名称", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "参数值", + "type": "string" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "description": "多用户角色", + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "description": "用户角色", + "allOf": [ + { + "$ref": "#/definitions/system.SysAuthority" + } + ] + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "originSetting": { + "description": "配置", + "allOf": [ + { + "$ref": "#/definitions/common.JSONMap" + } + ] + }, + "phone": { + "description": "用户手机号", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userName": { + "description": "用户登录名", + "type": "string" + }, + "uuid": { + "description": "用户UUID", + "type": "string" + } + } + }, + "system.System": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-token", + "in": "header" + } + }, + "tags": [ + { + "name": "Base" + }, + { + "description": "用户", + "name": "SysUser" + } + ] +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: global.Version, + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Gin-Vue-Admin Swagger API接口文档", + Description: "使用gin+vue进行极速开发的全栈开发基础平台", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/server/docs/swagger.json b/server/docs/swagger.json new file mode 100644 index 0000000..9eb3da4 --- /dev/null +++ b/server/docs/swagger.json @@ -0,0 +1,9286 @@ +{ + "swagger": "2.0", + "info": { + "description": "使用gin+vue进行极速开发的全栈开发基础平台", + "title": "Gin-Vue-Admin Swagger API接口文档", + "contact": {}, + "version": "v2.7.9-beta" + }, + "paths": { + "/api/createApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "创建基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "创建基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "删除api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/deleteApisByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "删除选中Api", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "删除选中Api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/enterSyncApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "确认同步API", + "responses": { + "200": { + "description": "确认同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/freshCasbin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "刷新casbin缓存", + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getAllApis": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取所有的Api 不分页", + "responses": { + "200": { + "description": "获取所有的Api 不分页,返回包括api列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIListResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "根据id获取api", + "parameters": [ + { + "description": "根据id获取api", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取api,返回包括api详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAPIResponse" + } + } + } + ] + } + } + } + } + }, + "/api/getApiGroups": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "获取API分组", + "responses": { + "200": { + "description": "获取API分组", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/getApiList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "分页获取API列表", + "parameters": [ + { + "description": "分页获取API列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SearchApiParams" + } + } + ], + "responses": { + "200": { + "description": "分页获取API列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/ignoreApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "IgnoreApi" + ], + "summary": "忽略API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/syncApi": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "同步API", + "responses": { + "200": { + "description": "同步API", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/updateApi": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysApi" + ], + "summary": "修改基础api", + "parameters": [ + { + "description": "api路径, api中文描述, api组, 方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysApi" + } + } + ], + "responses": { + "200": { + "description": "修改基础api", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/addCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddCategory" + ], + "summary": "添加媒体库分类", + "parameters": [ + { + "description": "媒体库分类数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + } + ], + "responses": {} + } + }, + "/attachmentCategory/deleteCategory": { + "post": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DeleteCategory" + ], + "summary": "删除分类", + "parameters": [ + { + "description": "分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除分类", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/attachmentCategory/getCategoryList": { + "get": { + "security": [ + { + "AttachmentCategory": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "GetCategoryList" + ], + "summary": "媒体库分类列表", + "responses": { + "200": { + "description": "媒体库分类列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/copyAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "拷贝角色", + "parameters": [ + { + "description": "旧角色id, 新权限id, 新权限名, 新父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.SysAuthorityCopyResponse" + } + } + ], + "responses": { + "200": { + "description": "拷贝角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/createAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "创建角色,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/deleteAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "删除角色", + "parameters": [ + { + "description": "删除角色", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "删除角色", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/getAuthorityList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "分页获取角色列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取角色列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/setDataAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "设置角色资源权限", + "parameters": [ + { + "description": "设置角色资源权限", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "设置角色资源权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authority/updateAuthority": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authority" + ], + "summary": "更新角色信息", + "parameters": [ + { + "description": "权限id, 权限名, 父角色id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysAuthority" + } + } + ], + "responses": { + "200": { + "description": "更新角色信息,返回包括系统角色详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/canRemoveAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/getAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "获取权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysAuthorityBtnRes" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/authorityBtn/setAuthorityBtn": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityBtn" + ], + "summary": "设置权限按钮", + "parameters": [ + { + "description": "菜单id, 角色id, 选中的按钮id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAuthorityBtnReq" + } + } + ], + "responses": { + "200": { + "description": "返回列表成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/addFunc": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AddFunc" + ], + "summary": "增加方法", + "parameters": [ + { + "description": "增加方法", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/createPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "创建package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoCodePackageCreate" + } + } + ], + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/createTemp": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "自动代码模板", + "parameters": [ + { + "description": "创建自动代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/autoCode/delPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除package", + "parameters": [ + { + "description": "创建package", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/delSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "删除回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除回滚记录", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getColumn": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前表所有字段", + "responses": { + "200": { + "description": "获取当前表所有字段", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getDB": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前所有数据库", + "responses": { + "200": { + "description": "获取当前所有数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getMeta": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取meta信息", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "获取meta信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getPackage": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getSysHistory": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "查询回滚记录", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "查询回滚记录,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTables": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "获取当前数据库所有表", + "responses": { + "200": { + "description": "获取当前数据库所有表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/getTemplates": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePackage" + ], + "summary": "获取package", + "responses": { + "200": { + "description": "创建package成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initAPI": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/initMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/installPlugin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "plug", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "安装插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object" + } + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/preview": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodeTemplate" + ], + "summary": "预览创建后的代码", + "parameters": [ + { + "description": "预览创建代码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AutoCode" + } + } + ], + "responses": { + "200": { + "description": "预览创建后的代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/pubPlug": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCodePlugin" + ], + "summary": "打包插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "plugName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "打包插件成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/autoCode/rollback": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AutoCode" + ], + "summary": "回滚自动生成代码", + "parameters": [ + { + "description": "请求参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SysAutoHistoryRollBack" + } + } + ], + "responses": { + "200": { + "description": "回滚自动生成代码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/captcha": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysCaptchaResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/base/login": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Base" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "用户名, 密码, 验证码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Login" + } + } + ], + "responses": { + "200": { + "description": "返回包括用户信息,token,过期时间", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.LoginResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/UpdateCasbin": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "更新角色api权限", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "更新角色api权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/casbin/getPolicyPathByAuthorityId": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Casbin" + ], + "summary": "获取权限列表", + "parameters": [ + { + "description": "权限id, 权限模型列表", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CasbinInReceive" + } + } + ], + "responses": { + "200": { + "description": "获取权限列表,返回包括casbin详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PolicyPathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customer": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "获取单一客户信息", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "客户名", + "name": "customerName", + "in": "query" + }, + { + "type": "string", + "description": "客户手机号", + "name": "customerPhoneData", + "in": "query" + }, + { + "type": "integer", + "description": "管理角色ID", + "name": "sysUserAuthorityID", + "in": "query" + }, + { + "type": "integer", + "description": "管理ID", + "name": "sysUserId", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取单一客户信息,返回包括客户详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaCustomerResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "更新客户信息", + "parameters": [ + { + "description": "客户ID, 客户信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "更新客户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "创建客户", + "parameters": [ + { + "description": "客户用户名, 客户手机号码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "创建客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "删除客户", + "parameters": [ + { + "description": "客户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + ], + "responses": { + "200": { + "description": "删除客户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/customer/customerList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaCustomer" + ], + "summary": "分页获取权限客户列表", + "parameters": [ + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取权限客户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/email/emailTest": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送测试邮件", + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/email/sendEmail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "发送邮件", + "parameters": [ + { + "description": "发送邮件必须的参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/response.Email" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"发送成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/fileUploadAndDownload/breakpointContinue": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "断点续传到服务器", + "parameters": [ + { + "type": "file", + "description": "an example for breakpoint resume, 断点续传示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "断点续传到服务器", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/deleteFile": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除文件", + "parameters": [ + { + "description": "传入文件里面id即可", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "删除文件", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/findFile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "查找文件", + "parameters": [ + { + "type": "file", + "description": "Find the file, 查找文件", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "查找文件,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "创建文件", + "parameters": [ + { + "type": "file", + "description": "上传文件完成", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "创建文件,返回包括文件路径", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.FilePathResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/getFileList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "分页文件列表", + "parameters": [ + { + "description": "页码, 每页大小, 分类id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ExaAttachmentCategorySearch" + } + } + ], + "responses": { + "200": { + "description": "分页文件列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/importURL": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "导入URL", + "parameters": [ + { + "description": "对象", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + ], + "responses": { + "200": { + "description": "导入URL", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/removeChunk": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "删除切片", + "parameters": [ + { + "type": "file", + "description": "删除缓存切片", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "删除切片", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/fileUploadAndDownload/upload": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ExaFileUploadAndDownload" + ], + "summary": "上传文件示例", + "parameters": [ + { + "type": "file", + "description": "上传文件示例", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传文件示例,返回包括文件详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.ExaFileResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/createInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "创建公告", + "parameters": [ + { + "description": "创建公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfo": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "删除公告", + "parameters": [ + { + "description": "删除公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/deleteInfoByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "批量删除公告", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/findInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "用id查询公告", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "内容", + "name": "content", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "标题", + "name": "title", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "作者", + "name": "userID", + "in": "query" + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/model.Info" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoDataSource": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "获取Info的数据源", + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "分页获取公告列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/getInfoPublic": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "不需要鉴权的公告接口", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/info/updateInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Info" + ], + "summary": "更新公告", + "parameters": [ + { + "description": "更新公告", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Info" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/checkdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "CheckDB" + ], + "summary": "初始化用户数据库", + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/init/initdb": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "InitDB" + ], + "summary": "初始化用户数据库", + "parameters": [ + { + "description": "初始化数据库参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.InitDB" + } + } + ], + "responses": { + "200": { + "description": "初始化用户数据库", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/jwt/jsonInBlacklist": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Jwt" + ], + "summary": "jwt加入黑名单", + "responses": { + "200": { + "description": "jwt加入黑名单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "新增菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "新增菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/addMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "增加menu和角色关联关系", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AddMenuAuthorityInfo" + } + } + ], + "responses": { + "200": { + "description": "增加menu和角色关联关系", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/deleteBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "删除菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuById": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "根据id获取菜单", + "parameters": [ + { + "description": "菜单id", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "根据id获取菜单,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenuResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getBaseMenuTree": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysBaseMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取用户动态路由", + "parameters": [ + { + "description": "空", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Empty" + } + } + ], + "responses": { + "200": { + "description": "获取用户动态路由,返回包括系统菜单详情列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysMenusResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AuthorityMenu" + ], + "summary": "获取指定角色menu", + "parameters": [ + { + "description": "角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetAuthorityId" + } + } + ], + "responses": { + "200": { + "description": "获取指定角色menu", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/getMenuList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "分页获取基础menu列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "分页获取基础menu列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/menu/updateBaseMenu": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Menu" + ], + "summary": "更新菜单", + "parameters": [ + { + "description": "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + ], + "responses": { + "200": { + "description": "更新菜单", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/createSysDictionary": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "创建SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/deleteSysDictionary": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "删除SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/findSysDictionary": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "用id查询SysDictionary", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "描述", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "字典名(中)", + "name": "name", + "in": "query" + }, + { + "type": "boolean", + "description": "状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "字典名(英)", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/getSysDictionaryList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "分页获取SysDictionary列表", + "responses": { + "200": { + "description": "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionary/updateSysDictionary": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionary" + ], + "summary": "更新SysDictionary", + "parameters": [ + { + "description": "SysDictionary模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionary" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionary", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/createSysDictionaryDetail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "创建SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "创建SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/deleteSysDictionaryDetail": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "删除SysDictionaryDetail", + "parameters": [ + { + "description": "SysDictionaryDetail模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "删除SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/findSysDictionaryDetail": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "用id查询SysDictionaryDetail", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/getSysDictionaryDetailList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "分页获取SysDictionaryDetail列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "扩展值", + "name": "extend", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "展示值", + "name": "label", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "排序标记", + "name": "sort", + "in": "query" + }, + { + "type": "boolean", + "description": "启用状态", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "关联标记", + "name": "sysDictionaryID", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "字典值", + "name": "value", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysDictionaryDetail/updateSysDictionaryDetail": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysDictionaryDetail" + ], + "summary": "更新SysDictionaryDetail", + "parameters": [ + { + "description": "更新SysDictionaryDetail", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + } + ], + "responses": { + "200": { + "description": "更新SysDictionaryDetail", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysExportTemplate/ExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格模板", + "responses": {} + } + }, + "/sysExportTemplate/createSysExportTemplate": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "创建导出模板", + "parameters": [ + { + "description": "创建导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"创建成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplate": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "删除导出模板", + "parameters": [ + { + "description": "删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/deleteSysExportTemplateByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "批量删除导出模板", + "parameters": [ + { + "description": "批量删除导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"批量删除成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/exportExcel": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "导出表格", + "responses": {} + } + }, + "/sysExportTemplate/findSysExportTemplate": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "用id查询导出模板", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"查询成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/getSysExportTemplateList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "分页获取导出模板列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "数据库名称", + "name": "dbName", + "in": "query" + }, + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "模板名称", + "name": "name", + "in": "query" + }, + { + "type": "string", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + }, + { + "type": "string", + "description": "表名称", + "name": "tableName", + "in": "query" + }, + { + "type": "string", + "description": "模板标识", + "name": "templateID", + "in": "query" + }, + { + "type": "string", + "description": "模板信息", + "name": "templateInfo", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"获取成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysExportTemplate/importExcel": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysImportTemplate" + ], + "summary": "导入表格", + "responses": {} + } + }, + "/sysExportTemplate/updateSysExportTemplate": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysExportTemplate" + ], + "summary": "更新导出模板", + "parameters": [ + { + "description": "更新导出模板", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysExportTemplate" + } + } + ], + "responses": { + "200": { + "description": "{\"success\":true,\"data\":{},\"msg\":\"更新成功\"}", + "schema": { + "type": "string" + } + } + } + } + }, + "/sysOperationRecord/createSysOperationRecord": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "创建SysOperationRecord", + "parameters": [ + { + "description": "创建SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "创建SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecord": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "删除SysOperationRecord", + "parameters": [ + { + "description": "SysOperationRecord模型", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysOperationRecord" + } + } + ], + "responses": { + "200": { + "description": "删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/deleteSysOperationRecordByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "批量删除SysOperationRecord", + "parameters": [ + { + "description": "批量删除SysOperationRecord", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.IdsReq" + } + } + ], + "responses": { + "200": { + "description": "批量删除SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/findSysOperationRecord": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "用id查询SysOperationRecord", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用id查询SysOperationRecord", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysOperationRecord/getSysOperationRecordList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysOperationRecord" + ], + "summary": "分页获取SysOperationRecord列表", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "代理", + "name": "agent", + "in": "query" + }, + { + "type": "string", + "description": "请求Body", + "name": "body", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "错误信息", + "name": "error_message", + "in": "query" + }, + { + "type": "string", + "description": "请求ip", + "name": "ip", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "延迟", + "name": "latency", + "in": "query" + }, + { + "type": "string", + "description": "请求方法", + "name": "method", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "请求路径", + "name": "path", + "in": "query" + }, + { + "type": "string", + "description": "响应Body", + "name": "resp", + "in": "query" + }, + { + "type": "integer", + "description": "请求状态", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "integer", + "description": "用户id", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/createSysParams": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "创建参数", + "parameters": [ + { + "description": "创建参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParams": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "删除参数", + "parameters": [ + { + "description": "删除参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/deleteSysParamsByIds": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "批量删除参数", + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/findSysParams": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "用id查询参数", + "parameters": [ + { + "type": "integer", + "description": "主键ID", + "name": "ID", + "in": "query" + }, + { + "type": "string", + "description": "创建时间", + "name": "createdAt", + "in": "query" + }, + { + "type": "string", + "description": "参数说明", + "name": "desc", + "in": "query" + }, + { + "type": "string", + "description": "参数键", + "name": "key", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "参数名称", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "更新时间", + "name": "updatedAt", + "in": "query" + }, + { + "type": "string", + "description": "参数值", + "name": "value", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "查询成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParam": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "根据key获取参数value", + "parameters": [ + { + "type": "string", + "description": "key", + "name": "key", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/system.SysParams" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/getSysParamsList": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "分页获取参数列表", + "parameters": [ + { + "type": "string", + "name": "endCreatedAt", + "in": "query" + }, + { + "type": "string", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "name": "startCreatedAt", + "in": "query" + } + ], + "responses": { + "200": { + "description": "获取成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/sysParams/updateSysParams": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysParams" + ], + "summary": "更新参数", + "parameters": [ + { + "description": "更新参数", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysParams" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getServerInfo": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取服务器信息", + "responses": { + "200": { + "description": "获取服务器信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/getSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "获取配置文件内容", + "responses": { + "200": { + "description": "获取配置文件内容,返回包括系统配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysConfigResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/reloadSystem": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "重启系统", + "responses": { + "200": { + "description": "重启系统", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/system/setSystemConfig": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "System" + ], + "summary": "设置配置文件内容", + "parameters": [ + { + "description": "设置配置文件内容", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.System" + } + } + ], + "responses": { + "200": { + "description": "设置配置文件内容", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/SetSelfSetting": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户配置", + "parameters": [ + { + "description": "用户配置数据", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "设置用户配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/admin_register": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户注册账号", + "parameters": [ + { + "description": "用户名, 昵称, 密码, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.Register" + } + } + ], + "responses": { + "200": { + "description": "用户注册账号,返回包括用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.SysUserResponse" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "用户修改密码", + "parameters": [ + { + "description": "用户名, 原密码, 新密码", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.ChangePasswordReq" + } + } + ], + "responses": { + "200": { + "description": "用户修改密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/deleteUser": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "删除用户", + "parameters": [ + { + "description": "用户ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetById" + } + } + ], + "responses": { + "200": { + "description": "删除用户", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserInfo": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "获取用户信息", + "responses": { + "200": { + "description": "获取用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/getUserList": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "分页获取用户列表", + "parameters": [ + { + "description": "页码, 每页大小", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.GetUserList" + } + } + ], + "responses": { + "200": { + "description": "分页获取用户列表,返回包括列表,总数,页码,每页数量", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/response.PageResult" + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/resetPassword": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "重置用户密码", + "parameters": [ + { + "description": "ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "重置用户密码", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthorities": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuthorities" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserAuthority": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "更改用户权限", + "parameters": [ + { + "description": "用户UUID, 角色ID", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.SetUserAuth" + } + } + ], + "responses": { + "200": { + "description": "设置用户权限", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/user/setUserInfo": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SysUser" + ], + "summary": "设置用户信息", + "parameters": [ + { + "description": "ID, 用户名, 昵称, 头像链接", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/system.SysUser" + } + } + ], + "responses": { + "200": { + "description": "设置用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "msg": { + "type": "string" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "common.JSONMap": { + "type": "object", + "additionalProperties": true + }, + "config.AliyunOSS": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + } + }, + "config.Autocode": { + "type": "object", + "properties": { + "ai-path": { + "type": "string" + }, + "module": { + "type": "string" + }, + "root": { + "type": "string" + }, + "server": { + "type": "string" + }, + "web": { + "type": "string" + } + } + }, + "config.AwsS3": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "disable-ssl": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "s3-force-path-style": { + "type": "boolean" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.CORS": { + "type": "object", + "properties": { + "mode": { + "type": "string" + }, + "whitelist": { + "type": "array", + "items": { + "$ref": "#/definitions/config.CORSWhitelist" + } + } + } + }, + "config.CORSWhitelist": { + "type": "object", + "properties": { + "allow-credentials": { + "type": "boolean" + }, + "allow-headers": { + "type": "string" + }, + "allow-methods": { + "type": "string" + }, + "allow-origin": { + "type": "string" + }, + "expose-headers": { + "type": "string" + } + } + }, + "config.Captcha": { + "type": "object", + "properties": { + "img-height": { + "description": "验证码高度", + "type": "integer" + }, + "img-width": { + "description": "验证码宽度", + "type": "integer" + }, + "key-long": { + "description": "验证码长度", + "type": "integer" + }, + "open-captcha": { + "description": "防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码", + "type": "integer" + }, + "open-captcha-timeout": { + "description": "防爆破验证码超时时间,单位:s(秒)", + "type": "integer" + } + } + }, + "config.CloudflareR2": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "account-id": { + "type": "string" + }, + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-access-key": { + "type": "string" + } + } + }, + "config.DiskList": { + "type": "object", + "properties": { + "mount-point": { + "type": "string" + } + } + }, + "config.Excel": { + "type": "object", + "properties": { + "dir": { + "type": "string" + } + } + }, + "config.HuaWeiObs": { + "type": "object", + "properties": { + "access-key": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "path": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.JWT": { + "type": "object", + "properties": { + "buffer-time": { + "description": "缓冲时间", + "type": "string" + }, + "expires-time": { + "description": "过期时间", + "type": "string" + }, + "issuer": { + "description": "签发者", + "type": "string" + }, + "signing-key": { + "description": "jwt签名", + "type": "string" + } + } + }, + "config.Local": { + "type": "object", + "properties": { + "path": { + "description": "本地文件访问路径", + "type": "string" + }, + "store-path": { + "description": "本地文件存储路径", + "type": "string" + } + } + }, + "config.Minio": { + "type": "object", + "properties": { + "access-key-id": { + "type": "string" + }, + "access-key-secret": { + "type": "string" + }, + "base-path": { + "type": "string" + }, + "bucket-name": { + "type": "string" + }, + "bucket-url": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "use-ssl": { + "type": "boolean" + } + } + }, + "config.Mongo": { + "type": "object", + "properties": { + "auth-source": { + "description": "验证数据库", + "type": "string" + }, + "coll": { + "description": "collection name", + "type": "string" + }, + "connect-timeout-ms": { + "description": "连接超时时间", + "type": "integer" + }, + "database": { + "description": "database name", + "type": "string" + }, + "hosts": { + "description": "主机列表", + "type": "array", + "items": { + "$ref": "#/definitions/config.MongoHost" + } + }, + "is-zap": { + "description": "是否开启zap日志", + "type": "boolean" + }, + "max-pool-size": { + "description": "最大连接池", + "type": "integer" + }, + "min-pool-size": { + "description": "最小连接池", + "type": "integer" + }, + "options": { + "description": "mongodb options", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "socket-timeout-ms": { + "description": "socket超时时间", + "type": "integer" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "config.MongoHost": { + "type": "object", + "properties": { + "host": { + "description": "ip地址", + "type": "string" + }, + "port": { + "description": "端口", + "type": "string" + } + } + }, + "config.Mssql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Mysql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Oracle": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Pgsql": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Qiniu": { + "type": "object", + "properties": { + "access-key": { + "description": "秘钥AK", + "type": "string" + }, + "bucket": { + "description": "空间名称", + "type": "string" + }, + "img-path": { + "description": "CDN加速域名", + "type": "string" + }, + "secret-key": { + "description": "秘钥SK", + "type": "string" + }, + "use-cdn-domains": { + "description": "上传是否使用CDN上传加速", + "type": "boolean" + }, + "use-https": { + "description": "是否使用https", + "type": "boolean" + }, + "zone": { + "description": "存储区域", + "type": "string" + } + } + }, + "config.Redis": { + "type": "object", + "properties": { + "addr": { + "description": "服务器地址:端口", + "type": "string" + }, + "clusterAddrs": { + "description": "集群模式下的节点地址列表", + "type": "array", + "items": { + "type": "string" + } + }, + "db": { + "description": "单实例模式下redis的哪个数据库", + "type": "integer" + }, + "name": { + "description": "代表当前实例的名字", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "useCluster": { + "description": "是否使用集群模式", + "type": "boolean" + } + } + }, + "config.Server": { + "type": "object", + "properties": { + "aliyun-oss": { + "$ref": "#/definitions/config.AliyunOSS" + }, + "autocode": { + "description": "auto", + "allOf": [ + { + "$ref": "#/definitions/config.Autocode" + } + ] + }, + "aws-s3": { + "$ref": "#/definitions/config.AwsS3" + }, + "captcha": { + "$ref": "#/definitions/config.Captcha" + }, + "cloudflare-r2": { + "$ref": "#/definitions/config.CloudflareR2" + }, + "cors": { + "description": "跨域配置", + "allOf": [ + { + "$ref": "#/definitions/config.CORS" + } + ] + }, + "db-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.SpecializedDB" + } + }, + "disk-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.DiskList" + } + }, + "email": { + "$ref": "#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email" + }, + "excel": { + "$ref": "#/definitions/config.Excel" + }, + "hua-wei-obs": { + "$ref": "#/definitions/config.HuaWeiObs" + }, + "jwt": { + "$ref": "#/definitions/config.JWT" + }, + "local": { + "description": "oss", + "allOf": [ + { + "$ref": "#/definitions/config.Local" + } + ] + }, + "minio": { + "$ref": "#/definitions/config.Minio" + }, + "mongo": { + "$ref": "#/definitions/config.Mongo" + }, + "mssql": { + "$ref": "#/definitions/config.Mssql" + }, + "mysql": { + "description": "gorm", + "allOf": [ + { + "$ref": "#/definitions/config.Mysql" + } + ] + }, + "oracle": { + "$ref": "#/definitions/config.Oracle" + }, + "pgsql": { + "$ref": "#/definitions/config.Pgsql" + }, + "qiniu": { + "$ref": "#/definitions/config.Qiniu" + }, + "redis": { + "$ref": "#/definitions/config.Redis" + }, + "redis-list": { + "type": "array", + "items": { + "$ref": "#/definitions/config.Redis" + } + }, + "sqlite": { + "$ref": "#/definitions/config.Sqlite" + }, + "system": { + "$ref": "#/definitions/config.System" + }, + "tencent-cos": { + "$ref": "#/definitions/config.TencentCOS" + }, + "zap": { + "$ref": "#/definitions/config.Zap" + } + } + }, + "config.SpecializedDB": { + "type": "object", + "properties": { + "alias-name": { + "type": "string" + }, + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "type": { + "type": "string" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.Sqlite": { + "type": "object", + "properties": { + "config": { + "description": "高级配置", + "type": "string" + }, + "db-name": { + "description": "数据库名", + "type": "string" + }, + "engine": { + "description": "数据库引擎,默认InnoDB", + "type": "string", + "default": "InnoDB" + }, + "log-mode": { + "description": "是否开启Gorm全局日志", + "type": "string" + }, + "log-zap": { + "description": "是否通过zap写入日志文件", + "type": "boolean" + }, + "max-idle-conns": { + "description": "空闲中的最大连接数", + "type": "integer" + }, + "max-open-conns": { + "description": "打开到数据库的最大连接数", + "type": "integer" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "path": { + "description": "数据库地址", + "type": "string" + }, + "port": { + "description": "数据库端口", + "type": "string" + }, + "prefix": { + "description": "数据库前缀", + "type": "string" + }, + "singular": { + "description": "是否开启全局禁用复数,true表示开启", + "type": "boolean" + }, + "username": { + "description": "数据库账号", + "type": "string" + } + } + }, + "config.System": { + "type": "object", + "properties": { + "addr": { + "description": "端口值", + "type": "integer" + }, + "db-type": { + "description": "数据库类型:mysql(默认)|sqlite|sqlserver|postgresql", + "type": "string" + }, + "iplimit-count": { + "type": "integer" + }, + "iplimit-time": { + "type": "integer" + }, + "oss-type": { + "description": "Oss类型", + "type": "string" + }, + "router-prefix": { + "type": "string" + }, + "use-mongo": { + "description": "使用mongo", + "type": "boolean" + }, + "use-multipoint": { + "description": "多点登录拦截", + "type": "boolean" + }, + "use-redis": { + "description": "使用redis", + "type": "boolean" + }, + "use-strict-auth": { + "description": "使用树形角色分配模式", + "type": "boolean" + } + } + }, + "config.TencentCOS": { + "type": "object", + "properties": { + "base-url": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "path-prefix": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secret-id": { + "type": "string" + }, + "secret-key": { + "type": "string" + } + } + }, + "config.Zap": { + "type": "object", + "properties": { + "director": { + "description": "日志文件夹", + "type": "string" + }, + "encode-level": { + "description": "编码级", + "type": "string" + }, + "format": { + "description": "输出", + "type": "string" + }, + "level": { + "description": "级别", + "type": "string" + }, + "log-in-console": { + "description": "输出控制台", + "type": "boolean" + }, + "prefix": { + "description": "日志前缀", + "type": "string" + }, + "retention-day": { + "description": "日志保留天数", + "type": "integer" + }, + "show-line": { + "description": "显示行", + "type": "boolean" + }, + "stacktrace-key": { + "description": "栈名", + "type": "string" + } + } + }, + "example.ExaAttachmentCategory": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaAttachmentCategory" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "name": { + "type": "string" + }, + "pid": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaCustomer": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "customerName": { + "description": "客户名", + "type": "string" + }, + "customerPhoneData": { + "description": "客户手机号", + "type": "string" + }, + "sysUser": { + "description": "管理详情", + "allOf": [ + { + "$ref": "#/definitions/system.SysUser" + } + ] + }, + "sysUserAuthorityID": { + "description": "管理角色ID", + "type": "integer" + }, + "sysUserId": { + "description": "管理ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFile": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "chunkTotal": { + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileChunk": { + "type": "array", + "items": { + "$ref": "#/definitions/example.ExaFileChunk" + } + }, + "fileMd5": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "isFinish": { + "type": "boolean" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileChunk": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "exaFileID": { + "type": "integer" + }, + "fileChunkNumber": { + "type": "integer" + }, + "fileChunkPath": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "example.ExaFileUploadAndDownload": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "classId": { + "description": "分类id", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "编号", + "type": "string" + }, + "name": { + "description": "文件名", + "type": "string" + }, + "tag": { + "description": "文件标签", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "url": { + "description": "文件地址", + "type": "string" + } + } + }, + "github_com_flipped-aurora_gin-vue-admin_server_config.Email": { + "type": "object", + "properties": { + "from": { + "description": "发件人 你自己要发邮件的邮箱", + "type": "string" + }, + "host": { + "description": "服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议", + "type": "string" + }, + "is-ssl": { + "description": "是否SSL 是否开启SSL", + "type": "boolean" + }, + "nickname": { + "description": "昵称 发件人昵称 通常为自己的邮箱", + "type": "string" + }, + "port": { + "description": "端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465", + "type": "integer" + }, + "secret": { + "description": "密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥", + "type": "string" + }, + "to": { + "description": "收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用", + "type": "string" + } + } + }, + "model.Info": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "attachments": { + "description": "附件", + "type": "array", + "items": { + "type": "object" + } + }, + "content": { + "description": "内容", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "title": { + "description": "标题", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userID": { + "description": "作者", + "type": "integer" + } + } + }, + "request.AddMenuAuthorityInfo": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "request.AutoCode": { + "type": "object", + "properties": { + "abbreviation": { + "description": "Struct简称", + "type": "string", + "example": "Struct简称" + }, + "autoCreateApiToSql": { + "description": "是否自动创建api", + "type": "boolean", + "example": false + }, + "autoCreateBtnAuth": { + "description": "是否自动创建按钮权限", + "type": "boolean", + "example": false + }, + "autoCreateMenuToSql": { + "description": "是否自动创建menu", + "type": "boolean", + "example": false + }, + "autoCreateResource": { + "description": "是否自动创建资源标识", + "type": "boolean", + "example": false + }, + "autoMigrate": { + "description": "是否自动迁移表结构", + "type": "boolean", + "example": false + }, + "businessDB": { + "description": "业务数据库", + "type": "string", + "example": "业务数据库" + }, + "description": { + "description": "Struct中文名称", + "type": "string", + "example": "Struct中文名称" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/request.AutoCodeField" + } + }, + "generateServer": { + "description": "是否生成server", + "type": "boolean", + "example": true + }, + "generateWeb": { + "description": "是否生成web", + "type": "boolean", + "example": true + }, + "gvaModel": { + "description": "是否使用gva默认Model", + "type": "boolean", + "example": false + }, + "humpPackageName": { + "description": "go文件名称", + "type": "string", + "example": "go文件名称" + }, + "isAdd": { + "description": "是否新增", + "type": "boolean", + "example": false + }, + "isTree": { + "description": "是否树形结构", + "type": "boolean", + "example": false + }, + "onlyTemplate": { + "description": "是否只生成模板", + "type": "boolean", + "example": false + }, + "package": { + "type": "string" + }, + "packageName": { + "description": "文件名称", + "type": "string", + "example": "文件名称" + }, + "primaryField": { + "$ref": "#/definitions/request.AutoCodeField" + }, + "structName": { + "description": "Struct名称", + "type": "string", + "example": "Struct名称" + }, + "tableName": { + "description": "表名", + "type": "string", + "example": "表名" + }, + "treeJson": { + "description": "展示的树json字段", + "type": "string", + "example": "展示的树json字段" + } + } + }, + "request.AutoCodeField": { + "type": "object", + "properties": { + "checkDataSource": { + "description": "是否检查数据源", + "type": "boolean" + }, + "clearable": { + "description": "是否可清空", + "type": "boolean" + }, + "columnName": { + "description": "数据库字段", + "type": "string" + }, + "comment": { + "description": "数据库字段描述", + "type": "string" + }, + "dataSource": { + "description": "数据源", + "allOf": [ + { + "$ref": "#/definitions/request.DataSource" + } + ] + }, + "dataTypeLong": { + "description": "数据库字段长度", + "type": "string" + }, + "defaultValue": { + "description": "是否必填", + "type": "string" + }, + "desc": { + "description": "是否前端详情", + "type": "boolean" + }, + "dictType": { + "description": "字典", + "type": "string" + }, + "errorText": { + "description": "校验失败文字", + "type": "string" + }, + "excel": { + "description": "是否导入/导出", + "type": "boolean" + }, + "fieldDesc": { + "description": "中文名", + "type": "string" + }, + "fieldIndexType": { + "description": "索引类型", + "type": "string" + }, + "fieldJson": { + "description": "FieldJson", + "type": "string" + }, + "fieldName": { + "description": "Field名", + "type": "string" + }, + "fieldSearchHide": { + "description": "是否隐藏查询条件", + "type": "boolean" + }, + "fieldSearchType": { + "description": "搜索条件", + "type": "string" + }, + "fieldType": { + "description": "Field数据类型", + "type": "string" + }, + "form": { + "description": "Front bool `json:\"front\"` // 是否前端可见", + "type": "boolean" + }, + "primaryKey": { + "description": "是否主键", + "type": "boolean" + }, + "require": { + "description": "是否必填", + "type": "boolean" + }, + "sort": { + "description": "是否增加排序", + "type": "boolean" + }, + "table": { + "description": "是否前端表格列", + "type": "boolean" + } + } + }, + "request.CasbinInReceive": { + "type": "object", + "properties": { + "authorityId": { + "description": "权限id", + "type": "integer" + }, + "casbinInfos": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "request.CasbinInfo": { + "type": "object", + "properties": { + "method": { + "description": "方法", + "type": "string" + }, + "path": { + "description": "路径", + "type": "string" + } + } + }, + "request.ChangePasswordReq": { + "type": "object", + "properties": { + "newPassword": { + "description": "新密码", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + } + } + }, + "request.DataSource": { + "type": "object", + "properties": { + "association": { + "description": "关联关系 1 一对一 2 一对多", + "type": "integer" + }, + "dbName": { + "type": "string" + }, + "hasDeletedAt": { + "type": "boolean" + }, + "label": { + "type": "string" + }, + "table": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "request.Empty": { + "type": "object" + }, + "request.ExaAttachmentCategorySearch": { + "type": "object", + "properties": { + "classId": { + "type": "integer" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.GetAuthorityId": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.GetById": { + "type": "object", + "properties": { + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "request.GetUserList": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "nickName": { + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "request.IdsReq": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.InitDB": { + "type": "object", + "required": [ + "adminPassword", + "dbName" + ], + "properties": { + "adminPassword": { + "type": "string" + }, + "dbName": { + "description": "数据库名", + "type": "string" + }, + "dbPath": { + "description": "sqlite数据库文件路径", + "type": "string" + }, + "dbType": { + "description": "数据库类型", + "type": "string" + }, + "host": { + "description": "服务器地址", + "type": "string" + }, + "password": { + "description": "数据库密码", + "type": "string" + }, + "port": { + "description": "数据库连接端口", + "type": "string" + }, + "template": { + "description": "postgresql指定template", + "type": "string" + }, + "userName": { + "description": "数据库用户名", + "type": "string" + } + } + }, + "request.Login": { + "type": "object", + "properties": { + "captcha": { + "description": "验证码", + "type": "string" + }, + "captchaId": { + "description": "验证码ID", + "type": "string" + }, + "password": { + "description": "密码", + "type": "string" + }, + "username": { + "description": "用户名", + "type": "string" + } + } + }, + "request.PageInfo": { + "type": "object", + "properties": { + "keyword": { + "description": "关键字", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + } + } + }, + "request.Register": { + "type": "object", + "properties": { + "authorityId": { + "type": "string", + "example": "int 角色id" + }, + "authorityIds": { + "type": "string", + "example": "[]uint 角色id" + }, + "email": { + "type": "string", + "example": "电子邮箱" + }, + "enable": { + "type": "string", + "example": "int 是否启用" + }, + "headerImg": { + "type": "string", + "example": "头像链接" + }, + "nickName": { + "type": "string", + "example": "昵称" + }, + "passWord": { + "type": "string", + "example": "密码" + }, + "phone": { + "type": "string", + "example": "电话号码" + }, + "userName": { + "type": "string", + "example": "用户名" + } + } + }, + "request.SearchApiParams": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "排序方式:升序false(默认)|降序true", + "type": "boolean" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "orderKey": { + "description": "排序", + "type": "string" + }, + "page": { + "description": "页码", + "type": "integer" + }, + "pageSize": { + "description": "每页大小", + "type": "integer" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "request.SetUserAuth": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + } + } + }, + "request.SetUserAuthorities": { + "type": "object", + "properties": { + "authorityIds": { + "description": "角色ID", + "type": "array", + "items": { + "type": "integer" + } + }, + "id": { + "type": "integer" + } + } + }, + "request.SysAuthorityBtnReq": { + "type": "object", + "properties": { + "authorityId": { + "type": "integer" + }, + "menuID": { + "type": "integer" + }, + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "request.SysAutoCodePackageCreate": { + "type": "object", + "properties": { + "desc": { + "type": "string", + "example": "描述" + }, + "label": { + "type": "string", + "example": "展示名" + }, + "packageName": { + "type": "string", + "example": "包名" + }, + "template": { + "type": "string", + "example": "模版" + } + } + }, + "request.SysAutoHistoryRollBack": { + "type": "object", + "properties": { + "deleteApi": { + "description": "是否删除接口", + "type": "boolean" + }, + "deleteMenu": { + "description": "是否删除菜单", + "type": "boolean" + }, + "deleteTable": { + "description": "是否删除表", + "type": "boolean" + }, + "id": { + "description": "主键ID", + "type": "integer" + } + } + }, + "response.Email": { + "type": "object", + "properties": { + "body": { + "description": "邮件内容", + "type": "string" + }, + "subject": { + "description": "邮件标题", + "type": "string" + }, + "to": { + "description": "邮件发送给谁", + "type": "string" + } + } + }, + "response.ExaCustomerResponse": { + "type": "object", + "properties": { + "customer": { + "$ref": "#/definitions/example.ExaCustomer" + } + } + }, + "response.ExaFileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFileUploadAndDownload" + } + } + }, + "response.FilePathResponse": { + "type": "object", + "properties": { + "filePath": { + "type": "string" + } + } + }, + "response.FileResponse": { + "type": "object", + "properties": { + "file": { + "$ref": "#/definitions/example.ExaFile" + } + } + }, + "response.LoginResponse": { + "type": "object", + "properties": { + "expiresAt": { + "type": "integer" + }, + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "response.PageResult": { + "type": "object", + "properties": { + "list": {}, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "response.PolicyPathResponse": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/request.CasbinInfo" + } + } + } + }, + "response.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "msg": { + "type": "string" + } + } + }, + "response.SysAPIListResponse": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysApi" + } + } + } + }, + "response.SysAPIResponse": { + "type": "object", + "properties": { + "api": { + "$ref": "#/definitions/system.SysApi" + } + } + }, + "response.SysAuthorityBtnRes": { + "type": "object", + "properties": { + "selected": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "response.SysAuthorityCopyResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + }, + "oldAuthorityId": { + "description": "旧角色ID", + "type": "integer" + } + } + }, + "response.SysAuthorityResponse": { + "type": "object", + "properties": { + "authority": { + "$ref": "#/definitions/system.SysAuthority" + } + } + }, + "response.SysBaseMenuResponse": { + "type": "object", + "properties": { + "menu": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + }, + "response.SysBaseMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + } + } + }, + "response.SysCaptchaResponse": { + "type": "object", + "properties": { + "captchaId": { + "type": "string" + }, + "captchaLength": { + "type": "integer" + }, + "openCaptcha": { + "type": "boolean" + }, + "picPath": { + "type": "string" + } + } + }, + "response.SysConfigResponse": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + }, + "response.SysMenusResponse": { + "type": "object", + "properties": { + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + } + } + }, + "response.SysUserResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/system.SysUser" + } + } + }, + "system.Condition": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "column": { + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "from": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.JoinTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "joins": { + "type": "string" + }, + "on": { + "type": "string" + }, + "table": { + "type": "string" + }, + "templateID": { + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.Meta": { + "type": "object", + "properties": { + "activeName": { + "type": "string" + }, + "closeTab": { + "description": "自动关闭tab", + "type": "boolean" + }, + "defaultMenu": { + "description": "是否是基础路由(开发中)", + "type": "boolean" + }, + "icon": { + "description": "菜单图标", + "type": "string" + }, + "keepAlive": { + "description": "是否缓存", + "type": "boolean" + }, + "title": { + "description": "菜单名", + "type": "string" + } + } + }, + "system.SysApi": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "apiGroup": { + "description": "api组", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "description": { + "description": "api中文描述", + "type": "string" + }, + "method": { + "description": "方法:创建POST(默认)|查看GET|更新PUT|删除DELETE", + "type": "string" + }, + "path": { + "description": "api路径", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysAuthority": { + "type": "object", + "properties": { + "authorityId": { + "description": "角色ID", + "type": "integer" + }, + "authorityName": { + "description": "角色名", + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dataAuthorityId": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "defaultRouter": { + "description": "默认菜单(默认dashboard)", + "type": "string" + }, + "deletedAt": { + "type": "string" + }, + "menus": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "parentId": { + "description": "父角色ID", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuBtn": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "type": "string" + }, + "name": { + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysBaseMenuParameter": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "key": { + "description": "地址栏携带参数的key", + "type": "string" + }, + "sysBaseMenuID": { + "type": "integer" + }, + "type": { + "description": "地址栏携带参数为params还是query", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "地址栏携带参数的值", + "type": "string" + } + } + }, + "system.SysDictionary": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "描述", + "type": "string" + }, + "name": { + "description": "字典名(中)", + "type": "string" + }, + "status": { + "description": "状态", + "type": "boolean" + }, + "sysDictionaryDetails": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysDictionaryDetail" + } + }, + "type": { + "description": "字典名(英)", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysDictionaryDetail": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "extend": { + "description": "扩展值", + "type": "string" + }, + "label": { + "description": "展示值", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "status": { + "description": "启用状态", + "type": "boolean" + }, + "sysDictionaryID": { + "description": "关联标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "字典值", + "type": "string" + } + } + }, + "system.SysExportTemplate": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/system.Condition" + } + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "dbName": { + "description": "数据库名称", + "type": "string" + }, + "joinTemplate": { + "type": "array", + "items": { + "$ref": "#/definitions/system.JoinTemplate" + } + }, + "limit": { + "type": "integer" + }, + "name": { + "description": "模板名称", + "type": "string" + }, + "order": { + "type": "string" + }, + "tableName": { + "description": "表名称", + "type": "string" + }, + "templateID": { + "description": "模板标识", + "type": "string" + }, + "templateInfo": { + "description": "模板信息", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysMenu": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authoritys": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "btns": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysMenu" + } + }, + "component": { + "description": "对应前端文件路径", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "hidden": { + "description": "是否在列表隐藏", + "type": "boolean" + }, + "menuBtn": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuBtn" + } + }, + "menuId": { + "type": "integer" + }, + "meta": { + "description": "附加属性", + "allOf": [ + { + "$ref": "#/definitions/system.Meta" + } + ] + }, + "name": { + "description": "路由name", + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/system.SysBaseMenuParameter" + } + }, + "parentId": { + "description": "父菜单ID", + "type": "integer" + }, + "path": { + "description": "路由path", + "type": "string" + }, + "sort": { + "description": "排序标记", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + } + } + }, + "system.SysOperationRecord": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "agent": { + "description": "代理", + "type": "string" + }, + "body": { + "description": "请求Body", + "type": "string" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "error_message": { + "description": "错误信息", + "type": "string" + }, + "ip": { + "description": "请求ip", + "type": "string" + }, + "latency": { + "description": "延迟", + "type": "string" + }, + "method": { + "description": "请求方法", + "type": "string" + }, + "path": { + "description": "请求路径", + "type": "string" + }, + "resp": { + "description": "响应Body", + "type": "string" + }, + "status": { + "description": "请求状态", + "type": "integer" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "user": { + "$ref": "#/definitions/system.SysUser" + }, + "user_id": { + "description": "用户id", + "type": "integer" + } + } + }, + "system.SysParams": { + "type": "object", + "required": [ + "key", + "name", + "value" + ], + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "desc": { + "description": "参数说明", + "type": "string" + }, + "key": { + "description": "参数键", + "type": "string" + }, + "name": { + "description": "参数名称", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "value": { + "description": "参数值", + "type": "string" + } + } + }, + "system.SysUser": { + "type": "object", + "properties": { + "ID": { + "description": "主键ID", + "type": "integer" + }, + "authorities": { + "description": "多用户角色", + "type": "array", + "items": { + "$ref": "#/definitions/system.SysAuthority" + } + }, + "authority": { + "description": "用户角色", + "allOf": [ + { + "$ref": "#/definitions/system.SysAuthority" + } + ] + }, + "authorityId": { + "description": "用户角色ID", + "type": "integer" + }, + "createdAt": { + "description": "创建时间", + "type": "string" + }, + "email": { + "description": "用户邮箱", + "type": "string" + }, + "enable": { + "description": "用户是否被冻结 1正常 2冻结", + "type": "integer" + }, + "headerImg": { + "description": "用户头像", + "type": "string" + }, + "nickName": { + "description": "用户昵称", + "type": "string" + }, + "originSetting": { + "description": "配置", + "allOf": [ + { + "$ref": "#/definitions/common.JSONMap" + } + ] + }, + "phone": { + "description": "用户手机号", + "type": "string" + }, + "updatedAt": { + "description": "更新时间", + "type": "string" + }, + "userName": { + "description": "用户登录名", + "type": "string" + }, + "uuid": { + "description": "用户UUID", + "type": "string" + } + } + }, + "system.System": { + "type": "object", + "properties": { + "config": { + "$ref": "#/definitions/config.Server" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "x-token", + "in": "header" + } + }, + "tags": [ + { + "name": "Base" + }, + { + "description": "用户", + "name": "SysUser" + } + ] +} \ No newline at end of file diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml new file mode 100644 index 0000000..1a1f8cc --- /dev/null +++ b/server/docs/swagger.yaml @@ -0,0 +1,5677 @@ +definitions: + common.JSONMap: + additionalProperties: true + type: object + config.AliyunOSS: + properties: + access-key-id: + type: string + access-key-secret: + type: string + base-path: + type: string + bucket-name: + type: string + bucket-url: + type: string + endpoint: + type: string + type: object + config.Autocode: + properties: + ai-path: + type: string + module: + type: string + root: + type: string + server: + type: string + web: + type: string + type: object + config.AwsS3: + properties: + base-url: + type: string + bucket: + type: string + disable-ssl: + type: boolean + endpoint: + type: string + path-prefix: + type: string + region: + type: string + s3-force-path-style: + type: boolean + secret-id: + type: string + secret-key: + type: string + type: object + config.CORS: + properties: + mode: + type: string + whitelist: + items: + $ref: '#/definitions/config.CORSWhitelist' + type: array + type: object + config.CORSWhitelist: + properties: + allow-credentials: + type: boolean + allow-headers: + type: string + allow-methods: + type: string + allow-origin: + type: string + expose-headers: + type: string + type: object + config.Captcha: + properties: + img-height: + description: 验证码高度 + type: integer + img-width: + description: 验证码宽度 + type: integer + key-long: + description: 验证码长度 + type: integer + open-captcha: + description: 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 + type: integer + open-captcha-timeout: + description: 防爆破验证码超时时间,单位:s(秒) + type: integer + type: object + config.CloudflareR2: + properties: + access-key-id: + type: string + account-id: + type: string + base-url: + type: string + bucket: + type: string + path: + type: string + secret-access-key: + type: string + type: object + config.DiskList: + properties: + mount-point: + type: string + type: object + config.Excel: + properties: + dir: + type: string + type: object + config.HuaWeiObs: + properties: + access-key: + type: string + bucket: + type: string + endpoint: + type: string + path: + type: string + secret-key: + type: string + type: object + config.JWT: + properties: + buffer-time: + description: 缓冲时间 + type: string + expires-time: + description: 过期时间 + type: string + issuer: + description: 签发者 + type: string + signing-key: + description: jwt签名 + type: string + type: object + config.Local: + properties: + path: + description: 本地文件访问路径 + type: string + store-path: + description: 本地文件存储路径 + type: string + type: object + config.Minio: + properties: + access-key-id: + type: string + access-key-secret: + type: string + base-path: + type: string + bucket-name: + type: string + bucket-url: + type: string + endpoint: + type: string + use-ssl: + type: boolean + type: object + config.Mongo: + properties: + auth-source: + description: 验证数据库 + type: string + coll: + description: collection name + type: string + connect-timeout-ms: + description: 连接超时时间 + type: integer + database: + description: database name + type: string + hosts: + description: 主机列表 + items: + $ref: '#/definitions/config.MongoHost' + type: array + is-zap: + description: 是否开启zap日志 + type: boolean + max-pool-size: + description: 最大连接池 + type: integer + min-pool-size: + description: 最小连接池 + type: integer + options: + description: mongodb options + type: string + password: + description: 密码 + type: string + socket-timeout-ms: + description: socket超时时间 + type: integer + username: + description: 用户名 + type: string + type: object + config.MongoHost: + properties: + host: + description: ip地址 + type: string + port: + description: 端口 + type: string + type: object + config.Mssql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Mysql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Oracle: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Pgsql: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.Qiniu: + properties: + access-key: + description: 秘钥AK + type: string + bucket: + description: 空间名称 + type: string + img-path: + description: CDN加速域名 + type: string + secret-key: + description: 秘钥SK + type: string + use-cdn-domains: + description: 上传是否使用CDN上传加速 + type: boolean + use-https: + description: 是否使用https + type: boolean + zone: + description: 存储区域 + type: string + type: object + config.Redis: + properties: + addr: + description: 服务器地址:端口 + type: string + clusterAddrs: + description: 集群模式下的节点地址列表 + items: + type: string + type: array + db: + description: 单实例模式下redis的哪个数据库 + type: integer + name: + description: 代表当前实例的名字 + type: string + password: + description: 密码 + type: string + useCluster: + description: 是否使用集群模式 + type: boolean + type: object + config.Server: + properties: + aliyun-oss: + $ref: '#/definitions/config.AliyunOSS' + autocode: + allOf: + - $ref: '#/definitions/config.Autocode' + description: auto + aws-s3: + $ref: '#/definitions/config.AwsS3' + captcha: + $ref: '#/definitions/config.Captcha' + cloudflare-r2: + $ref: '#/definitions/config.CloudflareR2' + cors: + allOf: + - $ref: '#/definitions/config.CORS' + description: 跨域配置 + db-list: + items: + $ref: '#/definitions/config.SpecializedDB' + type: array + disk-list: + items: + $ref: '#/definitions/config.DiskList' + type: array + email: + $ref: '#/definitions/github_com_flipped-aurora_gin-vue-admin_server_config.Email' + excel: + $ref: '#/definitions/config.Excel' + hua-wei-obs: + $ref: '#/definitions/config.HuaWeiObs' + jwt: + $ref: '#/definitions/config.JWT' + local: + allOf: + - $ref: '#/definitions/config.Local' + description: oss + minio: + $ref: '#/definitions/config.Minio' + mongo: + $ref: '#/definitions/config.Mongo' + mssql: + $ref: '#/definitions/config.Mssql' + mysql: + allOf: + - $ref: '#/definitions/config.Mysql' + description: gorm + oracle: + $ref: '#/definitions/config.Oracle' + pgsql: + $ref: '#/definitions/config.Pgsql' + qiniu: + $ref: '#/definitions/config.Qiniu' + redis: + $ref: '#/definitions/config.Redis' + redis-list: + items: + $ref: '#/definitions/config.Redis' + type: array + sqlite: + $ref: '#/definitions/config.Sqlite' + system: + $ref: '#/definitions/config.System' + tencent-cos: + $ref: '#/definitions/config.TencentCOS' + zap: + $ref: '#/definitions/config.Zap' + type: object + config.SpecializedDB: + properties: + alias-name: + type: string + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + disable: + type: boolean + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + type: + type: string + username: + description: 数据库账号 + type: string + type: object + config.Sqlite: + properties: + config: + description: 高级配置 + type: string + db-name: + description: 数据库名 + type: string + engine: + default: InnoDB + description: 数据库引擎,默认InnoDB + type: string + log-mode: + description: 是否开启Gorm全局日志 + type: string + log-zap: + description: 是否通过zap写入日志文件 + type: boolean + max-idle-conns: + description: 空闲中的最大连接数 + type: integer + max-open-conns: + description: 打开到数据库的最大连接数 + type: integer + password: + description: 数据库密码 + type: string + path: + description: 数据库地址 + type: string + port: + description: 数据库端口 + type: string + prefix: + description: 数据库前缀 + type: string + singular: + description: 是否开启全局禁用复数,true表示开启 + type: boolean + username: + description: 数据库账号 + type: string + type: object + config.System: + properties: + addr: + description: 端口值 + type: integer + db-type: + description: 数据库类型:mysql(默认)|sqlite|sqlserver|postgresql + type: string + iplimit-count: + type: integer + iplimit-time: + type: integer + oss-type: + description: Oss类型 + type: string + router-prefix: + type: string + use-mongo: + description: 使用mongo + type: boolean + use-multipoint: + description: 多点登录拦截 + type: boolean + use-redis: + description: 使用redis + type: boolean + use-strict-auth: + description: 使用树形角色分配模式 + type: boolean + type: object + config.TencentCOS: + properties: + base-url: + type: string + bucket: + type: string + path-prefix: + type: string + region: + type: string + secret-id: + type: string + secret-key: + type: string + type: object + config.Zap: + properties: + director: + description: 日志文件夹 + type: string + encode-level: + description: 编码级 + type: string + format: + description: 输出 + type: string + level: + description: 级别 + type: string + log-in-console: + description: 输出控制台 + type: boolean + prefix: + description: 日志前缀 + type: string + retention-day: + description: 日志保留天数 + type: integer + show-line: + description: 显示行 + type: boolean + stacktrace-key: + description: 栈名 + type: string + type: object + example.ExaAttachmentCategory: + properties: + ID: + description: 主键ID + type: integer + children: + items: + $ref: '#/definitions/example.ExaAttachmentCategory' + type: array + createdAt: + description: 创建时间 + type: string + name: + type: string + pid: + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaCustomer: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + customerName: + description: 客户名 + type: string + customerPhoneData: + description: 客户手机号 + type: string + sysUser: + allOf: + - $ref: '#/definitions/system.SysUser' + description: 管理详情 + sysUserAuthorityID: + description: 管理角色ID + type: integer + sysUserId: + description: 管理ID + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFile: + properties: + ID: + description: 主键ID + type: integer + chunkTotal: + type: integer + createdAt: + description: 创建时间 + type: string + exaFileChunk: + items: + $ref: '#/definitions/example.ExaFileChunk' + type: array + fileMd5: + type: string + fileName: + type: string + filePath: + type: string + isFinish: + type: boolean + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFileChunk: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + exaFileID: + type: integer + fileChunkNumber: + type: integer + fileChunkPath: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + example.ExaFileUploadAndDownload: + properties: + ID: + description: 主键ID + type: integer + classId: + description: 分类id + type: integer + createdAt: + description: 创建时间 + type: string + key: + description: 编号 + type: string + name: + description: 文件名 + type: string + tag: + description: 文件标签 + type: string + updatedAt: + description: 更新时间 + type: string + url: + description: 文件地址 + type: string + type: object + github_com_flipped-aurora_gin-vue-admin_server_config.Email: + properties: + from: + description: 发件人 你自己要发邮件的邮箱 + type: string + host: + description: 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + type: string + is-ssl: + description: 是否SSL 是否开启SSL + type: boolean + nickname: + description: 昵称 发件人昵称 通常为自己的邮箱 + type: string + port: + description: 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + type: integer + secret: + description: 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + type: string + to: + description: 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + type: string + type: object + model.Info: + properties: + ID: + description: 主键ID + type: integer + attachments: + description: 附件 + items: + type: object + type: array + content: + description: 内容 + type: string + createdAt: + description: 创建时间 + type: string + title: + description: 标题 + type: string + updatedAt: + description: 更新时间 + type: string + userID: + description: 作者 + type: integer + type: object + request.AddMenuAuthorityInfo: + properties: + authorityId: + description: 角色ID + type: integer + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + type: object + request.AutoCode: + properties: + abbreviation: + description: Struct简称 + example: Struct简称 + type: string + autoCreateApiToSql: + description: 是否自动创建api + example: false + type: boolean + autoCreateBtnAuth: + description: 是否自动创建按钮权限 + example: false + type: boolean + autoCreateMenuToSql: + description: 是否自动创建menu + example: false + type: boolean + autoCreateResource: + description: 是否自动创建资源标识 + example: false + type: boolean + autoMigrate: + description: 是否自动迁移表结构 + example: false + type: boolean + businessDB: + description: 业务数据库 + example: 业务数据库 + type: string + description: + description: Struct中文名称 + example: Struct中文名称 + type: string + fields: + items: + $ref: '#/definitions/request.AutoCodeField' + type: array + generateServer: + description: 是否生成server + example: true + type: boolean + generateWeb: + description: 是否生成web + example: true + type: boolean + gvaModel: + description: 是否使用gva默认Model + example: false + type: boolean + humpPackageName: + description: go文件名称 + example: go文件名称 + type: string + isAdd: + description: 是否新增 + example: false + type: boolean + isTree: + description: 是否树形结构 + example: false + type: boolean + onlyTemplate: + description: 是否只生成模板 + example: false + type: boolean + package: + type: string + packageName: + description: 文件名称 + example: 文件名称 + type: string + primaryField: + $ref: '#/definitions/request.AutoCodeField' + structName: + description: Struct名称 + example: Struct名称 + type: string + tableName: + description: 表名 + example: 表名 + type: string + treeJson: + description: 展示的树json字段 + example: 展示的树json字段 + type: string + type: object + request.AutoCodeField: + properties: + checkDataSource: + description: 是否检查数据源 + type: boolean + clearable: + description: 是否可清空 + type: boolean + columnName: + description: 数据库字段 + type: string + comment: + description: 数据库字段描述 + type: string + dataSource: + allOf: + - $ref: '#/definitions/request.DataSource' + description: 数据源 + dataTypeLong: + description: 数据库字段长度 + type: string + defaultValue: + description: 是否必填 + type: string + desc: + description: 是否前端详情 + type: boolean + dictType: + description: 字典 + type: string + errorText: + description: 校验失败文字 + type: string + excel: + description: 是否导入/导出 + type: boolean + fieldDesc: + description: 中文名 + type: string + fieldIndexType: + description: 索引类型 + type: string + fieldJson: + description: FieldJson + type: string + fieldName: + description: Field名 + type: string + fieldSearchHide: + description: 是否隐藏查询条件 + type: boolean + fieldSearchType: + description: 搜索条件 + type: string + fieldType: + description: Field数据类型 + type: string + form: + description: Front bool `json:"front"` // 是否前端可见 + type: boolean + primaryKey: + description: 是否主键 + type: boolean + require: + description: 是否必填 + type: boolean + sort: + description: 是否增加排序 + type: boolean + table: + description: 是否前端表格列 + type: boolean + type: object + request.CasbinInReceive: + properties: + authorityId: + description: 权限id + type: integer + casbinInfos: + items: + $ref: '#/definitions/request.CasbinInfo' + type: array + type: object + request.CasbinInfo: + properties: + method: + description: 方法 + type: string + path: + description: 路径 + type: string + type: object + request.ChangePasswordReq: + properties: + newPassword: + description: 新密码 + type: string + password: + description: 密码 + type: string + type: object + request.DataSource: + properties: + association: + description: 关联关系 1 一对一 2 一对多 + type: integer + dbName: + type: string + hasDeletedAt: + type: boolean + label: + type: string + table: + type: string + value: + type: string + type: object + request.Empty: + type: object + request.ExaAttachmentCategorySearch: + properties: + classId: + type: integer + keyword: + description: 关键字 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + type: object + request.GetAuthorityId: + properties: + authorityId: + description: 角色ID + type: integer + type: object + request.GetById: + properties: + id: + description: 主键ID + type: integer + type: object + request.GetUserList: + properties: + email: + type: string + keyword: + description: 关键字 + type: string + nickName: + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + phone: + type: string + username: + type: string + type: object + request.IdsReq: + properties: + ids: + items: + type: integer + type: array + type: object + request.InitDB: + properties: + adminPassword: + type: string + dbName: + description: 数据库名 + type: string + dbPath: + description: sqlite数据库文件路径 + type: string + dbType: + description: 数据库类型 + type: string + host: + description: 服务器地址 + type: string + password: + description: 数据库密码 + type: string + port: + description: 数据库连接端口 + type: string + template: + description: postgresql指定template + type: string + userName: + description: 数据库用户名 + type: string + required: + - adminPassword + - dbName + type: object + request.Login: + properties: + captcha: + description: 验证码 + type: string + captchaId: + description: 验证码ID + type: string + password: + description: 密码 + type: string + username: + description: 用户名 + type: string + type: object + request.PageInfo: + properties: + keyword: + description: 关键字 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + type: object + request.Register: + properties: + authorityId: + example: int 角色id + type: string + authorityIds: + example: '[]uint 角色id' + type: string + email: + example: 电子邮箱 + type: string + enable: + example: int 是否启用 + type: string + headerImg: + example: 头像链接 + type: string + nickName: + example: 昵称 + type: string + passWord: + example: 密码 + type: string + phone: + example: 电话号码 + type: string + userName: + example: 用户名 + type: string + type: object + request.SearchApiParams: + properties: + ID: + description: 主键ID + type: integer + apiGroup: + description: api组 + type: string + createdAt: + description: 创建时间 + type: string + desc: + description: 排序方式:升序false(默认)|降序true + type: boolean + description: + description: api中文描述 + type: string + keyword: + description: 关键字 + type: string + method: + description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + type: string + orderKey: + description: 排序 + type: string + page: + description: 页码 + type: integer + pageSize: + description: 每页大小 + type: integer + path: + description: api路径 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + request.SetUserAuth: + properties: + authorityId: + description: 角色ID + type: integer + type: object + request.SetUserAuthorities: + properties: + authorityIds: + description: 角色ID + items: + type: integer + type: array + id: + type: integer + type: object + request.SysAuthorityBtnReq: + properties: + authorityId: + type: integer + menuID: + type: integer + selected: + items: + type: integer + type: array + type: object + request.SysAutoCodePackageCreate: + properties: + desc: + example: 描述 + type: string + label: + example: 展示名 + type: string + packageName: + example: 包名 + type: string + template: + example: 模版 + type: string + type: object + request.SysAutoHistoryRollBack: + properties: + deleteApi: + description: 是否删除接口 + type: boolean + deleteMenu: + description: 是否删除菜单 + type: boolean + deleteTable: + description: 是否删除表 + type: boolean + id: + description: 主键ID + type: integer + type: object + response.Email: + properties: + body: + description: 邮件内容 + type: string + subject: + description: 邮件标题 + type: string + to: + description: 邮件发送给谁 + type: string + type: object + response.ExaCustomerResponse: + properties: + customer: + $ref: '#/definitions/example.ExaCustomer' + type: object + response.ExaFileResponse: + properties: + file: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + type: object + response.FilePathResponse: + properties: + filePath: + type: string + type: object + response.FileResponse: + properties: + file: + $ref: '#/definitions/example.ExaFile' + type: object + response.LoginResponse: + properties: + expiresAt: + type: integer + token: + type: string + user: + $ref: '#/definitions/system.SysUser' + type: object + response.PageResult: + properties: + list: {} + page: + type: integer + pageSize: + type: integer + total: + type: integer + type: object + response.PolicyPathResponse: + properties: + paths: + items: + $ref: '#/definitions/request.CasbinInfo' + type: array + type: object + response.Response: + properties: + code: + type: integer + data: {} + msg: + type: string + type: object + response.SysAPIListResponse: + properties: + apis: + items: + $ref: '#/definitions/system.SysApi' + type: array + type: object + response.SysAPIResponse: + properties: + api: + $ref: '#/definitions/system.SysApi' + type: object + response.SysAuthorityBtnRes: + properties: + selected: + items: + type: integer + type: array + type: object + response.SysAuthorityCopyResponse: + properties: + authority: + $ref: '#/definitions/system.SysAuthority' + oldAuthorityId: + description: 旧角色ID + type: integer + type: object + response.SysAuthorityResponse: + properties: + authority: + $ref: '#/definitions/system.SysAuthority' + type: object + response.SysBaseMenuResponse: + properties: + menu: + $ref: '#/definitions/system.SysBaseMenu' + type: object + response.SysBaseMenusResponse: + properties: + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + type: object + response.SysCaptchaResponse: + properties: + captchaId: + type: string + captchaLength: + type: integer + openCaptcha: + type: boolean + picPath: + type: string + type: object + response.SysConfigResponse: + properties: + config: + $ref: '#/definitions/config.Server' + type: object + response.SysMenusResponse: + properties: + menus: + items: + $ref: '#/definitions/system.SysMenu' + type: array + type: object + response.SysUserResponse: + properties: + user: + $ref: '#/definitions/system.SysUser' + type: object + system.Condition: + properties: + ID: + description: 主键ID + type: integer + column: + type: string + createdAt: + description: 创建时间 + type: string + from: + type: string + operator: + type: string + templateID: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.JoinTemplate: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + joins: + type: string + "on": + type: string + table: + type: string + templateID: + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.Meta: + properties: + activeName: + type: string + closeTab: + description: 自动关闭tab + type: boolean + defaultMenu: + description: 是否是基础路由(开发中) + type: boolean + icon: + description: 菜单图标 + type: string + keepAlive: + description: 是否缓存 + type: boolean + title: + description: 菜单名 + type: string + type: object + system.SysApi: + properties: + ID: + description: 主键ID + type: integer + apiGroup: + description: api组 + type: string + createdAt: + description: 创建时间 + type: string + description: + description: api中文描述 + type: string + method: + description: 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + type: string + path: + description: api路径 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysAuthority: + properties: + authorityId: + description: 角色ID + type: integer + authorityName: + description: 角色名 + type: string + children: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + createdAt: + description: 创建时间 + type: string + dataAuthorityId: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + defaultRouter: + description: 默认菜单(默认dashboard) + type: string + deletedAt: + type: string + menus: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + parentId: + description: 父角色ID + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenu: + properties: + ID: + description: 主键ID + type: integer + authoritys: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + children: + items: + $ref: '#/definitions/system.SysBaseMenu' + type: array + component: + description: 对应前端文件路径 + type: string + createdAt: + description: 创建时间 + type: string + hidden: + description: 是否在列表隐藏 + type: boolean + menuBtn: + items: + $ref: '#/definitions/system.SysBaseMenuBtn' + type: array + meta: + allOf: + - $ref: '#/definitions/system.Meta' + description: 附加属性 + name: + description: 路由name + type: string + parameters: + items: + $ref: '#/definitions/system.SysBaseMenuParameter' + type: array + parentId: + description: 父菜单ID + type: integer + path: + description: 路由path + type: string + sort: + description: 排序标记 + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenuBtn: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + type: string + name: + type: string + sysBaseMenuID: + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysBaseMenuParameter: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + key: + description: 地址栏携带参数的key + type: string + sysBaseMenuID: + type: integer + type: + description: 地址栏携带参数为params还是query + type: string + updatedAt: + description: 更新时间 + type: string + value: + description: 地址栏携带参数的值 + type: string + type: object + system.SysDictionary: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + description: 描述 + type: string + name: + description: 字典名(中) + type: string + status: + description: 状态 + type: boolean + sysDictionaryDetails: + items: + $ref: '#/definitions/system.SysDictionaryDetail' + type: array + type: + description: 字典名(英) + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysDictionaryDetail: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + extend: + description: 扩展值 + type: string + label: + description: 展示值 + type: string + sort: + description: 排序标记 + type: integer + status: + description: 启用状态 + type: boolean + sysDictionaryID: + description: 关联标记 + type: integer + updatedAt: + description: 更新时间 + type: string + value: + description: 字典值 + type: string + type: object + system.SysExportTemplate: + properties: + ID: + description: 主键ID + type: integer + conditions: + items: + $ref: '#/definitions/system.Condition' + type: array + createdAt: + description: 创建时间 + type: string + dbName: + description: 数据库名称 + type: string + joinTemplate: + items: + $ref: '#/definitions/system.JoinTemplate' + type: array + limit: + type: integer + name: + description: 模板名称 + type: string + order: + type: string + tableName: + description: 表名称 + type: string + templateID: + description: 模板标识 + type: string + templateInfo: + description: 模板信息 + type: string + updatedAt: + description: 更新时间 + type: string + type: object + system.SysMenu: + properties: + ID: + description: 主键ID + type: integer + authoritys: + items: + $ref: '#/definitions/system.SysAuthority' + type: array + btns: + additionalProperties: + type: integer + type: object + children: + items: + $ref: '#/definitions/system.SysMenu' + type: array + component: + description: 对应前端文件路径 + type: string + createdAt: + description: 创建时间 + type: string + hidden: + description: 是否在列表隐藏 + type: boolean + menuBtn: + items: + $ref: '#/definitions/system.SysBaseMenuBtn' + type: array + menuId: + type: integer + meta: + allOf: + - $ref: '#/definitions/system.Meta' + description: 附加属性 + name: + description: 路由name + type: string + parameters: + items: + $ref: '#/definitions/system.SysBaseMenuParameter' + type: array + parentId: + description: 父菜单ID + type: integer + path: + description: 路由path + type: string + sort: + description: 排序标记 + type: integer + updatedAt: + description: 更新时间 + type: string + type: object + system.SysOperationRecord: + properties: + ID: + description: 主键ID + type: integer + agent: + description: 代理 + type: string + body: + description: 请求Body + type: string + createdAt: + description: 创建时间 + type: string + error_message: + description: 错误信息 + type: string + ip: + description: 请求ip + type: string + latency: + description: 延迟 + type: string + method: + description: 请求方法 + type: string + path: + description: 请求路径 + type: string + resp: + description: 响应Body + type: string + status: + description: 请求状态 + type: integer + updatedAt: + description: 更新时间 + type: string + user: + $ref: '#/definitions/system.SysUser' + user_id: + description: 用户id + type: integer + type: object + system.SysParams: + properties: + ID: + description: 主键ID + type: integer + createdAt: + description: 创建时间 + type: string + desc: + description: 参数说明 + type: string + key: + description: 参数键 + type: string + name: + description: 参数名称 + type: string + updatedAt: + description: 更新时间 + type: string + value: + description: 参数值 + type: string + required: + - key + - name + - value + type: object + system.SysUser: + properties: + ID: + description: 主键ID + type: integer + authorities: + description: 多用户角色 + items: + $ref: '#/definitions/system.SysAuthority' + type: array + authority: + allOf: + - $ref: '#/definitions/system.SysAuthority' + description: 用户角色 + authorityId: + description: 用户角色ID + type: integer + createdAt: + description: 创建时间 + type: string + email: + description: 用户邮箱 + type: string + enable: + description: 用户是否被冻结 1正常 2冻结 + type: integer + headerImg: + description: 用户头像 + type: string + nickName: + description: 用户昵称 + type: string + originSetting: + allOf: + - $ref: '#/definitions/common.JSONMap' + description: 配置 + phone: + description: 用户手机号 + type: string + updatedAt: + description: 更新时间 + type: string + userName: + description: 用户登录名 + type: string + uuid: + description: 用户UUID + type: string + type: object + system.System: + properties: + config: + $ref: '#/definitions/config.Server' + type: object +info: + contact: {} + description: 使用gin+vue进行极速开发的全栈开发基础平台 + title: Gin-Vue-Admin Swagger API接口文档 + version: v2.7.9-beta +paths: + /api/createApi: + post: + consumes: + - application/json + parameters: + - description: api路径, api中文描述, api组, 方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 创建基础api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建基础api + tags: + - SysApi + /api/deleteApi: + post: + consumes: + - application/json + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 删除api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除api + tags: + - SysApi + /api/deleteApisByIds: + delete: + consumes: + - application/json + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 删除选中Api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除选中Api + tags: + - SysApi + /api/enterSyncApi: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 确认同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 确认同步API + tags: + - SysApi + /api/freshCasbin: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 刷新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + summary: 刷新casbin缓存 + tags: + - SysApi + /api/getAllApis: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取所有的Api 不分页,返回包括api列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAPIListResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取所有的Api 不分页 + tags: + - SysApi + /api/getApiById: + post: + consumes: + - application/json + parameters: + - description: 根据id获取api + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 根据id获取api,返回包括api详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAPIResponse' + type: object + security: + - ApiKeyAuth: [] + summary: 根据id获取api + tags: + - SysApi + /api/getApiGroups: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取API分组 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取API分组 + tags: + - SysApi + /api/getApiList: + post: + consumes: + - application/json + parameters: + - description: 分页获取API列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SearchApiParams' + produces: + - application/json + responses: + "200": + description: 分页获取API列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取API列表 + tags: + - SysApi + /api/ignoreApi: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 忽略API + tags: + - IgnoreApi + /api/syncApi: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 同步API + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 同步API + tags: + - SysApi + /api/updateApi: + post: + consumes: + - application/json + parameters: + - description: api路径, api中文描述, api组, 方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysApi' + produces: + - application/json + responses: + "200": + description: 修改基础api + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 修改基础api + tags: + - SysApi + /attachmentCategory/addCategory: + post: + consumes: + - application/json + parameters: + - description: 媒体库分类数据 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaAttachmentCategory' + produces: + - application/json + responses: {} + security: + - AttachmentCategory: [] + summary: 添加媒体库分类 + tags: + - AddCategory + /attachmentCategory/deleteCategory: + post: + consumes: + - application/json + parameters: + - description: 分类id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除分类 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - AttachmentCategory: [] + summary: 删除分类 + tags: + - DeleteCategory + /attachmentCategory/getCategoryList: + get: + produces: + - application/json + responses: + "200": + description: 媒体库分类列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/example.ExaAttachmentCategory' + msg: + type: string + type: object + security: + - AttachmentCategory: [] + summary: 媒体库分类列表 + tags: + - GetCategoryList + /authority/copyAuthority: + post: + consumes: + - application/json + parameters: + - description: 旧角色id, 新权限id, 新权限名, 新父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/response.SysAuthorityCopyResponse' + produces: + - application/json + responses: + "200": + description: 拷贝角色,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 拷贝角色 + tags: + - Authority + /authority/createAuthority: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限名, 父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 创建角色,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建角色 + tags: + - Authority + /authority/deleteAuthority: + post: + consumes: + - application/json + parameters: + - description: 删除角色 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 删除角色 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除角色 + tags: + - Authority + /authority/getAuthorityList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页获取角色列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取角色列表 + tags: + - Authority + /authority/setDataAuthority: + post: + consumes: + - application/json + parameters: + - description: 设置角色资源权限 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 设置角色资源权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置角色资源权限 + tags: + - Authority + /authority/updateAuthority: + put: + consumes: + - application/json + parameters: + - description: 权限id, 权限名, 父角色id + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysAuthority' + produces: + - application/json + responses: + "200": + description: 更新角色信息,返回包括系统角色详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新角色信息 + tags: + - Authority + /authorityBtn/canRemoveAuthorityBtn: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置权限按钮 + tags: + - AuthorityBtn + /authorityBtn/getAuthorityBtn: + post: + consumes: + - application/json + parameters: + - description: 菜单id, 角色id, 选中的按钮id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAuthorityBtnReq' + produces: + - application/json + responses: + "200": + description: 返回列表成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysAuthorityBtnRes' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取权限按钮 + tags: + - AuthorityBtn + /authorityBtn/setAuthorityBtn: + post: + consumes: + - application/json + parameters: + - description: 菜单id, 角色id, 选中的按钮id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAuthorityBtnReq' + produces: + - application/json + responses: + "200": + description: 返回列表成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置权限按钮 + tags: + - AuthorityBtn + /autoCode/addFunc: + post: + consumes: + - application/json + parameters: + - description: 增加方法 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 增加方法 + tags: + - AddFunc + /autoCode/createPackage: + post: + consumes: + - application/json + parameters: + - description: 创建package + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAutoCodePackageCreate' + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建package + tags: + - AutoCodePackage + /autoCode/createTemp: + post: + consumes: + - application/json + parameters: + - description: 创建自动代码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 自动代码模板 + tags: + - AutoCodeTemplate + /autoCode/delPackage: + post: + consumes: + - application/json + parameters: + - description: 创建package + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除package + tags: + - AutoCode + /autoCode/delSysHistory: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除回滚记录 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除回滚记录 + tags: + - AutoCode + /autoCode/getColumn: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前表所有字段 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前表所有字段 + tags: + - AutoCode + /autoCode/getDB: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前所有数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前所有数据库 + tags: + - AutoCode + /autoCode/getMeta: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 获取meta信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取meta信息 + tags: + - AutoCode + /autoCode/getPackage: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取package + tags: + - AutoCodePackage + /autoCode/getSysHistory: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 查询回滚记录,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 查询回滚记录 + tags: + - AutoCode + /autoCode/getTables: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取当前数据库所有表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取当前数据库所有表 + tags: + - AutoCode + /autoCode/getTemplates: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 创建package成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取package + tags: + - AutoCodePackage + /autoCode/initAPI: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 打包插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 打包插件 + tags: + - AutoCodePlugin + /autoCode/initMenu: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 打包插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 打包插件 + tags: + - AutoCodePlugin + /autoCode/installPlugin: + post: + consumes: + - multipart/form-data + parameters: + - description: this is a test file + in: formData + name: plug + required: true + type: file + produces: + - application/json + responses: + "200": + description: 安装插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + items: + type: object + type: array + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 安装插件 + tags: + - AutoCodePlugin + /autoCode/preview: + post: + consumes: + - application/json + parameters: + - description: 预览创建代码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AutoCode' + produces: + - application/json + responses: + "200": + description: 预览创建后的代码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 预览创建后的代码 + tags: + - AutoCodeTemplate + /autoCode/pubPlug: + post: + consumes: + - application/json + parameters: + - description: 插件名称 + in: query + name: plugName + required: true + type: string + produces: + - application/json + responses: + "200": + description: 打包插件成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 打包插件 + tags: + - AutoCodePlugin + /autoCode/rollback: + post: + consumes: + - application/json + parameters: + - description: 请求参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SysAutoHistoryRollBack' + produces: + - application/json + responses: + "200": + description: 回滚自动生成代码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 回滚自动生成代码 + tags: + - AutoCode + /base/captcha: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysCaptchaResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 生成验证码 + tags: + - Base + /base/login: + post: + parameters: + - description: 用户名, 密码, 验证码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Login' + produces: + - application/json + responses: + "200": + description: 返回包括用户信息,token,过期时间 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.LoginResponse' + msg: + type: string + type: object + summary: 用户登录 + tags: + - Base + /casbin/UpdateCasbin: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限模型列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CasbinInReceive' + produces: + - application/json + responses: + "200": + description: 更新角色api权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新角色api权限 + tags: + - Casbin + /casbin/getPolicyPathByAuthorityId: + post: + consumes: + - application/json + parameters: + - description: 权限id, 权限模型列表 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.CasbinInReceive' + produces: + - application/json + responses: + "200": + description: 获取权限列表,返回包括casbin详情列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PolicyPathResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取权限列表 + tags: + - Casbin + /customer/customer: + delete: + consumes: + - application/json + parameters: + - description: 客户ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 删除客户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除客户 + tags: + - ExaCustomer + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 客户名 + in: query + name: customerName + type: string + - description: 客户手机号 + in: query + name: customerPhoneData + type: string + - description: 管理角色ID + in: query + name: sysUserAuthorityID + type: integer + - description: 管理ID + in: query + name: sysUserId + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取单一客户信息,返回包括客户详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.ExaCustomerResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取单一客户信息 + tags: + - ExaCustomer + post: + consumes: + - application/json + parameters: + - description: 客户用户名, 客户手机号码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 创建客户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建客户 + tags: + - ExaCustomer + put: + consumes: + - application/json + parameters: + - description: 客户ID, 客户信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaCustomer' + produces: + - application/json + responses: + "200": + description: 更新客户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新客户信息 + tags: + - ExaCustomer + /customer/customerList: + get: + consumes: + - application/json + parameters: + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + produces: + - application/json + responses: + "200": + description: 分页获取权限客户列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取权限客户列表 + tags: + - ExaCustomer + /email/emailTest: + post: + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发送成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 发送测试邮件 + tags: + - System + /email/sendEmail: + post: + parameters: + - description: 发送邮件必须的参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/response.Email' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"发送成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 发送邮件 + tags: + - System + /fileUploadAndDownload/breakpointContinue: + post: + consumes: + - multipart/form-data + parameters: + - description: an example for breakpoint resume, 断点续传示例 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 断点续传到服务器 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 断点续传到服务器 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/deleteFile: + post: + parameters: + - description: 传入文件里面id即可 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + produces: + - application/json + responses: + "200": + description: 删除文件 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除文件 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/findFile: + get: + consumes: + - multipart/form-data + parameters: + - description: Find the file, 查找文件 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 查找文件,返回包括文件详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.FileResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 查找文件 + tags: + - ExaFileUploadAndDownload + post: + consumes: + - multipart/form-data + parameters: + - description: 上传文件完成 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 创建文件,返回包括文件路径 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.FilePathResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建文件 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/getFileList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小, 分类id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.ExaAttachmentCategorySearch' + produces: + - application/json + responses: + "200": + description: 分页文件列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页文件列表 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/importURL: + post: + parameters: + - description: 对象 + in: body + name: data + required: true + schema: + $ref: '#/definitions/example.ExaFileUploadAndDownload' + produces: + - application/json + responses: + "200": + description: 导入URL + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 导入URL + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/removeChunk: + post: + consumes: + - multipart/form-data + parameters: + - description: 删除缓存切片 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 删除切片 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除切片 + tags: + - ExaFileUploadAndDownload + /fileUploadAndDownload/upload: + post: + consumes: + - multipart/form-data + parameters: + - description: 上传文件示例 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传文件示例,返回包括文件详情 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.ExaFileResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 上传文件示例 + tags: + - ExaFileUploadAndDownload + /info/createInfo: + post: + consumes: + - application/json + parameters: + - description: 创建公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建公告 + tags: + - Info + /info/deleteInfo: + delete: + consumes: + - application/json + parameters: + - description: 删除公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除公告 + tags: + - Info + /info/deleteInfoByIds: + delete: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 批量删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 批量删除公告 + tags: + - Info + /info/findInfo: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 内容 + in: query + name: content + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 标题 + in: query + name: title + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 作者 + in: query + name: userID + type: integer + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/model.Info' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询公告 + tags: + - Info + /info/getInfoDataSource: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: object + msg: + type: string + type: object + summary: 获取Info的数据源 + tags: + - Info + /info/getInfoList: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取公告列表 + tags: + - Info + /info/getInfoPublic: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: object + msg: + type: string + type: object + summary: 不需要鉴权的公告接口 + tags: + - Info + /info/updateInfo: + put: + consumes: + - application/json + parameters: + - description: 更新公告 + in: body + name: data + required: true + schema: + $ref: '#/definitions/model.Info' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新公告 + tags: + - Info + /init/checkdb: + post: + produces: + - application/json + responses: + "200": + description: 初始化用户数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + summary: 初始化用户数据库 + tags: + - CheckDB + /init/initdb: + post: + parameters: + - description: 初始化数据库参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.InitDB' + produces: + - application/json + responses: + "200": + description: 初始化用户数据库 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + summary: 初始化用户数据库 + tags: + - InitDB + /jwt/jsonInBlacklist: + post: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: jwt加入黑名单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: jwt加入黑名单 + tags: + - Jwt + /menu/addBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysBaseMenu' + produces: + - application/json + responses: + "200": + description: 新增菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 新增菜单 + tags: + - Menu + /menu/addMenuAuthority: + post: + consumes: + - application/json + parameters: + - description: 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.AddMenuAuthorityInfo' + produces: + - application/json + responses: + "200": + description: 增加menu和角色关联关系 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 增加menu和角色关联关系 + tags: + - AuthorityMenu + /menu/deleteBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 菜单id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除菜单 + tags: + - Menu + /menu/getBaseMenuById: + post: + consumes: + - application/json + parameters: + - description: 菜单id + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 根据id获取菜单,返回包括系统菜单列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysBaseMenuResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 根据id获取菜单 + tags: + - Menu + /menu/getBaseMenuTree: + post: + parameters: + - description: 空 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Empty' + produces: + - application/json + responses: + "200": + description: 获取用户动态路由,返回包括系统菜单列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysBaseMenusResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户动态路由 + tags: + - AuthorityMenu + /menu/getMenu: + post: + parameters: + - description: 空 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Empty' + produces: + - application/json + responses: + "200": + description: 获取用户动态路由,返回包括系统菜单详情列表 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysMenusResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户动态路由 + tags: + - AuthorityMenu + /menu/getMenuAuthority: + post: + consumes: + - application/json + parameters: + - description: 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetAuthorityId' + produces: + - application/json + responses: + "200": + description: 获取指定角色menu + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取指定角色menu + tags: + - AuthorityMenu + /menu/getMenuList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.PageInfo' + produces: + - application/json + responses: + "200": + description: 分页获取基础menu列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取基础menu列表 + tags: + - Menu + /menu/updateBaseMenu: + post: + consumes: + - application/json + parameters: + - description: 路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysBaseMenu' + produces: + - application/json + responses: + "200": + description: 更新菜单 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新菜单 + tags: + - Menu + /sysDictionary/createSysDictionary: + post: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 创建SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysDictionary + tags: + - SysDictionary + /sysDictionary/deleteSysDictionary: + delete: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 删除SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysDictionary + tags: + - SysDictionary + /sysDictionary/findSysDictionary: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 描述 + in: query + name: desc + type: string + - description: 字典名(中) + in: query + name: name + type: string + - description: 状态 + in: query + name: status + type: boolean + - description: 字典名(英) + in: query + name: type + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: 用id查询SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysDictionary + tags: + - SysDictionary + /sysDictionary/getSysDictionaryList: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysDictionary列表 + tags: + - SysDictionary + /sysDictionary/updateSysDictionary: + put: + consumes: + - application/json + parameters: + - description: SysDictionary模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionary' + produces: + - application/json + responses: + "200": + description: 更新SysDictionary + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新SysDictionary + tags: + - SysDictionary + /sysDictionaryDetail/createSysDictionaryDetail: + post: + consumes: + - application/json + parameters: + - description: SysDictionaryDetail模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 创建SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/deleteSysDictionaryDetail: + delete: + consumes: + - application/json + parameters: + - description: SysDictionaryDetail模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 删除SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/findSysDictionaryDetail: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 扩展值 + in: query + name: extend + type: string + - description: 展示值 + in: query + name: label + type: string + - description: 排序标记 + in: query + name: sort + type: integer + - description: 启用状态 + in: query + name: status + type: boolean + - description: 关联标记 + in: query + name: sysDictionaryID + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 字典值 + in: query + name: value + type: string + produces: + - application/json + responses: + "200": + description: 用id查询SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysDictionaryDetail/getSysDictionaryDetailList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 扩展值 + in: query + name: extend + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 展示值 + in: query + name: label + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - description: 排序标记 + in: query + name: sort + type: integer + - description: 启用状态 + in: query + name: status + type: boolean + - description: 关联标记 + in: query + name: sysDictionaryID + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 字典值 + in: query + name: value + type: string + produces: + - application/json + responses: + "200": + description: 分页获取SysDictionaryDetail列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysDictionaryDetail列表 + tags: + - SysDictionaryDetail + /sysDictionaryDetail/updateSysDictionaryDetail: + put: + consumes: + - application/json + parameters: + - description: 更新SysDictionaryDetail + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysDictionaryDetail' + produces: + - application/json + responses: + "200": + description: 更新SysDictionaryDetail + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新SysDictionaryDetail + tags: + - SysDictionaryDetail + /sysExportTemplate/ExportTemplate: + get: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导出表格模板 + tags: + - SysExportTemplate + /sysExportTemplate/createSysExportTemplate: + post: + consumes: + - application/json + parameters: + - description: 创建导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"创建成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 创建导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/deleteSysExportTemplate: + delete: + consumes: + - application/json + parameters: + - description: 删除导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"删除成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 删除导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/deleteSysExportTemplateByIds: + delete: + consumes: + - application/json + parameters: + - description: 批量删除导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"批量删除成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 批量删除导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/exportExcel: + get: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导出表格 + tags: + - SysExportTemplate + /sysExportTemplate/findSysExportTemplate: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 数据库名称 + in: query + name: dbName + type: string + - in: query + name: limit + type: integer + - description: 模板名称 + in: query + name: name + type: string + - in: query + name: order + type: string + - description: 表名称 + in: query + name: tableName + type: string + - description: 模板标识 + in: query + name: templateID + type: string + - description: 模板信息 + in: query + name: templateInfo + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"查询成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 用id查询导出模板 + tags: + - SysExportTemplate + /sysExportTemplate/getSysExportTemplateList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 数据库名称 + in: query + name: dbName + type: string + - in: query + name: endCreatedAt + type: string + - description: 关键字 + in: query + name: keyword + type: string + - in: query + name: limit + type: integer + - description: 模板名称 + in: query + name: name + type: string + - in: query + name: order + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + - description: 表名称 + in: query + name: tableName + type: string + - description: 模板标识 + in: query + name: templateID + type: string + - description: 模板信息 + in: query + name: templateInfo + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"获取成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 分页获取导出模板列表 + tags: + - SysExportTemplate + /sysExportTemplate/importExcel: + post: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: 导入表格 + tags: + - SysImportTemplate + /sysExportTemplate/updateSysExportTemplate: + put: + consumes: + - application/json + parameters: + - description: 更新导出模板 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysExportTemplate' + produces: + - application/json + responses: + "200": + description: '{"success":true,"data":{},"msg":"更新成功"}' + schema: + type: string + security: + - ApiKeyAuth: [] + summary: 更新导出模板 + tags: + - SysExportTemplate + /sysOperationRecord/createSysOperationRecord: + post: + consumes: + - application/json + parameters: + - description: 创建SysOperationRecord + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysOperationRecord' + produces: + - application/json + responses: + "200": + description: 创建SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/deleteSysOperationRecord: + delete: + consumes: + - application/json + parameters: + - description: SysOperationRecord模型 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysOperationRecord' + produces: + - application/json + responses: + "200": + description: 删除SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/deleteSysOperationRecordByIds: + delete: + consumes: + - application/json + parameters: + - description: 批量删除SysOperationRecord + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.IdsReq' + produces: + - application/json + responses: + "200": + description: 批量删除SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 批量删除SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/findSysOperationRecord: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 代理 + in: query + name: agent + type: string + - description: 请求Body + in: query + name: body + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 错误信息 + in: query + name: error_message + type: string + - description: 请求ip + in: query + name: ip + type: string + - description: 延迟 + in: query + name: latency + type: string + - description: 请求方法 + in: query + name: method + type: string + - description: 请求路径 + in: query + name: path + type: string + - description: 响应Body + in: query + name: resp + type: string + - description: 请求状态 + in: query + name: status + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 用户id + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: 用id查询SysOperationRecord + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询SysOperationRecord + tags: + - SysOperationRecord + /sysOperationRecord/getSysOperationRecordList: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 代理 + in: query + name: agent + type: string + - description: 请求Body + in: query + name: body + type: string + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 错误信息 + in: query + name: error_message + type: string + - description: 请求ip + in: query + name: ip + type: string + - description: 关键字 + in: query + name: keyword + type: string + - description: 延迟 + in: query + name: latency + type: string + - description: 请求方法 + in: query + name: method + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - description: 请求路径 + in: query + name: path + type: string + - description: 响应Body + in: query + name: resp + type: string + - description: 请求状态 + in: query + name: status + type: integer + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 用户id + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: 分页获取SysOperationRecord列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取SysOperationRecord列表 + tags: + - SysOperationRecord + /sysParams/createSysParams: + post: + consumes: + - application/json + parameters: + - description: 创建参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 创建参数 + tags: + - SysParams + /sysParams/deleteSysParams: + delete: + consumes: + - application/json + parameters: + - description: 删除参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除参数 + tags: + - SysParams + /sysParams/deleteSysParamsByIds: + delete: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 批量删除成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 批量删除参数 + tags: + - SysParams + /sysParams/findSysParams: + get: + consumes: + - application/json + parameters: + - description: 主键ID + in: query + name: ID + type: integer + - description: 创建时间 + in: query + name: createdAt + type: string + - description: 参数说明 + in: query + name: desc + type: string + - description: 参数键 + in: query + name: key + required: true + type: string + - description: 参数名称 + in: query + name: name + required: true + type: string + - description: 更新时间 + in: query + name: updatedAt + type: string + - description: 参数值 + in: query + name: value + required: true + type: string + produces: + - application/json + responses: + "200": + description: 查询成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.SysParams' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用id查询参数 + tags: + - SysParams + /sysParams/getSysParam: + get: + consumes: + - application/json + parameters: + - description: key + in: query + name: key + required: true + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/system.SysParams' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 根据key获取参数value + tags: + - SysParams + /sysParams/getSysParamsList: + get: + consumes: + - application/json + parameters: + - in: query + name: endCreatedAt + type: string + - in: query + name: key + type: string + - description: 关键字 + in: query + name: keyword + type: string + - in: query + name: name + type: string + - description: 页码 + in: query + name: page + type: integer + - description: 每页大小 + in: query + name: pageSize + type: integer + - in: query + name: startCreatedAt + type: string + produces: + - application/json + responses: + "200": + description: 获取成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取参数列表 + tags: + - SysParams + /sysParams/updateSysParams: + put: + consumes: + - application/json + parameters: + - description: 更新参数 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysParams' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更新参数 + tags: + - SysParams + /system/getServerInfo: + post: + produces: + - application/json + responses: + "200": + description: 获取服务器信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取服务器信息 + tags: + - System + /system/getSystemConfig: + post: + produces: + - application/json + responses: + "200": + description: 获取配置文件内容,返回包括系统配置 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysConfigResponse' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取配置文件内容 + tags: + - System + /system/reloadSystem: + post: + produces: + - application/json + responses: + "200": + description: 重启系统 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 重启系统 + tags: + - System + /system/setSystemConfig: + post: + parameters: + - description: 设置配置文件内容 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.System' + produces: + - application/json + responses: + "200": + description: 设置配置文件内容 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置配置文件内容 + tags: + - System + /user/SetSelfInfo: + put: + consumes: + - application/json + parameters: + - description: ID, 用户名, 昵称, 头像链接 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 设置用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户信息 + tags: + - SysUser + /user/SetSelfSetting: + put: + consumes: + - application/json + parameters: + - description: 用户配置数据 + in: body + name: data + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: 设置用户配置 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户配置 + tags: + - SysUser + /user/admin_register: + post: + parameters: + - description: 用户名, 昵称, 密码, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.Register' + produces: + - application/json + responses: + "200": + description: 用户注册账号,返回包括用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.SysUserResponse' + msg: + type: string + type: object + summary: 用户注册账号 + tags: + - SysUser + /user/changePassword: + post: + parameters: + - description: 用户名, 原密码, 新密码 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.ChangePasswordReq' + produces: + - application/json + responses: + "200": + description: 用户修改密码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 用户修改密码 + tags: + - SysUser + /user/deleteUser: + delete: + consumes: + - application/json + parameters: + - description: 用户ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetById' + produces: + - application/json + responses: + "200": + description: 删除用户 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 删除用户 + tags: + - SysUser + /user/getUserInfo: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: 获取用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 获取用户信息 + tags: + - SysUser + /user/getUserList: + post: + consumes: + - application/json + parameters: + - description: 页码, 每页大小 + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.GetUserList' + produces: + - application/json + responses: + "200": + description: 分页获取用户列表,返回包括列表,总数,页码,每页数量 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + $ref: '#/definitions/response.PageResult' + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 分页获取用户列表 + tags: + - SysUser + /user/resetPassword: + post: + parameters: + - description: ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 重置用户密码 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 重置用户密码 + tags: + - SysUser + /user/setUserAuthorities: + post: + consumes: + - application/json + parameters: + - description: 用户UUID, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SetUserAuthorities' + produces: + - application/json + responses: + "200": + description: 设置用户权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户权限 + tags: + - SysUser + /user/setUserAuthority: + post: + consumes: + - application/json + parameters: + - description: 用户UUID, 角色ID + in: body + name: data + required: true + schema: + $ref: '#/definitions/request.SetUserAuth' + produces: + - application/json + responses: + "200": + description: 设置用户权限 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 更改用户权限 + tags: + - SysUser + /user/setUserInfo: + put: + consumes: + - application/json + parameters: + - description: ID, 用户名, 昵称, 头像链接 + in: body + name: data + required: true + schema: + $ref: '#/definitions/system.SysUser' + produces: + - application/json + responses: + "200": + description: 设置用户信息 + schema: + allOf: + - $ref: '#/definitions/response.Response' + - properties: + data: + additionalProperties: true + type: object + msg: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: 设置用户信息 + tags: + - SysUser +securityDefinitions: + ApiKeyAuth: + in: header + name: x-token + type: apiKey +swagger: "2.0" +tags: +- name: Base +- description: 用户 + name: SysUser diff --git a/server/global/global.go b/server/global/global.go new file mode 100644 index 0000000..97b57c4 --- /dev/null +++ b/server/global/global.go @@ -0,0 +1,68 @@ +package global + +import ( + "fmt" + "github.com/mark3labs/mcp-go/server" + "sync" + + "github.com/gin-gonic/gin" + "github.com/qiniu/qmgo" + + "github.com/flipped-aurora/gin-vue-admin/server/utils/timer" + "github.com/songzhibin97/gkit/cache/local_cache" + + "golang.org/x/sync/singleflight" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + + "github.com/redis/go-redis/v9" + "github.com/spf13/viper" + "gorm.io/gorm" +) + +var ( + GVA_DB *gorm.DB + GVA_DBList map[string]*gorm.DB + GVA_REDIS redis.UniversalClient + GVA_REDISList map[string]redis.UniversalClient + GVA_MONGO *qmgo.QmgoClient + GVA_CONFIG config.Server + GVA_VP *viper.Viper + // GVA_LOG *oplogging.Logger + GVA_LOG *zap.Logger + GVA_Timer timer.Timer = timer.NewTimerTask() + GVA_Concurrency_Control = &singleflight.Group{} + GVA_ROUTERS gin.RoutesInfo + GVA_ACTIVE_DBNAME *string + GVA_MCP_SERVER *server.MCPServer + BlackCache local_cache.Cache + lock sync.RWMutex +) + +// GetGlobalDBByDBName 通过名称获取db list中的db +func GetGlobalDBByDBName(dbname string) *gorm.DB { + lock.RLock() + defer lock.RUnlock() + return GVA_DBList[dbname] +} + +// MustGetGlobalDBByDBName 通过名称获取db 如果不存在则panic +func MustGetGlobalDBByDBName(dbname string) *gorm.DB { + lock.RLock() + defer lock.RUnlock() + db, ok := GVA_DBList[dbname] + if !ok || db == nil { + panic("db no init") + } + return db +} + +func GetRedis(name string) redis.UniversalClient { + redis, ok := GVA_REDISList[name] + if !ok || redis == nil { + panic(fmt.Sprintf("redis `%s` no init", name)) + } + return redis +} diff --git a/server/global/model.go b/server/global/model.go new file mode 100644 index 0000000..9772eb3 --- /dev/null +++ b/server/global/model.go @@ -0,0 +1,14 @@ +package global + +import ( + "time" + + "gorm.io/gorm" +) + +type GVA_MODEL struct { + ID uint `gorm:"primarykey" json:"ID"` // 主键ID + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间 +} diff --git a/server/global/version.go b/server/global/version.go new file mode 100644 index 0000000..2c61af2 --- /dev/null +++ b/server/global/version.go @@ -0,0 +1,12 @@ +package global + +// Version 版本信息 +// 目前只有Version正式使用 其余为预留 +const ( + // Version 当前版本号 + Version = "v2.9.1" + // AppName 应用名称 + AppName = "Gin-Vue-Admin" + // Description 应用描述 + Description = "使用gin+vue进行极速开发的全栈开发基础平台" +) diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..308d285 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,207 @@ +module github.com/flipped-aurora/gin-vue-admin/server + +go 1.24.0 + +toolchain go1.24.2 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go-v2 v1.41.2 + github.com/aws/aws-sdk-go-v2/config v1.32.10 + github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 + github.com/casbin/casbin/v2 v2.103.0 + github.com/casbin/gorm-adapter/v3 v3.32.0 + github.com/dzwvip/gorm-oracle v0.1.2 + github.com/fsnotify/fsnotify v1.8.0 + github.com/gin-contrib/sse v1.0.0 + github.com/gin-gonic/gin v1.10.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/goccy/go-json v0.10.4 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 + github.com/gookit/color v1.5.4 + github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/mark3labs/mcp-go v0.41.1 + github.com/mholt/archives v0.1.1 + github.com/minio/minio-go/v7 v7.0.84 + github.com/mojocn/base64Captcha v1.3.8 + github.com/otiai10/copy v1.14.1 + github.com/pkg/errors v0.9.1 + github.com/qiniu/go-sdk/v7 v7.25.2 + github.com/qiniu/qmgo v1.1.9 + github.com/redis/go-redis/v9 v9.7.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/songzhibin97/gkit v1.2.13 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 + github.com/tencentyun/cos-go-sdk-v5 v0.7.60 + github.com/unrolled/secure v1.17.0 + github.com/xuri/excelize/v2 v2.9.0 + go.mongodb.org/mongo-driver v1.17.2 + go.uber.org/automaxprocs v1.6.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.37.0 + golang.org/x/sync v0.13.0 + golang.org/x/sys v0.32.0 + golang.org/x/text v0.24.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/datatypes v1.2.5 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlserver v1.5.4 + gorm.io/gen v0.3.26 + gorm.io/gorm v1.25.12 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect + github.com/aws/smithy-go v1.24.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/sonic v1.12.7 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/casbin/govaluate v1.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.12.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gammazero/toposort v0.1.1 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // 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 + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.8.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/mozillazg/go-httpheader v0.4.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sijms/go-ora/v2 v2.7.17 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/thoas/go-funk v0.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect + github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/arch v0.13.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/image v0.23.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.29.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gorm.io/hints v1.1.2 // indirect + gorm.io/plugin/dbresolver v1.5.3 // indirect + modernc.org/fileutil v1.3.0 // indirect + modernc.org/libc v1.61.9 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.34.5 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..875014e --- /dev/null +++ b/server/go.sum @@ -0,0 +1,880 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 h1:+mQ8NQBh7B7c2FBtppRnwkrmuwFON1XQQ+5yblomZKk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3/go.mod h1:u67RKh3BRmS4FYLH+rN3N4T5fqpd9m2ttAwBJYEdosU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= +github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= +github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= +github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus= +github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= +github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ4fSNB6mMRuYNvFWou7BZs6SZB925hPrnk= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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.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= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= +github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY= +github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= +github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w= +github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo= +github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= +github.com/qiniu/qmgo v1.1.9 h1:3G3h9RLyjIUW9YSAQEPP2WqqNnboZ2Z/zO3mugjVb3E= +github.com/qiniu/qmgo v1.1.9/go.mod h1:aba4tNSlMWrwUhe7RdILfwBRIgvBujt1y10X+T1YZSI= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= +github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/songzhibin97/gkit v1.2.13 h1:paY0XJkdRuy9/8k9nTnbdrzo8pC22jIIFldUkOQv5nU= +github.com/songzhibin97/gkit v1.2.13/go.mod h1:38CreNR27eTGaG1UMGihrXqI4xc3nGfYxLVKKVx6Ngg= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= +github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.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.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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/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= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/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= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.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.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= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +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.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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= +gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= +gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= +gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o= +gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg= +gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= +gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= +modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= +modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= +modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/server/initialize/db_list.go b/server/initialize/db_list.go new file mode 100644 index 0000000..90eef9e --- /dev/null +++ b/server/initialize/db_list.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm" +) + +const sys = "system" + +func DBList() { + dbMap := make(map[string]*gorm.DB) + for _, info := range global.GVA_CONFIG.DBList { + if info.Disable { + continue + } + switch info.Type { + case "mysql": + dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB}) + case "mssql": + dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB}) + case "pgsql": + dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB}) + case "oracle": + dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB}) + default: + continue + } + } + // 做特殊判断,是否有迁移 + // 适配低版本迁移多数据库版本 + if sysDB, ok := dbMap[sys]; ok { + global.GVA_DB = sysDB + } + global.GVA_DBList = dbMap +} diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go new file mode 100644 index 0000000..817b542 --- /dev/null +++ b/server/initialize/ensure_tables.go @@ -0,0 +1,113 @@ +package initialize + +import ( + "context" + adapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "gorm.io/gorm" +) + +const initOrderEnsureTables = system.InitOrderExternal - 1 + +type ensureTables struct{} + +// auto run +func init() { + system.RegisterInit(initOrderEnsureTables, &ensureTables{}) +} + +func (e *ensureTables) InitializerName() string { + return "ensure_tables_created" +} +func (e *ensureTables) InitializeData(ctx context.Context) (next context.Context, err error) { + return ctx, nil +} + +func (e *ensureTables) DataInserted(ctx context.Context) bool { + return true +} + +func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysAIWorkflowSession{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + sysModel.SysParams{}, + sysModel.SysVersion{}, + sysModel.SysError{}, + sysModel.SysLoginLog{}, + sysModel.SysApiToken{}, + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + } + for _, t := range tables { + _ = db.AutoMigrate(&t) + // 视图 authority_menu 会被当成表来创建,引发冲突错误(更新版本的gorm似乎不会) + // 由于 AutoMigrate() 基本无需考虑错误,因此显式忽略 + } + return ctx, nil +} + +func (e *ensureTables) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysAIWorkflowSession{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + } + yes := true + for _, t := range tables { + yes = yes && db.Migrator().HasTable(t) + } + return yes +} diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go new file mode 100644 index 0000000..b06f0f4 --- /dev/null +++ b/server/initialize/gorm.go @@ -0,0 +1,87 @@ +package initialize + +import ( + "os" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +func Gorm() *gorm.DB { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + case "pgsql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Pgsql.Dbname + return GormPgSql() + case "oracle": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Oracle.Dbname + return GormOracle() + case "mssql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mssql.Dbname + return GormMssql() + case "sqlite": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Sqlite.Dbname + return GormSqlite() + default: + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + } +} + +func RegisterTables() { + if global.GVA_CONFIG.System.DisableAutoMigrate { + global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration") + return + } + + db := global.GVA_DB + err := db.AutoMigrate( + + system.SysApi{}, + system.SysIgnoreApi{}, + system.SysUser{}, + system.SysBaseMenu{}, + system.JwtBlacklist{}, + system.SysAuthority{}, + system.SysDictionary{}, + system.SysOperationRecord{}, + system.SysAutoCodeHistory{}, + system.SysDictionaryDetail{}, + system.SysBaseMenuParameter{}, + system.SysBaseMenuBtn{}, + system.SysAuthorityBtn{}, + system.SysAutoCodePackage{}, + system.SysExportTemplate{}, + system.Condition{}, + system.JoinTemplate{}, + system.SysParams{}, + system.SysVersion{}, + system.SysError{}, + system.SysApiToken{}, + system.SysLoginLog{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + ) + if err != nil { + global.GVA_LOG.Error("register table failed", zap.Error(err)) + os.Exit(0) + } + + err = bizModel() + + if err != nil { + global.GVA_LOG.Error("register biz_table failed", zap.Error(err)) + os.Exit(0) + } + global.GVA_LOG.Info("register table success") +} diff --git a/server/initialize/gorm_biz.go b/server/initialize/gorm_biz.go new file mode 100644 index 0000000..9316ccc --- /dev/null +++ b/server/initialize/gorm_biz.go @@ -0,0 +1,14 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +func bizModel() error { + db := global.GVA_DB + err := db.AutoMigrate() + if err != nil { + return err + } + return nil +} diff --git a/server/initialize/gorm_mssql.go b/server/initialize/gorm_mssql.go new file mode 100644 index 0000000..530275a --- /dev/null +++ b/server/initialize/gorm_mssql.go @@ -0,0 +1,64 @@ +package initialize + +/* + * @Author: 逆光飞翔 191180776@qq.com + * @Date: 2022-12-08 17:25:49 + * @LastEditors: 逆光飞翔 191180776@qq.com + * @LastEditTime: 2022-12-08 18:00:00 + * @FilePath: \server\initialize\gorm_mssql.go + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" +) + +// GormMssql 初始化Mssql数据库 +// Author [LouisZhang](191180776@qq.com) +func GormMssql() *gorm.DB { + m := global.GVA_CONFIG.Mssql + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { + return nil + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} + +// GormMssqlByConfig 初始化Mysql数据库用过传入配置 +func GormMssqlByConfig(m config.Mssql) *gorm.DB { + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_mysql.go b/server/initialize/gorm_mysql.go new file mode 100644 index 0000000..cf949aa --- /dev/null +++ b/server/initialize/gorm_mysql.go @@ -0,0 +1,48 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + _ "github.com/go-sql-driver/mysql" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// GormMysql 初始化Mysql数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ByteZhou-2018](https://github.com/ByteZhou-2018) +func GormMysql() *gorm.DB { + m := global.GVA_CONFIG.Mysql + return initMysqlDatabase(m) +} + +// GormMysqlByConfig 通过传入配置初始化Mysql数据库 +func GormMysqlByConfig(m config.Mysql) *gorm.DB { + return initMysqlDatabase(m) +} + +// initMysqlDatabase 初始化Mysql数据库的辅助函数 +func initMysqlDatabase(m config.Mysql) *gorm.DB { + if m.Dbname == "" { + return nil + } + + mysqlConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: false, // 根据版本自动配置 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_oracle.go b/server/initialize/gorm_oracle.go new file mode 100644 index 0000000..9d3d663 --- /dev/null +++ b/server/initialize/gorm_oracle.go @@ -0,0 +1,37 @@ +package initialize + +import ( + oracle "github.com/dzwvip/gorm-oracle" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "gorm.io/gorm" +) + +// GormOracle 初始化oracle数据库 +func GormOracle() *gorm.DB { + m := global.GVA_CONFIG.Oracle + return initOracleDatabase(m) +} + +// GormOracleByConfig 初始化Oracle数据库用过传入配置 +func GormOracleByConfig(m config.Oracle) *gorm.DB { + return initOracleDatabase(m) +} + +// initOracleDatabase 初始化Oracle数据库的辅助函数 +func initOracleDatabase(m config.Oracle) *gorm.DB { + if m.Dbname == "" { + return nil + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_pgsql.go b/server/initialize/gorm_pgsql.go new file mode 100644 index 0000000..b5022db --- /dev/null +++ b/server/initialize/gorm_pgsql.go @@ -0,0 +1,43 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// GormPgSql 初始化 Postgresql 数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func GormPgSql() *gorm.DB { + p := global.GVA_CONFIG.Pgsql + return initPgSqlDatabase(p) +} + +// GormPgSqlByConfig 初始化 Postgresql 数据库 通过指定参数 +func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { + return initPgSqlDatabase(p) +} + +// initPgSqlDatabase 初始化 Postgresql 数据库的辅助函数 +func initPgSqlDatabase(p config.Pgsql) *gorm.DB { + if p.Dbname == "" { + return nil + } + pgsqlConfig := postgres.Config{ + DSN: p.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + } + // 数据库配置 + general := p.GeneralDB + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(p.MaxIdleConns) + sqlDB.SetMaxOpenConns(p.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_sqlite.go b/server/initialize/gorm_sqlite.go new file mode 100644 index 0000000..33510ee --- /dev/null +++ b/server/initialize/gorm_sqlite.go @@ -0,0 +1,38 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// GormSqlite 初始化Sqlite数据库 +func GormSqlite() *gorm.DB { + s := global.GVA_CONFIG.Sqlite + return initSqliteDatabase(s) +} + +// GormSqliteByConfig 初始化Sqlite数据库用过传入配置 +func GormSqliteByConfig(s config.Sqlite) *gorm.DB { + return initSqliteDatabase(s) +} + +// initSqliteDatabase 初始化Sqlite数据库辅助函数 +func initSqliteDatabase(s config.Sqlite) *gorm.DB { + if s.Dbname == "" { + return nil + } + + // 数据库配置 + general := s.GeneralDB + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(s.MaxIdleConns) + sqlDB.SetMaxOpenConns(s.MaxOpenConns) + return db + } +} diff --git a/server/initialize/init.go b/server/initialize/init.go new file mode 100644 index 0000000..4dc48f3 --- /dev/null +++ b/server/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +// 初始化全局函数 +func SetupHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/server/initialize/internal/gorm.go b/server/initialize/internal/gorm.go new file mode 100644 index 0000000..28f4097 --- /dev/null +++ b/server/initialize/internal/gorm.go @@ -0,0 +1,31 @@ +package internal + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +var Gorm = new(_gorm) + +type _gorm struct{} + +// Config gorm 自定义配置 +// Author [SliverHorn](https://github.com/SliverHorn) +func (g *_gorm) Config(general config.GeneralDB) *gorm.Config { + return &gorm.Config{ + Logger: logger.New(NewWriter(general), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: general.LogLevel(), + Colorful: true, + }), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: general.Prefix, + SingularTable: general.Singular, + }, + DisableForeignKeyConstraintWhenMigrating: true, + } +} diff --git a/server/initialize/internal/gorm_logger_writer.go b/server/initialize/internal/gorm_logger_writer.go new file mode 100644 index 0000000..9774c96 --- /dev/null +++ b/server/initialize/internal/gorm_logger_writer.go @@ -0,0 +1,41 @@ +package internal + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm/logger" +) + +type Writer struct { + config config.GeneralDB + writer logger.Writer +} + +func NewWriter(config config.GeneralDB) *Writer { + return &Writer{config: config} +} + +// Printf 格式化打印日志 +func (c *Writer) Printf(message string, data ...any) { + + // 当有日志时候均需要输出到控制台 + fmt.Printf(message, data...) + + // 当开启了zap的情况,会打印到日志记录 + if c.config.LogZap { + switch c.config.LogLevel() { + case logger.Silent: + global.GVA_LOG.Debug(fmt.Sprintf(message, data...)) + case logger.Error: + global.GVA_LOG.Error(fmt.Sprintf(message, data...)) + case logger.Warn: + global.GVA_LOG.Warn(fmt.Sprintf(message, data...)) + case logger.Info: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + default: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + } + return + } +} diff --git a/server/initialize/internal/mongo.go b/server/initialize/internal/mongo.go new file mode 100644 index 0000000..c4992d7 --- /dev/null +++ b/server/initialize/internal/mongo.go @@ -0,0 +1,29 @@ +package internal + +import ( + "context" + "fmt" + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/event" + opt "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap" +) + +var Mongo = new(mongo) + +type mongo struct{} + +func (m *mongo) GetClientOptions() []options.ClientOptions { + cmdMonitor := &event.CommandMonitor{ + Started: func(ctx context.Context, event *event.CommandStartedEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d][database:%s] %s\n", event.RequestID, event.DatabaseName, event.Command), zap.String("business", "mongo")) + }, + Succeeded: func(ctx context.Context, event *event.CommandSucceededEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Reply), zap.String("business", "mongo")) + }, + Failed: func(ctx context.Context, event *event.CommandFailedEvent) { + zap.L().Error(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Failure), zap.String("business", "mongo")) + }, + } + return []options.ClientOptions{{ClientOptions: &opt.ClientOptions{Monitor: cmdMonitor}}} +} diff --git a/server/initialize/mongo.go b/server/initialize/mongo.go new file mode 100644 index 0000000..adefb58 --- /dev/null +++ b/server/initialize/mongo.go @@ -0,0 +1,155 @@ +package initialize + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/pkg/errors" + "github.com/qiniu/qmgo" + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/bson" + option "go.mongodb.org/mongo-driver/mongo/options" + "sort" + "strings" +) + +var Mongo = new(mongo) + +type ( + mongo struct{} + Index struct { + V any `bson:"v"` + Ns any `bson:"ns"` + Key []bson.E `bson:"key"` + Name string `bson:"name"` + } +) + +func (m *mongo) Indexes(ctx context.Context) error { + // 表名:索引列表 列: "表名": [][]string{{"index1", "index2"}} + indexMap := map[string][][]string{} + for collection, indexes := range indexMap { + err := m.CreateIndexes(ctx, collection, indexes) + if err != nil { + return err + } + } + return nil +} + +func (m *mongo) Initialization() error { + var opts []options.ClientOptions + if global.GVA_CONFIG.Mongo.IsZap { + opts = internal.Mongo.GetClientOptions() + } + ctx := context.Background() + config := &qmgo.Config{ + Uri: global.GVA_CONFIG.Mongo.Uri(), + Coll: global.GVA_CONFIG.Mongo.Coll, + Database: global.GVA_CONFIG.Mongo.Database, + MinPoolSize: &global.GVA_CONFIG.Mongo.MinPoolSize, + MaxPoolSize: &global.GVA_CONFIG.Mongo.MaxPoolSize, + SocketTimeoutMS: &global.GVA_CONFIG.Mongo.SocketTimeoutMs, + ConnectTimeoutMS: &global.GVA_CONFIG.Mongo.ConnectTimeoutMs, + } + if global.GVA_CONFIG.Mongo.Username != "" && global.GVA_CONFIG.Mongo.Password != "" { + config.Auth = &qmgo.Credential{ + Username: global.GVA_CONFIG.Mongo.Username, + Password: global.GVA_CONFIG.Mongo.Password, + AuthSource: global.GVA_CONFIG.Mongo.AuthSource, + } + } + client, err := qmgo.Open(ctx, config, opts...) + + if err != nil { + return errors.Wrap(err, "链接mongodb数据库失败!") + } + global.GVA_MONGO = client + err = m.Indexes(ctx) + if err != nil { + return err + } + return nil +} + +func (m *mongo) CreateIndexes(ctx context.Context, name string, indexes [][]string) error { + collection, err := global.GVA_MONGO.Database.Collection(name).CloneCollection() + if err != nil { + return errors.Wrapf(err, "获取[%s]的表对象失败!", name) + } + list, err := collection.Indexes().List(ctx) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引对象失败!", name) + } + var entities []Index + err = list.All(ctx, &entities) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引列表失败!", name) + } + length := len(indexes) + indexMap1 := make(map[string][]string, length) + for i := 0; i < length; i++ { + sort.Strings(indexes[i]) // 对索引key进行排序, 在使用bson.M搜索时, bson会自动按照key的字母顺序进行排序 + length1 := len(indexes[i]) + keys := make([]string, 0, length1) + for j := 0; j < length1; j++ { + if indexes[i][i][0] == '-' { + keys = append(keys, indexes[i][j], "-1") + continue + } + keys = append(keys, indexes[i][j], "1") + } + key := strings.Join(keys, "_") + _, o1 := indexMap1[key] + if o1 { + return errors.Errorf("索引[%s]重复!", key) + } + indexMap1[key] = indexes[i] + } + length = len(entities) + indexMap2 := make(map[string]map[string]string, length) + for i := 0; i < length; i++ { + v1, o1 := indexMap2[entities[i].Name] + if !o1 { + keyLength := len(entities[i].Key) + v1 = make(map[string]string, keyLength) + for j := 0; j < keyLength; j++ { + v2, o2 := v1[entities[i].Key[j].Key] + if !o2 { + v1 = make(map[string]string) + } + v2 = entities[i].Key[j].Key + v1[entities[i].Key[j].Key] = v2 + indexMap2[entities[i].Name] = v1 + } + } + } + for k1, v1 := range indexMap1 { + _, o2 := indexMap2[k1] + if o2 { + continue + } // 索引存在 + if len(fmt.Sprintf("%s.%s.$%s", collection.Name(), name, v1)) > 127 { + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间, 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + return nil + } + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetExpireAfterSeconds(86400), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间(秒), 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + } + return nil +} diff --git a/server/initialize/other.go b/server/initialize/other.go new file mode 100644 index 0000000..f272a81 --- /dev/null +++ b/server/initialize/other.go @@ -0,0 +1,32 @@ +package initialize + +import ( + "bufio" + "github.com/songzhibin97/gkit/cache/local_cache" + "os" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +func OtherInit() { + dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + panic(err) + } + _, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + if err != nil { + panic(err) + } + + global.BlackCache = local_cache.NewCache( + local_cache.SetDefaultExpire(dr), + ) + file, err := os.Open("go.mod") + if err == nil && global.GVA_CONFIG.AutoCode.Module == "" { + scanner := bufio.NewScanner(file) + scanner.Scan() + global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ") + } +} diff --git a/server/initialize/plugin.go b/server/initialize/plugin.go new file mode 100644 index 0000000..7ee33c2 --- /dev/null +++ b/server/initialize/plugin.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gin-gonic/gin" +) + +func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) { + if global.GVA_DB == nil { + global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装") + return + } + bizPluginV1(PrivateGroup, PublicRouter) + _ = engine +} diff --git a/server/initialize/plugin_biz_v1.go b/server/initialize/plugin_biz_v1.go new file mode 100644 index 0000000..1585db3 --- /dev/null +++ b/server/initialize/plugin_biz_v1.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email" + "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin" + "github.com/gin-gonic/gin" +) + +func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) { + for i := range Plugin { + fmt.Println(Plugin[i].RouterPath(), "注册开始!") + PluginGroup := group.Group(Plugin[i].RouterPath()) + Plugin[i].Register(PluginGroup) + fmt.Println(Plugin[i].RouterPath(), "注册成功!") + } +} + +func bizPluginV1(group ...*gin.RouterGroup) { + private := group[0] + public := group[1] + // 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同 + PluginInit(private, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, + )) + holder(public, private) +} diff --git a/server/initialize/plugin_biz_v2.go b/server/initialize/plugin_biz_v2.go new file mode 100644 index 0000000..bde58fa --- /dev/null +++ b/server/initialize/plugin_biz_v2.go @@ -0,0 +1,16 @@ +package initialize + +import ( + _ "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" +) + +func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) { + for i := 0; i < len(plugins); i++ { + plugins[i].Register(group) + } +} +func bizPluginV2(engine *gin.Engine) { + PluginInitV2(engine, plugin.Registered()...) +} diff --git a/server/initialize/redis.go b/server/initialize/redis.go new file mode 100644 index 0000000..2d9c8f4 --- /dev/null +++ b/server/initialize/redis.go @@ -0,0 +1,59 @@ +package initialize + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +func initRedisClient(redisCfg config.Redis) (redis.UniversalClient, error) { + var client redis.UniversalClient + // 使用集群模式 + if redisCfg.UseCluster { + client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: redisCfg.ClusterAddrs, + Password: redisCfg.Password, + }) + } else { + // 使用单例模式 + client = redis.NewClient(&redis.Options{ + Addr: redisCfg.Addr, + Password: redisCfg.Password, + DB: redisCfg.DB, + }) + } + pong, err := client.Ping(context.Background()).Result() + if err != nil { + global.GVA_LOG.Error("redis connect ping failed, err:", zap.String("name", redisCfg.Name), zap.Error(err)) + return nil, err + } + + global.GVA_LOG.Info("redis connect ping response:", zap.String("name", redisCfg.Name), zap.String("pong", pong)) + return client, nil +} + +func Redis() { + redisClient, err := initRedisClient(global.GVA_CONFIG.Redis) + if err != nil { + panic(err) + } + global.GVA_REDIS = redisClient +} + +func RedisList() { + redisMap := make(map[string]redis.UniversalClient) + + for _, redisCfg := range global.GVA_CONFIG.RedisList { + client, err := initRedisClient(redisCfg) + if err != nil { + panic(err) + } + redisMap[redisCfg.Name] = client + } + + global.GVA_REDISList = redisMap +} diff --git a/server/initialize/register_init.go b/server/initialize/register_init.go new file mode 100644 index 0000000..a249661 --- /dev/null +++ b/server/initialize/register_init.go @@ -0,0 +1,10 @@ +package initialize + +import ( + _ "github.com/flipped-aurora/gin-vue-admin/server/source/example" + _ "github.com/flipped-aurora/gin-vue-admin/server/source/system" +) + +func init() { + // do nothing,only import source package so that inits can be registered +} diff --git a/server/initialize/reload.go b/server/initialize/reload.go new file mode 100644 index 0000000..8fd27e6 --- /dev/null +++ b/server/initialize/reload.go @@ -0,0 +1,45 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +// Reload 优雅地重新加载系统配置 +func Reload() error { + global.GVA_LOG.Info("正在重新加载系统配置...") + + // 重新加载配置文件 + if err := global.GVA_VP.ReadInConfig(); err != nil { + global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err)) + return err + } + + // 重新初始化数据库连接 + if global.GVA_DB != nil { + db, _ := global.GVA_DB.DB() + err := db.Close() + if err != nil { + global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err)) + return err + } + } + + // 重新建立数据库连接 + global.GVA_DB = Gorm() + + // 重新初始化其他配置 + OtherInit() + DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + RegisterTables() + } + + // 重新初始化定时任务 + Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil +} diff --git a/server/initialize/router.go b/server/initialize/router.go new file mode 100644 index 0000000..48e885e --- /dev/null +++ b/server/initialize/router.go @@ -0,0 +1,117 @@ +package initialize + +import ( + "net/http" + "os" + + "github.com/flipped-aurora/gin-vue-admin/server/docs" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/router" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +type justFilesFilesystem struct { + fs http.FileSystem +} + +func (fs justFilesFilesystem) Open(name string) (http.File, error) { + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + + stat, err := f.Stat() + if err == nil && stat.IsDir() { + return nil, os.ErrPermission + } + + return f, nil +} + +// 初始化总路由 + +func Routers() *gin.Engine { + Router := gin.New() + // 使用自定义的 Recovery 中间件,记录 panic 并入库 + Router.Use(middleware.GinRecovery(true)) + if gin.Mode() == gin.DebugMode { + Router.Use(gin.Logger()) + } + + systemRouter := router.RouterGroupApp.System + exampleRouter := router.RouterGroupApp.Example + // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 + // VUE_APP_BASE_API = / + // VUE_APP_BASE_PATH = http://localhost + // 然后执行打包命令 npm run build。在打开下面3行注释 + // Router.StaticFile("/favicon.ico", "./dist/favicon.ico") + // Router.Static("/assets", "./dist/assets") // dist里面的静态资源 + // Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面 + + Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) + // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件") + // 跨域,如需跨域可以打开下面的注释 + // Router.Use(middleware.Cors()) // 直接放行全部跨域请求 + // Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求 + // global.GVA_LOG.Info("use middleware cors") + docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix + Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + global.GVA_LOG.Info("register swagger handler") + // 方便统一添加路由组前缀 多服务器上线使用 + + PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + + PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + + { + // 健康监测 + PublicGroup.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, "ok") + }) + } + { + systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权 + systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关 + } + + { + systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 + systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 + systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 + systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 + systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由 + systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 + systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 + systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 + systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理 + systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史 + systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录 + systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理 + systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 + systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板 + systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志 + systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志 + systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发 + systemRouter.InitSkillsRouter(PrivateGroup, PublicGroup) // Skills 定义器 + exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 + exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 + exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 + } + + //插件路由安装 + InstallPlugin(PrivateGroup, PublicGroup, Router) + + // 注册业务路由 + initBizRouter(PrivateGroup, PublicGroup) + + global.GVA_ROUTERS = Router.Routes() + + global.GVA_LOG.Info("router register success") + return Router +} diff --git a/server/initialize/router_biz.go b/server/initialize/router_biz.go new file mode 100644 index 0000000..279127d --- /dev/null +++ b/server/initialize/router_biz.go @@ -0,0 +1,19 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/router" + "github.com/gin-gonic/gin" +) + +// 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。 +func holder(routers ...*gin.RouterGroup) { + _ = routers + _ = router.RouterGroupApp +} + +func initBizRouter(routers ...*gin.RouterGroup) { + privateGroup := routers[0] + publicGroup := routers[1] + + holder(publicGroup, privateGroup) +} diff --git a/server/initialize/timer.go b/server/initialize/timer.go new file mode 100644 index 0000000..ab5a7a5 --- /dev/null +++ b/server/initialize/timer.go @@ -0,0 +1,37 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/task" + + "github.com/robfig/cron/v3" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +func Timer() { + go func() { + var option []cron.Option + option = append(option, cron.WithSeconds()) + // 清理DB定时任务 + _, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() { + err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中 + if err != nil { + fmt.Println("timer error:", err) + } + }, "定时清理数据库【日志,黑名单】内容", option...) + if err != nil { + fmt.Println("add timer error:", err) + } + + // 其他定时任务定在这里 参考上方使用方法 + + //_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() { + // 具体执行内容... + // ...... + //}, option...) + //if err != nil { + // fmt.Println("add timer error:", err) + //} + }() +} diff --git a/server/initialize/validator.go b/server/initialize/validator.go new file mode 100644 index 0000000..79aea66 --- /dev/null +++ b/server/initialize/validator.go @@ -0,0 +1,22 @@ +package initialize + +import "github.com/flipped-aurora/gin-vue-admin/server/utils" + +func init() { + _ = utils.RegisterRule("PageVerify", + utils.Rules{ + "Page": {utils.NotEmpty()}, + "PageSize": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("IdVerify", + utils.Rules{ + "Id": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("AuthorityIdVerify", + utils.Rules{ + "AuthorityId": {utils.NotEmpty()}, + }, + ) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..b8b2222 --- /dev/null +++ b/server/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/core" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/initialize" + _ "go.uber.org/automaxprocs" + "go.uber.org/zap" +) + +//go:generate go env -w GO111MODULE=on +//go:generate go env -w GOPROXY=https://goproxy.cn,direct +//go:generate go mod tidy +//go:generate go mod download + +// 这部分 @Tag 设置用于排序, 需要排序的接口请按照下面的格式添加 +// swag init 对 @Tag 只会从入口文件解析, 默认 main.go +// 也可通过 --generalInfo flag 指定其他文件 +// @Tag.Name Base +// @Tag.Name SysUser +// @Tag.Description 用户 + +// @title Gin-Vue-Admin Swagger API接口文档 +// @version v2.9.1 +// @description 使用gin+vue进行极速开发的全栈开发基础平台 +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name x-token +// @BasePath / +func main() { + // 初始化系统 + initializeSystem() + // 运行服务器 + core.RunServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { + global.GVA_VP = core.Viper() // 初始化Viper + initialize.OtherInit() + global.GVA_LOG = core.Zap() // 初始化zap日志库 + zap.ReplaceGlobals(global.GVA_LOG) + global.GVA_DB = initialize.Gorm() // gorm连接数据库 + initialize.Timer() + initialize.DBList() + initialize.SetupHandlers() // 注册全局函数 + if global.GVA_DB != nil { + initialize.RegisterTables() // 初始化表 + } +} diff --git a/server/mcp/api_creator.go b/server/mcp/api_creator.go new file mode 100644 index 0000000..9016a50 --- /dev/null +++ b/server/mcp/api_creator.go @@ -0,0 +1,159 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + commonReq "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" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&ApiCreator{}) +} + +type ApiCreateRequest struct { + Path string `json:"path"` + Description string `json:"description"` + ApiGroup string `json:"apiGroup"` + Method string `json:"method"` +} + +type ApiCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + ApiID uint `json:"apiId"` + Path string `json:"path"` + Method string `json:"method"` +} + +type ApiCreator struct{} + +func (a *ApiCreator) New() mcp.Tool { + return mcp.NewTool("create_api", + mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具 +- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`), + mcp.WithString("path", + mcp.Required(), + mcp.Description("API路径,如:/user/create"), + ), + mcp.WithString("description", + mcp.Required(), + mcp.Description("API中文描述,如:创建用户"), + ), + mcp.WithString("apiGroup", + mcp.Required(), + mcp.Description("API组名称,用于分类管理,如:用户管理"), + ), + mcp.WithString("method", + mcp.Description("HTTP方法"), + mcp.DefaultString("POST"), + ), + mcp.WithString("apis", + mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"), + ), + ) +} + +func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + var apis []ApiCreateRequest + if apisStr, ok := args["apis"].(string); ok && apisStr != "" { + if err := json.Unmarshal([]byte(apisStr), &apis); err != nil { + return nil, fmt.Errorf("apis 参数格式错误: %w", err) + } + } else { + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + description, ok := args["description"].(string) + if !ok || description == "" { + return nil, errors.New("description 参数是必需的") + } + apiGroup, ok := args["apiGroup"].(string) + if !ok || apiGroup == "" { + return nil, errors.New("apiGroup 参数是必需的") + } + + method := "POST" + if value, ok := args["method"].(string); ok && value != "" { + method = value + } + + apis = append(apis, ApiCreateRequest{ + Path: path, + Description: description, + ApiGroup: apiGroup, + Method: method, + }) + } + + if len(apis) == 0 { + return nil, errors.New("没有要创建的API") + } + + responses := make([]ApiCreateResponse, 0, len(apis)) + successCount := 0 + + for _, apiReq := range apis { + _, err := postUpstream[map[string]any](ctx, "/api/createApi", system.SysApi{ + Path: apiReq.Path, + Description: apiReq.Description, + ApiGroup: apiReq.ApiGroup, + Method: apiReq.Method, + }) + if err != nil { + responses = append(responses, ApiCreateResponse{ + Success: false, + Message: fmt.Sprintf("创建API失败: %v", err), + Path: apiReq.Path, + Method: apiReq.Method, + }) + continue + } + + lookupResp, lookupErr := postUpstream[pageResultData[[]system.SysApi]](ctx, "/api/getApiList", systemReq.SearchApiParams{ + SysApi: system.SysApi{ + Path: apiReq.Path, + Method: apiReq.Method, + }, + PageInfo: commonReq.PageInfo{ + Page: 1, + PageSize: 1, + }, + }) + + var apiID uint + if lookupErr == nil && len(lookupResp.Data.List) > 0 { + apiID = lookupResp.Data.List[0].ID + } + + responses = append(responses, ApiCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path), + ApiID: apiID, + Path: apiReq.Path, + Method: apiReq.Method, + }) + successCount++ + } + + result := map[string]any{ + "success": successCount > 0, + "totalCount": len(apis), + "successCount": successCount, + "failedCount": len(apis) - successCount, + "details": responses, + } + + return textResultWithJSON("API创建结果:", result) +} diff --git a/server/mcp/api_lister.go b/server/mcp/api_lister.go new file mode 100644 index 0000000..0357a61 --- /dev/null +++ b/server/mcp/api_lister.go @@ -0,0 +1,95 @@ +package mcpTool + +import ( + "context" + + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/gin-gonic/gin" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&ApiLister{}) +} + +type ApiInfo struct { + ID uint `json:"id,omitempty"` + Path string `json:"path"` + Description string `json:"description,omitempty"` + ApiGroup string `json:"apiGroup,omitempty"` + Method string `json:"method"` + Source string `json:"source"` +} + +type ApiListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DatabaseApis []ApiInfo `json:"databaseApis"` + GinApis []ApiInfo `json:"ginApis"` + TotalCount int `json:"totalCount"` +} + +type mcpRoutesResponse struct { + Routes gin.RoutesInfo `json:"routes"` +} + +type ApiLister struct{} + +func (a *ApiLister) New() mcp.Tool { + return mcp.NewTool("list_all_apis", + mcp.WithDescription(`获取系统中所有的API接口,分为两组: + +**功能说明:** +- 返回数据库中已注册的API列表 +- 返回gin框架中实际注册的路由API列表 +- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用 + +**返回数据结构:** +- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息) +- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`), + mcp.WithString("_placeholder", + mcp.Description("占位符,防止json schema校验失败"), + ), + ) +} + +func (a *ApiLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + apiResp, err := postUpstream[systemRes.SysAPIListResponse](ctx, "/api/getAllApis", map[string]any{}) + if err != nil { + return nil, err + } + + routeResp, err := postUpstream[mcpRoutesResponse](ctx, "/autoCode/mcpRoutes", map[string]any{}) + if err != nil { + return nil, err + } + + databaseApis := make([]ApiInfo, 0, len(apiResp.Data.Apis)) + for _, api := range apiResp.Data.Apis { + databaseApis = append(databaseApis, ApiInfo{ + ID: api.ID, + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + Source: "database", + }) + } + + ginApis := make([]ApiInfo, 0, len(routeResp.Data.Routes)) + for _, route := range routeResp.Data.Routes { + ginApis = append(ginApis, ApiInfo{ + Path: route.Path, + Method: route.Method, + Source: "gin", + }) + } + + return textResultWithJSON("", ApiListResponse{ + Success: true, + Message: "获取API列表成功", + DatabaseApis: databaseApis, + GinApis: ginApis, + TotalCount: len(databaseApis) + len(ginApis), + }) +} diff --git a/server/mcp/autocode_http.go b/server/mcp/autocode_http.go new file mode 100644 index 0000000..7e66dcb --- /dev/null +++ b/server/mcp/autocode_http.go @@ -0,0 +1,48 @@ +package mcpTool + +import ( + "context" + + commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +func fetchAutoCodePackages(ctx context.Context) ([]model.SysAutoCodePackage, error) { + resp, err := postUpstream[map[string][]model.SysAutoCodePackage](ctx, "/autoCode/getPackage", map[string]any{}) + if err != nil { + return nil, err + } + return resp.Data["pkgs"], nil +} + +func fetchAutoCodeHistories(ctx context.Context) ([]model.SysAutoCodeHistory, error) { + resp, err := postUpstream[pageResultData[[]model.SysAutoCodeHistory]](ctx, "/autoCode/getSysHistory", commonReq.PageInfo{ + Page: 1, + PageSize: 10000, + }) + if err != nil { + return nil, err + } + return resp.Data.List, nil +} + +func createAutoCodePackage(ctx context.Context, info *systemReq.SysAutoCodePackageCreate) error { + _, err := postUpstream[map[string]any](ctx, "/autoCode/createPackage", info) + return err +} + +func createAutoCodeModule(ctx context.Context, info systemReq.AutoCode) error { + _, err := postUpstream[map[string]any](ctx, "/autoCode/createTemp", info) + return err +} + +func deleteAutoCodePackage(ctx context.Context, id uint) error { + _, err := postUpstream[map[string]any](ctx, "/autoCode/delPackage", commonReq.GetById{ID: int(id)}) + return err +} + +func deleteAutoCodeHistory(ctx context.Context, id uint) error { + _, err := postUpstream[map[string]any](ctx, "/autoCode/delSysHistory", commonReq.GetById{ID: int(id)}) + return err +} diff --git a/server/mcp/client/client.go b/server/mcp/client/client.go new file mode 100644 index 0000000..ff02459 --- /dev/null +++ b/server/mcp/client/client.go @@ -0,0 +1,44 @@ +package client + +import ( + "context" + "errors" + + mcpClient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/client/transport" + "github.com/mark3labs/mcp-go/mcp" +) + +func NewClient(baseURL, name, version, serverName string, headers ...map[string]string) (*mcpClient.Client, error) { + options := make([]transport.StreamableHTTPCOption, 0, 1) + if len(headers) > 0 && len(headers[0]) > 0 { + options = append(options, transport.WithHTTPHeaders(headers[0])) + } + + client, err := mcpClient.NewStreamableHttpClient(baseURL, options...) + if err != nil { + return nil, err + } + + ctx := context.Background() + if err := client.Start(ctx); err != nil { + return nil, err + } + + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: name, + Version: version, + } + + result, err := client.Initialize(ctx, initRequest) + if err != nil { + return nil, err + } + if result.ServerInfo.Name != serverName { + return nil, errors.New("server name mismatch") + } + + return client, nil +} diff --git a/server/mcp/client/client_test.go b/server/mcp/client/client_test.go new file mode 100644 index 0000000..917d22d --- /dev/null +++ b/server/mcp/client/client_test.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "fmt" + "github.com/mark3labs/mcp-go/mcp" + "testing" +) + +// 测试 MCP 客户端连接 +func TestMcpClientConnection(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestTools(t *testing.T) { + t.Run("currentTime", func(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + request := mcp.CallToolRequest{} + request.Params.Name = "currentTime" + request.Params.Arguments = map[string]interface{}{ + "timezone": "UTC+8", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) + + t.Run("getNickname", func(t *testing.T) { + + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Initialize + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "test-client", + Version: "1.0.0", + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + t.Fatalf("初始化失败: %v", err) + } + + request := mcp.CallToolRequest{} + request.Params.Name = "getNickname" + request.Params.Arguments = map[string]interface{}{ + "username": "admin", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) +} + +func TestGetTools(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + toolsRequest := mcp.ListToolsRequest{} + + toolListResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + t.Fatalf("获取工具列表失败: %v", err) + } + for i := range toolListResult.Tools { + tool := toolListResult.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } +} diff --git a/server/mcp/context.go b/server/mcp/context.go new file mode 100644 index 0000000..932aa12 --- /dev/null +++ b/server/mcp/context.go @@ -0,0 +1,66 @@ +package mcpTool + +import ( + "context" + "net/http" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type mcpContextKey string + +const authTokenContextKey mcpContextKey = "mcp-auth-token" + +func WithHTTPRequestContext(ctx context.Context, r *http.Request) context.Context { + token := extractIncomingAuthToken(r.Header) + return context.WithValue(ctx, authTokenContextKey, token) +} + +func configuredAuthHeader() string { + if header := strings.TrimSpace(global.GVA_CONFIG.MCP.AuthHeader); header != "" { + return header + } + return "x-token" +} + +func ConfiguredAuthHeader() string { + return configuredAuthHeader() +} + +func authTokenFromContext(ctx context.Context) string { + token, _ := ctx.Value(authTokenContextKey).(string) + return strings.TrimSpace(token) +} + +func extractIncomingAuthToken(headers http.Header) string { + candidates := []string{ + configuredAuthHeader(), + "x-token", + "token", + "authorization", + } + + seen := make(map[string]struct{}, len(candidates)) + for _, name := range candidates { + key := strings.ToLower(strings.TrimSpace(name)) + if key == "" { + continue + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + + value := strings.TrimSpace(headers.Get(name)) + if value == "" { + continue + } + if key == "authorization" { + return strings.TrimSpace(strings.TrimPrefix(value, "Bearer ")) + } + return value + } + + return "" +} diff --git a/server/mcp/dictionary_generator.go b/server/mcp/dictionary_generator.go new file mode 100644 index 0000000..7fa5b26 --- /dev/null +++ b/server/mcp/dictionary_generator.go @@ -0,0 +1,174 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&DictionaryOptionsGenerator{}) +} + +type DictionaryOptionsGenerator struct{} + +type DictionaryOption struct { + Label string `json:"label"` + Value string `json:"value"` + Sort int `json:"sort"` +} + +type DictionaryGenerateRequest struct { + DictType string `json:"dictType"` + FieldDesc string `json:"fieldDesc"` + Options []DictionaryOption `json:"options"` + DictName string `json:"dictName"` + Description string `json:"description"` +} + +type DictionaryGenerateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DictType string `json:"dictType"` + OptionsCount int `json:"optionsCount"` +} + +func (d *DictionaryOptionsGenerator) New() mcp.Tool { + return mcp.NewTool("generate_dictionary_options", + mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"), + mcp.WithString("dictType", + mcp.Required(), + mcp.Description("字典类型,用于标识字典的唯一性"), + ), + mcp.WithString("fieldDesc", + mcp.Required(), + mcp.Description("字段描述,用于AI理解字段含义"), + ), + mcp.WithString("options", + mcp.Required(), + mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"), + ), + mcp.WithString("dictName", + mcp.Description("字典名称,如果不提供将自动生成"), + ), + mcp.WithString("description", + mcp.Description("字典描述"), + ), + ) +} + +func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + dictType, ok := args["dictType"].(string) + if !ok || dictType == "" { + return nil, errors.New("dictType 参数是必需的") + } + fieldDesc, ok := args["fieldDesc"].(string) + if !ok || fieldDesc == "" { + return nil, errors.New("fieldDesc 参数是必需的") + } + optionsStr, ok := args["options"].(string) + if !ok || optionsStr == "" { + return nil, errors.New("options 参数是必需的") + } + + var options []DictionaryOption + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return nil, fmt.Errorf("options 参数格式错误: %v", err) + } + if len(options) == 0 { + return nil, errors.New("options 不能为空") + } + + req := &DictionaryGenerateRequest{ + DictType: dictType, + FieldDesc: fieldDesc, + Options: options, + DictName: stringValue(args["dictName"]), + Description: stringValue(args["description"]), + } + + result, err := d.createDictionaryWithOptions(ctx, req) + if err != nil { + return nil, err + } + + return textResultWithJSON("字典选项生成结果:", result) +} + +func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) { + existingDict, err := findDictionaryByType(ctx, req.DictType) + if err != nil { + return nil, fmt.Errorf("检查字典是否存在失败: %v", err) + } + if existingDict != nil { + return &DictionaryGenerateResponse{ + Success: false, + Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType), + DictType: req.DictType, + OptionsCount: 0, + }, nil + } + + dictName := req.DictName + if dictName == "" { + dictName = d.generateDictionaryName(req.DictType, req.FieldDesc) + } + + if err := createDictionary(ctx, system.SysDictionary{ + Name: dictName, + Type: req.DictType, + Status: enabledBoolPointer(), + Desc: req.Description, + }); err != nil { + return nil, fmt.Errorf("创建字典失败: %v", err) + } + + createdDict, err := findDictionaryByType(ctx, req.DictType) + if err != nil { + return nil, fmt.Errorf("获取创建的字典失败: %v", err) + } + if createdDict == nil { + return nil, fmt.Errorf("获取创建的字典失败") + } + + successCount := 0 + for _, option := range req.Options { + err := createDictionaryDetail(ctx, system.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: enabledBoolPointer(), + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + }) + if err == nil { + successCount++ + } + } + + return &DictionaryGenerateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount), + DictType: req.DictType, + OptionsCount: successCount, + }, nil +} + +func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string { + if fieldDesc != "" { + return fmt.Sprintf("%s字典", fieldDesc) + } + return fmt.Sprintf("%s字典", dictType) +} + +func stringValue(value any) string { + if str, ok := value.(string); ok { + return str + } + return "" +} diff --git a/server/mcp/dictionary_http.go b/server/mcp/dictionary_http.go new file mode 100644 index 0000000..96c8c03 --- /dev/null +++ b/server/mcp/dictionary_http.go @@ -0,0 +1,73 @@ +package mcpTool + +import ( + "context" + "net/url" + "strconv" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +type exportedDictionary struct { + Name string `json:"name"` + Type string `json:"type"` + Status *bool `json:"status"` + Desc string `json:"desc"` + SysDictionaryDetails []system.SysDictionaryDetail `json:"sysDictionaryDetails"` +} + +func fetchDictionaryList(ctx context.Context, keyword string) ([]system.SysDictionary, error) { + query := url.Values{} + if keyword != "" { + query.Set("name", keyword) + } + + resp, err := getUpstream[[]system.SysDictionary](ctx, "/sysDictionary/getSysDictionaryList", query) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func findDictionaryByType(ctx context.Context, dictType string) (*system.SysDictionary, error) { + dictionaries, err := fetchDictionaryList(ctx, dictType) + if err != nil { + return nil, err + } + + for _, dictionary := range dictionaries { + if dictionary.Type == dictType { + dict := dictionary + return &dict, nil + } + } + + return nil, nil +} + +func exportDictionary(ctx context.Context, id uint) (*exportedDictionary, error) { + query := url.Values{} + query.Set("id", strconv.FormatUint(uint64(id), 10)) + + resp, err := getUpstream[exportedDictionary](ctx, "/sysDictionary/exportSysDictionary", query) + if err != nil { + return nil, err + } + + return &resp.Data, nil +} + +func createDictionary(ctx context.Context, dictionary system.SysDictionary) error { + _, err := postUpstream[map[string]any](ctx, "/sysDictionary/createSysDictionary", dictionary) + return err +} + +func createDictionaryDetail(ctx context.Context, detail system.SysDictionaryDetail) error { + _, err := postUpstream[map[string]any](ctx, "/sysDictionaryDetail/createSysDictionaryDetail", detail) + return err +} + +func enabledBoolPointer() *bool { + return utils.Pointer(true) +} diff --git a/server/mcp/dictionary_query.go b/server/mcp/dictionary_query.go new file mode 100644 index 0000000..9d704cd --- /dev/null +++ b/server/mcp/dictionary_query.go @@ -0,0 +1,139 @@ +package mcpTool + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&DictionaryQuery{}) +} + +type DictionaryPre struct { + Type string `json:"type"` + Desc string `json:"desc"` +} + +type DictionaryInfo struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Status *bool `json:"status"` + Desc string `json:"desc"` + Details []DictionaryDetailInfo `json:"details"` +} + +type DictionaryDetailInfo struct { + ID uint `json:"id"` + Label string `json:"label"` + Value string `json:"value"` + Extend string `json:"extend"` + Status *bool `json:"status"` + Sort int `json:"sort"` +} + +type DictionaryQueryResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Total int `json:"total"` + Dictionaries []DictionaryInfo `json:"dictionaries"` +} + +type DictionaryQuery struct{} + +func (d *DictionaryQuery) New() mcp.Tool { + return mcp.NewTool("query_dictionaries", + mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"), + mcp.WithString("dictType", + mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"), + ), + mcp.WithBoolean("includeDisabled", + mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"), + ), + mcp.WithBoolean("detailsOnly", + mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"), + ), + ) +} + +func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + dictType := stringValue(args["dictType"]) + includeDisabled, _ := args["includeDisabled"].(bool) + detailsOnly, _ := args["detailsOnly"].(bool) + + dictionaries, err := fetchDictionaryList(ctx, dictType) + if err != nil { + return nil, err + } + + result := make([]DictionaryInfo, 0) + for _, dictionary := range dictionaries { + if dictType != "" && dictionary.Type != dictType { + continue + } + if !includeDisabled && dictionary.Status != nil && !*dictionary.Status { + continue + } + + dictInfo, err := buildDictionaryInfo(ctx, dictionary, includeDisabled) + if err != nil { + return nil, err + } + result = append(result, dictInfo) + } + + if detailsOnly { + details := make([]DictionaryDetailInfo, 0) + for _, dictionary := range result { + details = append(details, dictionary.Details...) + } + return textResultWithJSON("", map[string]any{ + "success": true, + "message": "查询字典详情成功", + "total": len(details), + "details": details, + }) + } + + return textResultWithJSON("", DictionaryQueryResponse{ + Success: true, + Message: "查询字典成功", + Total: len(result), + Dictionaries: result, + }) +} + +func buildDictionaryInfo(ctx context.Context, dictionary system.SysDictionary, includeDisabled bool) (DictionaryInfo, error) { + exported, err := exportDictionary(ctx, dictionary.ID) + if err != nil { + return DictionaryInfo{}, err + } + + info := DictionaryInfo{ + ID: dictionary.ID, + Name: exported.Name, + Type: exported.Type, + Status: exported.Status, + Desc: exported.Desc, + } + + for _, detail := range exported.SysDictionaryDetails { + if !includeDisabled && detail.Status != nil && !*detail.Status { + continue + } + info.Details = append(info.Details, DictionaryDetailInfo{ + ID: detail.ID, + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + }) + } + + return info, nil +} diff --git a/server/mcp/enter.go b/server/mcp/enter.go new file mode 100644 index 0000000..ca19f54 --- /dev/null +++ b/server/mcp/enter.go @@ -0,0 +1,31 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // New 返回工具注册信息 + New() mcp.Tool +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + mcpTool := tool.New() + toolRegister[mcpTool.Name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.MCPServer) { + for _, tool := range toolRegister { + mcpServer.AddTool(tool.New(), tool.Handle) + } +} diff --git a/server/mcp/gva_analyze.go b/server/mcp/gva_analyze.go new file mode 100644 index 0000000..6b5f74f --- /dev/null +++ b/server/mcp/gva_analyze.go @@ -0,0 +1,494 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/mark3labs/mcp-go/mcp" +) + +// 注册工具 +func init() { + RegisterTool(&GVAAnalyzer{}) +} + +// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module +type GVAAnalyzer struct{} + +// AnalyzeRequest 分析请求结构体 +type AnalyzeRequest struct { + Requirement string `json:"requirement" binding:"required"` // 用户需求描述 +} + +// AnalyzeResponse 分析响应结构体 +type AnalyzeResponse struct { + ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息 +PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息 + Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息 + CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有) +} + +// ModuleInfo 模块信息 +type ModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + StructName string `json:"structName"` // 结构体名称 + TableName string `json:"tableName"` // 表名 + Description string `json:"description"` // 描述 +FilePaths []string `json:"filePaths"` // 相关文件路径 +} + +// PackageInfo 包信息 +type PackageInfo struct { + PackageName string `json:"packageName"` // 包名 +Template string `json:"template"` // 模板类型 + Label string `json:"label"` // 标签 + Desc string `json:"desc"` // 描述 + Module string `json:"module"` // 模块 + IsEmpty bool `json:"isEmpty"` // 是否为空包 +} + +// PredesignedModuleInfo 预设计模块信息 +type PredesignedModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 +Template string `json:"template"` // 模板类型 + FilePaths []string `json:"filePaths"` // 文件路径列表 + Description string `json:"description"` // 描述 +} + +// CleanupInfo 清理信息 +type CleanupInfo struct { + DeletedPackages []string `json:"deletedPackages"` // 已删除的包 + DeletedModules []string `json:"deletedModules"` // 已删除的模块 + CleanupMessage string `json:"cleanupMessage"` // 清理消息 +} + +// New 创建GVA分析器工具 +func (g *GVAAnalyzer) New() mcp.Tool { + return mcp.NewTool("gva_analyze", + mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"), + mcp.WithString("requirement", + mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"), + mcp.Required(), + ), + ) +} + +// Handle 处理分析请求 +func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + requirementStr, ok := request.GetArguments()["requirement"].(string) + if !ok || requirementStr == "" { + return nil, errors.New("参数错误:requirement 必须是非空字符串") + } + + // 创建分析请求 + analyzeReq := AnalyzeRequest{ + Requirement: requirementStr, + } + + // 执行分析逻辑 + response, err := g.performAnalysis(ctx, analyzeReq) + if err != nil { + return nil, fmt.Errorf("分析失败: %v", err) + } + + // 序列化响应 + responseJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil +} + +// performAnalysis 执行分析逻辑 +func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) { + _ = req + + packages, err := fetchAutoCodePackages(ctx) + if err != nil { + return nil, fmt.Errorf("获取包信息失败: %v", err) + } + + histories, err := fetchAutoCodeHistories(ctx) + if err != nil { + return nil, fmt.Errorf("获取历史记录失败: %v", err) + } + + cleanupInfo := &CleanupInfo{ + DeletedPackages: []string{}, + DeletedModules: []string{}, + } + + validPackages := make([]PackageInfo, 0, len(packages)) + var emptyPackageHistoryIDs []uint + + for _, pkg := range packages { + isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template) + if err != nil { +global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err)) + continue + } + + if isEmpty { + if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) + } else { + cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName) + } + + if err := deleteAutoCodePackage(ctx, pkg.ID); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err)) + } + + for _, history := range histories { + if history.Package == pkg.PackageName { + emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID) + cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName) + } + } + continue + } + + validPackages = append(validPackages, PackageInfo{ + PackageName: pkg.PackageName, + Template: pkg.Template, + Label: pkg.Label, + Desc: pkg.Desc, + Module: pkg.Module, + IsEmpty: false, + }) + } + + var dirtyHistoryIDs []uint + for _, history := range histories { + for _, emptyID := range emptyPackageHistoryIDs { + if history.ID == emptyID { + dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) + break + } + } + } + + if len(dirtyHistoryIDs) > 0 { + deletedCount := 0 + for _, historyID := range dirtyHistoryIDs { + if err := deleteAutoCodeHistory(ctx, historyID); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) + continue + } + deletedCount++ + } + if deletedCount > 0 { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", deletedCount)) + } + + if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err)) + } + } + + predesignedModules, err := g.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err)) + predesignedModules = []PredesignedModuleInfo{} + } + + filteredModules := []PredesignedModuleInfo{} + for _, module := range predesignedModules { + isDeleted := false + for _, deletedPkg := range cleanupInfo.DeletedPackages { + if module.PackageName == deletedPkg { + isDeleted = true + break + } + } + if !isDeleted { + filteredModules = append(filteredModules, module) + } + } + + dictionaries := []DictionaryPre{} + dictEntities, err := fetchDictionaryList(ctx, "") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err)) + } else { + for _, dictionary := range dictEntities { + dictionaries = append(dictionaries, DictionaryPre{ + Type: dictionary.Type, + Desc: dictionary.Desc, + }) + } + } + + var cleanupResult *CleanupInfo + if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 { + var message strings.Builder + message.WriteString("**系统清理完成**\n\n") + if len(cleanupInfo.DeletedPackages) > 0 { +message.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", "))) + } + if len(cleanupInfo.DeletedModules) > 0 { +message.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", "))) + } + cleanupInfo.CleanupMessage = message.String() + cleanupResult = cleanupInfo + } + + response := &AnalyzeResponse{ + ExistingPackages: validPackages, + PredesignedModules: filteredModules, + Dictionaries: dictionaries, + CleanupInfo: cleanupResult, + } + + return response, nil +} + +// isPackageFolderEmpty 检查包文件夹是否为空 +func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { + // 根据模板类型确定基础路径 + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName) + } + + // 检查文件夹是否存在 + if _, err := os.Stat(basePath); os.IsNotExist(err) { + return true, nil // 文件夹不存在,认为空 + } else if err != nil { + return false, err // 其他错误 + } + // 递归检查是否有.go文件 + return g.hasGoFilesRecursive(basePath) +} + +// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件 +func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) { + entries, err := os.ReadDir(dirPath) + if err != nil { + return true, err // 读取失败,返回空 + } + + // 检查当前目录下的.go文件 + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + return false, nil // 找到.go文件,不为空 + } + } + + // 递归检查子目录 + for _, entry := range entries { + if entry.IsDir() { + subDirPath := filepath.Join(dirPath, entry.Name()) + isEmpty, err := g.hasGoFilesRecursive(subDirPath) + if err != nil { + continue // 忽略子目录的错误,继续检查其他目录 + } + if !isEmpty { + return false, nil // 子目录中找到.go文件,不为空 + } + } + } + + return true, nil // 没有找到.go文件,为空 +} + +// removeEmptyPackageFolder 删除空包文件夹 +func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error { + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + // 对于package类型,需要删除多个目录 + paths := []string{ + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), + } + for _, path := range paths { + if err := g.removeDirectoryIfExists(path); err != nil { + return err + } + } + return nil + } + + return g.removeDirectoryIfExists(basePath) +} + +// removeDirectoryIfExists 删除目录(如果存在) +func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error { + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + return nil // 目录不存在,无需删除 + } else if err != nil { + return err // 其他错误 + } + + // 检查目录中是否包含go文件 + noGoFiles, err := g.hasGoFilesRecursive(dirPath) + if err != nil { + return err + } + // hasGoFilesRecursive 返回 false 表示发现了 go 文件 + if noGoFiles { + return os.RemoveAll(dirPath) + } + return nil +} + +// cleanupRelatedApiAndMenus 清理相关的API和菜单记录 +func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { + if len(historyIDs) == 0 { + return nil + } + + // 这里可以根据需要实现具体的API和菜单清理逻辑 + // 由于涉及到具体的业务逻辑,这里只做日志记录 + global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs)) + + // 可以调用service层的相关方法进行清理 + // 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs) + // 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs) + + return nil +} + +// scanPredesignedModules 扫描预设计模块 +func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { + // 获取autocode配置路径 + autocodeRoot := global.GVA_CONFIG.AutoCode.Root + if autocodeRoot == "" { + return nil, errors.New("autocode根路径未配置") + } + + var modules []PredesignedModuleInfo + + // 扫描plugin目录 + pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err)) + } else { + modules = append(modules, pluginModules...) + } + + // 扫描model目录 + modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err)) + } else { + modules = append(modules, modelModules...) + } + + return modules, nil +} + +// scanPluginModules 扫描插件模块 +func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(pluginDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(pluginDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + pluginName := entry.Name() + pluginPath := filepath.Join(pluginDir, pluginName) + + // 查找model目录 + modelDir := filepath.Join(pluginPath, "model") + if _, err := os.Stat(modelDir); err == nil { + // 扫描model目录下的模块 + pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err)) + continue + } + modules = append(modules, pluginModules...) + } + } + } + + return modules, nil +} + +// scanModelModules 扫描模型模块 +func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(modelDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(modelDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + packageName := entry.Name() + packagePath := filepath.Join(modelDir, packageName) + + // 扫描包目录下的模块 + packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err)) + continue + } + modules = append(modules, packageModules...) + } + } + + return modules, nil +} + +// scanModulesInDirectory 扫描目录中的模块 +func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + moduleName := strings.TrimSuffix(entry.Name(), ".go") + filePath := filepath.Join(dir, entry.Name()) + + module := PredesignedModuleInfo{ + ModuleName: moduleName, + PackageName: packageName, + Template: template, + FilePaths: []string{filePath}, + Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName), + } + modules = append(modules, module) + } + } + + return modules, nil +} diff --git a/server/mcp/gva_execute.go b/server/mcp/gva_execute.go new file mode 100644 index 0000000..c44de47 --- /dev/null +++ b/server/mcp/gva_execute.go @@ -0,0 +1,750 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/mark3labs/mcp-go/mcp" +) + +// 注册工具 +func init() { + RegisterTool(&GVAExecutor{}) +} + +// GVAExecutor GVA代码生成器 +type GVAExecutor struct{} + +// ExecuteRequest 执行请求结构体 +type ExecuteRequest struct { + ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划 + Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录) +} + +// ExecuteResponse 执行响应结构体 +type ExecuteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + PackageID uint `json:"packageId,omitempty"` + HistoryID uint `json:"historyId,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + GeneratedPaths []string `json:"generatedPaths,omitempty"` + NextActions []string `json:"nextActions,omitempty"` +} + +// ExecutionPlan 执行计划结构体 +type ExecutionPlan struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + NeedCreatedDictionaries bool `json:"needCreatedDictionaries"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"` +} + +// New 创建GVA代码生成执行器工具 +func (g *GVAExecutor) New() mcp.Tool { + return mcp.NewTool("gva_execute", + mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤** + +**核心功能:** +根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。 + +**使用场景:** +在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。 + +**重要提示:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具 +- 字段使用字典类型时,系统会自动检查并创建字典 +- 字典创建会在模块创建之前执行 +- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`), + mcp.WithObject("executionPlan", + mcp.Description("执行计划,包含包信息、模块与字典信息"), + mcp.Required(), + mcp.Properties(map[string]interface{}{ + "packageName": map[string]interface{}{ + "type": "string", + "description": "包名(小写开头)", + }, + "packageType": map[string]interface{}{ + "type": "string", + "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", + "enum": []string{"package", "plugin"}, + }, + "needCreatedPackage": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建包,为true时packageInfo必需", + }, + "needCreatedModules": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建模块,为true时modulesInfo必需", + }, + "needCreatedDictionaries": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建字典,为true时dictionariesInfo必需", + }, + "packageInfo": map[string]interface{}{ + "type": "object", + "description": "包创建信息,当needCreatedPackage=true时必需", + "properties": map[string]interface{}{ + "desc": map[string]interface{}{"type": "string", "description": "包描述"}, + "label": map[string]interface{}{"type": "string", "description": "展示名"}, + "template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}}, + "packageName": map[string]interface{}{"type": "string", "description": "包名"}, + }, + }, + "modulesInfo": map[string]interface{}{ + "type": "array", + "description": "模块配置列表,支持批量创建多个模块", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"}, + "tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"}, + "businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"}, + "structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"}, + "packageName": map[string]interface{}{"type": "string", "description": "文件名称"}, + "description": map[string]interface{}{"type": "string", "description": "中文描述"}, + "abbreviation": map[string]interface{}{"type": "string", "description": "简称"}, + "humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"}, + "gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"}, + "autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"}, + "autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"}, + "autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"}, + "autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"}, + "autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"}, + "onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"}, + "isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"}, + "treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"}, + "isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"}, + "generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"}, + "generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"}, + "fields": map[string]interface{}{ + "type": "array", + "description": "字段列表", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"}, + "fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"}, + "fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"}, + "dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"}, + "comment": map[string]interface{}{"type": "string", "description": "注释"}, + "columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"}, + "fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"}, + "fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"}, + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"}, + "form": map[string]interface{}{"type": "boolean", "description": "表单显示"}, + "table": map[string]interface{}{"type": "boolean", "description": "表格显示"}, + "desc": map[string]interface{}{"type": "boolean", "description": "详情显示"}, + "excel": map[string]interface{}{"type": "boolean", "description": "导入导出"}, + "require": map[string]interface{}{"type": "boolean", "description": "是否必填"}, + "defaultValue": map[string]interface{}{"type": "string", "description": "默认值"}, + "errorText": map[string]interface{}{"type": "string", "description": "错误提示"}, + "clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"}, + "sort": map[string]interface{}{"type": "boolean", "description": "是否排序"}, + "primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"}, + "dataSource": map[string]interface{}{ + "type": "object", + "description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空", + "properties": map[string]interface{}{ + "dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"}, + "table": map[string]interface{}{"type": "string", "description": "关联的表名"}, + "label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"}, + "value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"}, + "association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"}, + "hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"}, + }, + }, + "checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"}, + "fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"}, + }, + }, + }, + }, + }, + }, + "paths": map[string]interface{}{ + "type": "object", + "description": "生成的文件路径映射", + "additionalProperties": map[string]interface{}{"type": "string"}, + }, + "dictionariesInfo": map[string]interface{}{ + "type": "array", + "description": "字典创建信息,字典创建会在模块创建之前执行", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"}, + "dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"}, + "description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"}, + "status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"}, + "options": map[string]interface{}{ + "type": "array", + "description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"}, + "value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"}, + "sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"}, + }, + }, + }, + }, + }, + }, + }), + mcp.AdditionalProperties(false), + ), + mcp.WithString("requirement", + mcp.Description("原始需求描述(可选,用于日志记录)"), + ), + ) +} + +// Handle 处理执行请求(移除确认步骤) +func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := g.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 获取原始需求(可选) + var originalRequirement string + if reqData, ok := request.GetArguments()["requirement"]; ok { + if reqStr, ok := reqData.(string); ok { + originalRequirement = reqStr + } + } + + // 直接执行创建操作(无确认步骤) + result := g.executeCreation(ctx, &plan) + + // 如果执行成功且有原始需求,提供代码复检建议 + var reviewMessage string + if result.Success && originalRequirement != "" { + global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...") + + // 构建文件路径信息供AI使用 + var pathsInfo []string + for _, path := range result.GeneratedPaths { + pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path)) + } + + reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n")) + } else if originalRequirement == "" { + reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。" + } + + // 序列化响应 + response := ExecuteResponse{ + Success: result.Success, + Message: result.Message, + PackageID: result.PackageID, + HistoryID: result.HistoryID, + Paths: result.Paths, + GeneratedPaths: result.GeneratedPaths, + NextActions: result.NextActions, + } + + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)), + }, + }, nil +} + +// validateExecutionPlan 验证执行计划的完整性 +func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error { + if plan.PackageName == "" { + return errors.New("packageName 不能为空") + } + if plan.PackageType != "package" && plan.PackageType != "plugin" { + return errors.New("packageType 必须是 'package' 或 'plugin'") + } + + if plan.NeedCreatedPackage && plan.PackageInfo != nil && plan.PackageType != plan.PackageInfo.Template { + return errors.New("packageType 和 packageInfo.template 必须保持一致") + } + + if plan.NeedCreatedPackage { + if plan.PackageInfo == nil { + return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") + } + if plan.PackageInfo.PackageName == "" { + return errors.New("packageInfo.packageName 不能为空") + } + if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { + return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") + } + if plan.PackageInfo.Label == "" { + return errors.New("packageInfo.label 不能为空") + } + if plan.PackageInfo.Desc == "" { + return errors.New("packageInfo.desc 不能为空") + } + } + + if plan.NeedCreatedModules { + if len(plan.ModulesInfo) == 0 { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + + for moduleIndex, moduleInfo := range plan.ModulesInfo { + if moduleInfo.Package == "" { + return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) + } + if moduleInfo.StructName == "" { + return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) + } + if moduleInfo.TableName == "" { + return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) + } + if moduleInfo.Description == "" { + return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) + } + if moduleInfo.Abbreviation == "" { + return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) + } + if moduleInfo.PackageName == "" { + return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) + } + if moduleInfo.HumpPackageName == "" { + return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) + } + if len(moduleInfo.Fields) == 0 { + return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) + } + + for i, field := range moduleInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) + } + if len(field.FieldName) > 0 { + firstChar := string(field.FieldName[0]) + if firstChar >= "a" && firstChar <= "z" { + moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] + } + } + if field.FieldDesc == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) + } + if field.FieldType == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) + } + + validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} + validType := false + for _, validFieldType := range validFieldTypes { + if field.FieldType == validFieldType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持", moduleIndex+1, i+1, field.FieldType) + } + + if field.FieldSearchType != "" { + validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} + validSearchType := false + for _, validSearchTypeValue := range validSearchTypes { + if field.FieldSearchType == validSearchTypeValue { + validSearchType = true + break + } + } + if !validSearchType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持", moduleIndex+1, i+1, field.FieldSearchType) + } + } + + if field.DataSource != nil { + associationValue := field.DataSource.Association + if associationValue == 2 && field.FieldType != "array" { + global.GVA_LOG.Info(fmt.Sprintf("module %d field %d association=2, force fieldType to array", moduleIndex+1, i+1)) + moduleInfo.Fields[i].FieldType = "array" + } + if associationValue != 1 && associationValue != 2 { + return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1 或 2", moduleIndex+1, i+1) + } + } + } + + if !moduleInfo.GvaModel { + primaryKeyCount := 0 + for _, field := range moduleInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) + } + if primaryKeyCount > 1 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) + } + } else { + for i, field := range moduleInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false", moduleIndex+1, i+1) + } + } + } + } + } + + return nil +} + +// executeCreation 执行创建操作 +func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse { + result := &ExecuteResponse{ + Success: false, + Paths: make(map[string]string), + GeneratedPaths: []string{}, // 初始化生成文件路径列表 + } + + // 无论如何都先构建目录结构信息,确保paths始终返回 + result.Paths = g.buildDirectoryStructure(plan) + + // 记录预期生成的文件路径 + result.GeneratedPaths = g.collectExpectedFilePaths(plan) + + if !plan.NeedCreatedModules { + result.Success = true + result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " + return result + } + + // 创建包(如果需要) + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + err := createAutoCodePackage(ctx, plan.PackageInfo) + if err != nil { + result.Message = fmt.Sprintf("创建包失败: %v", err) + // 即使创建包失败,也要返回paths信息 + return result + } + result.Message += "包创建成功; " + } + + // 创建指定字典(如果需要) + if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 { + dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo) + result.Message += dictResult + } + + // 批量创建字典和模块(如果需要) + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + // 遍历所有模块进行创建 + for _, moduleInfo := range plan.ModulesInfo { + + // 创建模块 + err := moduleInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + + err = createAutoCodeModule(ctx, *moduleInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) + } + + result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) + + // 添加重要提醒:不要使用其他MCP工具 + result.Message += "\n\n⚠️ 重要提醒:\n" + result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" + result.Message += "- api_creator:API权限已在模块创建时自动生成\n" + result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" + result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// buildDirectoryStructure 构建目录结构信息 +func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { + paths := make(map[string]string) + + // 获取配置信息 + autoCodeConfig := global.GVA_CONFIG.AutoCode + + // 构建基础路径 + rootPath := autoCodeConfig.Root + serverPath := autoCodeConfig.Server + webPath := autoCodeConfig.Web + moduleName := autoCodeConfig.Module + + // 如果计划中有包名,使用计划中的包名,否则使用默认 + packageName := "example" + if plan.PackageName != "" { + packageName = plan.PackageName + } + + // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 + structName := "ExampleStruct" + if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { + structName = plan.ModulesInfo[0].StructName + } + + // 根据包类型构建不同的路径结构 + packageType := plan.PackageType + if packageType == "" { + packageType = "package" // 默认为package模式 + } + + // 构建服务端路径 + if serverPath != "" { + serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) + + if packageType == "plugin" { + // Plugin 模式:所有文件都在 /plugin/packageName/ 目录中 + plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) + + // API 路径 + paths["api"] = fmt.Sprintf("%s/api", plugingBasePath) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service", plugingBasePath) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model", plugingBasePath) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router", plugingBasePath) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath) + + // Plugin 特有文件 + paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath) + paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath) + paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath) + } else { + // Package 模式:传统的目录结构 + // API 路径 + paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) + } + } + + // 构建前端路径(两种模式相同) + if webPath != "" { + webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) + + if packageType == "plugin" { + // Plugin 模式:前端文件也在 /plugin/packageName/ 目录中 + pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName) + + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath) + } else { + // Package 模式:传统的目录结构 + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) + } + } + + // 添加模块信息 + paths["module"] = moduleName + paths["package_name"] = packageName + paths["package_type"] = packageType + paths["struct_name"] = structName + paths["root_path"] = rootPath + paths["server_path"] = serverPath + paths["web_path"] = webPath + + return paths +} + +// collectExpectedFilePaths 收集预期生成的文件路径 +func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string { + var paths []string + + // 获取目录结构 + dirPaths := g.buildDirectoryStructure(plan) + + // 如果需要创建模块,添加预期的文件路径 + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + for _, moduleInfo := range plan.ModulesInfo { + structName := moduleInfo.StructName + + // 后端文件 + if apiPath, ok := dirPaths["api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName))) + } + if servicePath, ok := dirPaths["service"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName))) + } + if modelPath, ok := dirPaths["model"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName))) + } + if routerPath, ok := dirPaths["router"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName))) + } + if requestPath, ok := dirPaths["request"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName))) + } + if responsePath, ok := dirPaths["response"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName))) + } + + // 前端文件 + if vuePage, ok := dirPaths["vue_page"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName))) + } + if vueApi, ok := dirPaths["vue_api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName))) + } + } + } + + return paths +} + +// checkDictionaryExists 检查字典是否存在 +func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) { + dictionary, err := findDictionaryByType(context.Background(), dictType) + if err != nil { + return false, err + } + return dictionary != nil, nil +} + +// createDictionariesFromInfo 根据 DictionariesInfo 创建字典 +func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string { + var messages []string + + messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo))) + + for _, dictInfo := range dictionariesInfo { + exists, err := g.checkDictionaryExists(dictInfo.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err)) + continue + } + + if !exists { + err = createDictionary(ctx, system.SysDictionary{ + Name: dictInfo.DictName, + Type: dictInfo.DictType, + Status: enabledBoolPointer(), + Desc: dictInfo.Description, + }) + if err != nil { + messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err)) + continue + } + + messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName)) + + createdDict, err := findDictionaryByType(ctx, dictInfo.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err)) + continue + } + if createdDict == nil { + messages = append(messages, fmt.Sprintf("获取创建的字典失败: %s; ", dictInfo.DictType)) + continue + } + + if len(dictInfo.Options) > 0 { + successCount := 0 + for _, option := range dictInfo.Options { + dictionaryDetail := system.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: enabledBoolPointer(), + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + } + + err = createDictionaryDetail(ctx, dictionaryDetail) + if err == nil { + successCount++ + } + } + messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount)) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType)) + } + } + + return strings.Join(messages, "") +} diff --git a/server/mcp/gva_review.go b/server/mcp/gva_review.go new file mode 100644 index 0000000..a32a544 --- /dev/null +++ b/server/mcp/gva_review.go @@ -0,0 +1,170 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/mark3labs/mcp-go/mcp" +) + +// GVAReviewer GVA代码审查工具 +type GVAReviewer struct{} + +// init 注册工具 +func init() { + RegisterTool(&GVAReviewer{}) +} + +// ReviewRequest 审查请求结构 +type ReviewRequest struct { + UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求 + GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表 +} + +// ReviewResponse 审查响应结构 +type ReviewResponse struct { + Success bool `json:"success"` // 是否审查成功 + Message string `json:"message"` // 审查结果消息 + AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示 + ReviewDetails string `json:"reviewDetails"` // 详细的审查结果 +} + +// New 创建GVA代码审查工具 +func (g *GVAReviewer) New() mcp.Tool { + return mcp.NewTool("gva_review", + mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用** + +**核心功能:** +- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表 +- 分析生成的代码是否满足用户的原始需求 +- 检查是否涉及到关联、交互等复杂功能 +- 如果代码不满足需求,提供调整建议和新的prompt + +**使用场景:** +- 在gva_execute成功执行后调用 +- 用于验证生成的代码是否完整满足用户需求 +- 检查模块间的关联关系是否正确实现 +- 发现缺失的交互功能或业务逻辑 + +**工作流程:** +1. 接收用户原始需求和生成的文件列表 +2. 分析需求中的关键功能点 +3. 检查生成的文件是否覆盖所有功能 +4. 识别缺失的关联关系、交互功能等 +5. 生成调整建议和新的开发prompt + +**输出内容:** +- 审查结果和是否需要调整 +- 详细的缺失功能分析 +- 针对性的代码调整建议 +- 可直接使用的开发prompt + +**重要提示:** +- 本工具专门用于代码质量审查,不执行实际的代码修改 +- 重点关注模块间关联、用户交互、业务流程完整性 +- 提供的调整建议应该具体可执行`), + mcp.WithString("userRequirement", + mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"), + mcp.Required(), + ), + mcp.WithString("generatedFiles", + mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"), + mcp.Required(), + ), + ) +} + +// Handle 处理审查请求 +func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取用户需求 + userRequirementData, ok := request.GetArguments()["userRequirement"] + if !ok { + return nil, errors.New("参数错误:userRequirement 必须提供") + } + + userRequirement, ok := userRequirementData.(string) + if !ok { + return nil, errors.New("参数错误:userRequirement 必须是字符串类型") + } + + // 获取生成的文件列表 + generatedFilesData, ok := request.GetArguments()["generatedFiles"] + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须提供") + } + + generatedFilesStr, ok := generatedFilesData.(string) + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串") + } + + // 解析JSON字符串为字符串数组 + var generatedFiles []string + err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles) + if err != nil { + return nil, fmt.Errorf("解析generatedFiles失败: %v", err) + } + + if len(generatedFiles) == 0 { + return nil, errors.New("参数错误:generatedFiles 不能为空") + } + + // 直接生成调整提示,不进行复杂分析 + adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles) + + // 构建简化的审查详情 + reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles)) + + // 构建审查结果 + reviewResult := &ReviewResponse{ + Success: true, + Message: "代码审查完成", + AdjustmentPrompt: adjustmentPrompt, + ReviewDetails: reviewDetails, + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(reviewResult, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化审查结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))), + }, + }, nil +} + +// generateAdjustmentPrompt 生成调整代码的提示 +func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string { + var prompt strings.Builder + + prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n") + prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement)) + prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n") + for _, file := range generatedFiles { + prompt.WriteString(fmt.Sprintf("- %s\n", file)) + } + prompt.WriteString("\n") + + prompt.WriteString("**请帮我优化和完善代码,确保:**\n") + prompt.WriteString("1. 代码完全满足用户的原始需求\n") + prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n") + prompt.WriteString("3. 实现所有必要的用户交互功能\n") + prompt.WriteString("4. 保持代码的完整性和可维护性\n") + prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n") + prompt.WriteString("6. 确保前后端功能完整对接\n") + prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n") + prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n") + prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n") + prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n") + prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n") + prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n") + prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**") + + return prompt.String() +} diff --git a/server/mcp/http_client.go b/server/mcp/http_client.go new file mode 100644 index 0000000..e2e9e74 --- /dev/null +++ b/server/mcp/http_client.go @@ -0,0 +1,153 @@ +package mcpTool + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type upstreamEnvelope[T any] struct { + Code int `json:"code"` + Data T `json:"data"` + Msg string `json:"msg"` +} + +func ResolveMCPServiceURL() string { + baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.BaseURL) + if baseURL != "" { + return strings.TrimRight(baseURL, "/") + } + + addr := global.GVA_CONFIG.MCP.Addr + if addr <= 0 { + addr = 8889 + } + + path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path) + if path == "" { + path = "/mcp" + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + return fmt.Sprintf("http://127.0.0.1:%d%s", addr, path) +} + +func upstreamBaseURL() string { + baseURL := strings.TrimSpace(global.GVA_CONFIG.MCP.UpstreamBaseURL) + if baseURL != "" { + return strings.TrimRight(baseURL, "/") + } + + return "http://127.0.0.1:8888" +} + +func requestTimeout() time.Duration { + timeout := global.GVA_CONFIG.MCP.RequestTimeout + if timeout <= 0 { + timeout = 15 + } + return time.Duration(timeout) * time.Second +} + +func getUpstream[T any](ctx context.Context, endpoint string, query url.Values) (*upstreamEnvelope[T], error) { + return doUpstream[T](ctx, http.MethodGet, endpoint, query, nil) +} + +func postUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) { + return doUpstream[T](ctx, http.MethodPost, endpoint, nil, body) +} + +func deleteUpstream[T any](ctx context.Context, endpoint string, body any) (*upstreamEnvelope[T], error) { + return doUpstream[T](ctx, http.MethodDelete, endpoint, nil, body) +} + +func doUpstream[T any](ctx context.Context, method, endpoint string, query url.Values, body any) (*upstreamEnvelope[T], error) { + token := authTokenFromContext(ctx) + if token == "" { + return nil, fmt.Errorf("缺少MCP鉴权请求头: %s", configuredAuthHeader()) + } + + endpoint = strings.TrimSpace(endpoint) + if endpoint == "" { + return nil, fmt.Errorf("上游接口路径不能为空") + } + if !strings.HasPrefix(endpoint, "/") { + endpoint = "/" + endpoint + } + + baseURL := upstreamBaseURL() + requestURL, err := url.Parse(baseURL + endpoint) + if err != nil { + return nil, fmt.Errorf("构建上游请求地址失败: %w", err) + } + if len(query) > 0 { + requestURL.RawQuery = query.Encode() + } + + var reader io.Reader + if body != nil { + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("序列化上游请求失败: %w", err) + } + reader = bytes.NewReader(payload) + } + + timeoutCtx, cancel := context.WithTimeout(ctx, requestTimeout()) + defer cancel() + + req, err := http.NewRequestWithContext(timeoutCtx, method, requestURL.String(), reader) + if err != nil { + return nil, fmt.Errorf("创建上游请求失败: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set(configuredAuthHeader(), token) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("请求上游服务失败: %w", err) + } + defer resp.Body.Close() + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取上游响应失败: %w", err) + } + + var result upstreamEnvelope[T] + if len(rawBody) > 0 { + if err := json.Unmarshal(rawBody, &result); err != nil { + return nil, fmt.Errorf("解析上游响应失败: %w", err) + } + } + + if resp.StatusCode >= http.StatusBadRequest { + if result.Msg != "" { + return nil, errors.New(result.Msg) + } + return nil, fmt.Errorf("上游请求失败,状态码: %d", resp.StatusCode) + } + + if result.Code != 0 { + if result.Msg != "" { + return nil, errors.New(result.Msg) + } + return nil, fmt.Errorf("上游请求失败,业务码: %d", result.Code) + } + + return &result, nil +} diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go new file mode 100644 index 0000000..da97b0e --- /dev/null +++ b/server/mcp/menu_creator.go @@ -0,0 +1,228 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&MenuCreator{}) +} + +type MenuCreateRequest struct { + ParentId uint `json:"parentId"` + Path string `json:"path"` + Name string `json:"name"` + Hidden bool `json:"hidden"` + Component string `json:"component"` + Sort int `json:"sort"` + Title string `json:"title"` + Icon string `json:"icon"` + KeepAlive bool `json:"keepAlive"` + DefaultMenu bool `json:"defaultMenu"` + CloseTab bool `json:"closeTab"` + ActiveName string `json:"activeName"` + Parameters []MenuParameterRequest `json:"parameters"` + MenuBtn []MenuButtonRequest `json:"menuBtn"` +} + +type MenuParameterRequest struct { + Type string `json:"type"` + Key string `json:"key"` + Value string `json:"value"` +} + +type MenuButtonRequest struct { + Name string `json:"name"` + Desc string `json:"desc"` +} + +type MenuCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + MenuID uint `json:"menuId"` + Name string `json:"name"` + Path string `json:"path"` +} + +type MenuCreator struct{} + +func (m *MenuCreator) New() mcp.Tool { + return mcp.NewTool("create_menu", + mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具 +- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`), + mcp.WithNumber("parentId", + mcp.Description("父菜单ID,0表示根菜单"), + mcp.DefaultNumber(0), + ), + mcp.WithString("path", + mcp.Required(), + mcp.Description("路由path,如:userList"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("路由name,用于Vue Router,如:userList"), + ), + mcp.WithBoolean("hidden", + mcp.Description("是否在菜单列表中隐藏"), + ), + mcp.WithString("component", + mcp.Required(), + mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"), + ), + mcp.WithNumber("sort", + mcp.Description("菜单排序号,数字越小越靠前"), + mcp.DefaultNumber(1), + ), + mcp.WithString("title", + mcp.Required(), + mcp.Description("菜单显示标题"), + ), + mcp.WithString("icon", + mcp.Description("菜单图标名称"), + mcp.DefaultString("menu"), + ), + mcp.WithBoolean("keepAlive", + mcp.Description("是否缓存页面"), + ), + mcp.WithBoolean("defaultMenu", + mcp.Description("是否是基础路由"), + ), + mcp.WithBoolean("closeTab", + mcp.Description("是否自动关闭tab"), + ), + mcp.WithString("activeName", + mcp.Description("高亮菜单名称"), + ), + mcp.WithString("parameters", + mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"), + ), + mcp.WithString("menuBtn", + mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"), + ), + ) +} + +func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + name, ok := args["name"].(string) + if !ok || name == "" { + return nil, errors.New("name 参数是必需的") + } + component, ok := args["component"].(string) + if !ok || component == "" { + return nil, errors.New("component 参数是必需的") + } + title, ok := args["title"].(string) + if !ok || title == "" { + return nil, errors.New("title 参数是必需的") + } + + parentID := uint(0) + if value, ok := args["parentId"].(float64); ok { + parentID = uint(value) + } + hidden, _ := args["hidden"].(bool) + sort := 1 + if value, ok := args["sort"].(float64); ok { + sort = int(value) + } + icon := "menu" + if value, ok := args["icon"].(string); ok && value != "" { + icon = value + } + keepAlive, _ := args["keepAlive"].(bool) + defaultMenu, _ := args["defaultMenu"].(bool) + closeTab, _ := args["closeTab"].(bool) + activeName, _ := args["activeName"].(string) + + parameters := make([]system.SysBaseMenuParameter, 0) + if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" { + var paramReqs []MenuParameterRequest + if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil { + return nil, fmt.Errorf("parameters 参数格式错误: %v", err) + } + for _, param := range paramReqs { + parameters = append(parameters, system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + }) + } + } + + menuBtns := make([]system.SysBaseMenuBtn, 0) + if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" { + var buttonReqs []MenuButtonRequest + if err := json.Unmarshal([]byte(menuBtnStr), &buttonReqs); err != nil { + return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err) + } + for _, button := range buttonReqs { + menuBtns = append(menuBtns, system.SysBaseMenuBtn{ + Name: button.Name, + Desc: button.Desc, + }) + } + } + + menu := system.SysBaseMenu{ + ParentId: parentID, + Path: path, + Name: name, + Hidden: hidden, + Component: component, + Sort: sort, + Meta: system.Meta{ + Title: title, + Icon: icon, + KeepAlive: keepAlive, + DefaultMenu: defaultMenu, + CloseTab: closeTab, + ActiveName: activeName, + }, + Parameters: parameters, + MenuBtn: menuBtns, + } + + if _, err := postUpstream[map[string]any](ctx, "/menu/addBaseMenu", menu); err != nil { + return nil, fmt.Errorf("创建菜单失败: %v", err) + } + + menuID := uint(0) + if menuListResp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{}); err == nil { + menuID = findMenuID(menuListResp.Data, name, path) + } + + return textResultWithJSON("菜单创建结果:", &MenuCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建菜单 %s", title), + MenuID: menuID, + Name: name, + Path: path, + }) +} + +func findMenuID(menus []system.SysBaseMenu, name, path string) uint { + for _, menu := range menus { + if menu.Name == name && menu.Path == path { + return menu.ID + } + if id := findMenuID(menu.Children, name, path); id != 0 { + return id + } + } + return 0 +} diff --git a/server/mcp/menu_lister.go b/server/mcp/menu_lister.go new file mode 100644 index 0000000..841663b --- /dev/null +++ b/server/mcp/menu_lister.go @@ -0,0 +1,59 @@ +package mcpTool + +import ( + "context" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&MenuLister{}) +} + +type MenuListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Menus []system.SysBaseMenu `json:"menus"` + TotalCount int `json:"totalCount"` + Description string `json:"description"` +} + +type MenuLister struct{} + +func (m *MenuLister) New() mcp.Tool { + return mcp.NewTool("list_all_menus", + mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转 + +**功能说明:** +- 返回完整的菜单树形结构 +- 包含路由配置信息(path、name、component) +- 包含菜单元数据(title、icon、keepAlive等) +- 包含菜单参数和按钮配置 +- 支持父子菜单关系展示 + +**使用场景:** +- 前端路由配置:获取所有菜单信息用于配置vue-router +- 菜单权限管理:了解系统中所有可用的菜单项 +- 导航组件开发:构建动态导航菜单 +- 系统架构分析:了解系统的菜单结构和页面组织`), + mcp.WithString("_placeholder", + mcp.Description("占位符,防止json schema校验失败"), + ), + ) +} + +func (m *MenuLister) Handle(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + resp, err := postUpstream[[]system.SysBaseMenu](ctx, "/menu/getMenuList", map[string]any{}) + if err != nil { + return nil, err + } + + return textResultWithJSON("", MenuListResponse{ + Success: true, + Message: "获取菜单列表成功", + Menus: resp.Data, + TotalCount: len(resp.Data), + Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息", + }) +} diff --git a/server/mcp/page.go b/server/mcp/page.go new file mode 100644 index 0000000..92c8832 --- /dev/null +++ b/server/mcp/page.go @@ -0,0 +1,8 @@ +package mcpTool + +type pageResultData[T any] struct { + List T `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} diff --git a/server/mcp/process_utils_unix.go b/server/mcp/process_utils_unix.go new file mode 100644 index 0000000..79c3461 --- /dev/null +++ b/server/mcp/process_utils_unix.go @@ -0,0 +1,37 @@ +//go:build !windows + +package mcpTool + +import ( + "errors" + "os/exec" + "syscall" +) + +func prepareDetachedProcess(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} + +func processExists(pid int) bool { + if pid <= 0 { + return false + } + + err := syscall.Kill(pid, 0) + return err == nil || errors.Is(err, syscall.EPERM) +} + +func terminateProcess(pid int) error { + if pid <= 0 { + return nil + } + + err := syscall.Kill(pid, syscall.SIGTERM) + if err == nil || errors.Is(err, syscall.ESRCH) { + return nil + } + + return err +} diff --git a/server/mcp/process_utils_windows.go b/server/mcp/process_utils_windows.go new file mode 100644 index 0000000..7e7b9ca --- /dev/null +++ b/server/mcp/process_utils_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package mcpTool + +import ( + "errors" + "os" + "os/exec" + "syscall" + + "golang.org/x/sys/windows" +) + +const windowsStillActive = 259 + +func prepareDetachedProcess(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: 0x00000008 | 0x00000200 | 0x08000000, + } +} + +func processExists(pid int) bool { + if pid <= 0 { + return false + } + + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return false + } + defer windows.CloseHandle(handle) + + var code uint32 + if err := windows.GetExitCodeProcess(handle, &code); err != nil { + return false + } + + return code == windowsStillActive +} + +func terminateProcess(pid int) error { + if pid <= 0 { + return nil + } + + process, err := os.FindProcess(pid) + if err != nil { + return err + } + + err = process.Kill() + if err == nil || errors.Is(err, os.ErrProcessDone) { + return nil + } + + return err +} diff --git a/server/mcp/requirement_analyzer.go b/server/mcp/requirement_analyzer.go new file mode 100644 index 0000000..765b750 --- /dev/null +++ b/server/mcp/requirement_analyzer.go @@ -0,0 +1,199 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&RequirementAnalyzer{}) +} + +type RequirementAnalyzer struct{} + +// RequirementAnalysisRequest 需求分析请求 +type RequirementAnalysisRequest struct { + UserRequirement string `json:"userRequirement"` +} + +// RequirementAnalysisResponse 需求分析响应 +type RequirementAnalysisResponse struct { + AIPrompt string `json:"aiPrompt"` // 给AI的提示词 +} + +// New 返回工具注册信息 +func (t *RequirementAnalyzer) New() mcp.Tool { + return mcp.NewTool("requirement_analyzer", + mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)** + +** 重要提示:这是所有MCP工具的首选入口,请优先使用!** + +** 核心能力:** +作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构 + +** 核心功能:** +1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系 +2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能 +3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求 +4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议 + +** 输出内容:** +- 模块数量和架构设计 +- 每个模块的详细字段清单 +- 数据类型和关联关系设计 +- 字典需求和类型定义 +- 模块间关系图和扩展建议 + +** 适用场景:** +- 用户需求描述不完整,需要智能补全 +- 复杂业务系统的模块架构设计 +- 需要专业的数据库设计建议 +- 想要快速搭建生产级业务系统 + +** 推荐工作流:** + requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具 + + `), + mcp.WithString("userRequirement", + mcp.Required(), + mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"), + ), + ) +} + +// Handle 处理工具调用 +func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userRequirement, ok := request.GetArguments()["userRequirement"].(string) + if !ok || userRequirement == "" { + return nil, errors.New("参数错误:userRequirement 必须是非空字符串") + } + + // 分析用户需求 + analysisResponse, err := t.analyzeRequirement(userRequirement) + if err != nil { + return nil, fmt.Errorf("需求分析失败: %v", err) + } + + // 序列化响应 + responseData, err := json.Marshal(analysisResponse) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseData)), + }, + }, nil +} + +// analyzeRequirement 分析用户需求 - 专注于AI需求传递 +func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) { + // 生成AI提示词 - 这是唯一功能 + aiPrompt := t.generateAIPrompt(userRequirement) + + return &RequirementAnalysisResponse{ + AIPrompt: aiPrompt, + }, nil +} + +// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构 +func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string { + prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务 + +## 用户原始需求 +%s + +## 核心任务 +你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。 + +## 分析步骤 + +### 第一步:需求解构分析 +请仔细分析用户需求,识别出: +1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等) +2. **业务流程**(如:注册、购买、记录、管理等) +3. **数据关系**(实体间的关联关系) +4. **功能模块**(需要哪些独立的管理模块) + +### 第二步:模块架构设计 +基于需求分析,设计出模块架构,格式如下: + +**模块1:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系,明确一对一/一对多] +- 字典需求:[需要哪些字典类型] + +**模块2:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系] +- 字典需求:[需要哪些字典类型] + +**...** + +### 第三步:字段详细设计 +为每个模块详细设计字段: + +#### 模块1字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +#### 模块2字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +## 智能分析指导原则 + +### 模块拆分原则 +1. **单一职责**:每个模块只负责一个核心业务实体 +2. **数据完整性**:相关数据应该在同一模块中 +3. **业务独立性**:模块应该能够独立完成特定业务功能 +4. **扩展性考虑**:为未来功能扩展预留空间 + +### 字段设计原则 +1. **必要性**:只包含业务必需的字段 +2. **规范性**:遵循数据库设计规范 +3. **关联性**:正确识别实体间关系 +4. **字典化**:状态、类型等枚举值使用字典 + +### 关联关系识别 +- **一对一**:一个实体只能关联另一个实体的一个记录 +- **一对多**:一个实体可以关联另一个实体的多个记录 +- **多对多**:通过中间表实现复杂关联 + +## 特殊场景处理 + +### 复杂实体识别 +当用户提到某个概念时,要判断它是否需要独立模块: +- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等) +- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录) + +## 输出要求 + +### 必须包含的信息 +1. **模块数量**:明确需要几个模块 +2. **模块关系图**:用文字描述模块间关系 +3. **核心字段**:每个模块的关键字段(至少5-10个) +4. **数据类型**:string、int、bool、time.Time、float64等 +5. **关联设计**:明确哪些字段是关联字段 +6. **字典需求**:列出需要创建的字典类型 + +### 严格遵循用户输入 +- 如果用户提供了具体字段,**必须使用**用户提供的字段 +- 如果用户提供了SQL文件,**严格按照**SQL结构设计 +- **不要**随意发散,**不要**添加用户未提及的功能 +--- + +**现在请开始深度分析用户需求:"%s"** + +请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement) + + return prompt +} diff --git a/server/mcp/result.go b/server/mcp/result.go new file mode 100644 index 0000000..70b84eb --- /dev/null +++ b/server/mcp/result.go @@ -0,0 +1,29 @@ +package mcpTool + +import ( + "encoding/json" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func textResultWithJSON(title string, payload any) (*mcp.CallToolResult, error) { + resultJSON, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %w", err) + } + + text := string(resultJSON) + if title != "" { + text = fmt.Sprintf("%s\n\n%s", title, text) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: text, + }, + }, + }, nil +} diff --git a/server/mcp/server.go b/server/mcp/server.go new file mode 100644 index 0000000..933f1cc --- /dev/null +++ b/server/mcp/server.go @@ -0,0 +1,52 @@ +package mcpTool + +import ( + "net/http" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + mcpServer "github.com/mark3labs/mcp-go/server" +) + +func NewMCPServer() *mcpServer.MCPServer { + config := global.GVA_CONFIG.MCP + + s := mcpServer.NewMCPServer( + config.Name, + config.Version, + ) + + global.GVA_MCP_SERVER = s + RegisterAllTools(s) + + return s +} + +func NewStreamableHTTPServer() *mcpServer.StreamableHTTPServer { + config := global.GVA_CONFIG.MCP + path := config.Path + if path == "" { + path = "/mcp" + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + mux := http.NewServeMux() + httpSrv := &http.Server{ + Handler: mux, + } + + handler := mcpServer.NewStreamableHTTPServer( + NewMCPServer(), + mcpServer.WithHTTPContextFunc(WithHTTPRequestContext), + mcpServer.WithStreamableHTTPServer(httpSrv), + ) + mux.Handle(path, handler) + mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + return handler +} diff --git a/server/mcp/standalone_manager.go b/server/mcp/standalone_manager.go new file mode 100644 index 0000000..a7ab497 --- /dev/null +++ b/server/mcp/standalone_manager.go @@ -0,0 +1,479 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +const ( + mcpRuntimeDirName = ".tmp" + mcpRuntimeSubDir = "mcp" + mcpRuntimeMetaName = "managed-process.json" + mcpRuntimeLogName = "mcp.log" + mcpHealthCheckTimeout = 2 * time.Second + mcpStartWaitTimeout = 20 * time.Second + mcpStopWaitTimeout = 8 * time.Second + mcpBuildTimeout = 2 * time.Minute +) + +type ManagedStandaloneStatus struct { + State string `json:"state"` + Managed bool `json:"managed"` + Reachable bool `json:"reachable"` + Starting bool `json:"starting"` + BaseURL string `json:"baseURL"` + HealthURL string `json:"healthURL"` + ListenAddr string `json:"listenAddr"` + Path string `json:"path"` + AuthHeader string `json:"authHeader"` + PID int `json:"pid,omitempty"` + LogPath string `json:"logPath,omitempty"` + StartedAt string `json:"startedAt,omitempty"` + LastError string `json:"lastError,omitempty"` + Message string `json:"message,omitempty"` +} + +type managedProcessMeta struct { + PID int `json:"pid"` + StartedAt string `json:"startedAt"` + LogPath string `json:"logPath"` + ConfigPath string `json:"configPath"` + WorkDir string `json:"workDir"` + Command string `json:"command"` + Args []string `json:"args"` +} + +func ResolveMCPListenAddr() string { + addr := global.GVA_CONFIG.MCP.Addr + if addr <= 0 { + addr = 8889 + } + return fmt.Sprintf(":%d", addr) +} + +func ResolveMCPPath() string { + path := strings.TrimSpace(global.GVA_CONFIG.MCP.Path) + if path == "" { + path = "/mcp" + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return path +} + +func ResolveMCPHealthURL() string { + baseURL, err := url.Parse(ResolveMCPServiceURL()) + if err != nil || baseURL.Scheme == "" || baseURL.Host == "" { + return fmt.Sprintf("http://127.0.0.1%s/health", ResolveMCPListenAddr()) + } + baseURL.Path = "/health" + baseURL.RawQuery = "" + baseURL.Fragment = "" + return baseURL.String() +} + +func GetManagedStandaloneStatus(ctx context.Context) ManagedStandaloneStatus { + reachable, reachErr := checkMCPHealth(ctx) + meta, _ := readManagedProcessMeta() + + if meta != nil && meta.PID > 0 && !processExists(meta.PID) { + _ = removeManagedProcessMeta() + meta = nil + } + + status := ManagedStandaloneStatus{ + Managed: meta != nil, + Reachable: reachable, + BaseURL: ResolveMCPServiceURL(), + HealthURL: ResolveMCPHealthURL(), + ListenAddr: ResolveMCPListenAddr(), + Path: ResolveMCPPath(), + AuthHeader: ConfiguredAuthHeader(), + } + + if meta != nil { + status.PID = meta.PID + status.LogPath = meta.LogPath + status.StartedAt = meta.StartedAt + } + + switch { + case meta != nil && reachable: + status.State = "running" + status.Message = "MCP 独立服务运行中" + case meta != nil && processExists(meta.PID): + status.State = "starting" + status.Managed = true + status.Starting = true + status.Message = "MCP 独立服务启动中" + case reachable: + status.State = "external" + status.Managed = false + status.Message = "检测到 MCP 服务已运行,但不是由页面启动的托管进程" + case meta != nil: + status.State = "stopped" + status.Managed = false + status.Message = "上次托管的 MCP 进程已退出" + default: + status.State = "stopped" + status.Message = "MCP 独立服务未启动" + } + + if !reachable && reachErr != nil && status.State != "stopped" { + status.LastError = reachErr.Error() + } + + return status +} + +func StartManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) { + current := GetManagedStandaloneStatus(ctx) + if current.Reachable { + return current, nil + } + + meta, _ := readManagedProcessMeta() + if meta != nil && meta.PID > 0 && processExists(meta.PID) { + return waitForManagedProcess(ctx, meta) + } + + commandPath, commandArgs, workDir, configPath, err := resolveManagedStartCommand() + if err != nil { + return GetManagedStandaloneStatus(context.Background()), err + } + + runtimeDir, err := ensureMCPRuntimeDir() + if err != nil { + return GetManagedStandaloneStatus(context.Background()), err + } + + logPath := filepath.Join(runtimeDir, mcpRuntimeLogName) + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return GetManagedStandaloneStatus(context.Background()), err + } + defer logFile.Close() + + cmd := exec.Command(commandPath, commandArgs...) + cmd.Dir = workDir + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.Env = append(os.Environ(), "GVA_MCP_CONFIG="+configPath) + prepareDetachedProcess(cmd) + + if err := cmd.Start(); err != nil { + return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("启动 MCP 独立服务失败: %w", err) + } + + pid := cmd.Process.Pid + _ = cmd.Process.Release() + + meta = &managedProcessMeta{ + PID: pid, + StartedAt: time.Now().Format(time.RFC3339), + LogPath: logPath, + ConfigPath: configPath, + WorkDir: workDir, + Command: commandPath, + Args: append([]string{}, commandArgs...), + } + if err := writeManagedProcessMeta(meta); err != nil { + return GetManagedStandaloneStatus(context.Background()), err + } + + return waitForManagedProcess(ctx, meta) +} + +func StopManagedStandalone(ctx context.Context) (ManagedStandaloneStatus, error) { + meta, err := readManagedProcessMeta() + if err != nil { + status := GetManagedStandaloneStatus(ctx) + if status.Reachable { + return status, errors.New("当前 MCP 服务不是由页面启动的,无法自动停用") + } + return status, nil + } + + if meta.PID > 0 && processExists(meta.PID) { + if err := terminateProcess(meta.PID); err != nil { + return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("停用 MCP 独立服务失败: %w", err) + } + } + + deadline := time.NewTimer(mcpStopWaitTimeout) + ticker := time.NewTicker(200 * time.Millisecond) + defer deadline.Stop() + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return GetManagedStandaloneStatus(context.Background()), ctx.Err() + case <-deadline.C: + _ = removeManagedProcessMeta() + return GetManagedStandaloneStatus(context.Background()), nil + case <-ticker.C: + if meta.PID <= 0 || !processExists(meta.PID) { + _ = removeManagedProcessMeta() + status := GetManagedStandaloneStatus(context.Background()) + if status.Reachable { + status.State = "external" + status.Message = "托管进程已停止,但检测到还有其他 MCP 服务在运行" + } + return status, nil + } + } + } +} + +func checkMCPHealth(ctx context.Context) (bool, error) { + timeoutCtx, cancel := context.WithTimeout(context.Background(), mcpHealthCheckTimeout) + defer cancel() + + if ctx != nil { + if deadline, ok := ctx.Deadline(); ok { + timeoutCtx, cancel = context.WithDeadline(context.Background(), deadline) + defer cancel() + } + } + + req, err := http.NewRequestWithContext(timeoutCtx, http.MethodGet, ResolveMCPHealthURL(), nil) + if err != nil { + return false, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + return true, nil + } + + return false, fmt.Errorf("MCP 健康检查失败: %s", resp.Status) +} + +func waitForManagedProcess(ctx context.Context, meta *managedProcessMeta) (ManagedStandaloneStatus, error) { + deadline := time.NewTimer(mcpStartWaitTimeout) + ticker := time.NewTicker(300 * time.Millisecond) + defer deadline.Stop() + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return GetManagedStandaloneStatus(context.Background()), ctx.Err() + case <-deadline.C: + return GetManagedStandaloneStatus(context.Background()), fmt.Errorf("等待 MCP 独立服务启动超时,请查看日志: %s", meta.LogPath) + case <-ticker.C: + current := GetManagedStandaloneStatus(context.Background()) + if current.Reachable { + return current, nil + } + if meta.PID > 0 && !processExists(meta.PID) { + return current, fmt.Errorf("MCP 独立进程已退出,请查看日志: %s", meta.LogPath) + } + } + } +} + +func resolveManagedStartCommand() (string, []string, string, string, error) { + serverRoot := resolveMCPServerRoot() + if serverRoot == "" { + return "", nil, "", "", errors.New("未找到 server 根目录,无法启动 MCP 独立服务") + } + + configPath, err := resolveMCPConfigPath(serverRoot) + if err != nil { + return "", nil, "", "", err + } + + if explicit := strings.TrimSpace(os.Getenv("GVA_MCP_BIN")); explicit != "" { + if !fileExists(explicit) { + return "", nil, "", "", fmt.Errorf("GVA_MCP_BIN 指向的文件不存在: %s", explicit) + } + return explicit, []string{"-config", configPath}, filepath.Dir(explicit), configPath, nil + } + + binaryPath, err := ensureManagedBinary(serverRoot) + if err != nil { + return "", nil, "", "", err + } + + return binaryPath, []string{"-config", configPath}, serverRoot, configPath, nil +} + +func ensureManagedBinary(serverRoot string) (string, error) { + runtimeDir, err := ensureMCPRuntimeDir() + if err != nil { + return "", err + } + + binaryPath := filepath.Join(runtimeDir, managedBinaryName()) + sourceDir := filepath.Join(serverRoot, "cmd", "mcp") + + goBin, lookErr := exec.LookPath("go") + if lookErr == nil && isDir(sourceDir) { + buildCtx, cancel := context.WithTimeout(context.Background(), mcpBuildTimeout) + defer cancel() + + cmd := exec.CommandContext(buildCtx, goBin, "build", "-o", binaryPath, "./cmd/mcp") + cmd.Dir = serverRoot + output, err := cmd.CombinedOutput() + if err != nil { + message := strings.TrimSpace(string(output)) + if message != "" { + return "", fmt.Errorf("构建 MCP 独立服务失败: %w, 输出: %s", err, message) + } + return "", fmt.Errorf("构建 MCP 独立服务失败: %w", err) + } + return binaryPath, nil + } + + if fileExists(binaryPath) { + return binaryPath, nil + } + + return "", errors.New("未检测到可用的 Go 环境,且本地没有可复用的 MCP 独立二进制") +} + +func resolveMCPServerRoot() string { + root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root) + serverDir := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Server) + if serverDir == "" { + serverDir = "server" + } + + candidates := []string{} + if root != "" { + candidates = append(candidates, filepath.Join(root, filepath.FromSlash(serverDir))) + candidates = append(candidates, root) + } + + if cwd, err := os.Getwd(); err == nil { + candidates = append(candidates, cwd) + candidates = append(candidates, filepath.Join(cwd, "server")) + } + + for _, candidate := range candidates { + if isDir(filepath.Join(candidate, "cmd", "mcp")) { + return candidate + } + } + + if len(candidates) > 0 { + return candidates[0] + } + + return "" +} + +func resolveMCPConfigPath(serverRoot string) (string, error) { + candidates := []string{ + filepath.Join(serverRoot, "cmd", "mcp", "config.yaml"), + filepath.Join(serverRoot, "config.yaml"), + } + + for _, candidate := range candidates { + if fileExists(candidate) { + return candidate, nil + } + } + + return "", errors.New("未找到 MCP 配置文件,请确认 cmd/mcp/config.yaml 或 server/config.yaml 存在") +} + +func ensureMCPRuntimeDir() (string, error) { + runtimeDir := filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir) + if err := os.MkdirAll(runtimeDir, 0o755); err != nil { + return "", err + } + return runtimeDir, nil +} + +func resolveMCPProjectRoot() string { + root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root) + if root != "" { + return root + } + + serverRoot := resolveMCPServerRoot() + if serverRoot == "" { + return "." + } + + if fileExists(filepath.Join(serverRoot, "go.mod")) { + return filepath.Dir(serverRoot) + } + + return serverRoot +} + +func readManagedProcessMeta() (*managedProcessMeta, error) { + data, err := os.ReadFile(managedProcessMetaPath()) + if err != nil { + return nil, err + } + + var meta managedProcessMeta + if err := json.Unmarshal(data, &meta); err != nil { + return nil, err + } + return &meta, nil +} + +func writeManagedProcessMeta(meta *managedProcessMeta) error { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return err + } + return os.WriteFile(managedProcessMetaPath(), data, 0o644) +} + +func removeManagedProcessMeta() error { + err := os.Remove(managedProcessMetaPath()) + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err +} + +func managedProcessMetaPath() string { + runtimeDir, err := ensureMCPRuntimeDir() + if err != nil { + return filepath.Join(resolveMCPProjectRoot(), mcpRuntimeDirName, mcpRuntimeSubDir, mcpRuntimeMetaName) + } + return filepath.Join(runtimeDir, mcpRuntimeMetaName) +} + +func managedBinaryName() string { + if runtime.GOOS == "windows" { + return "gva-mcp-standalone.exe" + } + return "gva-mcp-standalone" +} + +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + +func isDir(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go new file mode 100644 index 0000000..f71aecd --- /dev/null +++ b/server/middleware/casbin_rbac.go @@ -0,0 +1,32 @@ +package middleware + +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/utils" + "github.com/gin-gonic/gin" + "strconv" + "strings" +) + +// CasbinHandler 拦截器 +func CasbinHandler() gin.HandlerFunc { + return func(c *gin.Context) { + waitUse, _ := utils.GetClaims(c) + //获取请求的PATH + path := c.Request.URL.Path + obj := strings.TrimPrefix(path, global.GVA_CONFIG.System.RouterPrefix) + // 获取请求方法 + act := c.Request.Method + // 获取用户的角色 + sub := strconv.Itoa(int(waitUse.AuthorityId)) + e := utils.GetCasbin() // 判断策略中是否存在 + success, _ := e.Enforce(sub, obj, act) + if !success { + response.FailWithDetailed(gin.H{}, "权限不足", c) + c.Abort() + return + } + c.Next() + } +} diff --git a/server/middleware/cors.go b/server/middleware/cors.go new file mode 100644 index 0000000..d7e3ccd --- /dev/null +++ b/server/middleware/cors.go @@ -0,0 +1,73 @@ +package middleware + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/gin-gonic/gin" + "net/http" +) + +// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 +func Cors() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + origin := c.Request.Header.Get("Origin") + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id") + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT") + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At") + c.Header("Access-Control-Allow-Credentials", "true") + + // 放行所有OPTIONS方法 + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // 处理请求 + c.Next() + } +} + +// CorsByRules 按照配置处理跨域请求 +func CorsByRules() gin.HandlerFunc { + // 放行全部 + if global.GVA_CONFIG.Cors.Mode == "allow-all" { + return Cors() + } + return func(c *gin.Context) { + whitelist := checkCors(c.GetHeader("origin")) + + // 通过检查, 添加请求头 + if whitelist != nil { + c.Header("Access-Control-Allow-Origin", whitelist.AllowOrigin) + c.Header("Access-Control-Allow-Headers", whitelist.AllowHeaders) + c.Header("Access-Control-Allow-Methods", whitelist.AllowMethods) + c.Header("Access-Control-Expose-Headers", whitelist.ExposeHeaders) + if whitelist.AllowCredentials { + c.Header("Access-Control-Allow-Credentials", "true") + } + } + + // 严格白名单模式且未通过检查,直接拒绝处理请求 + if whitelist == nil && global.GVA_CONFIG.Cors.Mode == "strict-whitelist" && !(c.Request.Method == "GET" && c.Request.URL.Path == "/health") { + c.AbortWithStatus(http.StatusForbidden) + } else { + // 非严格白名单模式,无论是否通过检查均放行所有 OPTIONS 方法 + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + } + } + + // 处理请求 + c.Next() + } +} + +func checkCors(currentOrigin string) *config.CORSWhitelist { + for _, whitelist := range global.GVA_CONFIG.Cors.Whitelist { + // 遍历配置中的跨域头,寻找匹配项 + if currentOrigin == whitelist.AllowOrigin { + return &whitelist + } + } + return nil +} diff --git a/server/middleware/email.go b/server/middleware/email.go new file mode 100644 index 0000000..1bc976c --- /dev/null +++ b/server/middleware/email.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "bytes" + "io" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" + utils2 "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func ErrorToEmail() gin.HandlerFunc { + return func(c *gin.Context) { + var username string + claims, _ := utils2.GetClaims(c) + if claims.Username != "" { + username = claims.Username + } else { + id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) + var u system.SysUser + err := global.GVA_DB.Where("id = ?", id).First(&u).Error + if err != nil { + username = "Unknown" + } + username = u.Username + } + body, _ := io.ReadAll(c.Request.Body) + // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: string(body), + } + now := time.Now() + + c.Next() + + latency := time.Since(now) + status := c.Writer.Status() + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + str := "接收到的请求为" + record.Body + "\n" + "请求方式为" + record.Method + "\n" + "报错信息如下" + record.ErrorMessage + "\n" + "耗时" + latency.String() + "\n" + if status != 200 { + subject := username + "" + record.Ip + "调用了" + record.Path + "报错了" + if err := utils.ErrorToEmail(subject, str); err != nil { + global.GVA_LOG.Error("ErrorToEmail Failed, err:", zap.Error(err)) + } + } + } +} diff --git a/server/middleware/error.go b/server/middleware/error.go new file mode 100644 index 0000000..c9f02df --- /dev/null +++ b/server/middleware/error.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "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" +) + +// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 +func GinRecovery(stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + global.GVA_LOG.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + // If the connection is dead, we can't write a status to it. + _ = c.Error(err.(error)) // nolint: errcheck + c.Abort() + return + } + + 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)), + ) + } 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)), + ) + } + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + c.Next() + } +} diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go new file mode 100644 index 0000000..f4c0296 --- /dev/null +++ b/server/middleware/jwt.go @@ -0,0 +1,89 @@ +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" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-gonic/gin" +) + +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 + token := utils.GetToken(c) + if token == "" { + response.NoAuth("未登录或非法访问,请登录", c) + c.Abort() + return + } + if isBlacklist(token) { + response.NoAuth("您的帐户异地登陆或令牌失效", c) + utils.ClearToken(c) + c.Abort() + return + } + j := utils.NewJWT() + // parseToken 解析token包含的信息 + claims, err := j.ParseToken(token) + if err != nil { + if errors.Is(err, utils.TokenExpired) { + response.NoAuth("登录已过期,请重新登录", c) + utils.ClearToken(c) + c.Abort() + return + } + response.NoAuth(err.Error(), c) + utils.ClearToken(c) + c.Abort() + return + } + + // 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开 + // 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开 + + //if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 { + // _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token}) + // response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + // c.Abort() + //} + c.Set("claims", claims) + if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime { + dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr)) + newToken, _ := j.CreateTokenByOldToken(token, *claims) + 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()/60)) + if global.GVA_CONFIG.System.UseMultipoint { + // 记录新的活跃jwt + _ = utils.SetRedisJWT(newToken, newClaims.Username) + } + } + c.Next() + + if newToken, exists := c.Get("new-token"); exists { + c.Header("new-token", newToken.(string)) + } + if newExpiresAt, exists := c.Get("new-expires-at"); exists { + c.Header("new-expires-at", newExpiresAt.(string)) + } + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func isBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok +} diff --git a/server/middleware/limit_ip.go b/server/middleware/limit_ip.go new file mode 100644 index 0000000..acc6e98 --- /dev/null +++ b/server/middleware/limit_ip.go @@ -0,0 +1,92 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "time" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-gonic/gin" +) + +type LimitConfig struct { + // GenerationKey 根据业务生成key 下面CheckOrMark查询生成 + GenerationKey func(c *gin.Context) string + // 检查函数,用户可修改具体逻辑,更加灵活 + CheckOrMark func(key string, expire int, limit int) error + // Expire key 过期时间 + Expire int + // Limit 周期时间 + Limit int +} + +func (l LimitConfig) LimitWithTime() gin.HandlerFunc { + return func(c *gin.Context) { + if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil { + c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.Error()}) + c.Abort() + return + } else { + c.Next() + } + } +} + +// DefaultGenerationKey 默认生成key +func DefaultGenerationKey(c *gin.Context) string { + return "GVA_Limit" + c.ClientIP() +} + +func DefaultCheckOrMark(key string, expire int, limit int) (err error) { + // 判断是否开启redis + if global.GVA_REDIS == nil { + return err + } + if err = SetLimitWithTime(key, limit, time.Duration(expire)*time.Second); err != nil { + global.GVA_LOG.Error("limit", zap.Error(err)) + } + return err +} + +func DefaultLimit() gin.HandlerFunc { + return LimitConfig{ + GenerationKey: DefaultGenerationKey, + CheckOrMark: DefaultCheckOrMark, + Expire: global.GVA_CONFIG.System.LimitTimeIP, + Limit: global.GVA_CONFIG.System.LimitCountIP, + }.LimitWithTime() +} + +// SetLimitWithTime 设置访问次数 +func SetLimitWithTime(key string, limit int, expiration time.Duration) error { + count, err := global.GVA_REDIS.Exists(context.Background(), key).Result() + if err != nil { + return err + } + if count == 0 { + pipe := global.GVA_REDIS.TxPipeline() + pipe.Incr(context.Background(), key) + pipe.Expire(context.Background(), key, expiration) + _, err = pipe.Exec(context.Background()) + return err + } else { + // 次数 + if times, err := global.GVA_REDIS.Get(context.Background(), key).Int(); err != nil { + return err + } else { + if times >= limit { + if t, err := global.GVA_REDIS.PTTL(context.Background(), key).Result(); err != nil { + return errors.New("请求太过频繁,请稍后再试") + } else { + return errors.New("请求太过频繁, 请 " + t.String() + " 秒后尝试") + } + } else { + return global.GVA_REDIS.Incr(context.Background(), key).Err() + } + } + } +} diff --git a/server/middleware/loadtls.go b/server/middleware/loadtls.go new file mode 100644 index 0000000..a17cf65 --- /dev/null +++ b/server/middleware/loadtls.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/unrolled/secure" +) + +// 用https把这个中间件在router里面use一下就好 + +func LoadTls() gin.HandlerFunc { + return func(c *gin.Context) { + middleware := secure.New(secure.Options{ + SSLRedirect: true, + SSLHost: "localhost:443", + }) + err := middleware.Process(c.Writer, c.Request) + if err != nil { + // 如果出现错误,请不要继续 + fmt.Println(err) + return + } + // 继续往下处理 + c.Next() + } +} diff --git a/server/middleware/logger.go b/server/middleware/logger.go new file mode 100644 index 0000000..fabc334 --- /dev/null +++ b/server/middleware/logger.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// LogLayout 日志layout +type LogLayout struct { + Time time.Time + Metadata map[string]interface{} // 存储自定义原数据 + Path string // 访问路径 + Query string // 携带query + Body string // 携带body数据 + IP string // ip地址 + UserAgent string // 代理 + Error string // 错误 + Cost time.Duration // 花费时间 + Source string // 来源 +} + +type Logger struct { + // Filter 用户自定义过滤 + Filter func(c *gin.Context) bool + // FilterKeyword 关键字过滤(key) + FilterKeyword func(layout *LogLayout) bool + // AuthProcess 鉴权处理 + AuthProcess func(c *gin.Context, layout *LogLayout) + // 日志处理 + Print func(LogLayout) + // Source 服务唯一标识 + Source string +} + +func (l Logger) SetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + var body []byte + if l.Filter != nil && !l.Filter(c) { + body, _ = c.GetRawData() + // 将原body塞回去 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + c.Next() + cost := time.Since(start) + layout := LogLayout{ + Time: time.Now(), + Path: path, + Query: query, + IP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"), + Cost: cost, + Source: l.Source, + } + if l.Filter != nil && !l.Filter(c) { + layout.Body = string(body) + } + if l.AuthProcess != nil { + // 处理鉴权需要的信息 + l.AuthProcess(c, &layout) + } + if l.FilterKeyword != nil { + // 自行判断key/value 脱敏等 + l.FilterKeyword(&layout) + } + // 自行处理日志 + l.Print(layout) + } +} + +func DefaultLogger() gin.HandlerFunc { + return Logger{ + Print: func(layout LogLayout) { + // 标准输出,k8s做收集 + v, _ := json.Marshal(layout) + fmt.Println(string(v)) + }, + Source: "GVA", + }.SetLoggerMiddleware() +} diff --git a/server/middleware/operation.go b/server/middleware/operation.go new file mode 100644 index 0000000..dd545f5 --- /dev/null +++ b/server/middleware/operation.go @@ -0,0 +1,129 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var respPool sync.Pool +var bufferSize = 1024 + +func init() { + respPool.New = func() interface{} { + return make([]byte, bufferSize) + } +} + +func OperationRecord() gin.HandlerFunc { + return func(c *gin.Context) { + var body []byte + var userId int + if c.Request.Method != http.MethodGet { + var err error + body, err = io.ReadAll(c.Request.Body) + if err != nil { + global.GVA_LOG.Error("read body from request error:", zap.Error(err)) + } else { + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + } else { + query := c.Request.URL.RawQuery + query, _ = url.QueryUnescape(query) + split := strings.Split(query, "&") + m := make(map[string]string) + for _, v := range split { + kv := strings.Split(v, "=") + if len(kv) == 2 { + m[kv[0]] = kv[1] + } + } + body, _ = json.Marshal(&m) + } + claims, _ := utils.GetClaims(c) + if claims != nil && claims.BaseClaims.ID != 0 { + userId = int(claims.BaseClaims.ID) + } else { + id, err := strconv.Atoi(c.Request.Header.Get("x-user-id")) + if err != nil { + userId = 0 + } + userId = id + } + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: "", + UserID: userId, + } + + // 上传文件时候 中间件日志进行裁断操作 + if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { + record.Body = "[文件]" + } else { + if len(body) > bufferSize { + record.Body = "[超出记录长度]" + } else { + record.Body = string(body) + } + } + + writer := responseBodyWriter{ + ResponseWriter: c.Writer, + body: &bytes.Buffer{}, + } + c.Writer = writer + now := time.Now() + + c.Next() + + latency := time.Since(now) + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + record.Status = c.Writer.Status() + record.Latency = latency + record.Resp = writer.body.String() + + if strings.Contains(c.Writer.Header().Get("Pragma"), "public") || + strings.Contains(c.Writer.Header().Get("Expires"), "0") || + strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/vnd.ms-excel") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") || + strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") || + strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") { + if len(record.Resp) > bufferSize { + // 截断 + record.Body = "超出记录长度" + } + } + if err := global.GVA_DB.Create(&record).Error; err != nil { + global.GVA_LOG.Error("create operation record error:", zap.Error(err)) + } + } +} + +type responseBodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseBodyWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} diff --git a/server/middleware/timeout.go b/server/middleware/timeout.go new file mode 100644 index 0000000..473abf6 --- /dev/null +++ b/server/middleware/timeout.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +// TimeoutMiddleware 创建超时中间件 +// 入参 timeout 设置超时时间(例如:time.Second * 5) +// 使用示例 xxx.Get("path",middleware.TimeoutMiddleware(30*time.Second),HandleFunc) +func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + c.Request = c.Request.WithContext(ctx) + + // 使用 buffered channel 避免 goroutine 泄漏 + done := make(chan struct{}, 1) + panicChan := make(chan interface{}, 1) + + go func() { + defer func() { + if p := recover(); p != nil { + select { + case panicChan <- p: + default: + } + } + select { + case done <- struct{}{}: + default: + } + }() + c.Next() + }() + + select { + case p := <-panicChan: + panic(p) + case <-done: + return + case <-ctx.Done(): + // 确保服务器超时设置足够长 + c.Header("Connection", "close") + c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{ + "code": 504, + "msg": "请求超时", + }) + return + } + } +} diff --git a/server/model/common/basetypes.go b/server/model/common/basetypes.go new file mode 100644 index 0000000..1a133d5 --- /dev/null +++ b/server/model/common/basetypes.go @@ -0,0 +1,43 @@ +package common + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +type JSONMap map[string]interface{} + +func (m JSONMap) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + return json.Marshal(m) +} + +func (m *JSONMap) Scan(value interface{}) error { + if value == nil { + *m = make(map[string]interface{}) + return nil + } + var err error + switch value.(type) { + case []byte: + err = json.Unmarshal(value.([]byte), m) + case string: + err = json.Unmarshal([]byte(value.(string)), m) + default: + err = errors.New("basetypes.JSONMap.Scan: invalid value type") + } + if err != nil { + return err + } + return nil +} + +type TreeNode[T any] interface { + GetChildren() []T + SetChildren(children T) + GetID() int + GetParentID() int +} diff --git a/server/model/common/clearDB.go b/server/model/common/clearDB.go new file mode 100644 index 0000000..e7fc757 --- /dev/null +++ b/server/model/common/clearDB.go @@ -0,0 +1,7 @@ +package common + +type ClearDB struct { + TableName string + CompareField string + Interval string +} diff --git a/server/model/common/request/common.go b/server/model/common/request/common.go new file mode 100644 index 0000000..c729f3d --- /dev/null +++ b/server/model/common/request/common.go @@ -0,0 +1,48 @@ +package request + +import ( + "gorm.io/gorm" +) + +// PageInfo Paging common input parameter structure +type PageInfo struct { + Page int `json:"page" form:"page"` // 页码 + PageSize int `json:"pageSize" form:"pageSize"` // 每页大小 + Keyword string `json:"keyword" form:"keyword"` // 关键字 +} + +func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if r.Page <= 0 { + r.Page = 1 + } + switch { + case r.PageSize > 100: + r.PageSize = 100 + case r.PageSize <= 0: + r.PageSize = 10 + } + offset := (r.Page - 1) * r.PageSize + return db.Offset(offset).Limit(r.PageSize) + } +} + +// GetById Find by id structure +type GetById struct { + ID int `json:"id" form:"id"` // 主键ID +} + +func (r *GetById) Uint() uint { + return uint(r.ID) +} + +type IdsReq struct { + Ids []int `json:"ids" form:"ids"` +} + +// GetAuthorityId Get role by id structure +type GetAuthorityId struct { + AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID +} + +type Empty struct{} diff --git a/server/model/common/response/common.go b/server/model/common/response/common.go new file mode 100644 index 0000000..7461096 --- /dev/null +++ b/server/model/common/response/common.go @@ -0,0 +1,8 @@ +package response + +type PageResult struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} diff --git a/server/model/common/response/response.go b/server/model/common/response/response.go new file mode 100644 index 0000000..f0e0e53 --- /dev/null +++ b/server/model/common/response/response.go @@ -0,0 +1,62 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` +} + +const ( + ERROR = 7 + SUCCESS = 0 +) + +func Result(code int, data interface{}, msg string, c *gin.Context) { + c.JSON(http.StatusOK, Response{ + code, + data, + msg, + }) +} + +func Ok(c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, "操作成功", c) +} + +func OkWithMessage(message string, c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, message, c) +} + +func OkWithData(data interface{}, c *gin.Context) { + Result(SUCCESS, data, "成功", c) +} + +func OkWithDetailed(data interface{}, message string, c *gin.Context) { + Result(SUCCESS, data, message, c) +} + +func Fail(c *gin.Context) { + Result(ERROR, map[string]interface{}{}, "操作失败", c) +} + +func FailWithMessage(message string, c *gin.Context) { + Result(ERROR, map[string]interface{}{}, message, c) +} + +func NoAuth(message string, c *gin.Context) { + c.JSON(http.StatusUnauthorized, Response{ + 7, + nil, + message, + }) +} + +func FailWithDetailed(data interface{}, message string, c *gin.Context) { + Result(ERROR, data, message, c) +} diff --git a/server/model/example/exa_attachment_category.go b/server/model/example/exa_attachment_category.go new file mode 100644 index 0000000..a643b61 --- /dev/null +++ b/server/model/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type ExaAttachmentCategory struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"default:null;type:varchar(255);column:name;comment:分类名称;"` + Pid uint `json:"pid" form:"pid" gorm:"default:0;type:int;column:pid;comment:父节点ID;"` + Children []*ExaAttachmentCategory `json:"children" gorm:"-"` +} + +func (ExaAttachmentCategory) TableName() string { + return "exa_attachment_category" +} diff --git a/server/model/example/exa_breakpoint_continue.go b/server/model/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..3c2924b --- /dev/null +++ b/server/model/example/exa_breakpoint_continue.go @@ -0,0 +1,24 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// file struct, 文件结构体 +type ExaFile struct { + global.GVA_MODEL + FileName string + FileMd5 string + FilePath string + ExaFileChunk []ExaFileChunk + ChunkTotal int + IsFinish bool +} + +// file chunk struct, 切片结构体 +type ExaFileChunk struct { + global.GVA_MODEL + ExaFileID uint + FileChunkNumber int + FileChunkPath string +} diff --git a/server/model/example/exa_customer.go b/server/model/example/exa_customer.go new file mode 100644 index 0000000..e78dd09 --- /dev/null +++ b/server/model/example/exa_customer.go @@ -0,0 +1,15 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type ExaCustomer struct { + global.GVA_MODEL + CustomerName string `json:"customerName" form:"customerName" gorm:"comment:客户名"` // 客户名 + CustomerPhoneData string `json:"customerPhoneData" form:"customerPhoneData" gorm:"comment:客户手机号"` // 客户手机号 + SysUserID uint `json:"sysUserId" form:"sysUserId" gorm:"comment:管理ID"` // 管理ID + SysUserAuthorityID uint `json:"sysUserAuthorityID" form:"sysUserAuthorityID" gorm:"comment:管理角色ID"` // 管理角色ID + SysUser system.SysUser `json:"sysUser" form:"sysUser" gorm:"comment:管理详情"` // 管理详情 +} diff --git a/server/model/example/exa_file_upload_download.go b/server/model/example/exa_file_upload_download.go new file mode 100644 index 0000000..4fe342d --- /dev/null +++ b/server/model/example/exa_file_upload_download.go @@ -0,0 +1,18 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type ExaFileUploadAndDownload struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:文件名"` // 文件名 + ClassId int `json:"classId" form:"classId" gorm:"default:0;type:int;column:class_id;comment:分类id;"` // 分类id + Url string `json:"url" form:"url" gorm:"column:url;comment:文件地址"` // 文件地址 + Tag string `json:"tag" form:"tag" gorm:"column:tag;comment:文件标签"` // 文件标签 + Key string `json:"key" form:"key" gorm:"column:key;comment:编号"` // 编号 +} + +func (ExaFileUploadAndDownload) TableName() string { + return "exa_file_upload_and_downloads" +} diff --git a/server/model/example/request/exa_file_upload_and_downloads.go b/server/model/example/request/exa_file_upload_and_downloads.go new file mode 100644 index 0000000..b9ff684 --- /dev/null +++ b/server/model/example/request/exa_file_upload_and_downloads.go @@ -0,0 +1,10 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" +) + +type ExaAttachmentCategorySearch struct { + ClassId int `json:"classId" form:"classId"` + request.PageInfo +} diff --git a/server/model/example/response/exa_breakpoint_continue.go b/server/model/example/response/exa_breakpoint_continue.go new file mode 100644 index 0000000..54aa351 --- /dev/null +++ b/server/model/example/response/exa_breakpoint_continue.go @@ -0,0 +1,11 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type FilePathResponse struct { + FilePath string `json:"filePath"` +} + +type FileResponse struct { + File example.ExaFile `json:"file"` +} diff --git a/server/model/example/response/exa_customer.go b/server/model/example/response/exa_customer.go new file mode 100644 index 0000000..7fd26f9 --- /dev/null +++ b/server/model/example/response/exa_customer.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type ExaCustomerResponse struct { + Customer example.ExaCustomer `json:"customer"` +} diff --git a/server/model/example/response/exa_file_upload_download.go b/server/model/example/response/exa_file_upload_download.go new file mode 100644 index 0000000..c1b7931 --- /dev/null +++ b/server/model/example/response/exa_file_upload_download.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/example" + +type ExaFileResponse struct { + File example.ExaFileUploadAndDownload `json:"file"` +} diff --git a/server/model/system/request/jwt.go b/server/model/system/request/jwt.go new file mode 100644 index 0000000..1e1615d --- /dev/null +++ b/server/model/system/request/jwt.go @@ -0,0 +1,21 @@ +package request + +import ( + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +// CustomClaims structure +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type BaseClaims struct { + UUID uuid.UUID + ID uint + Username string + NickName string + AuthorityId uint +} diff --git a/server/model/system/request/sys_ai_workflow_session.go b/server/model/system/request/sys_ai_workflow_session.go new file mode 100644 index 0000000..6f0c5ed --- /dev/null +++ b/server/model/system/request/sys_ai_workflow_session.go @@ -0,0 +1,40 @@ +package request + +import ( + common "github.com/flipped-aurora/gin-vue-admin/server/model/common" + commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + system "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAIWorkflowSessionUpsert struct { + ID uint `json:"id"` + Tab string `json:"tab"` + Title string `json:"title"` + Summary string `json:"summary"` + ConversationID string `json:"conversationId"` + MessageID string `json:"messageId"` + CurrentNodeID string `json:"currentNodeId"` + Settings common.JSONMap `json:"settings"` + FormData common.JSONMap `json:"formData"` + ResultData common.JSONMap `json:"resultData"` + Messages []system.AIWorkflowMessage `json:"messages"` +} + +type SysAIWorkflowSessionSearch struct { + commonReq.PageInfo + Tab string `json:"tab" form:"tab"` +} + +type SysAIWorkflowMarkdownDump struct { + ID uint `json:"id"` + Tab string `json:"tab"` + Title string `json:"title"` + Summary string `json:"summary"` + ConversationID string `json:"conversationId"` + MessageID string `json:"messageId"` + CurrentNodeID string `json:"currentNodeId"` + Settings common.JSONMap `json:"settings"` + FormData common.JSONMap `json:"formData"` + ResultData common.JSONMap `json:"resultData"` + Messages []system.AIWorkflowMessage `json:"messages"` +} diff --git a/server/model/system/request/sys_api.go b/server/model/system/request/sys_api.go new file mode 100644 index 0000000..5567ee8 --- /dev/null +++ b/server/model/system/request/sys_api.go @@ -0,0 +1,21 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// api分页条件查询及排序结构体 +type SearchApiParams struct { + system.SysApi + request.PageInfo + OrderKey string `json:"orderKey"` // 排序 + Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true +} + +// SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表 +type SetApiAuthorities struct { + Path string `json:"path" form:"path"` // API路径 + Method string `json:"method" form:"method"` // 请求方法 + AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表 +} diff --git a/server/model/system/request/sys_api_token.go b/server/model/system/request/sys_api_token.go new file mode 100644 index 0000000..e9e1c02 --- /dev/null +++ b/server/model/system/request/sys_api_token.go @@ -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"` +} diff --git a/server/model/system/request/sys_authority_btn.go b/server/model/system/request/sys_authority_btn.go new file mode 100644 index 0000000..98493ff --- /dev/null +++ b/server/model/system/request/sys_authority_btn.go @@ -0,0 +1,7 @@ +package request + +type SysAuthorityBtnReq struct { + MenuID uint `json:"menuID"` + AuthorityId uint `json:"authorityId"` + Selected []uint `json:"selected"` +} diff --git a/server/model/system/request/sys_auto_code.go b/server/model/system/request/sys_auto_code.go new file mode 100644 index 0000000..1990fdc --- /dev/null +++ b/server/model/system/request/sys_auto_code.go @@ -0,0 +1,291 @@ +package request + +import ( + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/pkg/errors" + "go/token" + "strings" +) + +type AutoCode struct { + Package string `json:"package"` + PackageT string `json:"-"` + TableName string `json:"tableName" example:"表名"` // 表名 + BusinessDB string `json:"businessDB" example:"业务数据库"` // 业务数据库 + StructName string `json:"structName" example:"Struct名称"` // Struct名称 + PackageName string `json:"packageName" example:"文件名称"` // 文件名称 + Description string `json:"description" example:"Struct中文名称"` // Struct中文名称 + Abbreviation string `json:"abbreviation" example:"Struct简称"` // Struct简称 + HumpPackageName string `json:"humpPackageName" example:"go文件名称"` // go文件名称 + GvaModel bool `json:"gvaModel" example:"false"` // 是否使用gva默认Model + AutoMigrate bool `json:"autoMigrate" example:"false"` // 是否自动迁移表结构 + AutoCreateResource bool `json:"autoCreateResource" example:"false"` // 是否自动创建资源标识 + AutoCreateApiToSql bool `json:"autoCreateApiToSql" example:"false"` // 是否自动创建api + AutoCreateMenuToSql bool `json:"autoCreateMenuToSql" example:"false"` // 是否自动创建menu + AutoCreateBtnAuth bool `json:"autoCreateBtnAuth" example:"false"` // 是否自动创建按钮权限 + OnlyTemplate bool `json:"onlyTemplate" example:"false"` // 是否只生成模板 + IsTree bool `json:"isTree" example:"false"` // 是否树形结构 + TreeJson string `json:"treeJson" example:"展示的树json字段"` // 展示的树json字段 + IsAdd bool `json:"isAdd" example:"false"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` + GenerateWeb bool `json:"generateWeb" example:"true"` // 是否生成web + GenerateServer bool `json:"generateServer" example:"true"` // 是否生成server + Module string `json:"-"` + DictTypes []string `json:"-"` + PrimaryField *AutoCodeField `json:"primaryField"` + DataSourceMap map[string]*DataSource `json:"-"` + HasPic bool `json:"-"` + HasFile bool `json:"-"` + HasTimer bool `json:"-"` + NeedSort bool `json:"-"` + NeedJSON bool `json:"-"` + HasRichText bool `json:"-"` + HasDataSource bool `json:"-"` + HasSearchTimer bool `json:"-"` + HasArray bool `json:"-"` + HasExcel bool `json:"-"` +} + +type DataSource struct { + DBName string `json:"dbName"` + Table string `json:"table"` + Label string `json:"label"` + Value string `json:"value"` + Association int `json:"association"` // 关联关系 1 一对一 2 一对多 + HasDeletedAt bool `json:"hasDeletedAt"` +} + +func (r *AutoCode) Apis() []model.SysApi { + return []model.SysApi{ + { + Path: "/" + r.Abbreviation + "/" + "create" + r.StructName, + Description: "新增" + r.Description, + ApiGroup: r.Description, + Method: "POST", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName, + Description: "删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName + "ByIds", + Description: "批量删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "update" + r.StructName, + Description: "更新" + r.Description, + ApiGroup: r.Description, + Method: "PUT", + }, + { + Path: "/" + r.Abbreviation + "/" + "find" + r.StructName, + Description: "根据ID获取" + r.Description, + ApiGroup: r.Description, + Method: "GET", + }, + { + Path: "/" + r.Abbreviation + "/" + "get" + r.StructName + "List", + Description: "获取" + r.Description + "列表", + ApiGroup: r.Description, + Method: "GET", + }, + } +} + +func (r *AutoCode) Menu(template string) model.SysBaseMenu { + component := fmt.Sprintf("view/%s/%s/%s.vue", r.Package, r.PackageName, r.PackageName) + if template != "package" { + component = fmt.Sprintf("plugin/%s/view/%s.vue", r.Package, r.PackageName) + } + return model.SysBaseMenu{ + ParentId: 0, + Path: r.Abbreviation, + Name: r.Abbreviation, + Component: component, + Meta: model.Meta{ + Title: r.Description, + }, + } +} + +// Pretreatment 预处理 +// Author [SliverHorn](https://github.com/SliverHorn) +func (r *AutoCode) Pretreatment() error { + r.Module = global.GVA_CONFIG.AutoCode.Module + if token.IsKeyword(r.Abbreviation) { + r.Abbreviation = r.Abbreviation + "_" + } // go 关键字处理 + if strings.HasSuffix(r.HumpPackageName, "test") { + r.HumpPackageName = r.HumpPackageName + "_" + } // test + length := len(r.Fields) + dict := make(map[string]string, length) + r.DataSourceMap = make(map[string]*DataSource, length) + for i := 0; i < length; i++ { + if r.Fields[i].Excel { + r.HasExcel = true + } + if r.Fields[i].DictType != "" { + dict[r.Fields[i].DictType] = "" + } + if r.Fields[i].Sort { + r.NeedSort = true + } + switch r.Fields[i].FieldType { + case "file": + r.HasFile = true + r.NeedJSON = true + case "json": + r.NeedJSON = true + case "array": + r.NeedJSON = true + r.HasArray = true + case "video": + r.HasPic = true + case "richtext": + r.HasRichText = true + case "picture": + r.HasPic = true + case "pictures": + r.HasPic = true + r.NeedJSON = true + case "time.Time": + r.HasTimer = true + if r.Fields[i].FieldSearchType != "" && r.Fields[i].FieldSearchType != "BETWEEN" && r.Fields[i].FieldSearchType != "NOT BETWEEN" { + r.HasSearchTimer = true + } + } + if r.Fields[i].DataSource != nil { + if r.Fields[i].DataSource.Table != "" && r.Fields[i].DataSource.Label != "" && r.Fields[i].DataSource.Value != "" { + r.HasDataSource = true + r.Fields[i].CheckDataSource = true + r.DataSourceMap[r.Fields[i].FieldJson] = r.Fields[i].DataSource + } + } + if !r.GvaModel && r.PrimaryField == nil && r.Fields[i].PrimaryKey { + r.PrimaryField = r.Fields[i] + } // 自定义主键 + } + { + for key := range dict { + r.DictTypes = append(r.DictTypes, key) + } + } // DictTypes => 字典 + { + if r.GvaModel { + r.PrimaryField = &AutoCodeField{ + FieldName: "ID", + FieldType: "uint", + FieldDesc: "ID", + FieldJson: "ID", + DataTypeLong: "20", + Comment: "主键ID", + ColumnName: "id", + } + } + } // GvaModel + { + if r.IsAdd && r.PrimaryField == nil { + r.PrimaryField = new(AutoCodeField) + } + } // 新增字段模式下不关注主键 + if r.Package == "" { + return errors.New("Package为空!") + } // 增加判断:Package不为空 + packages := []rune(r.Package) + if len(packages) > 0 { + if packages[0] >= 97 && packages[0] <= 122 { + packages[0] = packages[0] - 32 + } + r.PackageT = string(packages) + } // PackageT 是 Package 的首字母大写 + return nil +} + +func (r *AutoCode) History() SysAutoHistoryCreate { + bytes, _ := json.Marshal(r) + return SysAutoHistoryCreate{ + Table: r.TableName, + Package: r.Package, + Request: string(bytes), + StructName: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + } +} + +type AutoCodeField struct { + FieldName string `json:"fieldName"` // Field名 + FieldDesc string `json:"fieldDesc"` // 中文名 + FieldType string `json:"fieldType"` // Field数据类型 + FieldJson string `json:"fieldJson"` // FieldJson + DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 + Comment string `json:"comment"` // 数据库字段描述 + ColumnName string `json:"columnName"` // 数据库字段 + FieldSearchType string `json:"fieldSearchType"` // 搜索条件 + FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 + DictType string `json:"dictType"` // 字典 + //Front bool `json:"front"` // 是否前端可见 + Form bool `json:"form"` // 是否前端新建/编辑 + Table bool `json:"table"` // 是否前端表格列 + Desc bool `json:"desc"` // 是否前端详情 + Excel bool `json:"excel"` // 是否导入/导出 + Require bool `json:"require"` // 是否必填 + DefaultValue string `json:"defaultValue"` // 是否必填 + ErrorText string `json:"errorText"` // 校验失败文字 + Clearable bool `json:"clearable"` // 是否可清空 + Sort bool `json:"sort"` // 是否增加排序 + PrimaryKey bool `json:"primaryKey"` // 是否主键 + DataSource *DataSource `json:"dataSource"` // 数据源 + CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 + FieldIndexType string `json:"fieldIndexType"` // 索引类型 +} + +type AutoFunc struct { + Package string `json:"package"` + FuncName string `json:"funcName"` // 方法名称 + Router string `json:"router"` // 路由名称 + FuncDesc string `json:"funcDesc"` // 方法介绍 + BusinessDB string `json:"businessDB"` // 业务库 + StructName string `json:"structName"` // Struct名称 + PackageName string `json:"packageName"` // 文件名称 + Description string `json:"description"` // Struct中文名称 + Abbreviation string `json:"abbreviation"` // Struct简称 + HumpPackageName string `json:"humpPackageName"` // go文件名称 + Method string `json:"method"` // 方法 + IsPlugin bool `json:"isPlugin"` // 是否插件 + IsAuth bool `json:"isAuth"` // 是否鉴权 + IsPreview bool `json:"isPreview"` // 是否预览 + IsAi bool `json:"isAi"` // 是否AI + ApiFunc string `json:"apiFunc"` // API方法 + ServerFunc string `json:"serverFunc"` // 服务方法 + JsFunc string `json:"jsFunc"` // JS方法 +} + +type InitMenu struct { + PlugName string `json:"plugName"` + ParentMenu string `json:"parentMenu"` + Menus []uint `json:"menus"` +} + +type InitApi struct { + PlugName string `json:"plugName"` + 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;"` //模式 +} diff --git a/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 0000000..a52ec7c --- /dev/null +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,16 @@ +package request + +type AutoMcpTool struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Params []struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array + Required bool `json:"required" form:"required"` + Default string `json:"default" form:"default"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/server/model/system/request/sys_auto_code_package.go b/server/model/system/request/sys_auto_code_package.go new file mode 100644 index 0000000..679303a --- /dev/null +++ b/server/model/system/request/sys_auto_code_package.go @@ -0,0 +1,31 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAutoCodePackageCreate struct { + Desc string `json:"desc" example:"描述"` + Label string `json:"label" example:"展示名"` + Template string `json:"template" example:"模版"` + PackageName string `json:"packageName" example:"包名"` + Module string `json:"-" example:"模块"` +} + +func (r *SysAutoCodePackageCreate) AutoCode() AutoCode { + return AutoCode{ + Package: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} + +func (r *SysAutoCodePackageCreate) Create() model.SysAutoCodePackage { + return model.SysAutoCodePackage{ + Desc: r.Desc, + Label: r.Label, + Template: r.Template, + PackageName: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} diff --git a/server/model/system/request/sys_auto_history.go b/server/model/system/request/sys_auto_history.go new file mode 100644 index 0000000..e9e3243 --- /dev/null +++ b/server/model/system/request/sys_auto_history.go @@ -0,0 +1,57 @@ +package request + +import ( + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAutoHistoryCreate struct { + Table string // 表名 + Package string // 模块名/插件名 + Request string // 前端传入的结构化信息 + StructName string // 结构体名称 + BusinessDB string // 业务库 + Description string // Struct中文名称 + Injections map[string]string // 注入路径 + Templates map[string]string // 模板信息 + ApiIDs []uint // api表注册内容 + MenuID uint // 菜单ID + ExportTemplateID uint // 导出模板ID +} + +func (r *SysAutoHistoryCreate) Create() model.SysAutoCodeHistory { + entity := model.SysAutoCodeHistory{ + Package: r.Package, + Request: r.Request, + Table: r.Table, + StructName: r.StructName, + Abbreviation: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + Injections: r.Injections, + Templates: r.Templates, + ApiIDs: r.ApiIDs, + MenuID: r.MenuID, + ExportTemplateID: r.ExportTemplateID, + } + if entity.Table == "" { + entity.Table = r.StructName + } + return entity +} + +type SysAutoHistoryRollBack struct { + common.GetById + DeleteApi bool `json:"deleteApi" form:"deleteApi"` // 是否删除接口 + DeleteMenu bool `json:"deleteMenu" form:"deleteMenu"` // 是否删除菜单 + DeleteTable bool `json:"deleteTable" form:"deleteTable"` // 是否删除表 +} + +func (r *SysAutoHistoryRollBack) ApiIds(entity model.SysAutoCodeHistory) common.IdsReq { + length := len(entity.ApiIDs) + ids := make([]int, 0) + for i := 0; i < length; i++ { + ids = append(ids, int(entity.ApiIDs[i])) + } + return common.IdsReq{Ids: ids} +} diff --git a/server/model/system/request/sys_casbin.go b/server/model/system/request/sys_casbin.go new file mode 100644 index 0000000..3ca4212 --- /dev/null +++ b/server/model/system/request/sys_casbin.go @@ -0,0 +1,27 @@ +package request + +// CasbinInfo Casbin info structure +type CasbinInfo struct { + Path string `json:"path"` // 路径 + Method string `json:"method"` // 方法 +} + +// CasbinInReceive Casbin structure for input parameters +type CasbinInReceive struct { + AuthorityId uint `json:"authorityId"` // 权限id + CasbinInfos []CasbinInfo `json:"casbinInfos"` +} + +func DefaultCasbin() []CasbinInfo { + return []CasbinInfo{ + {Path: "/menu/getMenu", Method: "POST"}, + {Path: "/jwt/jsonInBlacklist", Method: "POST"}, + {Path: "/base/login", Method: "POST"}, + {Path: "/user/changePassword", Method: "POST"}, + {Path: "/user/setUserAuthority", Method: "POST"}, + {Path: "/user/getUserInfo", Method: "GET"}, + {Path: "/user/setSelfInfo", Method: "PUT"}, + {Path: "/fileUploadAndDownload/upload", Method: "POST"}, + {Path: "/sysDictionary/findSysDictionary", Method: "GET"}, + } +} diff --git a/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go new file mode 100644 index 0000000..5a84796 --- /dev/null +++ b/server/model/system/request/sys_dictionary.go @@ -0,0 +1,9 @@ +package request + +type SysDictionarySearch struct { + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) +} + +type ImportSysDictionaryRequest struct { + Json string `json:"json" binding:"required"` // JSON字符串 +} diff --git a/server/model/system/request/sys_dictionary_detail.go b/server/model/system/request/sys_dictionary_detail.go new file mode 100644 index 0000000..2419cc3 --- /dev/null +++ b/server/model/system/request/sys_dictionary_detail.go @@ -0,0 +1,43 @@ +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 SysDictionaryDetailSearch struct { + system.SysDictionaryDetail + request.PageInfo + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,用于查询指定父级下的子项 + Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据 +} + +// CreateSysDictionaryDetailRequest 创建字典详情请求 +type CreateSysDictionaryDetailRequest struct { + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// UpdateSysDictionaryDetailRequest 更新字典详情请求 +type UpdateSysDictionaryDetailRequest struct { + ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求 +type GetDictionaryDetailsByParentRequest struct { + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,为空时获取顶级 + IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据 +} diff --git a/server/model/system/request/sys_error.go b/server/model/system/request/sys_error.go new file mode 100644 index 0000000..ca1044d --- /dev/null +++ b/server/model/system/request/sys_error.go @@ -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 +} diff --git a/server/model/system/request/sys_export_template.go b/server/model/system/request/sys_export_template.go new file mode 100644 index 0000000..1010bf6 --- /dev/null +++ b/server/model/system/request/sys_export_template.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "time" +) + +type SysExportTemplateSearch struct { + system.SysExportTemplate + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} diff --git a/server/model/system/request/sys_init.go b/server/model/system/request/sys_init.go new file mode 100644 index 0000000..d04b1d2 --- /dev/null +++ b/server/model/system/request/sys_init.go @@ -0,0 +1,124 @@ +package request + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "os" +) + +type InitDB struct { + AdminPassword string `json:"adminPassword" binding:"required"` + DBType string `json:"dbType"` // 数据库类型 + Host string `json:"host"` // 服务器地址 + Port string `json:"port"` // 数据库连接端口 + UserName string `json:"userName"` // 数据库用户名 + Password string `json:"password"` // 数据库密码 + DBName string `json:"dbName" binding:"required"` // 数据库名 + DBPath string `json:"dbPath"` // sqlite数据库文件路径 + Template string `json:"template"` // postgresql指定template +} + +// MysqlEmptyDsn msyql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) MysqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "3306" + } + return fmt.Sprintf("%s:%s@tcp(%s:%s)/", i.UserName, i.Password, i.Host, i.Port) +} + +// PgsqlEmptyDsn pgsql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) PgsqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "5432" + } + return "host=" + i.Host + " user=" + i.UserName + " password=" + i.Password + " port=" + i.Port + " dbname=" + "postgres" + " " + "sslmode=disable TimeZone=Asia/Shanghai" +} + +// SqliteEmptyDsn sqlite 空数据库 建库链接 +// Author Kafumio +func (i *InitDB) SqliteEmptyDsn() string { + separator := string(os.PathSeparator) + return i.DBPath + separator + i.DBName + ".db" +} + +func (i *InitDB) MssqlEmptyDsn() string { + return "sqlserver://" + i.UserName + ":" + i.Password + "@" + i.Host + ":" + i.Port + "?database=" + i.DBName + "&encrypt=disable" +} + +// ToMysqlConfig 转换 config.Mysql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToMysqlConfig() config.Mysql { + return config.Mysql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "charset=utf8mb4&parseTime=True&loc=Local", + }, + } +} + +// ToPgsqlConfig 转换 config.Pgsql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToPgsqlConfig() config.Pgsql { + return config.Pgsql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "sslmode=disable TimeZone=Asia/Shanghai", + }, + } +} + +// ToSqliteConfig 转换 config.Sqlite +// Author [Kafumio](https://github.com/Kafumio) +func (i *InitDB) ToSqliteConfig() config.Sqlite { + return config.Sqlite{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} + +func (i *InitDB) ToMssqlConfig() config.Mssql { + return config.Mssql{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} diff --git a/server/model/system/request/sys_login_log.go b/server/model/system/request/sys_login_log.go new file mode 100644 index 0000000..48d7256 --- /dev/null +++ b/server/model/system/request/sys_login_log.go @@ -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 +} diff --git a/server/model/system/request/sys_menu.go b/server/model/system/request/sys_menu.go new file mode 100644 index 0000000..75d4a5c --- /dev/null +++ b/server/model/system/request/sys_menu.go @@ -0,0 +1,33 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// AddMenuAuthorityInfo Add menu authority info structure +type AddMenuAuthorityInfo struct { + Menus []system.SysBaseMenu `json:"menus"` + AuthorityId uint `json:"authorityId"` // 角色ID +} + +// SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表 +type SetMenuAuthorities struct { + MenuId uint `json:"menuId" form:"menuId"` // 菜单ID + AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表 +} + +func DefaultMenu() []system.SysBaseMenu { + return []system.SysBaseMenu{{ + GVA_MODEL: global.GVA_MODEL{ID: 1}, + ParentId: 0, + Path: "dashboard", + Name: "dashboard", + Component: "view/dashboard/index.vue", + Sort: 1, + Meta: system.Meta{ + Title: "仪表盘", + Icon: "setting", + }, + }} +} diff --git a/server/model/system/request/sys_operation_record.go b/server/model/system/request/sys_operation_record.go new file mode 100644 index 0000000..e58dd59 --- /dev/null +++ b/server/model/system/request/sys_operation_record.go @@ -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 SysOperationRecordSearch struct { + system.SysOperationRecord + request.PageInfo +} diff --git a/server/model/system/request/sys_params.go b/server/model/system/request/sys_params.go new file mode 100644 index 0000000..0009271 --- /dev/null +++ b/server/model/system/request/sys_params.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type SysParamsSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + Name string `json:"name" form:"name" ` + Key string `json:"key" form:"key" ` + request.PageInfo +} diff --git a/server/model/system/request/sys_skills.go b/server/model/system/request/sys_skills.go new file mode 100644 index 0000000..773605f --- /dev/null +++ b/server/model/system/request/sys_skills.go @@ -0,0 +1,80 @@ +package request + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SkillToolRequest struct { + Tool string `json:"tool"` +} + +type SkillDetailRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` +} + +type SkillDeleteRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` +} + +type SkillPackageRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` +} + +type SkillSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + Meta system.SkillMeta `json:"meta"` + Markdown string `json:"markdown"` + SyncTools []string `json:"syncTools"` +} + +type SkillScriptCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + ScriptType string `json:"scriptType"` +} + +type SkillResourceCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillReferenceCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillTemplateCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + Content string `json:"content"` +} + +type SkillGlobalConstraintSaveRequest struct { + Tool string `json:"tool"` + Content string `json:"content"` + SyncTools []string `json:"syncTools"` +} + +type DownloadOnlineSkillReq struct { + Tool string `json:"tool" binding:"required"` + ID uint `json:"id" binding:"required"` + Version string `json:"version" binding:"required"` +} diff --git a/server/model/system/request/sys_user.go b/server/model/system/request/sys_user.go new file mode 100644 index 0000000..7950774 --- /dev/null +++ b/server/model/system/request/sys_user.go @@ -0,0 +1,77 @@ +package request + +import ( + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +// Register User register structure +type Register struct { + Username string `json:"userName" example:"用户名"` + Password string `json:"passWord" example:"密码"` + NickName string `json:"nickName" example:"昵称"` + HeaderImg string `json:"headerImg" example:"头像链接"` + AuthorityId uint `json:"authorityId" swaggertype:"string" example:"int 角色id"` + Enable int `json:"enable" swaggertype:"string" example:"int 是否启用"` + AuthorityIds []uint `json:"authorityIds" swaggertype:"string" example:"[]uint 角色id"` + Phone string `json:"phone" example:"电话号码"` + Email string `json:"email" example:"电子邮箱"` +} + +// Login User login structure +type Login struct { + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + Captcha string `json:"captcha"` // 验证码 + CaptchaId string `json:"captchaId"` // 验证码ID +} + +// ChangePasswordReq Modify password structure +type ChangePasswordReq struct { + ID uint `json:"-"` // 从 JWT 中提取 user id,避免越权 + Password string `json:"password"` // 密码 + NewPassword string `json:"newPassword"` // 新密码 +} + +type ResetPassword struct { + ID uint `json:"ID" form:"ID"` + Password string `json:"password" form:"password" gorm:"comment:用户登录密码"` // 用户登录密码 +} + +// SetUserAuth Modify user's auth structure +type SetUserAuth struct { + AuthorityId uint `json:"authorityId"` // 角色ID +} + +// SetUserAuthorities Modify user's auth structure +type SetUserAuthorities struct { + ID uint + AuthorityIds []uint `json:"authorityIds"` // 角色ID +} + +type ChangeUserInfo struct { + ID uint `gorm:"primarykey"` // 主键ID + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户 + Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"` +} + +type GetUserList struct { + common.PageInfo + Username string `json:"username" form:"username"` + NickName string `json:"nickName" form:"nickName"` + Phone string `json:"phone" form:"phone"` + Email string `json:"email" form:"email"` + OrderKey string `json:"orderKey" form:"orderKey"` // 排序 + Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true +} + +// SetRoleUsers 通过角色ID全量覆盖关联用户列表 +type SetRoleUsers struct { + AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID + UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表 +} diff --git a/server/model/system/request/sys_version.go b/server/model/system/request/sys_version.go new file mode 100644 index 0000000..3d6d7ff --- /dev/null +++ b/server/model/system/request/sys_version.go @@ -0,0 +1,40 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "time" +) + +type SysVersionSearch struct { + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + VersionName *string `json:"versionName" form:"versionName"` + VersionCode *string `json:"versionCode" form:"versionCode"` + request.PageInfo +} + +// ExportVersionRequest 导出版本请求结构体 +type ExportVersionRequest struct { + VersionName string `json:"versionName" binding:"required"` // 版本名称 + VersionCode string `json:"versionCode" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表 + ApiIds []uint `json:"apiIds"` // 选中的API ID列表 + DictIds []uint `json:"dictIds"` // 选中的字典ID列表 +} + +// ImportVersionRequest 导入版本请求结构体 +type ImportVersionRequest struct { + VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 + ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi + ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} + +// VersionInfo 版本信息结构体 +type VersionInfo struct { + Name string `json:"name" binding:"required"` // 版本名称 + Code string `json:"code" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + ExportTime string `json:"exportTime"` // 导出时间 +} diff --git a/server/model/system/response/sys_ai_workflow_session.go b/server/model/system/response/sys_ai_workflow_session.go new file mode 100644 index 0000000..09eba73 --- /dev/null +++ b/server/model/system/response/sys_ai_workflow_session.go @@ -0,0 +1,21 @@ +package response + +import "time" + +type SysAIWorkflowSessionListItem struct { + ID uint `json:"ID"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + Tab string `json:"tab"` + Title string `json:"title"` + Summary string `json:"summary"` + ConversationID string `json:"conversationId"` + CurrentNodeID string `json:"currentNodeId"` +} + +type AIWorkflowMarkdownDumpResult struct { + FileName string `json:"fileName"` + FilePath string `json:"filePath"` + RelativePath string `json:"relativePath"` + Directory string `json:"directory"` +} diff --git a/server/model/system/response/sys_api.go b/server/model/system/response/sys_api.go new file mode 100644 index 0000000..20e382b --- /dev/null +++ b/server/model/system/response/sys_api.go @@ -0,0 +1,18 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysAPIResponse struct { + Api system.SysApi `json:"api"` +} + +type SysAPIListResponse struct { + Apis []system.SysApi `json:"apis"` +} + +type SysSyncApis struct { + NewApis []system.SysApi `json:"newApis"` + DeleteApis []system.SysApi `json:"deleteApis"` +} diff --git a/server/model/system/response/sys_authority.go b/server/model/system/response/sys_authority.go new file mode 100644 index 0000000..a055401 --- /dev/null +++ b/server/model/system/response/sys_authority.go @@ -0,0 +1,12 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SysAuthorityResponse struct { + Authority system.SysAuthority `json:"authority"` +} + +type SysAuthorityCopyResponse struct { + Authority system.SysAuthority `json:"authority"` + OldAuthorityId uint `json:"oldAuthorityId"` // 旧角色ID +} diff --git a/server/model/system/response/sys_authority_btn.go b/server/model/system/response/sys_authority_btn.go new file mode 100644 index 0000000..2f772cf --- /dev/null +++ b/server/model/system/response/sys_authority_btn.go @@ -0,0 +1,5 @@ +package response + +type SysAuthorityBtnRes struct { + Selected []uint `json:"selected"` +} diff --git a/server/model/system/response/sys_auto_code.go b/server/model/system/response/sys_auto_code.go new file mode 100644 index 0000000..e1f1d8b --- /dev/null +++ b/server/model/system/response/sys_auto_code.go @@ -0,0 +1,27 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type Db struct { + Database string `json:"database" gorm:"column:database"` +} + +type Table struct { + TableName string `json:"tableName" gorm:"column:table_name"` +} + +type Column struct { + DataType string `json:"dataType" gorm:"column:data_type"` + ColumnName string `json:"columnName" gorm:"column:column_name"` + DataTypeLong string `json:"dataTypeLong" gorm:"column:data_type_long"` + 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"` +} diff --git a/server/model/system/response/sys_captcha.go b/server/model/system/response/sys_captcha.go new file mode 100644 index 0000000..0c3995a --- /dev/null +++ b/server/model/system/response/sys_captcha.go @@ -0,0 +1,8 @@ +package response + +type SysCaptchaResponse struct { + CaptchaId string `json:"captchaId"` + PicPath string `json:"picPath"` + CaptchaLength int `json:"captchaLength"` + OpenCaptcha bool `json:"openCaptcha"` +} diff --git a/server/model/system/response/sys_casbin.go b/server/model/system/response/sys_casbin.go new file mode 100644 index 0000000..267bb42 --- /dev/null +++ b/server/model/system/response/sys_casbin.go @@ -0,0 +1,9 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type PolicyPathResponse struct { + Paths []request.CasbinInfo `json:"paths"` +} diff --git a/server/model/system/response/sys_menu.go b/server/model/system/response/sys_menu.go new file mode 100644 index 0000000..d8f80f3 --- /dev/null +++ b/server/model/system/response/sys_menu.go @@ -0,0 +1,15 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/model/system" + +type SysMenusResponse struct { + Menus []system.SysMenu `json:"menus"` +} + +type SysBaseMenusResponse struct { + Menus []system.SysBaseMenu `json:"menus"` +} + +type SysBaseMenuResponse struct { + Menu system.SysBaseMenu `json:"menu"` +} diff --git a/server/model/system/response/sys_system.go b/server/model/system/response/sys_system.go new file mode 100644 index 0000000..f19e965 --- /dev/null +++ b/server/model/system/response/sys_system.go @@ -0,0 +1,7 @@ +package response + +import "github.com/flipped-aurora/gin-vue-admin/server/config" + +type SysConfigResponse struct { + Config config.Server `json:"config"` +} diff --git a/server/model/system/response/sys_user.go b/server/model/system/response/sys_user.go new file mode 100644 index 0000000..d6f1074 --- /dev/null +++ b/server/model/system/response/sys_user.go @@ -0,0 +1,15 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type SysUserResponse struct { + User system.SysUser `json:"user"` +} + +type LoginResponse struct { + User system.SysUser `json:"user"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` +} diff --git a/server/model/system/response/sys_version.go b/server/model/system/response/sys_version.go new file mode 100644 index 0000000..b207ddc --- /dev/null +++ b/server/model/system/response/sys_version.go @@ -0,0 +1,14 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +// ExportVersionResponse 导出版本响应结构体 +type ExportVersionResponse struct { + Version request.VersionInfo `json:"version"` // 版本信息 + Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi + Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} diff --git a/server/model/system/sys_ai_workflow_session.go b/server/model/system/sys_ai_workflow_session.go new file mode 100644 index 0000000..6b7236a --- /dev/null +++ b/server/model/system/sys_ai_workflow_session.go @@ -0,0 +1,35 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common" +) + +type AIWorkflowMessage struct { + ID string `json:"id"` + Role string `json:"role"` + Content string `json:"content"` + Snapshot common.JSONMap `json:"snapshot"` + ConversationID string `json:"conversationId"` + MessageID string `json:"messageId"` + CreatedAt string `json:"createdAt"` +} + +type SysAIWorkflowSession struct { + global.GVA_MODEL + UserID uint `json:"userId" gorm:"column:user_id;index;comment:用户ID"` + Tab string `json:"tab" gorm:"column:tab;size:32;index;comment:会话类型"` + Title string `json:"title" gorm:"column:title;size:255;comment:会话标题"` + Summary string `json:"summary" gorm:"column:summary;type:text;comment:摘要"` + ConversationID string `json:"conversationId" gorm:"column:conversation_id;size:255;comment:Dify会话ID"` + MessageID string `json:"messageId" gorm:"column:message_id;size:255;comment:Dify消息ID"` + CurrentNodeID string `json:"currentNodeId" gorm:"column:current_node_id;size:64;comment:当前选中节点ID"` + Settings common.JSONMap `json:"settings" gorm:"column:settings;type:longtext;comment:页面设置"` + FormData common.JSONMap `json:"formData" gorm:"column:form_data;type:longtext;comment:表单数据"` + ResultData common.JSONMap `json:"resultData" gorm:"column:result_data;type:longtext;comment:当前展示结果"` + Messages []AIWorkflowMessage `json:"messages" gorm:"column:messages;serializer:json;type:longtext;comment:会话消息"` +} + +func (s *SysAIWorkflowSession) TableName() string { + return "sys_ai_workflow_sessions" +} diff --git a/server/model/system/sys_api.go b/server/model/system/sys_api.go new file mode 100644 index 0000000..853ddb0 --- /dev/null +++ b/server/model/system/sys_api.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Description string `json:"description" gorm:"comment:api中文描述"` // api中文描述 + ApiGroup string `json:"apiGroup" gorm:"comment:api组"` // api组 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE +} + +func (SysApi) TableName() string { + return "sys_apis" +} + +type SysIgnoreApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + Flag bool `json:"flag" gorm:"-"` // 是否忽略 +} + +func (SysIgnoreApi) TableName() string { + return "sys_ignore_apis" +} diff --git a/server/model/system/sys_api_token.go b/server/model/system/sys_api_token.go new file mode 100644 index 0000000..2bfbde8 --- /dev/null +++ b/server/model/system/sys_api_token.go @@ -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:备注"` +} diff --git a/server/model/system/sys_authority.go b/server/model/system/sys_authority.go new file mode 100644 index 0000000..01c5efa --- /dev/null +++ b/server/model/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "time" +) + +type SysAuthority struct { + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt *time.Time `sql:"index"` + AuthorityId uint `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"` // 角色ID + AuthorityName string `json:"authorityName" gorm:"comment:角色名"` // 角色名 + ParentId *uint `json:"parentId" gorm:"comment:父角色ID"` // 父角色ID + DataAuthorityId []*SysAuthority `json:"dataAuthorityId" gorm:"many2many:sys_data_authority_id;"` + Children []SysAuthority `json:"children" gorm:"-"` + SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"` + Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"` + DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"` // 默认菜单(默认dashboard) +} + +func (SysAuthority) TableName() string { + return "sys_authorities" +} diff --git a/server/model/system/sys_authority_btn.go b/server/model/system/sys_authority_btn.go new file mode 100644 index 0000000..e005984 --- /dev/null +++ b/server/model/system/sys_authority_btn.go @@ -0,0 +1,8 @@ +package system + +type SysAuthorityBtn struct { + AuthorityId uint `gorm:"comment:角色ID"` + SysMenuID uint `gorm:"comment:菜单ID"` + SysBaseMenuBtnID uint `gorm:"comment:菜单按钮ID"` + SysBaseMenuBtn SysBaseMenuBtn ` gorm:"comment:按钮详情"` +} diff --git a/server/model/system/sys_authority_menu.go b/server/model/system/sys_authority_menu.go new file mode 100644 index 0000000..4467a7e --- /dev/null +++ b/server/model/system/sys_authority_menu.go @@ -0,0 +1,19 @@ +package system + +type SysMenu struct { + SysBaseMenu + MenuId uint `json:"menuId" gorm:"comment:菜单ID"` + AuthorityId uint `json:"-" gorm:"comment:角色ID"` + Children []SysMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters" gorm:"foreignKey:SysBaseMenuID;references:MenuId"` + Btns map[string]uint `json:"btns" gorm:"-"` +} + +type SysAuthorityMenu struct { + MenuId string `json:"menuId" gorm:"comment:菜单ID;column:sys_base_menu_id"` + AuthorityId string `json:"-" gorm:"comment:角色ID;column:sys_authority_authority_id"` +} + +func (s SysAuthorityMenu) TableName() string { + return "sys_authority_menus" +} diff --git a/server/model/system/sys_auto_code_history.go b/server/model/system/sys_auto_code_history.go new file mode 100644 index 0000000..e41d553 --- /dev/null +++ b/server/model/system/sys_auto_code_history.go @@ -0,0 +1,68 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/gorm" + "os" + "path" + "path/filepath" + "strings" +) + +// SysAutoCodeHistory 自动迁移代码记录,用于回滚,重放使用 +type SysAutoCodeHistory struct { + global.GVA_MODEL + Table string `json:"tableName" gorm:"column:table_name;comment:表名"` + Package string `json:"package" gorm:"column:package;comment:模块名/插件名"` + Request string `json:"request" gorm:"type:text;column:request;comment:前端传入的结构化信息"` + StructName string `json:"structName" gorm:"column:struct_name;comment:结构体名称"` + Abbreviation string `json:"abbreviation" gorm:"column:abbreviation;comment:结构体名称缩写"` + BusinessDB string `json:"businessDb" gorm:"column:business_db;comment:业务库"` + Description string `json:"description" gorm:"column:description;comment:Struct中文名称"` + Templates map[string]string `json:"template" gorm:"serializer:json;type:text;column:templates;comment:模板信息"` + Injections map[string]string `json:"injections" gorm:"serializer:json;type:text;column:Injections;comment:注入路径"` + Flag int `json:"flag" gorm:"column:flag;comment:[0:创建,1:回滚]"` + ApiIDs []uint `json:"apiIDs" gorm:"serializer:json;column:api_ids;comment:api表注册内容"` + MenuID uint `json:"menuId" gorm:"column:menu_id;comment:菜单ID"` + ExportTemplateID uint `json:"exportTemplateID" gorm:"column:export_template_id;comment:导出模板ID"` + AutoCodePackage SysAutoCodePackage `json:"autoCodePackage" gorm:"foreignKey:ID;references:PackageID"` + PackageID uint `json:"packageID" gorm:"column:package_id;comment:包ID"` +} + +func (s *SysAutoCodeHistory) BeforeCreate(db *gorm.DB) error { + templates := make(map[string]string, len(s.Templates)) + for key, value := range s.Templates { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + { + hasServer := strings.Index(key, server) + if hasServer != -1 { + key = strings.TrimPrefix(key, server) + keys := strings.Split(key, string(os.PathSeparator)) + key = path.Join(keys...) + } + } // key + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + hasWeb := strings.Index(value, web) + if hasWeb != -1 { + value = strings.TrimPrefix(value, web) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + hasServer := strings.Index(value, server) + if hasServer != -1 { + value = strings.TrimPrefix(value, server) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + } + s.Templates = templates + return nil +} + +func (s *SysAutoCodeHistory) TableName() string { + return "sys_auto_code_histories" +} diff --git a/server/model/system/sys_auto_code_package.go b/server/model/system/sys_auto_code_package.go new file mode 100644 index 0000000..4099192 --- /dev/null +++ b/server/model/system/sys_auto_code_package.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysAutoCodePackage struct { + global.GVA_MODEL + Desc string `json:"desc" gorm:"comment:描述"` + Label string `json:"label" gorm:"comment:展示名"` + Template string `json:"template" gorm:"comment:模版"` + PackageName string `json:"packageName" gorm:"comment:包名"` + Module string `json:"-" example:"模块"` +} + +func (s *SysAutoCodePackage) TableName() string { + return "sys_auto_code_packages" +} diff --git a/server/model/system/sys_base_menu.go b/server/model/system/sys_base_menu.go new file mode 100644 index 0000000..6595db1 --- /dev/null +++ b/server/model/system/sys_base_menu.go @@ -0,0 +1,43 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type SysBaseMenu struct { + global.GVA_MODEL + MenuLevel uint `json:"-"` + ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID + Path string `json:"path" gorm:"comment:路由path"` // 路由path + Name string `json:"name" gorm:"comment:路由name"` // 路由name + Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 + Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 + Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 + Meta `json:"meta" gorm:"embedded"` // 附加属性 + SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"` + Children []SysBaseMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters"` + MenuBtn []SysBaseMenuBtn `json:"menuBtn"` +} + +type Meta struct { + ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` + KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) + Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 + Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 + CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + TransitionType string `json:"transitionType" gorm:"comment:路由切换动画"` // 路由切换动画 +} + +type SysBaseMenuParameter struct { + global.GVA_MODEL + SysBaseMenuID uint + Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"` // 地址栏携带参数为params还是query + Key string `json:"key" gorm:"comment:地址栏携带参数的key"` // 地址栏携带参数的key + Value string `json:"value" gorm:"comment:地址栏携带参数的值"` // 地址栏携带参数的值 +} + +func (SysBaseMenu) TableName() string { + return "sys_base_menus" +} diff --git a/server/model/system/sys_dictionary.go b/server/model/system/sys_dictionary.go new file mode 100644 index 0000000..0bc6084 --- /dev/null +++ b/server/model/system/sys_dictionary.go @@ -0,0 +1,22 @@ +// 自动生成模板SysDictionary +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionary struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) + Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) + Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID + Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典 + SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"` +} + +func (SysDictionary) TableName() string { + return "sys_dictionaries" +} diff --git a/server/model/system/sys_dictionary_detail.go b/server/model/system/sys_dictionary_detail.go new file mode 100644 index 0000000..1e9dfda --- /dev/null +++ b/server/model/system/sys_dictionary_detail.go @@ -0,0 +1,26 @@ +// 自动生成模板SysDictionaryDetail +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionaryDetail struct { + global.GVA_MODEL + Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 + Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 + Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 + Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID + Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情 + Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度,从0开始 + Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3" + Disabled bool `json:"disabled" gorm:"-"` // 禁用状态,根据status字段动态计算 +} + +func (SysDictionaryDetail) TableName() string { + return "sys_dictionary_details" +} diff --git a/server/model/system/sys_error.go b/server/model/system/sys_error.go new file mode 100644 index 0000000..7f5671b --- /dev/null +++ b/server/model/system/sys_error.go @@ -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" +} diff --git a/server/model/system/sys_export_template.go b/server/model/system/sys_export_template.go new file mode 100644 index 0000000..68dce3e --- /dev/null +++ b/server/model/system/sys_export_template.go @@ -0,0 +1,46 @@ +// 自动生成模板SysExportTemplate +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 导出模板 结构体 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;"` //模板信息 + 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:条件"` + JoinTemplate []JoinTemplate `json:"joinTemplate" form:"joinTemplate" gorm:"foreignKey:TemplateID;references:TemplateID;comment:关联"` +} + +type JoinTemplate struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + JOINS string `json:"joins" form:"joins" gorm:"column:joins;comment:关联"` + Table string `json:"table" form:"table" gorm:"column:table;comment:关联表"` + ON string `json:"on" form:"on" gorm:"column:on;comment:关联条件"` +} + +func (JoinTemplate) TableName() string { + return "sys_export_template_join" +} + +type Condition struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + From string `json:"from" form:"from" gorm:"column:from;comment:条件取的key"` + Column string `json:"column" form:"column" gorm:"column:column;comment:作为查询条件的字段"` + Operator string `json:"operator" form:"operator" gorm:"column:operator;comment:操作符"` +} + +func (Condition) TableName() string { + return "sys_export_template_condition" +} diff --git a/server/model/system/sys_jwt_blacklist.go b/server/model/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..4f9fa39 --- /dev/null +++ b/server/model/system/sys_jwt_blacklist.go @@ -0,0 +1,10 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +type JwtBlacklist struct { + global.GVA_MODEL + Jwt string `gorm:"type:text;comment:jwt"` +} diff --git a/server/model/system/sys_login_log.go b/server/model/system/sys_login_log.go new file mode 100644 index 0000000..b1fd08b --- /dev/null +++ b/server/model/system/sys_login_log.go @@ -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"` +} diff --git a/server/model/system/sys_menu_btn.go b/server/model/system/sys_menu_btn.go new file mode 100644 index 0000000..9d32761 --- /dev/null +++ b/server/model/system/sys_menu_btn.go @@ -0,0 +1,10 @@ +package system + +import "github.com/flipped-aurora/gin-vue-admin/server/global" + +type SysBaseMenuBtn struct { + global.GVA_MODEL + Name string `json:"name" gorm:"comment:按钮关键key"` + Desc string `json:"desc" gorm:"按钮备注"` + SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` +} diff --git a/server/model/system/sys_operation_record.go b/server/model/system/sys_operation_record.go new file mode 100644 index 0000000..3d201d3 --- /dev/null +++ b/server/model/system/sys_operation_record.go @@ -0,0 +1,24 @@ +// 自动生成模板SysOperationRecord +package system + +import ( + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysOperationRecord struct { + global.GVA_MODEL + Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip + Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法 + Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径 + Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态 + Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟 + Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理 + ErrorMessage string `json:"error_message" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息 + Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body + Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body + UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id + User SysUser `json:"user"` +} diff --git a/server/model/system/sys_params.go b/server/model/system/sys_params.go new file mode 100644 index 0000000..049c07f --- /dev/null +++ b/server/model/system/sys_params.go @@ -0,0 +1,20 @@ +// 自动生成模板SysParams +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 参数 结构体 SysParams +type SysParams struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:参数名称;" binding:"required"` //参数名称 + Key string `json:"key" form:"key" gorm:"column:key;comment:参数键;" binding:"required"` //参数键 + Value string `json:"value" form:"value" gorm:"column:value;comment:参数值;" binding:"required"` //参数值 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:参数说明;"` //参数说明 +} + +// TableName 参数 SysParams自定义表名 sys_params +func (SysParams) TableName() string { + return "sys_params" +} diff --git a/server/model/system/sys_skills.go b/server/model/system/sys_skills.go new file mode 100644 index 0000000..e7013f6 --- /dev/null +++ b/server/model/system/sys_skills.go @@ -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"` +} diff --git a/server/model/system/sys_system.go b/server/model/system/sys_system.go new file mode 100644 index 0000000..ad98311 --- /dev/null +++ b/server/model/system/sys_system.go @@ -0,0 +1,10 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" +) + +// 配置文件结构体 +type System struct { + Config config.Server `json:"config"` +} diff --git a/server/model/system/sys_user.go b/server/model/system/sys_user.go new file mode 100644 index 0000000..f20cd87 --- /dev/null +++ b/server/model/system/sys_user.go @@ -0,0 +1,62 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/google/uuid" +) + +type Login interface { + GetUsername() string + GetNickname() string + GetUUID() uuid.UUID + GetUserId() uint + GetAuthorityId() uint + GetUserInfo() any +} + +var _ Login = new(SysUser) + +type SysUser struct { + global.GVA_MODEL + UUID uuid.UUID `json:"uuid" gorm:"index;comment:用户UUID"` // 用户UUID + Username string `json:"userName" gorm:"index;comment:用户登录名"` // 用户登录名 + Password string `json:"-" gorm:"comment:用户登录密码"` // 用户登录密码 + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + AuthorityId uint `json:"authorityId" gorm:"default:888;comment:用户角色ID"` // 用户角色ID + Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"` // 用户角色 + Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"` // 多用户角色 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结 + OriginSetting common.JSONMap `json:"originSetting" form:"originSetting" gorm:"type:text;default:null;column:origin_setting;comment:配置;"` //配置 +} + +func (SysUser) TableName() string { + return "sys_users" +} + +func (s *SysUser) GetUsername() string { + return s.Username +} + +func (s *SysUser) GetNickname() string { + return s.NickName +} + +func (s *SysUser) GetUUID() uuid.UUID { + return s.UUID +} + +func (s *SysUser) GetUserId() uint { + return s.ID +} + +func (s *SysUser) GetAuthorityId() uint { + return s.AuthorityId +} + +func (s *SysUser) GetUserInfo() any { + return *s +} diff --git a/server/model/system/sys_user_authority.go b/server/model/system/sys_user_authority.go new file mode 100644 index 0000000..1aa83cb --- /dev/null +++ b/server/model/system/sys_user_authority.go @@ -0,0 +1,11 @@ +package system + +// SysUserAuthority 是 sysUser 和 sysAuthority 的连接表 +type SysUserAuthority struct { + SysUserId uint `gorm:"column:sys_user_id"` + SysAuthorityAuthorityId uint `gorm:"column:sys_authority_authority_id"` +} + +func (s *SysUserAuthority) TableName() string { + return "sys_user_authority" +} diff --git a/server/model/system/sys_version.go b/server/model/system/sys_version.go new file mode 100644 index 0000000..e7c0a82 --- /dev/null +++ b/server/model/system/sys_version.go @@ -0,0 +1,20 @@ +// 自动生成模板SysVersion +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 版本管理 结构体 SysVersion +type SysVersion struct { + global.GVA_MODEL + VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称 + VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号 + Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述 + VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据 +} + +// TableName 版本管理 SysVersion自定义表名 sys_versions +func (SysVersion) TableName() string { + return "sys_versions" +} diff --git a/server/plugin/announcement/api/enter.go b/server/plugin/announcement/api/enter.go new file mode 100644 index 0000000..7fee6fc --- /dev/null +++ b/server/plugin/announcement/api/enter.go @@ -0,0 +1,10 @@ +package api + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/service" + +var ( + Api = new(api) + serviceInfo = service.Service.Info +) + +type api struct{ Info info } diff --git a/server/plugin/announcement/api/info.go b/server/plugin/announcement/api/info.go new file mode 100644 index 0000000..dd0faa3 --- /dev/null +++ b/server/plugin/announcement/api/info.go @@ -0,0 +1,183 @@ +package api + +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/plugin/announcement/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var Info = new(info) + +type info struct{} + +// CreateInfo 创建公告 +// @Tags Info +// @Summary 创建公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "创建公告" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /info/createInfo [post] +func (a *info) CreateInfo(c *gin.Context) { + var info model.Info + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceInfo.CreateInfo(&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteInfo 删除公告 +// @Tags Info +// @Summary 删除公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "删除公告" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /info/deleteInfo [delete] +func (a *info) DeleteInfo(c *gin.Context) { + ID := c.Query("ID") + err := serviceInfo.DeleteInfo(ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteInfoByIds 批量删除公告 +// @Tags Info +// @Summary 批量删除公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /info/deleteInfoByIds [delete] +func (a *info) DeleteInfoByIds(c *gin.Context) { + IDs := c.QueryArray("IDs[]") + if err := serviceInfo.DeleteInfoByIds(IDs); err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败", c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateInfo 更新公告 +// @Tags Info +// @Summary 更新公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body model.Info true "更新公告" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /info/updateInfo [put] +func (a *info) UpdateInfo(c *gin.Context) { + var info model.Info + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = serviceInfo.UpdateInfo(info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindInfo 用id查询公告 +// @Tags Info +// @Summary 用id查询公告 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.Info true "用id查询公告" +// @Success 200 {object} response.Response{data=model.Info,msg=string} "查询成功" +// @Router /info/findInfo [get] +func (a *info) FindInfo(c *gin.Context) { + ID := c.Query("ID") + reinfo, err := serviceInfo.GetInfo(ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(reinfo, c) +} + +// GetInfoList 分页获取公告列表 +// @Tags Info +// @Summary 分页获取公告列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query request.InfoSearch true "分页获取公告列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /info/getInfoList [get] +func (a *info) GetInfoList(c *gin.Context) { + var pageInfo request.InfoSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := serviceInfo.GetInfoInfoList(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) +} + +// GetInfoDataSource 获取Info的数据源 +// @Tags Info +// @Summary 获取Info的数据源 +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /info/getInfoDataSource [get] +func (a *info) GetInfoDataSource(c *gin.Context) { + // 此接口为获取数据源定义的数据 + dataSource, err := serviceInfo.GetInfoDataSource() + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败", c) + return + } + response.OkWithData(dataSource, c) +} + +// GetInfoPublic 不需要鉴权的公告接口 +// @Tags Info +// @Summary 不需要鉴权的公告接口 +// @accept application/json +// @Produce application/json +// @Param data query request.InfoSearch true "分页获取公告列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /info/getInfoPublic [get] +func (a *info) GetInfoPublic(c *gin.Context) { + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + response.OkWithDetailed(gin.H{"info": "不需要鉴权的公告接口信息"}, "获取成功", c) +} diff --git a/server/plugin/announcement/config/config.go b/server/plugin/announcement/config/config.go new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/server/plugin/announcement/config/config.go @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/server/plugin/announcement/gen/gen.go b/server/plugin/announcement/gen/gen.go new file mode 100644 index 0000000..240749f --- /dev/null +++ b/server/plugin/announcement/gen/gen.go @@ -0,0 +1,17 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" //go:generate go mod tidy + //go:generate go mod download + //go:generate go run gen.go + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" +) + +func main() { + g := gen.NewGenerator(gen.Config{OutPath: filepath.Join("..", "..", "..", "announcement", "blender", "model", "dao"), Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface}) + g.ApplyBasic( + new(model.Info), + ) + g.Execute() +} diff --git a/server/plugin/announcement/initialize/api.go b/server/plugin/announcement/initialize/api.go new file mode 100644 index 0000000..6d0fed1 --- /dev/null +++ b/server/plugin/announcement/initialize/api.go @@ -0,0 +1,49 @@ +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 Api(ctx context.Context) { + entities := []model.SysApi{ + { + Path: "/info/createInfo", + Description: "新建公告", + ApiGroup: "公告", + Method: "POST", + }, + { + Path: "/info/deleteInfo", + Description: "删除公告", + ApiGroup: "公告", + Method: "DELETE", + }, + { + Path: "/info/deleteInfoByIds", + Description: "批量删除公告", + ApiGroup: "公告", + Method: "DELETE", + }, + { + Path: "/info/updateInfo", + Description: "更新公告", + ApiGroup: "公告", + Method: "PUT", + }, + { + Path: "/info/findInfo", + Description: "根据ID获取公告", + ApiGroup: "公告", + Method: "GET", + }, + { + Path: "/info/getInfoList", + Description: "获取公告列表", + ApiGroup: "公告", + Method: "GET", + }, + } + utils.RegisterApis(entities...) +} diff --git a/server/plugin/announcement/initialize/dictionary.go b/server/plugin/announcement/initialize/dictionary.go new file mode 100644 index 0000000..8ab340a --- /dev/null +++ b/server/plugin/announcement/initialize/dictionary.go @@ -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...) +} diff --git a/server/plugin/announcement/initialize/gorm.go b/server/plugin/announcement/initialize/gorm.go new file mode 100644 index 0000000..3a88ff2 --- /dev/null +++ b/server/plugin/announcement/initialize/gorm.go @@ -0,0 +1,20 @@ +package initialize + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate( + new(model.Info), + ) + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/plugin/announcement/initialize/menu.go b/server/plugin/announcement/initialize/menu.go new file mode 100644 index 0000000..58144bc --- /dev/null +++ b/server/plugin/announcement/initialize/menu.go @@ -0,0 +1,22 @@ +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 Menu(ctx context.Context) { + entities := []model.SysBaseMenu{ + { + ParentId: 9, + Path: "anInfo", + Name: "anInfo", + Hidden: false, + Component: "plugin/announcement/view/info.vue", + Sort: 5, + Meta: model.Meta{Title: "公告管理", Icon: "box"}, + }, + } + utils.RegisterMenus(entities...) +} diff --git a/server/plugin/announcement/initialize/router.go b/server/plugin/announcement/initialize/router.go new file mode 100644 index 0000000..e2c4f17 --- /dev/null +++ b/server/plugin/announcement/initialize/router.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/router" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + router.Router.Info.Init(public, private) +} diff --git a/server/plugin/announcement/initialize/viper.go b/server/plugin/announcement/initialize/viper.go new file mode 100644 index 0000000..68cfff6 --- /dev/null +++ b/server/plugin/announcement/initialize/viper.go @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("announcement", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/plugin/announcement/model/info.go b/server/plugin/announcement/model/info.go new file mode 100644 index 0000000..42d62fe --- /dev/null +++ b/server/plugin/announcement/model/info.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "gorm.io/datatypes" +) + +// Info 公告 结构体 +type Info struct { + global.GVA_MODEL + Title string `json:"title" form:"title" gorm:"column:title;comment:公告标题;"` //标题 + Content string `json:"content" form:"content" gorm:"column:content;comment:公告内容;type:text;"` //内容 + UserID *int `json:"userID" form:"userID" gorm:"column:user_id;comment:发布者;"` //作者 + Attachments datatypes.JSON `json:"attachments" form:"attachments" gorm:"column:attachments;comment:相关附件;" swaggertype:"array,object"` //附件 +} + +// TableName 公告 Info自定义表名 gva_announcements_info +func (Info) TableName() string { + return "gva_announcements_info" +} diff --git a/server/plugin/announcement/model/request/info.go b/server/plugin/announcement/model/request/info.go new file mode 100644 index 0000000..35be3e0 --- /dev/null +++ b/server/plugin/announcement/model/request/info.go @@ -0,0 +1,12 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type InfoSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} diff --git a/server/plugin/announcement/plugin.go b/server/plugin/announcement/plugin.go new file mode 100644 index 0000000..68e2464 --- /dev/null +++ b/server/plugin/announcement/plugin.go @@ -0,0 +1,32 @@ +package announcement + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/initialize" + interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +func init() { + interfaces.Register(Plugin) +} + +func (p *plugin) Register(group *gin.Engine) { + ctx := context.Background() + // 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key + // initialize.Viper() + // 安装插件时候自动注册的api数据请到下方法.Api方法中实现 + initialize.Api(ctx) + // 安装插件时候自动注册的Menu数据请到下方法.Menu方法中实现 + initialize.Menu(ctx) + // 安装插件时候自动注册的Dictionary数据请到下方法.Dictionary方法中实现 + initialize.Dictionary(ctx) + initialize.Gorm(ctx) + initialize.Router(group) +} diff --git a/server/plugin/announcement/plugin/plugin.go b/server/plugin/announcement/plugin/plugin.go new file mode 100644 index 0000000..4058239 --- /dev/null +++ b/server/plugin/announcement/plugin/plugin.go @@ -0,0 +1,5 @@ +package plugin + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/config" + +var Config config.Config diff --git a/server/plugin/announcement/router/enter.go b/server/plugin/announcement/router/enter.go new file mode 100644 index 0000000..543e0ff --- /dev/null +++ b/server/plugin/announcement/router/enter.go @@ -0,0 +1,10 @@ +package router + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/api" + +var ( + Router = new(router) + apiInfo = api.Api.Info +) + +type router struct{ Info info } diff --git a/server/plugin/announcement/router/info.go b/server/plugin/announcement/router/info.go new file mode 100644 index 0000000..8de316b --- /dev/null +++ b/server/plugin/announcement/router/info.go @@ -0,0 +1,31 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +var Info = new(info) + +type info struct{} + +// Init 初始化 公告 路由信息 +func (r *info) Init(public *gin.RouterGroup, private *gin.RouterGroup) { + { + group := private.Group("info").Use(middleware.OperationRecord()) + group.POST("createInfo", apiInfo.CreateInfo) // 新建公告 + group.DELETE("deleteInfo", apiInfo.DeleteInfo) // 删除公告 + group.DELETE("deleteInfoByIds", apiInfo.DeleteInfoByIds) // 批量删除公告 + group.PUT("updateInfo", apiInfo.UpdateInfo) // 更新公告 + } + { + group := private.Group("info") + group.GET("findInfo", apiInfo.FindInfo) // 根据ID获取公告 + group.GET("getInfoList", apiInfo.GetInfoList) // 获取公告列表 + } + { + group := public.Group("info") + group.GET("getInfoDataSource", apiInfo.GetInfoDataSource) // 获取公告数据源 + group.GET("getInfoPublic", apiInfo.GetInfoPublic) // 获取公告列表 + } +} diff --git a/server/plugin/announcement/service/enter.go b/server/plugin/announcement/service/enter.go new file mode 100644 index 0000000..988fbcd --- /dev/null +++ b/server/plugin/announcement/service/enter.go @@ -0,0 +1,5 @@ +package service + +var Service = new(service) + +type service struct{ Info info } diff --git a/server/plugin/announcement/service/info.go b/server/plugin/announcement/service/info.go new file mode 100644 index 0000000..b521553 --- /dev/null +++ b/server/plugin/announcement/service/info.go @@ -0,0 +1,78 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model/request" +) + +var Info = new(info) + +type info struct{} + +// CreateInfo 创建公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) CreateInfo(info *model.Info) (err error) { + err = global.GVA_DB.Create(info).Error + return err +} + +// DeleteInfo 删除公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) DeleteInfo(ID string) (err error) { + err = global.GVA_DB.Delete(&model.Info{}, "id = ?", ID).Error + return err +} + +// DeleteInfoByIds 批量删除公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) DeleteInfoByIds(IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]model.Info{}, "id in ?", IDs).Error + return err +} + +// UpdateInfo 更新公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) UpdateInfo(info model.Info) (err error) { + err = global.GVA_DB.Model(&model.Info{}).Where("id = ?", info.ID).Updates(&info).Error + return err +} + +// GetInfo 根据ID获取公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) GetInfo(ID string) (info model.Info, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&info).Error + return +} + +// GetInfoInfoList 分页获取公告记录 +// Author [piexlmax](https://github.com/piexlmax) +func (s *info) GetInfoInfoList(info request.InfoSearch) (list []model.Info, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&model.Info{}) + var infos []model.Info + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&infos).Error + return infos, total, err +} +func (s *info) GetInfoDataSource() (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + + userID := make([]map[string]any, 0) + global.GVA_DB.Table("sys_users").Select("nick_name as label,id as value").Scan(&userID) + res["userID"] = userID + return +} diff --git a/server/plugin/email/README.MD b/server/plugin/email/README.MD new file mode 100644 index 0000000..685cdd6 --- /dev/null +++ b/server/plugin/email/README.MD @@ -0,0 +1,78 @@ +## GVA 邮件发送功能插件 +#### 开发者:GIN-VUE-ADMIN 官方 + +### 使用步骤 + +#### 1. 前往GVA主程序下的initialize/router.go 在Routers 方法最末尾按照你需要的及安全模式添加本插件 + 例: + 本插件可以采用gva的配置文件 也可以直接写死内容作为配置 建议为gva添加配置文件结构 然后将配置传入 + PluginInit(PrivateGroup, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, + )) + + 同样也可以再传入时写死 + + PluginInit(PrivateGroup, email.CreateEmailPlug( + "a@qq.com", + "b@qq.com", + "smtp.qq.com", + "global.GVA_CONFIG.Email.Secret", + "登录密钥", + 465, + true, + true, + )) + +### 2. 配置说明 + +#### 2-1 全局配置结构体说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 + From string // 发件人 你自己要发邮件的邮箱 + Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string // 昵称 发件人昵称 自定义即可 可以不填 + Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool // 是否SSL 是否开启SSL + IsLoginAuth bool // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) + } +#### 2-2 入参结构说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + + +### 3. 方法API + + utils.EmailTest(邮件标题,邮件主体) 发送测试邮件 + 例:utils.EmailTest("测试邮件","测试邮件") + utils.ErrorToEmail(邮件标题,邮件主体) 错误监控 + 例:utils.ErrorToEmail("测试邮件","测试邮件") + utils.Email(目标邮箱多个的话用逗号分隔,邮件标题,邮件主体) 发送测试邮件 + 例:utils.Email(”a.qq.com,b.qq.com“,"测试邮件","测试邮件") + +### 4. 可直接调用的接口 + + 测试接口: /email/emailTest [post] 已配置swagger + + 发送邮件接口接口: /email/emailSend [post] 已配置swagger + 入参: + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + diff --git a/server/plugin/email/api/enter.go b/server/plugin/email/api/enter.go new file mode 100644 index 0000000..353404d --- /dev/null +++ b/server/plugin/email/api/enter.go @@ -0,0 +1,7 @@ +package api + +type ApiGroup struct { + EmailApi +} + +var ApiGroupApp = new(ApiGroup) diff --git a/server/plugin/email/api/sys_email.go b/server/plugin/email/api/sys_email.go new file mode 100644 index 0000000..fdc76ab --- /dev/null +++ b/server/plugin/email/api/sys_email.go @@ -0,0 +1,53 @@ +package api + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + email_response "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/model/response" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type EmailApi struct{} + +// EmailTest +// @Tags System +// @Summary 发送测试邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/emailTest [post] +func (s *EmailApi) EmailTest(c *gin.Context) { + err := service.ServiceGroupApp.EmailTest() + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} + +// SendEmail +// @Tags System +// @Summary 发送邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body email_response.Email true "发送邮件必须的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/sendEmail [post] +func (s *EmailApi) SendEmail(c *gin.Context) { + var email email_response.Email + err := c.ShouldBindJSON(&email) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body) + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} diff --git a/server/plugin/email/config/email.go b/server/plugin/email/config/email.go new file mode 100644 index 0000000..412b5a8 --- /dev/null +++ b/server/plugin/email/config/email.go @@ -0,0 +1,12 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证 +} diff --git a/server/plugin/email/global/gloabl.go b/server/plugin/email/global/gloabl.go new file mode 100644 index 0000000..13082d0 --- /dev/null +++ b/server/plugin/email/global/gloabl.go @@ -0,0 +1,5 @@ +package global + +import "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/config" + +var GlobalConfig = new(config.Email) diff --git a/server/plugin/email/main.go b/server/plugin/email/main.go new file mode 100644 index 0000000..37a6359 --- /dev/null +++ b/server/plugin/email/main.go @@ -0,0 +1,29 @@ +package email + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/router" + "github.com/gin-gonic/gin" +) + +type emailPlugin struct{} + +func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, IsLoginAuth bool) *emailPlugin { + global.GlobalConfig.To = To + global.GlobalConfig.From = From + global.GlobalConfig.Host = Host + global.GlobalConfig.Secret = Secret + global.GlobalConfig.Nickname = Nickname + global.GlobalConfig.Port = Port + global.GlobalConfig.IsSSL = IsSSL + global.GlobalConfig.IsLoginAuth = IsLoginAuth + return &emailPlugin{} +} + +func (*emailPlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitEmailRouter(group) +} + +func (*emailPlugin) RouterPath() string { + return "email" +} diff --git a/server/plugin/email/model/response/email.go b/server/plugin/email/model/response/email.go new file mode 100644 index 0000000..ed25475 --- /dev/null +++ b/server/plugin/email/model/response/email.go @@ -0,0 +1,7 @@ +package response + +type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 +} diff --git a/server/plugin/email/router/enter.go b/server/plugin/email/router/enter.go new file mode 100644 index 0000000..e081a54 --- /dev/null +++ b/server/plugin/email/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + EmailRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/server/plugin/email/router/sys_email.go b/server/plugin/email/router/sys_email.go new file mode 100644 index 0000000..1f9f07f --- /dev/null +++ b/server/plugin/email/router/sys_email.go @@ -0,0 +1,19 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/api" + "github.com/gin-gonic/gin" +) + +type EmailRouter struct{} + +func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) { + emailRouter := Router.Use(middleware.OperationRecord()) + EmailApi := api.ApiGroupApp.EmailApi.EmailTest + SendEmail := api.ApiGroupApp.EmailApi.SendEmail + { + emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 + emailRouter.POST("sendEmail", SendEmail) // 发送邮件 + } +} diff --git a/server/plugin/email/service/enter.go b/server/plugin/email/service/enter.go new file mode 100644 index 0000000..e96e267 --- /dev/null +++ b/server/plugin/email/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + EmailService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/server/plugin/email/service/sys_email.go b/server/plugin/email/service/sys_email.go new file mode 100644 index 0000000..5704276 --- /dev/null +++ b/server/plugin/email/service/sys_email.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/utils" +) + +type EmailService struct{} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error + +func (e *EmailService) EmailTest() (err error) { + subject := "test" + body := "test" + err = utils.EmailTest(subject, body) + return err +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error +//@params to string 收件人 +//@params subject string 标题(主题) +//@params body string 邮件内容 + +func (e *EmailService) SendEmail(to, subject, body string) (err error) { + err = utils.Email(to, subject, body) + return err +} diff --git a/server/plugin/email/utils/email.go b/server/plugin/email/utils/email.go new file mode 100644 index 0000000..dd732d8 --- /dev/null +++ b/server/plugin/email/utils/email.go @@ -0,0 +1,122 @@ +package utils + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/plugin/email/global" + + "github.com/jordan-wright/email" +) + +//@author: [maplepie](https://github.com/maplepie) +//@function: Email +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func Email(To, subject string, body string) error { + to := strings.Split(To, ",") + return send(to, subject, body) +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: ErrorToEmail +//@description: 给email中间件错误发送邮件到指定邮箱 +//@param: subject string, body string +//@return: error + +func ErrorToEmail(subject string, body string) error { + to := strings.Split(global.GlobalConfig.To, ",") + if to[len(to)-1] == "" { // 判断切片的最后一个元素是否为空,为空则移除 + to = to[:len(to)-1] + } + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: Email测试方法 +//@param: subject string, body string +//@return: error + +func EmailTest(subject string, body string) error { + to := []string{global.GlobalConfig.To} + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: send +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func send(to []string, subject string, body string) error { + from := global.GlobalConfig.From + nickname := global.GlobalConfig.Nickname + secret := global.GlobalConfig.Secret + host := global.GlobalConfig.Host + port := global.GlobalConfig.Port + isSSL := global.GlobalConfig.IsSSL + isLoginAuth := global.GlobalConfig.IsLoginAuth + + var auth smtp.Auth + if isLoginAuth { + auth = LoginAuth(from, secret) + } else { + auth = smtp.PlainAuth("", from, secret, host) + } + e := email.NewEmail() + if nickname != "" { + e.From = fmt.Sprintf("%s <%s>", nickname, from) + } else { + e.From = from + } + e.To = to + e.Subject = subject + e.HTML = []byte(body) + var err error + hostAddr := fmt.Sprintf("%s:%d", host, port) + if isSSL { + err = e.SendWithTLS(hostAddr, auth, &tls.Config{ServerName: host}) + } else { + err = e.Send(hostAddr, auth) + } + return err +} + +// LoginAuth 用于IBM、微软邮箱服务器的LOGIN认证方式 +type loginAuth struct { + username, password string +} + +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + // 邮箱服务器可能发送的其他提示信息 + prompt := strings.ToLower(string(fromServer)) + if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") { + return []byte(a.username), nil + } + if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") { + return []byte(a.password), nil + } + } + } + return nil, nil +} diff --git a/server/plugin/plugin-tool/utils/check.go b/server/plugin/plugin-tool/utils/check.go new file mode 100644 index 0000000..70631d7 --- /dev/null +++ b/server/plugin/plugin-tool/utils/check.go @@ -0,0 +1,138 @@ +package utils + +import ( + "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) { + name := getPluginName() + if name != "" { + rw.Lock() + ApiMap[name] = apis + rw.Unlock() + } + + 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 { + zap.L().Error("注册API失败", zap.Error(err)) + } +} + +func RegisterMenus(menus ...system.SysBaseMenu) { + name := getPluginName() + if name != "" { + rw.Lock() + MenuMap[name] = menus + rw.Unlock() + } + + parentMenu := menus[0] + otherMenus := menus[1:] + 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 + 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 { + 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] +} + diff --git a/server/plugin/register.go b/server/plugin/register.go new file mode 100644 index 0000000..11399d0 --- /dev/null +++ b/server/plugin/register.go @@ -0,0 +1,5 @@ +package plugin + +import ( + _ "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement" +) diff --git a/server/resource/function/api.go.tpl b/server/resource/function/api.go.tpl new file mode 100644 index 0000000..35a1cd5 --- /dev/null +++ b/server/resource/function/api.go.tpl @@ -0,0 +1,44 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func (a *{{.Abbreviation}}) {{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := service{{ .StructName }}.{{.FuncName}}(ctx) + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "成功" +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func ({{.Abbreviation}}Api *{{.StructName}}Api){{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := {{.Abbreviation}}Service.{{.FuncName}}(ctx) + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} +{{end}} diff --git a/server/resource/function/api.js.tpl b/server/resource/function/api.js.tpl new file mode 100644 index 0000000..a07b102 --- /dev/null +++ b/server/resource/function/api.js.tpl @@ -0,0 +1,32 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- end -}} diff --git a/server/resource/function/server.go.tpl b/server/resource/function/server.go.tpl new file mode 100644 index 0000000..7327604 --- /dev/null +++ b/server/resource/function/server.go.tpl @@ -0,0 +1,25 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{if .IsPlugin}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) {{.FuncName}}(ctx context.Context) (err error) { + db := {{$db}}.Model(&model.{{.StructName}}{}) + return db.Error +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service){{.FuncName}}(ctx context.Context) (err error) { + // 请在这里实现自己的业务逻辑 + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + return db.Error +} +{{end}} diff --git a/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl new file mode 100644 index 0000000..49bfa20 --- /dev/null +++ b/server/resource/mcp/tools.tpl @@ -0,0 +1,56 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&{{.Name | title}}{}) +} + +type {{.Name | title}} struct { +} + +// {{.Description}} +func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // TODO: 实现工具逻辑 + // 参数示例: + // {{- range .Params}} + // {{.Name}} := request.GetArguments()["{{.Name}}"] + // {{- end}} + return &mcp.CallToolResult{ + Content: []mcp.Content{ + {{- range .Response}} + mcp.{{.Type | title}}Content{ + Type: "{{.Type}}", + // TODO: 填充{{.Type}}内容 + }, + {{- end}} + }, + }, nil +} + +func (t *{{.Name | title}}) New() mcp.Tool { + return mcp.NewTool("{{.Name}}", + mcp.WithDescription("{{.Description}}"), + {{- range .Params}} + mcp.With{{.Type | title}}("{{.Name}}", + {{- if .Required}}mcp.Required(),{{end}} + mcp.Description("{{.Description}}"), + {{- if .Default}} + {{- if eq .Type "string"}} + mcp.DefaultString("{{.Default}}"), + {{- else if eq .Type "number"}} + mcp.DefaultNumber({{.Default}}), + {{- else if eq .Type "boolean"}} + mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}), + {{- else if eq .Type "array"}} + // 注意:数组默认值需要在后端代码中预处理为正确的格式 + // mcp.DefaultArray({{.Default}}), + {{- end}} + {{- end}} + ), + {{- end}} + ) +} diff --git a/server/resource/package/readme.txt.tpl b/server/resource/package/readme.txt.tpl new file mode 100644 index 0000000..25de230 --- /dev/null +++ b/server/resource/package/readme.txt.tpl @@ -0,0 +1,7 @@ +代码解压后把fe的api文件内容粘贴进前端api文件夹下并修改为自己想要的名字即可 + +后端代码解压后同理,放到自己想要的 mvc对应路径 并且到 initRouter中注册自动生成的路由 到registerTable中注册自动生成的model + +项目github:"https://github.com/piexlmax/github.com/flipped-aurora/gin-vue-admin/server" + +希望大家给个star多多鼓励 diff --git a/server/resource/package/server/api/api.go.tpl b/server/resource/package/server/api/api.go.tpl new file mode 100644 index 0000000..528487f --- /dev/null +++ b/server/resource/package/server/api/api.go.tpl @@ -0,0 +1,260 @@ +package {{.Package}} + +import ( + {{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- end }} + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} + {{- else}} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" + {{- end}} +) + +type {{.StructName}}Api struct {} + +{{if not .OnlyTemplate}} + +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.CreatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Create{{.StructName}}(ctx,&{{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.UpdatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Update{{.StructName}}(ctx,{{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data={{.Package}}.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表,Tree模式下不接受参数 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo {{.Package}}Req.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(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) +} +{{- end }} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} + +{{- end }} + +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + {{.Abbreviation}}Service.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的{{.Description}}接口信息", + }, "获取成功", c) +} diff --git a/server/resource/package/server/api/enter.go.tpl b/server/resource/package/server/api/enter.go.tpl new file mode 100644 index 0000000..778b314 --- /dev/null +++ b/server/resource/package/server/api/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ApiGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/model/model.go.tpl b/server/resource/package/server/model/model.go.tpl new file mode 100644 index 0000000..e1603ed --- /dev/null +++ b/server/resource/package/server/model/model.go.tpl @@ -0,0 +1,75 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + +{{ else }} +// 自动生成模板{{.StructName}} +package {{.Package}} + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.Description}} 结构体 {{.StructName}} +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} +{{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + +{{ end }} diff --git a/server/resource/package/server/model/request/request.go.tpl b/server/resource/package/server/model/request/request.go.tpl new file mode 100644 index 0000000..f8749f3 --- /dev/null +++ b/server/resource/package/server/model/request/request.go.tpl @@ -0,0 +1,39 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request + +import ( +{{- if not .OnlyTemplate }} + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} +{{- end }} +) + +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end}} +} +{{- end }} diff --git a/server/resource/package/server/router/enter.go.tpl b/server/resource/package/server/router/enter.go.tpl new file mode 100644 index 0000000..178aecf --- /dev/null +++ b/server/resource/package/server/router/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type RouterGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/router/router.go.tpl b/server/resource/package/server/router/router.go.tpl new file mode 100644 index 0000000..cac47ab --- /dev/null +++ b/server/resource/package/server/router/router.go.tpl @@ -0,0 +1,42 @@ +package {{.Package}} + +import ( + {{if .OnlyTemplate}}// {{ end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +type {{.StructName}}Router struct {} + +// Init{{.StructName}}Router 初始化 {{.Description}} 路由信息 +func (s *{{.StructName}}Router) Init{{.StructName}}Router(Router *gin.RouterGroup,PublicRouter *gin.RouterGroup) { + {{- if not .OnlyTemplate}} + {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- else }} + // {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- end}} + {{.Abbreviation}}RouterWithoutAuth := PublicRouter.Group("{{.Abbreviation}}") + {{- if not .OnlyTemplate}} + { + {{.Abbreviation}}Router.POST("create{{.StructName}}", {{.Abbreviation}}Api.Create{{.StructName}}) // 新建{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}", {{.Abbreviation}}Api.Delete{{.StructName}}) // 删除{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}ByIds", {{.Abbreviation}}Api.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + {{.Abbreviation}}Router.PUT("update{{.StructName}}", {{.Abbreviation}}Api.Update{{.StructName}}) // 更新{{.Description}} + } + { + {{.Abbreviation}}RouterWithoutRecord.GET("find{{.StructName}}", {{.Abbreviation}}Api.Find{{.StructName}}) // 根据ID获取{{.Description}} + {{.Abbreviation}}RouterWithoutRecord.GET("get{{.StructName}}List", {{.Abbreviation}}Api.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + {{- if .HasDataSource}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}DataSource", {{.Abbreviation}}Api.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{- else}} + { + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{ end }} +} diff --git a/server/resource/package/server/service/enter.go.tpl b/server/resource/package/server/service/enter.go.tpl new file mode 100644 index 0000000..adf1db0 --- /dev/null +++ b/server/resource/package/server/service/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ServiceGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/service/service.go.tpl b/server/resource/package/server/service/service.go.tpl new file mode 100644 index 0000000..acd57a3 --- /dev/null +++ b/server/resource/package/server/service/service.go.tpl @@ -0,0 +1,213 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + {{ GenerateSearchConditions .Fields }} +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{ $dataDB := "" }} +{{- if eq $value.DBName "" }} +{{ $dataDB = $db }} +{{- else}} +{{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} +{{- end}} +{{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package {{.Package}} + +import ( +{{- if not .OnlyTemplate }} + "context" + "{{.Module}}/global" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- else }} + "{{.Module}}/utils" + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- end }} +) + +type {{.StructName}}Service struct {} + +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&{{.Package}}.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&{{.Package}}.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} {{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context) (list []*{{.Package}}.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []*{{.Package}}.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context, info {{.Package}}Req.{{.StructName}}Search) (list []{{.Package}}.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []{{.Package}}.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } +{{- end }} + {{ GenerateSearchConditions .Fields }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} + +{{- end }} + +{{- if .HasDataSource }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{ $dataDB := "" }} + {{- if eq $value.DBName "" }} + {{ $dataDB = "global.GVA_DB" }} + {{- else}} + {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} + {{- end}} + {{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}Public(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} +{{- end }} diff --git a/server/resource/package/web/api/api.js.tpl b/server/resource/package/web/api/api.js.tpl new file mode 100644 index 0000000..a41ef6f --- /dev/null +++ b/server/resource/package/web/api/api.js.tpl @@ -0,0 +1,130 @@ +import service from '@/utils/request' + +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} + +{{- end}} + +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/server/resource/package/web/view/form.vue.tpl b/server/resource/package/web/view/form.vue.tpl new file mode 100644 index 0000000..28c1f02 --- /dev/null +++ b/server/resource/package/web/view/form.vue.tpl @@ -0,0 +1,274 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/server/resource/package/web/view/table.vue.tpl b/server/resource/package/web/view/table.vue.tpl new file mode 100644 index 0000000..b2662da --- /dev/null +++ b/server/resource/package/web/view/table.vue.tpl @@ -0,0 +1,694 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} + +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/server/resource/plugin/server/api/api.go.tpl b/server/resource/plugin/server/api/api.go.tpl new file mode 100644 index 0000000..e69ae82 --- /dev/null +++ b/server/resource/plugin/server/api/api.go.tpl @@ -0,0 +1,255 @@ +package api + +import ( +{{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree}} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- end }} + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} +{{- else }} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} +{{if not .OnlyTemplate}} +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func (a *{{.Abbreviation}}) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + info.CreatedBy = utils.GetUserID(c) + {{- end }} + err = service{{ .StructName }}.Create{{.StructName}}(ctx,&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func (a *{{.Abbreviation}}) Update{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } +{{- if .AutoCreateResource }} + info.UpdatedBy = utils.GetUserID(c) +{{- end }} + err = service{{ .StructName }}.Update{{.StructName}}(ctx,info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data=model.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func (a *{{.Abbreviation}}) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := service{{ .StructName }}.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := service{{ .StructName }}.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo request.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service{{ .StructName }}.Get{{.StructName}}InfoList(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) +} +{{- end }} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := service{{ .StructName }}.Get{{.StructName}}DataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} +{{- end }} +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + service{{ .StructName }}.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{"info": "不需要鉴权的{{.Description}}接口信息"}, "获取成功", c) +} diff --git a/server/resource/plugin/server/api/enter.go.tpl b/server/resource/plugin/server/api/enter.go.tpl new file mode 100644 index 0000000..989fb35 --- /dev/null +++ b/server/resource/plugin/server/api/enter.go.tpl @@ -0,0 +1,6 @@ +package api + +var Api = new(api) + +type api struct { +} diff --git a/server/resource/plugin/server/config/config.go.tpl b/server/resource/plugin/server/config/config.go.tpl new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/server/resource/plugin/server/config/config.go.tpl @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/server/resource/plugin/server/gen/gen.go.tpl b/server/resource/plugin/server/gen/gen.go.tpl new file mode 100644 index 0000000..5639d4a --- /dev/null +++ b/server/resource/plugin/server/gen/gen.go.tpl @@ -0,0 +1,18 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" +) + +//go:generate go mod tidy +//go:generate go mod download +//go:generate go run gen.go +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: filepath.Join("..", "..", "..", "{{ .Package }}", "blender", "model", "dao"), + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, + }) + g.ApplyBasic() + g.Execute() +} diff --git a/server/resource/plugin/server/initialize/api.go.tpl b/server/resource/plugin/server/initialize/api.go.tpl new file mode 100644 index 0000000..dfbea23 --- /dev/null +++ b/server/resource/plugin/server/initialize/api.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Api(ctx context.Context) { + entities := []model.SysApi{} + utils.RegisterApis(entities...) +} diff --git a/server/resource/plugin/server/initialize/dictionary.go.tpl b/server/resource/plugin/server/initialize/dictionary.go.tpl new file mode 100644 index 0000000..e61b42c --- /dev/null +++ b/server/resource/plugin/server/initialize/dictionary.go.tpl @@ -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...) +} diff --git a/server/resource/plugin/server/initialize/gorm.go.tpl b/server/resource/plugin/server/initialize/gorm.go.tpl new file mode 100644 index 0000000..52c8183 --- /dev/null +++ b/server/resource/plugin/server/initialize/gorm.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "context" + "fmt" + "{{.Module}}/global" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate() + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/resource/plugin/server/initialize/menu.go.tpl b/server/resource/plugin/server/initialize/menu.go.tpl new file mode 100644 index 0000000..8774f35 --- /dev/null +++ b/server/resource/plugin/server/initialize/menu.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Menu(ctx context.Context) { + entities := []model.SysBaseMenu{} + utils.RegisterMenus(entities...) +} diff --git a/server/resource/plugin/server/initialize/router.go.tpl b/server/resource/plugin/server/initialize/router.go.tpl new file mode 100644 index 0000000..fbf03a3 --- /dev/null +++ b/server/resource/plugin/server/initialize/router.go.tpl @@ -0,0 +1,14 @@ +package initialize + +import ( + "{{.Module}}/global" + "{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + public.Use() + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) +} diff --git a/server/resource/plugin/server/initialize/viper.go.tpl b/server/resource/plugin/server/initialize/viper.go.tpl new file mode 100644 index 0000000..e759ad6 --- /dev/null +++ b/server/resource/plugin/server/initialize/viper.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "{{.Module}}/global" + "{{.Module}}/plugin/{{ .Package }}/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("{{ .Package }}", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/resource/plugin/server/model/model.go.tpl b/server/resource/plugin/server/model/model.go.tpl new file mode 100644 index 0000000..283841c --- /dev/null +++ b/server/resource/plugin/server/model/model.go.tpl @@ -0,0 +1,76 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + +{{ else }} +package model + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.StructName}} {{.Description}} 结构体 +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} + {{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + + +{{ end }} diff --git a/server/resource/plugin/server/model/request/request.go.tpl b/server/resource/plugin/server/model/request/request.go.tpl new file mode 100644 index 0000000..60cf677 --- /dev/null +++ b/server/resource/plugin/server/model/request/request.go.tpl @@ -0,0 +1,38 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request +{{- if not .OnlyTemplate}} +import ( + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} +) +{{- end}} +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} + +{{- if .GvaModel }} + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end }} +} +{{- end }} diff --git a/server/resource/plugin/server/plugin.go.tpl b/server/resource/plugin/server/plugin.go.tpl new file mode 100644 index 0000000..43ed056 --- /dev/null +++ b/server/resource/plugin/server/plugin.go.tpl @@ -0,0 +1,33 @@ +package {{ .Package }} + +import ( + "context" + "{{.Module}}/plugin/{{ .Package }}/initialize" + interfaces "{{.Module}}/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +func init() { + interfaces.Register(Plugin) +} + + +// 如果需要配置文件,请到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) + initialize.Router(group) +} diff --git a/server/resource/plugin/server/plugin/plugin.go.tpl b/server/resource/plugin/server/plugin/plugin.go.tpl new file mode 100644 index 0000000..7e25e07 --- /dev/null +++ b/server/resource/plugin/server/plugin/plugin.go.tpl @@ -0,0 +1,5 @@ +package plugin + +import "{{.Module}}/plugin/{{ .Package }}/config" + +var Config config.Config diff --git a/server/resource/plugin/server/router/enter.go.tpl b/server/resource/plugin/server/router/enter.go.tpl new file mode 100644 index 0000000..78517b3 --- /dev/null +++ b/server/resource/plugin/server/router/enter.go.tpl @@ -0,0 +1,6 @@ +package router + +var Router = new(router) + +type router struct { +} diff --git a/server/resource/plugin/server/router/router.go.tpl b/server/resource/plugin/server/router/router.go.tpl new file mode 100644 index 0000000..34bf4d8 --- /dev/null +++ b/server/resource/plugin/server/router/router.go.tpl @@ -0,0 +1,46 @@ +package router + +import ( + {{if .OnlyTemplate }} // {{end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +// Init 初始化 {{.Description}} 路由信息 +func (r *{{.Abbreviation}}) Init(public *gin.RouterGroup, private *gin.RouterGroup) { +{{- if not .OnlyTemplate }} + { + group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + group.POST("create{{.StructName}}", api{{.StructName}}.Create{{.StructName}}) // 新建{{.Description}} + group.DELETE("delete{{.StructName}}", api{{.StructName}}.Delete{{.StructName}}) // 删除{{.Description}} + group.DELETE("delete{{.StructName}}ByIds", api{{.StructName}}.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + group.PUT("update{{.StructName}}", api{{.StructName}}.Update{{.StructName}}) // 更新{{.Description}} + } + { + group := private.Group("{{.Abbreviation}}") + group.GET("find{{.StructName}}", api{{.StructName}}.Find{{.StructName}}) // 根据ID获取{{.Description}} + group.GET("get{{.StructName}}List", api{{.StructName}}.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + group := public.Group("{{.Abbreviation}}") + {{- if .HasDataSource}} + group.GET("get{{.StructName}}DataSource", api{{.StructName}}.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- else}} + // { + // group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // } + // { + // group := private.Group("{{.Abbreviation}}") + // } + { + group := public.Group("{{.Abbreviation}}") + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- end}} +} diff --git a/server/resource/plugin/server/service/enter.go.tpl b/server/resource/plugin/server/service/enter.go.tpl new file mode 100644 index 0000000..034facb --- /dev/null +++ b/server/resource/plugin/server/service/enter.go.tpl @@ -0,0 +1,7 @@ +package service + +var Service = new(service) + +type service struct { +} + diff --git a/server/resource/plugin/server/service/service.go.tpl b/server/resource/plugin/server/service/service.go.tpl new file mode 100644 index 0000000..9743602 --- /dev/null +++ b/server/resource/plugin/server/service/service.go.tpl @@ -0,0 +1,211 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + + {{ GenerateSearchConditions .Fields }} + +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package service + +import ( +{{- if not .OnlyTemplate }} + "context" + "{{.Module}}/global" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree }} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- else }} + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- if .IsTree }} + "{{.Module}}/utils" +{{- end }} +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *model.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&model.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&model.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} model.{{.StructName}}) (err error) { + err = {{$db}}.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} model.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context) (list []*model.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []*model.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context, info request.{{.StructName}}Search) (list []model.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []model.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } +{{- end }} + {{ GenerateSearchConditions .Fields }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} +{{- end }} +{{- if .HasDataSource }} +func (s *{{.Abbreviation}})Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} + +func (s *{{.Abbreviation}})Get{{.StructName}}Public(ctx context.Context) { + +} +{{- end }} diff --git a/server/resource/plugin/web/api/api.js.tpl b/server/resource/plugin/web/api/api.js.tpl new file mode 100644 index 0000000..0462fde --- /dev/null +++ b/server/resource/plugin/web/api/api.js.tpl @@ -0,0 +1,127 @@ +import service from '@/utils/request' +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} +{{- end}} +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/server/resource/plugin/web/form/form.vue.tpl b/server/resource/plugin/web/form/form.vue.tpl new file mode 100644 index 0000000..7d3406a --- /dev/null +++ b/server/resource/plugin/web/form/form.vue.tpl @@ -0,0 +1,464 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/server/resource/plugin/web/view/view.vue.tpl b/server/resource/plugin/web/view/view.vue.tpl new file mode 100644 index 0000000..98b557a --- /dev/null +++ b/server/resource/plugin/web/view/view.vue.tpl @@ -0,0 +1,689 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data || [] + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/server/router/enter.go b/server/router/enter.go new file mode 100644 index 0000000..6e6d811 --- /dev/null +++ b/server/router/enter.go @@ -0,0 +1,13 @@ +package router + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/router/example" + "github.com/flipped-aurora/gin-vue-admin/server/router/system" +) + +var RouterGroupApp = new(RouterGroup) + +type RouterGroup struct { + System system.RouterGroup + Example example.RouterGroup +} diff --git a/server/router/example/enter.go b/server/router/example/enter.go new file mode 100644 index 0000000..17cd3d5 --- /dev/null +++ b/server/router/example/enter.go @@ -0,0 +1,19 @@ +package example + +import ( + api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" +) + +type RouterGroup struct { + CustomerRouter + + AttachmentCategoryRouter + FileUploadAndDownloadRouter +} + +var ( + exaCustomerApi = api.ApiGroupApp.ExampleApiGroup.CustomerApi + + attachmentCategoryApi = api.ApiGroupApp.ExampleApiGroup.AttachmentCategoryApi + exaFileUploadAndDownloadApi = api.ApiGroupApp.ExampleApiGroup.FileUploadAndDownloadApi +) diff --git a/server/router/example/exa_attachment_category.go b/server/router/example/exa_attachment_category.go new file mode 100644 index 0000000..4900292 --- /dev/null +++ b/server/router/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type AttachmentCategoryRouter struct{} + +func (r *AttachmentCategoryRouter) InitAttachmentCategoryRouterRouter(Router *gin.RouterGroup) { + router := Router.Group("attachmentCategory") + { + router.GET("getCategoryList", attachmentCategoryApi.GetCategoryList) // 分类列表 + router.POST("addCategory", attachmentCategoryApi.AddCategory) // 添加/编辑分类 + router.POST("deleteCategory", attachmentCategoryApi.DeleteCategory) // 删除分类 + } +} diff --git a/server/router/example/exa_customer.go b/server/router/example/exa_customer.go new file mode 100644 index 0000000..acdf3c7 --- /dev/null +++ b/server/router/example/exa_customer.go @@ -0,0 +1,22 @@ +package example + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type CustomerRouter struct{} + +func (e *CustomerRouter) InitCustomerRouter(Router *gin.RouterGroup) { + customerRouter := Router.Group("customer").Use(middleware.OperationRecord()) + customerRouterWithoutRecord := Router.Group("customer") + { + customerRouter.POST("customer", exaCustomerApi.CreateExaCustomer) // 创建客户 + customerRouter.PUT("customer", exaCustomerApi.UpdateExaCustomer) // 更新客户 + customerRouter.DELETE("customer", exaCustomerApi.DeleteExaCustomer) // 删除客户 + } + { + customerRouterWithoutRecord.GET("customer", exaCustomerApi.GetExaCustomer) // 获取单一客户信息 + customerRouterWithoutRecord.GET("customerList", exaCustomerApi.GetExaCustomerList) // 获取客户列表 + } +} diff --git a/server/router/example/exa_file_upload_and_download.go b/server/router/example/exa_file_upload_and_download.go new file mode 100644 index 0000000..84f6ecd --- /dev/null +++ b/server/router/example/exa_file_upload_and_download.go @@ -0,0 +1,22 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type FileUploadAndDownloadRouter struct{} + +func (e *FileUploadAndDownloadRouter) InitFileUploadAndDownloadRouter(Router *gin.RouterGroup) { + fileUploadAndDownloadRouter := Router.Group("fileUploadAndDownload") + { + fileUploadAndDownloadRouter.POST("upload", exaFileUploadAndDownloadApi.UploadFile) // 上传文件 + fileUploadAndDownloadRouter.POST("getFileList", exaFileUploadAndDownloadApi.GetFileList) // 获取上传文件列表 + fileUploadAndDownloadRouter.POST("deleteFile", exaFileUploadAndDownloadApi.DeleteFile) // 删除指定文件 + fileUploadAndDownloadRouter.POST("editFileName", exaFileUploadAndDownloadApi.EditFileName) // 编辑文件名或者备注 + fileUploadAndDownloadRouter.POST("breakpointContinue", exaFileUploadAndDownloadApi.BreakpointContinue) // 断点续传 + fileUploadAndDownloadRouter.GET("findFile", exaFileUploadAndDownloadApi.FindFile) // 查询当前文件成功的切片 + fileUploadAndDownloadRouter.POST("breakpointContinueFinish", exaFileUploadAndDownloadApi.BreakpointContinueFinish) // 切片传输完成 + fileUploadAndDownloadRouter.POST("removeChunk", exaFileUploadAndDownloadApi.RemoveChunk) // 删除切片 + fileUploadAndDownloadRouter.POST("importURL", exaFileUploadAndDownloadApi.ImportURL) // 导入URL + } +} diff --git a/server/router/system/enter.go b/server/router/system/enter.go new file mode 100644 index 0000000..7e5d23c --- /dev/null +++ b/server/router/system/enter.go @@ -0,0 +1,53 @@ +package system + +import api "github.com/flipped-aurora/gin-vue-admin/server/api/v1" + +type RouterGroup struct { + ApiRouter + JwtRouter + SysRouter + BaseRouter + InitRouter + MenuRouter + UserRouter + CasbinRouter + AutoCodeRouter + AuthorityRouter + DictionaryRouter + OperationRecordRouter + DictionaryDetailRouter + AuthorityBtnRouter + SysExportTemplateRouter + SysParamsRouter + SysVersionRouter + SysErrorRouter + LoginLogRouter + ApiTokenRouter + SkillsRouter +} + +var ( + dbApi = api.ApiGroupApp.SystemApiGroup.DBApi + jwtApi = api.ApiGroupApp.SystemApiGroup.JwtApi + baseApi = api.ApiGroupApp.SystemApiGroup.BaseApi + casbinApi = api.ApiGroupApp.SystemApiGroup.CasbinApi + systemApi = api.ApiGroupApp.SystemApiGroup.SystemApi + sysParamsApi = api.ApiGroupApp.SystemApiGroup.SysParamsApi + autoCodeApi = api.ApiGroupApp.SystemApiGroup.AutoCodeApi + authorityApi = api.ApiGroupApp.SystemApiGroup.AuthorityApi + apiRouterApi = api.ApiGroupApp.SystemApiGroup.SystemApiApi + dictionaryApi = api.ApiGroupApp.SystemApiGroup.DictionaryApi + authorityBtnApi = api.ApiGroupApp.SystemApiGroup.AuthorityBtnApi + authorityMenuApi = api.ApiGroupApp.SystemApiGroup.AuthorityMenuApi + autoCodePluginApi = api.ApiGroupApp.SystemApiGroup.AutoCodePluginApi + autocodeHistoryApi = api.ApiGroupApp.SystemApiGroup.AutoCodeHistoryApi + operationRecordApi = api.ApiGroupApp.SystemApiGroup.OperationRecordApi + autoCodePackageApi = api.ApiGroupApp.SystemApiGroup.AutoCodePackageApi + dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi + 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 + aiWorkflowSessionApi = api.ApiGroupApp.SystemApiGroup.AIWorkflowSessionApi +) diff --git a/server/router/system/sys_api.go b/server/router/system/sys_api.go new file mode 100644 index 0000000..0134a25 --- /dev/null +++ b/server/router/system/sys_api.go @@ -0,0 +1,35 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type ApiRouter struct{} + +func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) { + apiRouter := Router.Group("api").Use(middleware.OperationRecord()) + apiRouterWithoutRecord := Router.Group("api") + + apiPublicRouterWithoutRecord := RouterPub.Group("api") + { + apiRouter.GET("getApiGroups", apiRouterApi.GetApiGroups) // 获取路由组 + apiRouter.GET("syncApi", apiRouterApi.SyncApi) // 同步Api + apiRouter.POST("ignoreApi", apiRouterApi.IgnoreApi) // 忽略Api + apiRouter.POST("enterSyncApi", apiRouterApi.EnterSyncApi) // 确认同步Api + apiRouter.POST("createApi", apiRouterApi.CreateApi) // 创建Api + apiRouter.POST("deleteApi", apiRouterApi.DeleteApi) // 删除Api + apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息 + apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api + apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api + apiRouter.POST("setApiRoles", apiRouterApi.SetApiRoles) // 全量覆盖API关联角色 + } + { + apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api + apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表 + apiRouterWithoutRecord.GET("getApiRoles", apiRouterApi.GetApiRoles) // 获取API关联角色ID列表 + } + { + apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限 + } +} diff --git a/server/router/system/sys_api_token.go b/server/router/system/sys_api_token.go new file mode 100644 index 0000000..ebedeb0 --- /dev/null +++ b/server/router/system/sys_api_token.go @@ -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 + } +} diff --git a/server/router/system/sys_authority.go b/server/router/system/sys_authority.go new file mode 100644 index 0000000..67a2880 --- /dev/null +++ b/server/router/system/sys_authority.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type AuthorityRouter struct{} + +func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) { + authorityRouter := Router.Group("authority").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authority") + { + authorityRouter.POST("createAuthority", authorityApi.CreateAuthority) // 创建角色 + authorityRouter.POST("deleteAuthority", authorityApi.DeleteAuthority) // 删除角色 + authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色 + authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色 + authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限 + authorityRouter.POST("setRoleUsers", authorityApi.SetRoleUsers) // 全量覆盖角色关联用户 + } + { + authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表 + authorityRouterWithoutRecord.GET("getUsersByAuthority", authorityApi.GetUsersByAuthority) // 获取角色关联用户ID列表 + } +} diff --git a/server/router/system/sys_authority_btn.go b/server/router/system/sys_authority_btn.go new file mode 100644 index 0000000..370db85 --- /dev/null +++ b/server/router/system/sys_authority_btn.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AuthorityBtnRouter struct{} + +var AuthorityBtnRouterApp = new(AuthorityBtnRouter) + +func (s *AuthorityBtnRouter) InitAuthorityBtnRouterRouter(Router *gin.RouterGroup) { + // authorityRouter := Router.Group("authorityBtn").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authorityBtn") + { + authorityRouterWithoutRecord.POST("getAuthorityBtn", authorityBtnApi.GetAuthorityBtn) + authorityRouterWithoutRecord.POST("setAuthorityBtn", authorityBtnApi.SetAuthorityBtn) + authorityRouterWithoutRecord.POST("canRemoveAuthorityBtn", authorityBtnApi.CanRemoveAuthorityBtn) + } +} diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go new file mode 100644 index 0000000..deca5cc --- /dev/null +++ b/server/router/system/sys_auto_code.go @@ -0,0 +1,46 @@ +package system + +import "github.com/gin-gonic/gin" + +type AutoCodeRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPublic *gin.RouterGroup) { + autoCodeRouter := Router.Group("autoCode") + publicAutoCodeRouter := RouterPublic.Group("autoCode") + { + autoCodeRouter.GET("getDB", autoCodeApi.GetDB) + autoCodeRouter.GET("getTables", autoCodeApi.GetTables) + autoCodeRouter.GET("getColumn", autoCodeApi.GetColumn) + } + { + autoCodeRouter.POST("preview", autoCodeTemplateApi.Preview) + autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) + autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) + } + { + autoCodeRouter.POST("mcp", autoCodeTemplateApi.MCP) + autoCodeRouter.POST("mcpStatus", autoCodeTemplateApi.MCPStatus) + autoCodeRouter.POST("mcpStart", autoCodeTemplateApi.MCPStart) + autoCodeRouter.POST("mcpStop", autoCodeTemplateApi.MCPStop) + autoCodeRouter.POST("mcpList", autoCodeTemplateApi.MCPList) + autoCodeRouter.POST("mcpRoutes", autoCodeTemplateApi.MCPRoutes) + autoCodeRouter.POST("mcpTest", autoCodeTemplateApi.MCPTest) + } + { + autoCodeRouter.POST("getPackage", autoCodePackageApi.All) + autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) + autoCodeRouter.POST("createPackage", autoCodePackageApi.Create) + autoCodeRouter.POST("saveAIWorkflowSession", aiWorkflowSessionApi.Save) + autoCodeRouter.POST("getAIWorkflowSessionList", aiWorkflowSessionApi.GetList) + autoCodeRouter.POST("getAIWorkflowSessionDetail", aiWorkflowSessionApi.GetDetail) + autoCodeRouter.POST("deleteAIWorkflowSession", aiWorkflowSessionApi.Delete) + autoCodeRouter.POST("dumpAIWorkflowMarkdown", aiWorkflowSessionApi.DumpMarkdown) + } + { + autoCodeRouter.GET("getTemplates", autoCodePackageApi.Templates) + } + { + publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto) + publicAutoCodeRouter.POST("llmAutoSSE", autoCodeApi.LLMAutoSSE) + } +} diff --git a/server/router/system/sys_auto_code_history.go b/server/router/system/sys_auto_code_history.go new file mode 100644 index 0000000..42a2bef --- /dev/null +++ b/server/router/system/sys_auto_code_history.go @@ -0,0 +1,17 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AutoCodeHistoryRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeHistoryRouter(Router *gin.RouterGroup) { + autoCodeHistoryRouter := Router.Group("autoCode") + { + autoCodeHistoryRouter.POST("getMeta", autocodeHistoryApi.First) // 根据id获取meta信息 + autoCodeHistoryRouter.POST("rollback", autocodeHistoryApi.RollBack) // 回滚 + autoCodeHistoryRouter.POST("delSysHistory", autocodeHistoryApi.Delete) // 删除回滚记录 + autoCodeHistoryRouter.POST("getSysHistory", autocodeHistoryApi.GetList) // 获取回滚记录分页 + } +} diff --git a/server/router/system/sys_base.go b/server/router/system/sys_base.go new file mode 100644 index 0000000..7d959bb --- /dev/null +++ b/server/router/system/sys_base.go @@ -0,0 +1,16 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type BaseRouter struct{} + +func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + baseRouter := Router.Group("base") + { + baseRouter.POST("login", baseApi.Login) + baseRouter.POST("captcha", baseApi.Captcha) + } + return baseRouter +} diff --git a/server/router/system/sys_casbin.go b/server/router/system/sys_casbin.go new file mode 100644 index 0000000..e4a3eb1 --- /dev/null +++ b/server/router/system/sys_casbin.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type CasbinRouter struct{} + +func (s *CasbinRouter) InitCasbinRouter(Router *gin.RouterGroup) { + casbinRouter := Router.Group("casbin").Use(middleware.OperationRecord()) + casbinRouterWithoutRecord := Router.Group("casbin") + { + casbinRouter.POST("updateCasbin", casbinApi.UpdateCasbin) + } + { + casbinRouterWithoutRecord.POST("getPolicyPathByAuthorityId", casbinApi.GetPolicyPathByAuthorityId) + } +} diff --git a/server/router/system/sys_dictionary.go b/server/router/system/sys_dictionary.go new file mode 100644 index 0000000..c95e8ff --- /dev/null +++ b/server/router/system/sys_dictionary.go @@ -0,0 +1,24 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryRouter struct{} + +func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) { + sysDictionaryRouter := Router.Group("sysDictionary").Use(middleware.OperationRecord()) + sysDictionaryRouterWithoutRecord := Router.Group("sysDictionary") + { + 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 + sysDictionaryRouterWithoutRecord.GET("getSysDictionaryList", dictionaryApi.GetSysDictionaryList) // 获取SysDictionary列表 + } +} diff --git a/server/router/system/sys_dictionary_detail.go b/server/router/system/sys_dictionary_detail.go new file mode 100644 index 0000000..3f4aa5d --- /dev/null +++ b/server/router/system/sys_dictionary_detail.go @@ -0,0 +1,26 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryDetailRouter struct{} + +func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.RouterGroup) { + dictionaryDetailRouter := Router.Group("sysDictionaryDetail").Use(middleware.OperationRecord()) + dictionaryDetailRouterWithoutRecord := Router.Group("sysDictionaryDetail") + { + dictionaryDetailRouter.POST("createSysDictionaryDetail", dictionaryDetailApi.CreateSysDictionaryDetail) // 新建SysDictionaryDetail + dictionaryDetailRouter.DELETE("deleteSysDictionaryDetail", dictionaryDetailApi.DeleteSysDictionaryDetail) // 删除SysDictionaryDetail + dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail + } + { + dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail + dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径 + } +} diff --git a/server/router/system/sys_error.go b/server/router/system/sys_error.go new file mode 100644 index 0000000..ea38602 --- /dev/null +++ b/server/router/system/sys_error.go @@ -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) // 新建错误日志 + } +} diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go new file mode 100644 index 0000000..fb82fb1 --- /dev/null +++ b/server/router/system/sys_export_template.go @@ -0,0 +1,35 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysExportTemplateRouter struct { +} + +// InitSysExportTemplateRouter 初始化 导出模板 路由信息 +func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { + sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord()) + sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate") + sysExportTemplateRouterWithoutAuth := pubRouter.Group("sysExportTemplate") + + { + sysExportTemplateRouter.POST("createSysExportTemplate", exportTemplateApi.CreateSysExportTemplate) // 新建导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplate", exportTemplateApi.DeleteSysExportTemplate) // 删除导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplateByIds", exportTemplateApi.DeleteSysExportTemplateByIds) // 批量删除导出模板 + sysExportTemplateRouter.PUT("updateSysExportTemplate", exportTemplateApi.UpdateSysExportTemplate) // 更新导出模板 + sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 导入excel模板数据 + } + { + sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板 + 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导出表格 + sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板 + } +} diff --git a/server/router/system/sys_initdb.go b/server/router/system/sys_initdb.go new file mode 100644 index 0000000..3a6de50 --- /dev/null +++ b/server/router/system/sys_initdb.go @@ -0,0 +1,15 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type InitRouter struct{} + +func (s *InitRouter) InitInitRouter(Router *gin.RouterGroup) { + initRouter := Router.Group("init") + { + initRouter.POST("initdb", dbApi.InitDB) // 初始化数据库 + initRouter.POST("checkdb", dbApi.CheckDB) // 检测是否需要初始化数据库 + } +} diff --git a/server/router/system/sys_jwt.go b/server/router/system/sys_jwt.go new file mode 100644 index 0000000..4716031 --- /dev/null +++ b/server/router/system/sys_jwt.go @@ -0,0 +1,14 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type JwtRouter struct{} + +func (s *JwtRouter) InitJwtRouter(Router *gin.RouterGroup) { + jwtRouter := Router.Group("jwt") + { + jwtRouter.POST("jsonInBlacklist", jwtApi.JsonInBlacklist) // jwt加入黑名单 + } +} diff --git a/server/router/system/sys_login_log.go b/server/router/system/sys_login_log.go new file mode 100644 index 0000000..3ef672e --- /dev/null +++ b/server/router/system/sys_login_log.go @@ -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) // 获取登录日志列表 + } +} diff --git a/server/router/system/sys_menu.go b/server/router/system/sys_menu.go new file mode 100644 index 0000000..69fb65c --- /dev/null +++ b/server/router/system/sys_menu.go @@ -0,0 +1,29 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type MenuRouter struct{} + +func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + menuRouter := Router.Group("menu").Use(middleware.OperationRecord()) + menuRouterWithoutRecord := Router.Group("menu") + { + menuRouter.POST("addBaseMenu", authorityMenuApi.AddBaseMenu) // 新增菜单 + menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系 + menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单 + menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单 + menuRouter.POST("setMenuRoles", authorityMenuApi.SetMenuRoles) // 全量覆盖菜单关联角色 + } + { + menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树 + menuRouterWithoutRecord.POST("getMenuList", authorityMenuApi.GetMenuList) // 分页获取基础menu列表 + menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由 + menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu + menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单 + menuRouterWithoutRecord.GET("getMenuRoles", authorityMenuApi.GetMenuRoles) // 获取菜单关联角色ID列表 + } + return menuRouter +} diff --git a/server/router/system/sys_operation_record.go b/server/router/system/sys_operation_record.go new file mode 100644 index 0000000..d158d5e --- /dev/null +++ b/server/router/system/sys_operation_record.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type OperationRecordRouter struct{} + +func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { + operationRecordRouter := Router.Group("sysOperationRecord") + { + operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord + operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord + operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord + operationRecordRouter.GET("getSysOperationRecordList", operationRecordApi.GetSysOperationRecordList) // 获取SysOperationRecord列表 + + } +} diff --git a/server/router/system/sys_params.go b/server/router/system/sys_params.go new file mode 100644 index 0000000..50dd236 --- /dev/null +++ b/server/router/system/sys_params.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysParamsRouter struct{} + +// InitSysParamsRouter 初始化 参数 路由信息 +func (s *SysParamsRouter) InitSysParamsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + sysParamsRouter := Router.Group("sysParams").Use(middleware.OperationRecord()) + sysParamsRouterWithoutRecord := Router.Group("sysParams") + { + sysParamsRouter.POST("createSysParams", sysParamsApi.CreateSysParams) // 新建参数 + sysParamsRouter.DELETE("deleteSysParams", sysParamsApi.DeleteSysParams) // 删除参数 + sysParamsRouter.DELETE("deleteSysParamsByIds", sysParamsApi.DeleteSysParamsByIds) // 批量删除参数 + sysParamsRouter.PUT("updateSysParams", sysParamsApi.UpdateSysParams) // 更新参数 + } + { + sysParamsRouterWithoutRecord.GET("findSysParams", sysParamsApi.FindSysParams) // 根据ID获取参数 + sysParamsRouterWithoutRecord.GET("getSysParamsList", sysParamsApi.GetSysParamsList) // 获取参数列表 + sysParamsRouterWithoutRecord.GET("getSysParam", sysParamsApi.GetSysParam) // 根据Key获取参数 + } +} diff --git a/server/router/system/sys_skills.go b/server/router/system/sys_skills.go new file mode 100644 index 0000000..d9f21bf --- /dev/null +++ b/server/router/system/sys_skills.go @@ -0,0 +1,35 @@ +package system + +import "github.com/gin-gonic/gin" + +type SkillsRouter struct{} + +func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { + skillsRouter := Router.Group("skills") + skillsRouterPub := pubRouter.Group("skills") + { + skillsRouter.GET("getTools", skillsApi.GetTools) + skillsRouter.POST("getSkillList", skillsApi.GetSkillList) + skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail) + skillsRouter.POST("saveSkill", skillsApi.SaveSkill) + skillsRouter.POST("deleteSkill", skillsApi.DeleteSkill) + 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) + skillsRouter.POST("packageSkill", skillsApi.PackageSkill) + } + { + skillsRouterPub.POST("downloadOnlineSkill", skillsApi.DownloadOnlineSkill) + } +} diff --git a/server/router/system/sys_system.go b/server/router/system/sys_system.go new file mode 100644 index 0000000..1a9643f --- /dev/null +++ b/server/router/system/sys_system.go @@ -0,0 +1,22 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysRouter struct{} + +func (s *SysRouter) InitSystemRouter(Router *gin.RouterGroup) { + sysRouter := Router.Group("system").Use(middleware.OperationRecord()) + sysRouterWithoutRecord := Router.Group("system") + + { + sysRouter.POST("setSystemConfig", systemApi.SetSystemConfig) // 设置配置文件内容 + sysRouter.POST("reloadSystem", systemApi.ReloadSystem) // 重启服务 + } + { + sysRouterWithoutRecord.POST("getSystemConfig", systemApi.GetSystemConfig) // 获取配置文件内容 + sysRouterWithoutRecord.POST("getServerInfo", systemApi.GetServerInfo) // 获取服务器信息 + } +} diff --git a/server/router/system/sys_user.go b/server/router/system/sys_user.go new file mode 100644 index 0000000..0e076f7 --- /dev/null +++ b/server/router/system/sys_user.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type UserRouter struct{} + +func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { + userRouter := Router.Group("user").Use(middleware.OperationRecord()) + userRouterWithoutRecord := Router.Group("user") + { + userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号 + userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码 + userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限 + userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户 + userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 + userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 + userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 + userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码 + userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 + } + { + userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表 + userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息 + } +} diff --git a/server/router/system/sys_version.go b/server/router/system/sys_version.go new file mode 100644 index 0000000..249feb8 --- /dev/null +++ b/server/router/system/sys_version.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysVersionRouter struct{} + +// InitSysVersionRouter 初始化 版本管理 路由信息 +func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) { + sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord()) + sysVersionRouterWithoutRecord := Router.Group("sysVersion") + { + sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理 + sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理 + sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据 + sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据 + } + { + sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理 + sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表 + sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据 + } +} diff --git a/server/service/enter.go b/server/service/enter.go new file mode 100644 index 0000000..4dc990e --- /dev/null +++ b/server/service/enter.go @@ -0,0 +1,13 @@ +package service + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/service/example" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" +) + +var ServiceGroupApp = new(ServiceGroup) + +type ServiceGroup struct { + SystemServiceGroup system.ServiceGroup + ExampleServiceGroup example.ServiceGroup +} diff --git a/server/service/example/enter.go b/server/service/example/enter.go new file mode 100644 index 0000000..13f2557 --- /dev/null +++ b/server/service/example/enter.go @@ -0,0 +1,8 @@ +package example + +type ServiceGroup struct { + CustomerService + + AttachmentCategoryService + FileUploadAndDownloadService +} diff --git a/server/service/example/exa_attachment_category.go b/server/service/example/exa_attachment_category.go new file mode 100644 index 0000000..1e74fc9 --- /dev/null +++ b/server/service/example/exa_attachment_category.go @@ -0,0 +1,66 @@ +package example + +import ( + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "gorm.io/gorm" +) + +type AttachmentCategoryService struct{} + +// AddCategory 创建/更新的分类 +func (a *AttachmentCategoryService) AddCategory(req *example.ExaAttachmentCategory) (err error) { + // 检查是否已存在相同名称的分类 + if (!errors.Is(global.GVA_DB.Take(&example.ExaAttachmentCategory{}, "name = ? and pid = ?", req.Name, req.Pid).Error, gorm.ErrRecordNotFound)) { + return errors.New("分类名称已存在") + } + if req.ID > 0 { + if err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("id = ?", req.ID).Updates(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } else { + if err = global.GVA_DB.Create(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } + return nil +} + +// DeleteCategory 删除分类 +func (a *AttachmentCategoryService) DeleteCategory(id *int) error { + var childCount int64 + global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("pid = ?", id).Count(&childCount) + if childCount > 0 { + return errors.New("请先删除子级") + } + return global.GVA_DB.Where("id = ?", id).Unscoped().Delete(&example.ExaAttachmentCategory{}).Error +} + +// GetCategoryList 分类列表 +func (a *AttachmentCategoryService) GetCategoryList() (res []*example.ExaAttachmentCategory, err error) { + var fileLists []example.ExaAttachmentCategory + err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Find(&fileLists).Error + if err != nil { + return res, err + } + return a.getChildrenList(fileLists, 0), nil +} + +// getChildrenList 子类 +func (a *AttachmentCategoryService) getChildrenList(categories []example.ExaAttachmentCategory, parentID uint) []*example.ExaAttachmentCategory { + var tree []*example.ExaAttachmentCategory + for _, category := range categories { + if category.Pid == parentID { + category.Children = a.getChildrenList(categories, category.ID) + tree = append(tree, &category) + } + } + return tree +} diff --git a/server/service/example/exa_breakpoint_continue.go b/server/service/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..ddb6227 --- /dev/null +++ b/server/service/example/exa_breakpoint_continue.go @@ -0,0 +1,71 @@ +package example + +import ( + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "gorm.io/gorm" +) + +type FileUploadAndDownloadService struct{} + +var FileUploadAndDownloadServiceApp = new(FileUploadAndDownloadService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindOrCreateFile +//@description: 上传文件时检测当前文件属性,如果没有文件则创建,有则返回文件的当前切片 +//@param: fileMd5 string, fileName string, chunkTotal int +//@return: file model.ExaFile, err error + +func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName string, chunkTotal int) (file example.ExaFile, err error) { + var cfile example.ExaFile + cfile.FileMd5 = fileMd5 + cfile.FileName = fileName + cfile.ChunkTotal = chunkTotal + + 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 + } + cfile.IsFinish = true + cfile.FilePath = file.FilePath + err = global.GVA_DB.Create(&cfile).Error + return cfile, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateFileChunk +//@description: 创建文件切片记录 +//@param: id uint, fileChunkPath string, fileChunkNumber int +//@return: error + +func (e *FileUploadAndDownloadService) CreateFileChunk(id uint, fileChunkPath string, fileChunkNumber int) error { + var chunk example.ExaFileChunk + chunk.FileChunkPath = fileChunkPath + chunk.ExaFileID = id + chunk.FileChunkNumber = fileChunkNumber + err := global.GVA_DB.Create(&chunk).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除文件切片记录 +//@param: fileMd5 string, fileName string, filePath string +//@return: error + +func (e *FileUploadAndDownloadService) DeleteFileChunk(fileMd5 string, filePath string) error { + var chunks []example.ExaFileChunk + var file example.ExaFile + err := global.GVA_DB.Where("file_md5 = ?", fileMd5).First(&file). + Updates(map[string]interface{}{ + "IsFinish": true, + "file_path": filePath, + }).Error + if err != nil { + return err + } + err = global.GVA_DB.Where("exa_file_id = ?", file.ID).Delete(&chunks).Unscoped().Error + return err +} diff --git a/server/service/example/exa_customer.go b/server/service/example/exa_customer.go new file mode 100644 index 0000000..cf816f5 --- /dev/null +++ b/server/service/example/exa_customer.go @@ -0,0 +1,87 @@ +package example + +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/example" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemService "github.com/flipped-aurora/gin-vue-admin/server/service/system" +) + +type CustomerService struct{} + +var CustomerServiceApp = new(CustomerService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateExaCustomer +//@description: 创建客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) CreateExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Create(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) DeleteExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Delete(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateExaCustomer +//@description: 更新客户 +//@param: e *model.ExaCustomer +//@return: err error + +func (exa *CustomerService) UpdateExaCustomer(e *example.ExaCustomer) (err error) { + err = global.GVA_DB.Save(e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetExaCustomer +//@description: 获取客户信息 +//@param: id uint +//@return: customer model.ExaCustomer, err error + +func (exa *CustomerService) GetExaCustomer(id uint) (customer example.ExaCustomer, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&customer).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetCustomerInfoList +//@description: 分页获取客户列表 +//@param: sysUserAuthorityID string, info request.PageInfo +//@return: list interface{}, total int64, err error + +func (exa *CustomerService) GetCustomerInfoList(sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaCustomer{}) + var a system.SysAuthority + a.AuthorityId = sysUserAuthorityID + auth, err := systemService.AuthorityServiceApp.GetAuthorityInfo(a) + if err != nil { + return + } + var dataId []uint + for _, v := range auth.DataAuthorityId { + dataId = append(dataId, v.AuthorityId) + } + var CustomerList []example.ExaCustomer + err = db.Where("sys_user_authority_id in ?", dataId).Count(&total).Error + if err != nil { + return CustomerList, total, err + } else { + err = db.Limit(limit).Offset(offset).Preload("SysUser").Where("sys_user_authority_id in ?", dataId).Find(&CustomerList).Error + } + return CustomerList, total, err +} diff --git a/server/service/example/exa_file_upload_download.go b/server/service/example/exa_file_upload_download.go new file mode 100644 index 0000000..1a34262 --- /dev/null +++ b/server/service/example/exa_file_upload_download.go @@ -0,0 +1,130 @@ +package example + +import ( + "errors" + "mime/multipart" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "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) +//@function: Upload +//@description: 创建文件上传记录 +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) Upload(file example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindFile +//@description: 查询文件记录 +//@param: id uint +//@return: model.ExaFileUploadAndDownload, error + +func (e *FileUploadAndDownloadService) FindFile(id uint) (example.ExaFileUploadAndDownload, error) { + var file example.ExaFileUploadAndDownload + err := global.GVA_DB.Where("id = ?", id).First(&file).Error + return file, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFile +//@description: 删除文件记录 +//@param: file model.ExaFileUploadAndDownload +//@return: err error + +func (e *FileUploadAndDownloadService) DeleteFile(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + fileFromDb, err = e.FindFile(file.ID) + if err != nil { + return + } + oss := upload.NewOss() + if err = oss.DeleteFile(fileFromDb.Key); err != nil { + return errors.New("文件删除失败") + } + err = global.GVA_DB.Where("id = ?", file.ID).Unscoped().Delete(&file).Error + return err +} + +// EditFileName 编辑文件名或者备注 +func (e *FileUploadAndDownloadService) EditFileName(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + return global.GVA_DB.Where("id = ?", file.ID).First(&fileFromDb).Update("name", file.Name).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetFileRecordInfoList +//@description: 分页获取数据 +//@param: info request.ExaAttachmentCategorySearch +//@return: list interface{}, total int64, err error + +func (e *FileUploadAndDownloadService) GetFileRecordInfoList(info request.ExaAttachmentCategorySearch) (list []example.ExaFileUploadAndDownload, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaFileUploadAndDownload{}) + + if len(info.Keyword) > 0 { + db = db.Where("name LIKE ?", "%"+info.Keyword+"%") + } + + if info.ClassId > 0 { + db = db.Where("class_id = ?", info.ClassId) + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error + return list, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UploadFile +//@description: 根据配置文件判断是文件上传到本地或者七牛云 +//@param: header *multipart.FileHeader, noSave string +//@return: file model.ExaFileUploadAndDownload, err error + +func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, noSave string, classId int) (file example.ExaFileUploadAndDownload, err error) { + oss := upload.NewOss() + filePath, key, uploadErr := oss.UploadFile(header) + if uploadErr != nil { + return file, uploadErr + } + s := strings.Split(header.Filename, ".") + f := example.ExaFileUploadAndDownload{ + Url: filePath, + Name: header.Filename, + ClassId: classId, + Tag: s[len(s)-1], + Key: key, + } + if noSave == "0" { + // 检查是否已存在相同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 +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ImportURL +//@description: 导入URL +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) ImportURL(file *[]example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} diff --git a/server/service/system/ai_workflow_markdown.go b/server/service/system/ai_workflow_markdown.go new file mode 100644 index 0000000..96214ba --- /dev/null +++ b/server/service/system/ai_workflow_markdown.go @@ -0,0 +1,468 @@ +package system + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + "unicode" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common" + system "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemResp "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +const ( + aiWorkflowMarkdownRootDir = "ai-workflow-docs" + aiWorkflowAnalysisDir = "analysis" + aiWorkflowPromptDir = "prompt-workflow" +) + +func (s *aiWorkflowSession) DumpMarkdown(ctx context.Context, userID uint, info systemReq.SysAIWorkflowMarkdownDump) (result systemResp.AIWorkflowMarkdownDumpResult, err error) { + if userID == 0 { + return result, fmt.Errorf("用户未登录") + } + if info.Tab != "analysis" && info.Tab != "workflow" { + return result, fmt.Errorf("不支持的会话类型") + } + + root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root) + if root == "" { + return result, fmt.Errorf("autocode.root 未配置") + } + + session := system.SysAIWorkflowSession{ + GVA_MODEL: global.GVA_MODEL{ID: info.ID}, + UserID: userID, + Tab: info.Tab, + Title: strings.TrimSpace(info.Title), + Summary: strings.TrimSpace(info.Summary), + ConversationID: strings.TrimSpace(info.ConversationID), + MessageID: strings.TrimSpace(info.MessageID), + CurrentNodeID: strings.TrimSpace(info.CurrentNodeID), + Settings: cloneJSONMap(info.Settings), + FormData: cloneJSONMap(info.FormData), + ResultData: cloneJSONMap(info.ResultData), + Messages: sanitizeMessages(info.Messages), + } + if strings.TrimSpace(session.Title) == "" { + session.Title = s.titleFromMessages(session.Messages) + } + if strings.TrimSpace(session.Title) == "" { + session.Title = s.titleFromForm(systemReq.SysAIWorkflowSessionUpsert{ + Tab: info.Tab, + FormData: info.FormData, + }) + } + if strings.TrimSpace(session.Summary) == "" { + session.Summary = s.summaryFromResult(info.ResultData) + } + + markdown := buildAIWorkflowMarkdown(session) + if strings.TrimSpace(markdown) == "" { + return result, fmt.Errorf("没有可落盘的内容") + } + + targetDir := filepath.Join(root, aiWorkflowMarkdownRootDir, workflowMarkdownSubDir(info.Tab)) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return result, err + } + + fileName := buildWorkflowMarkdownFileName(info.Tab, session.Title, session.ID) + filePath := filepath.Join(targetDir, fileName) + if err := os.WriteFile(filePath, []byte(markdown), 0o644); err != nil { + return result, err + } + + relativePath, relErr := filepath.Rel(root, filePath) + if relErr != nil { + relativePath = filepath.Join(aiWorkflowMarkdownRootDir, workflowMarkdownSubDir(info.Tab), fileName) + } + + return systemResp.AIWorkflowMarkdownDumpResult{ + FileName: fileName, + FilePath: filePath, + RelativePath: relativePath, + Directory: targetDir, + }, nil +} + +func workflowMarkdownSubDir(tab string) string { + if tab == "analysis" { + return aiWorkflowAnalysisDir + } + return aiWorkflowPromptDir +} + +func buildWorkflowMarkdownFileName(tab, title string, sessionID uint) string { + prefix := "prompt-workflow" + if tab == "analysis" { + prefix = "analysis" + } + + stem := sanitizeWorkflowFileStem(title) + if stem == "" { + if sessionID > 0 { + stem = fmt.Sprintf("session-%d", sessionID) + } else { + stem = "session" + } + } + + return fmt.Sprintf( + "%s-%s-%s.md", + time.Now().Format("20060102-150405"), + prefix, + stem, + ) +} + +func sanitizeWorkflowFileStem(title string) string { + var builder strings.Builder + lastDash := false + + for _, r := range strings.TrimSpace(title) { + switch { + case unicode.IsLetter(r) || unicode.IsDigit(r): + builder.WriteRune(unicode.ToLower(r)) + lastDash = false + case r == '-' || r == '_' || unicode.IsSpace(r): + if !lastDash && builder.Len() > 0 { + builder.WriteByte('-') + lastDash = true + } + } + } + + return strings.Trim(builder.String(), "-") +} + +func buildAIWorkflowMarkdown(session system.SysAIWorkflowSession) string { + if session.Tab == "analysis" { + return buildAnalysisMarkdown(session) + } + return buildPromptWorkflowMarkdown(session) +} + +func buildAnalysisMarkdown(session system.SysAIWorkflowSession) string { + var builder strings.Builder + + writeMarkdownTitle(&builder, "# AI 需求分析") + writeMarkdownMeta(&builder, + "标题", firstNonEmpty(session.Title, "未命名需求"), + "摘要", firstNonEmpty(session.Summary, getString(session.ResultData, "summary")), + "会话类型", "analysis", + "会话ID", session.ConversationID, + "消息ID", session.MessageID, + "节点ID", session.CurrentNodeID, + ) + + writeMarkdownSection(&builder, "## 原始输入") + writeMarkdownKeyValue(&builder, + "原始需求", getString(session.FormData, "requirement"), + "目标形态", getString(session.FormData, "packageType"), + "业务场景", getString(session.FormData, "businessScene"), + "额外约束", getString(session.FormData, "extraConstraints"), + "是否有客户端页面", formatBool(getBool(session.FormData, "hasClientPage")), + "客户端页面说明", getString(session.FormData, "clientPageDescription"), + "客户端额外约束", getString(session.FormData, "clientPageConstraints"), + ) + + writeMarkdownSection(&builder, "## 整理后的需求") + writeMarkdownKeyValue(&builder, + "总结", getString(session.ResultData, "summary"), + "推荐形态", getString(session.ResultData, "recommendedPackageType"), + ) + + writeStringListSection(&builder, "### 待确认信息", getStringSlice(session.ResultData, "missingInfo")) + writeStringListSection(&builder, "### 建议事项", getStringSlice(session.ResultData, "suggestions")) + + modules := getMapSlice(session.ResultData, "modules") + if len(modules) > 0 { + writeMarkdownSection(&builder, "### 模块拆解") + for index, module := range modules { + builder.WriteString(fmt.Sprintf("#### %d. %s\n\n", index+1, firstNonEmpty(getString(module, "label"), getString(module, "name"), fmt.Sprintf("模块 %d", index+1)))) + writeMarkdownKeyValue(&builder, + "模块标识", getString(module, "name"), + "模块说明", getString(module, "description"), + ) + + fields := getMapSlice(module, "fields") + if len(fields) > 0 { + builder.WriteString("| 字段 | 标识 | 类型 | 必填 | 说明 |\n") + builder.WriteString("| --- | --- | --- | --- | --- |\n") + for _, field := range fields { + builder.WriteString(fmt.Sprintf( + "| %s | %s | %s | %s | %s |\n", + markdownCell(firstNonEmpty(getString(field, "label"), getString(field, "name"), "-")), + markdownCell(firstNonEmpty(getString(field, "name"), "-")), + markdownCell(firstNonEmpty(getString(field, "type"), "string")), + markdownCell(formatBool(getBool(field, "required"))), + markdownCell(firstNonEmpty(getString(field, "description"), "-")), + )) + } + builder.WriteString("\n") + } + } + } + + clientPages := getMapSlice(session.ResultData, "clientPages") + if len(clientPages) > 0 { + writeMarkdownSection(&builder, "### 客户端页面") + for index, page := range clientPages { + builder.WriteString(fmt.Sprintf("#### %d. %s\n\n", index+1, firstNonEmpty(getString(page, "label"), getString(page, "name"), fmt.Sprintf("页面 %d", index+1)))) + writeMarkdownKeyValue(&builder, + "页面标识", getString(page, "name"), + "页面类型", getString(page, "pageType"), + "页面说明", getString(page, "description"), + ) + writeStringListSection(&builder, "目标模块", getStringSlice(page, "targetModules")) + writeStringListSection(&builder, "交互行为", getStringSlice(page, "interactions")) + writeStringListSection(&builder, "字段关系", getStringSlice(page, "relations")) + } + } + + writeMarkdownAppendix(&builder, session.ResultData) + return strings.TrimSpace(builder.String()) + "\n" +} + +func buildPromptWorkflowMarkdown(session system.SysAIWorkflowSession) string { + var builder strings.Builder + + writeMarkdownTitle(&builder, "# Prompt Workflow") + writeMarkdownMeta(&builder, + "标题", firstNonEmpty(session.Title, "未命名工作流"), + "摘要", firstNonEmpty(session.Summary, getString(session.ResultData, "summary")), + "会话类型", "workflow", + "会话ID", session.ConversationID, + "消息ID", session.MessageID, + "节点ID", session.CurrentNodeID, + ) + + writeMarkdownSection(&builder, "## 输入上下文") + writeMarkdownKeyValue(&builder, + "来源需求", getString(session.FormData, "source"), + "工作流类型", getString(session.FormData, "flowType"), + "额外约束", getString(session.FormData, "extraConstraints"), + ) + + writeMarkdownSection(&builder, "## Prompt 工作流") + writeMarkdownKeyValue(&builder, "总结", getString(session.ResultData, "summary")) + + steps := getMapSlice(session.ResultData, "steps") + if len(steps) == 0 { + rawText := getString(session.ResultData, "rawText") + if strings.TrimSpace(rawText) != "" { + builder.WriteString(rawText) + builder.WriteString("\n\n") + } + } else { + for index, step := range steps { + builder.WriteString(fmt.Sprintf("### %d. %s\n\n", index+1, firstNonEmpty(getString(step, "title"), fmt.Sprintf("步骤 %d", index+1)))) + writeMarkdownKeyValue(&builder, + "目标", getString(step, "goal"), + "建议工具", getString(step, "suggestedTool"), + "可自动执行", formatBool(getBool(step, "autoExecutable")), + "预期输出", getString(step, "expectedOutput"), + ) + prompt := getString(step, "prompt") + if strings.TrimSpace(prompt) != "" { + builder.WriteString("#### Prompt\n\n") + builder.WriteString("```text\n") + builder.WriteString(prompt) + builder.WriteString("\n```\n\n") + } + } + } + + writeMarkdownAppendix(&builder, session.ResultData) + return strings.TrimSpace(builder.String()) + "\n" +} + +func writeMarkdownTitle(builder *strings.Builder, title string) { + builder.WriteString(title) + builder.WriteString("\n\n") +} + +func writeMarkdownMeta(builder *strings.Builder, pairs ...string) { + for i := 0; i+1 < len(pairs); i += 2 { + if strings.TrimSpace(pairs[i+1]) == "" { + continue + } + builder.WriteString(fmt.Sprintf("- **%s**: %s\n", pairs[i], pairs[i+1])) + } + builder.WriteString("\n") +} + +func writeMarkdownSection(builder *strings.Builder, title string) { + builder.WriteString(title) + builder.WriteString("\n\n") +} + +func writeMarkdownKeyValue(builder *strings.Builder, pairs ...string) { + hasValue := false + for i := 0; i+1 < len(pairs); i += 2 { + if strings.TrimSpace(pairs[i+1]) == "" { + continue + } + hasValue = true + builder.WriteString(fmt.Sprintf("- **%s**: %s\n", pairs[i], pairs[i+1])) + } + if hasValue { + builder.WriteString("\n") + } +} + +func writeStringListSection(builder *strings.Builder, title string, list []string) { + if len(list) == 0 { + return + } + builder.WriteString(title) + builder.WriteString("\n\n") + for _, item := range list { + if strings.TrimSpace(item) == "" { + continue + } + builder.WriteString("- ") + builder.WriteString(item) + builder.WriteString("\n") + } + builder.WriteString("\n") +} + +func writeMarkdownAppendix(builder *strings.Builder, resultData common.JSONMap) { + rawText := getString(resultData, "rawText") + rawJSON := getString(resultData, "rawJson") + + if strings.TrimSpace(rawText) != "" { + builder.WriteString("## 原始文本\n\n") + builder.WriteString("```text\n") + builder.WriteString(rawText) + builder.WriteString("\n```\n\n") + } + + if strings.TrimSpace(rawJSON) != "" { + builder.WriteString("## 原始结构化数据\n\n") + builder.WriteString("```json\n") + builder.WriteString(rawJSON) + builder.WriteString("\n```\n") + } +} + +func getString(data map[string]interface{}, key string) string { + if len(data) == 0 { + return "" + } + value, ok := data[key] + if !ok || value == nil { + return "" + } + switch typed := value.(type) { + case string: + return strings.TrimSpace(typed) + case fmt.Stringer: + return strings.TrimSpace(typed.String()) + default: + return strings.TrimSpace(fmt.Sprintf("%v", value)) + } +} + +func getBool(data map[string]interface{}, key string) bool { + if len(data) == 0 { + return false + } + value, ok := data[key] + if !ok || value == nil { + return false + } + switch typed := value.(type) { + case bool: + return typed + case string: + return strings.EqualFold(strings.TrimSpace(typed), "true") + default: + return false + } +} + +func getStringSlice(data map[string]interface{}, key string) []string { + value, ok := data[key] + if !ok || value == nil { + return nil + } + return toStringSlice(value) +} + +func getMapSlice(data map[string]interface{}, key string) []map[string]interface{} { + value, ok := data[key] + if !ok || value == nil { + return nil + } + return toMapSlice(value) +} + +func toStringSlice(value interface{}) []string { + switch typed := value.(type) { + case []string: + result := make([]string, 0, len(typed)) + for _, item := range typed { + if strings.TrimSpace(item) != "" { + result = append(result, strings.TrimSpace(item)) + } + } + return result + case []interface{}: + result := make([]string, 0, len(typed)) + for _, item := range typed { + text := strings.TrimSpace(fmt.Sprintf("%v", item)) + if text != "" { + result = append(result, text) + } + } + return result + default: + text := strings.TrimSpace(fmt.Sprintf("%v", value)) + if text == "" || text == "" { + return nil + } + return []string{text} + } +} + +func toMapSlice(value interface{}) []map[string]interface{} { + switch typed := value.(type) { + case []map[string]interface{}: + return typed + case []interface{}: + result := make([]map[string]interface{}, 0, len(typed)) + for _, item := range typed { + if row, ok := item.(map[string]interface{}); ok { + result = append(result, row) + } + } + return result + default: + return nil + } +} + +func formatBool(value bool) string { + if value { + return "是" + } + return "否" +} + +func markdownCell(value string) string { + text := strings.TrimSpace(value) + if text == "" { + return "-" + } + text = strings.ReplaceAll(text, "\n", "
") + text = strings.ReplaceAll(text, "|", "\\|") + return text +} diff --git a/server/service/system/ai_workflow_session.go b/server/service/system/ai_workflow_session.go new file mode 100644 index 0000000..4c6896e --- /dev/null +++ b/server/service/system/ai_workflow_session.go @@ -0,0 +1,184 @@ +package system + +import ( + "context" + "errors" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + system "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + systemResp "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" +) + +type aiWorkflowSession struct{} + +var AIWorkflowSessionServiceApp = new(aiWorkflowSession) + +func (s *aiWorkflowSession) Save(ctx context.Context, userID uint, info systemReq.SysAIWorkflowSessionUpsert) (session system.SysAIWorkflowSession, err error) { + if userID == 0 { + return session, errors.New("用户未登录") + } + if info.Tab != "analysis" && info.Tab != "workflow" { + return session, errors.New("不支持的会话类型") + } + + db := global.GVA_DB.WithContext(ctx) + if info.ID != 0 { + err = db.Where("id = ? AND user_id = ?", info.ID, userID).First(&session).Error + if err != nil { + return session, err + } + } + + session.UserID = userID + session.Tab = info.Tab + session.Title = truncateText(firstNonEmpty(strings.TrimSpace(info.Title), s.titleFromMessages(info.Messages), s.titleFromForm(info)), 255) + session.Summary = strings.TrimSpace(firstNonEmpty(info.Summary, s.summaryFromResult(info.ResultData))) + session.ConversationID = strings.TrimSpace(info.ConversationID) + session.MessageID = strings.TrimSpace(info.MessageID) + session.Settings = cloneJSONMap(info.Settings) + session.FormData = cloneJSONMap(info.FormData) + session.ResultData = cloneJSONMap(info.ResultData) + session.Messages = sanitizeMessages(info.Messages) + session.CurrentNodeID = strings.TrimSpace(info.CurrentNodeID) + if session.CurrentNodeID == "" { + session.CurrentNodeID = lastAssistantMessageID(session.Messages) + } + + if session.ID == 0 { + err = db.Create(&session).Error + return session, err + } + err = db.Save(&session).Error + return session, err +} + +func (s *aiWorkflowSession) GetList(ctx context.Context, userID uint, info systemReq.SysAIWorkflowSessionSearch) (list []systemResp.SysAIWorkflowSessionListItem, total int64, err error) { + db := global.GVA_DB.WithContext(ctx).Model(&system.SysAIWorkflowSession{}).Where("user_id = ?", userID) + if tab := strings.TrimSpace(info.Tab); tab != "" { + db = db.Where("tab = ?", tab) + } + if keyword := strings.TrimSpace(info.Keyword); keyword != "" { + like := "%" + keyword + "%" + db = db.Where("title LIKE ? OR summary LIKE ?", like, like) + } + + err = db.Count(&total).Error + if err != nil { + return nil, 0, err + } + + err = db.Select("id", "created_at", "updated_at", "tab", "title", "summary", "conversation_id", "current_node_id"). + Scopes(info.Paginate()). + Order("updated_at desc"). + Find(&list).Error + return list, total, err +} + +func (s *aiWorkflowSession) GetDetail(ctx context.Context, userID uint, id uint) (session system.SysAIWorkflowSession, err error) { + err = global.GVA_DB.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).First(&session).Error + return session, err +} + +func (s *aiWorkflowSession) Delete(ctx context.Context, userID uint, id uint) error { + result := global.GVA_DB.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&system.SysAIWorkflowSession{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func (s *aiWorkflowSession) titleFromMessages(messages []system.AIWorkflowMessage) string { + for _, item := range messages { + if item.Role == "user" && strings.TrimSpace(item.Content) != "" { + return strings.TrimSpace(item.Content) + } + } + return "" +} + +func (s *aiWorkflowSession) titleFromForm(info systemReq.SysAIWorkflowSessionUpsert) string { + if info.Tab == "analysis" { + if requirement, ok := info.FormData["requirement"].(string); ok { + return strings.TrimSpace(requirement) + } + } + if source, ok := info.FormData["source"].(string); ok { + return strings.TrimSpace(source) + } + return "" +} + +func (s *aiWorkflowSession) summaryFromResult(resultData map[string]interface{}) string { + if resultData == nil { + return "" + } + if summary, ok := resultData["summary"].(string); ok { + return strings.TrimSpace(summary) + } + return "" +} + +func sanitizeMessages(messages []system.AIWorkflowMessage) []system.AIWorkflowMessage { + if len(messages) == 0 { + return []system.AIWorkflowMessage{} + } + result := make([]system.AIWorkflowMessage, 0, len(messages)) + for _, item := range messages { + result = append(result, system.AIWorkflowMessage{ + ID: strings.TrimSpace(item.ID), + Role: strings.TrimSpace(item.Role), + Content: item.Content, + Snapshot: cloneJSONMap(item.Snapshot), + ConversationID: strings.TrimSpace(item.ConversationID), + MessageID: strings.TrimSpace(item.MessageID), + CreatedAt: strings.TrimSpace(item.CreatedAt), + }) + } + return result +} + +func cloneJSONMap(source map[string]interface{}) map[string]interface{} { + if len(source) == 0 { + return map[string]interface{}{} + } + target := make(map[string]interface{}, len(source)) + for key, value := range source { + target[key] = value + } + return target +} + +func lastAssistantMessageID(messages []system.AIWorkflowMessage) string { + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Role == "assistant" && strings.TrimSpace(messages[i].ID) != "" { + return messages[i].ID + } + } + return "" +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func truncateText(value string, size int) string { + if size <= 0 { + return "" + } + runes := []rune(strings.TrimSpace(value)) + if len(runes) <= size { + return string(runes) + } + return string(runes[:size]) +} diff --git a/server/service/system/auto_code_history.go b/server/service/system/auto_code_history.go new file mode 100644 index 0000000..d9d1676 --- /dev/null +++ b/server/service/system/auto_code_history.go @@ -0,0 +1,217 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/pkg/errors" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "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" + request "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "go.uber.org/zap" +) + +var AutocodeHistory = new(autoCodeHistory) + +type autoCodeHistory struct{} + +// Create 创建代码生成器历史记录 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error { + create := info.Create() + err := global.GVA_DB.WithContext(ctx).Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + return nil +} + +// First 根据id获取代码生成器历史的数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) { + var meta string + err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error + if err != nil { + return "", errors.Wrap(err, "获取失败!") + } + return meta, nil +} + +// Repeat 检测重复 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Repeat(businessDB, structName, abbreviation, Package string) bool { + var count int64 + global.GVA_DB.Model(&model.SysAutoCodeHistory{}).Where("business_db = ? and (struct_name = ? OR abbreviation = ?) and package = ? and flag = ?", businessDB, structName, abbreviation, Package, 0).Count(&count).Debug() + return count > 0 +} + +// RollBack 回滚 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) RollBack(ctx context.Context, info request.SysAutoHistoryRollBack) error { + var history model.SysAutoCodeHistory + err := global.GVA_DB.Where("id = ?", info.ID).First(&history).Error + if err != nil { + return err + } + if history.ExportTemplateID != 0 { + err = global.GVA_DB.Delete(&model.SysExportTemplate{}, "id = ?", history.ExportTemplateID).Error + if err != nil { + return err + } + } + if info.DeleteApi { + ids := info.ApiIds(history) + err = ApiServiceApp.DeleteApisByIds(ids) + if err != nil { + global.GVA_LOG.Error("ClearTag DeleteApiByIds:", zap.Error(err)) + } + } // 清除API表 + if info.DeleteMenu { + err = BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)) + if err != nil { + return errors.Wrap(err, "删除菜单失败!") + } + } // 清除菜单表 + if info.DeleteTable { + err = s.DropTable(history.BusinessDB, history.Table) + if err != nil { + return errors.Wrap(err, "删除表失败!") + } + } // 删除表 + templates := make(map[string]string, len(history.Templates)) + for key, template := range history.Templates { + { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(key, "/") + key = filepath.Join(keys...) + key = strings.TrimPrefix(key, server) + } // key + { + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + slices := strings.Split(template, "/") + template = filepath.Join(slices...) + ext := path.Ext(template) + switch ext { + case ".js", ".vue": + template = filepath.Join(web, template) + case ".go": + template = filepath.Join(server, template) + } + } // value + templates[key] = template + } + history.Templates = templates + for key, value := range history.Injections { + var injection ast.Ast + switch key { + case ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + + case ast.TypePackageApiModuleEnter, ast.TypePackageRouterModuleEnter, ast.TypePackageServiceModuleEnter: + var entity ast.PackageModuleEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeGorm: + var entity ast.PackageInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeRouter: + var entity ast.PackageInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginGen: + var entity ast.PluginGen + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginApiEnter, ast.TypePluginRouterEnter, ast.TypePluginServiceEnter: + var entity ast.PluginEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeGorm: + var entity ast.PluginInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeRouter: + var entity ast.PluginInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + } + if injection == nil { + continue + } + file, _ := injection.Parse("", nil) + if file != nil { + _ = injection.Rollback(file) + err = injection.Format("", nil, file) + if err != nil { + return err + } + fmt.Printf("[filepath:%s]回滚注入代码成功!\n", key) + } + } // 清除注入代码 + removeBasePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, "rm_file", strconv.FormatInt(int64(time.Now().Nanosecond()), 10)) + for _, value := range history.Templates { + if !filepath.IsAbs(value) { + continue + } + removePath := filepath.Join(removeBasePath, strings.TrimPrefix(value, global.GVA_CONFIG.AutoCode.Root)) + err = utils.FileMove(value, removePath) + if err != nil { + return errors.Wrapf(err, "[src:%s][dst:%s]文件移动失败!", value, removePath) + } + } // 移动文件 + err = global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Update("flag", 1).Error + if err != nil { + return errors.Wrap(err, "更新失败!") + } + return nil +} + +// Delete 删除历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Where("id = ?", info.Uint()).Delete(&model.SysAutoCodeHistory{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// GetList 获取系统历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) GetList(ctx context.Context, info common.PageInfo) (list []model.SysAutoCodeHistory, total int64, err error) { + var entities []model.SysAutoCodeHistory + db := global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}) + err = db.Count(&total).Error + if err != nil { + return nil, total, err + } + err = db.Scopes(info.Paginate()).Order("updated_at desc").Find(&entities).Error + return entities, total, err +} + +// DropTable 获取指定数据库和指定数据表的所有字段名,类型值等 +// @author: [piexlmax](https://github.com/piexlmax) +func (s *autoCodeHistory) DropTable(BusinessDb, tableName string) error { + if BusinessDb != "" { + return global.MustGetGlobalDBByDBName(BusinessDb).Exec("DROP TABLE " + tableName).Error + } else { + return global.GVA_DB.Exec("DROP TABLE " + tableName).Error + } +} diff --git a/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go new file mode 100644 index 0000000..fdb1d54 --- /dev/null +++ b/server/service/system/auto_code_llm.go @@ -0,0 +1,125 @@ +package system + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + 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" +) + +func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) { + path, err := buildLLMAutoPath(llm) + if err != nil { + return nil, err + } + + res, err := request.HttpRequestWithContextAndTimeout( + ctx, + path, + http.MethodPost, + nil, + nil, + llm, + ) + if err != nil { + return nil, fmt.Errorf("调用上游大模型服务失败: %w", err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("读取大模型响应失败: %w", err) + } + + bodyPreview := previewResponseBody(body) + contentType := res.Header.Get("Content-Type") + if res.StatusCode < 200 || res.StatusCode >= 300 { + return nil, fmt.Errorf("上游大模型服务返回非 2xx: status=%d content-type=%s body=%s", res.StatusCode, contentType, bodyPreview) + } + + var resStruct commonResp.Response + if err = json.Unmarshal(body, &resStruct); err != nil { + return nil, fmt.Errorf("解析大模型响应失败: status=%d content-type=%s body=%s err=%w", res.StatusCode, contentType, bodyPreview, err) + } + + if resStruct.Code != commonResp.SUCCESS { + return nil, fmt.Errorf("大模型服务返回业务错误: code=%d msg=%s body=%s", resStruct.Code, resStruct.Msg, bodyPreview) + } + + return resStruct.Data, nil +} + +func (s *AutoCodeService) LLMAutoStream(ctx context.Context, llm common.JSONMap) (*http.Response, error) { + path, err := buildLLMAutoPath(llm) + if err != nil { + return nil, err + } + + payload := cloneLLMAutoJSONMap(llm) + responseMode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", payload["response_mode"]))) + if responseMode == "" { + payload["response_mode"] = "streaming" + } + + res, err := request.HttpRequestWithContextAndTimeout( + ctx, + path, + http.MethodPost, + map[string]string{ + "Accept": "text/event-stream", + "Accept-Encoding": "identity", // 禁止 gzip,避免 SSE 流被压缩导致缓冲卡住 + "Cache-Control": "no-cache", + }, + nil, + payload, + -1, // 不设置 client.Timeout,SSE 流的生命周期由 ctx 控制 + ) + if err != nil { + return nil, fmt.Errorf("调用上游大模型流式服务失败: %w", err) + } + return res, nil +} + +func buildLLMAutoPath(llm common.JSONMap) (string, error) { + if global.GVA_CONFIG.AutoCode.AiPath == "" { + return "", errors.New("请先前往插件市场个人中心获取 AiPath 并填写到 config.yaml 中") + } + + mode := strings.TrimSpace(fmt.Sprintf("%v", llm["mode"])) + if mode == "" { + return "", errors.New("llmAuto 缺少 mode 参数") + } + + return strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode), nil +} + +func cloneLLMAutoJSONMap(src common.JSONMap) common.JSONMap { + dst := make(common.JSONMap, len(src)) + for key, value := range src { + dst[key] = value + } + return dst +} + +func previewResponseBody(body []byte) string { + text := strings.TrimSpace(string(body)) + text = strings.ReplaceAll(text, "\r", " ") + text = strings.ReplaceAll(text, "\n", " ") + text = strings.Join(strings.Fields(text), " ") + if text == "" { + return "" + } + runes := []rune(text) + if len(runes) > 300 { + return string(runes[:300]) + "..." + } + return text +} diff --git a/server/service/system/auto_code_mcp.go b/server/service/system/auto_code_mcp.go new file mode 100644 index 0000000..3b6eb84 --- /dev/null +++ b/server/service/system/auto_code_mcp.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "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/autocode" + "os" + "path/filepath" + "text/template" +) + +func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) { + mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl") + mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp") + + var files *template.Template + + templateName := filepath.Base(mcpTemplatePath) + + files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath) + if err != nil { + return + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath = filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return + } + + return + +} diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go new file mode 100644 index 0000000..763c56f --- /dev/null +++ b/server/service/system/auto_code_package.go @@ -0,0 +1,743 @@ +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" + "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/flipped-aurora/gin-vue-admin/server/utils/autocode" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var AutoCodePackage = new(autoCodePackage) + +type autoCodePackage struct{} + +// Create 创建包信息 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodePackageCreate) error { + switch { + case info.Template == "": + return errors.New("模板不能为空!") + case info.Template == "page": + return errors.New("page为表单生成器!") + case info.PackageName == "": + return errors.New("PackageName不能为空!") + case token.IsKeyword(info.PackageName): + return errors.Errorf("%s为go的关键字!", info.PackageName) + case info.Template == "package": + if info.PackageName == "system" || info.PackageName == "example" { + return errors.New("不能使用已保留的package name") + } + default: + break + } + if !errors.Is(global.GVA_DB.Where("package_name = ? and template = ?", info.PackageName, info.Template).First(&model.SysAutoCodePackage{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同PackageName") + } + create := info.Create() + return global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + code := info.AutoCode() + _, asts, creates, err := s.templates(ctx, create, code, true) + if err != nil { + return err + } + for key, value := range creates { // key 为 模版绝对路径 + var files *template.Template + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", key) + } + err = os.MkdirAll(filepath.Dir(value), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + var file *os.File + file, err = os.Create(value) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + err = files.Execute(file, code) + _ = file.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]生成失败!", value) + } + fmt.Printf("[template:%s][filepath:%s]生成成功!\n", key, value) + } + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + switch keys[1] { + case ast.TypePluginInitializeV2, ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + file, _ := value.Parse("", nil) + if file != nil { + err = value.Injection(file) + if err != nil { + return err + } + err = value.Format("", nil, file) + if err != nil { + return err + } + } + fmt.Printf("[type:%s]注入成功!\n", key) + } + } + } + return nil + }) +} + +// Delete 删除包记录 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, info.Uint()).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// DeleteByNames +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error { + if len(names) == 0 { + return nil + } + err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// All 获取所有包 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCodePackage, err error) { + server := make([]model.SysAutoCodePackage, 0) + plugin := make([]model.SysAutoCodePackage, 0) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service") + pluginPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin") + serverDir, err := os.ReadDir(serverPath) + if err != nil { + return nil, errors.Wrap(err, "读取service文件夹失败!") + } + pluginDir, err := os.ReadDir(pluginPath) + if err != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + for i := 0; i < len(serverDir); i++ { + if serverDir[i].IsDir() { + serverPackage := model.SysAutoCodePackage{ + PackageName: serverDir[i].Name(), + Template: "package", + Label: serverDir[i].Name() + "包", + Desc: "系统自动读取" + serverDir[i].Name() + "包", + Module: global.GVA_CONFIG.AutoCode.Module, + } + server = append(server, serverPackage) + } + } + for i := 0; i < len(pluginDir); i++ { + if pluginDir[i].IsDir() { + dirNameMap := map[string]bool{ + "api": true, + "config": true, + "initialize": true, + "plugin": true, + "router": true, + "service": true, + } + dir, e := os.ReadDir(filepath.Join(pluginPath, pluginDir[i].Name())) + if e != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + //dir目录需要包含所有的dirNameMap + for k := 0; k < len(dir); k++ { + if dir[k].IsDir() { + if ok := dirNameMap[dir[k].Name()]; ok { + delete(dirNameMap, dir[k].Name()) + } + } + } + + 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: desc, + Module: global.GVA_CONFIG.AutoCode.Module, + } + plugin = append(plugin, pluginPackage) + } + } + + err = global.GVA_DB.WithContext(ctx).Find(&entities).Error + if err != nil { + return nil, errors.Wrap(err, "获取所有包失败!") + } + entitiesMap := make(map[string]model.SysAutoCodePackage) + for i := 0; i < len(entities); i++ { + entitiesMap[entities[i].PackageName] = entities[i] + } + createEntity := []model.SysAutoCodePackage{} + for i := 0; i < len(server); i++ { + if _, ok := entitiesMap[server[i].PackageName]; !ok { + if server[i].Template == "package" { + createEntity = append(createEntity, server[i]) + } + } + } + for i := 0; i < len(plugin); i++ { + if _, ok := entitiesMap[plugin[i].PackageName]; !ok { + if plugin[i].Template == "plugin" { + createEntity = append(createEntity, plugin[i]) + } + } + } + + if len(createEntity) > 0 { + err = global.GVA_DB.WithContext(ctx).Create(&createEntity).Error + if err != nil { + return nil, errors.Wrap(err, "同步失败!") + } + entities = append(entities, createEntity...) + } + + // 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据 + existingPackageNames := make(map[string]bool) + // 收集所有存在的包名 + for i := 0; i < len(server); i++ { + existingPackageNames[server[i].PackageName] = true + } + for i := 0; i < len(plugin); i++ { + existingPackageNames[plugin[i].PackageName] = true + } + + // 找出需要删除的数据库记录 + deleteEntityIDs := []uint{} + for i := 0; i < len(entities); i++ { + if !existingPackageNames[entities[i].PackageName] { + deleteEntityIDs = append(deleteEntityIDs, entities[i].ID) + } + } + + // 删除数据库中不存在文件的记录 + if len(deleteEntityIDs) > 0 { + err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error + if err != nil { + return nil, errors.Wrap(err, "删除不存在的包记录失败!") + } + // 从返回结果中移除已删除的记录 + filteredEntities := []model.SysAutoCodePackage{} + for i := 0; i < len(entities); i++ { + if existingPackageNames[entities[i].PackageName] { + filteredEntities = append(filteredEntities, entities[i]) + } + } + entities = filteredEntities + } + + return entities, nil +} + +// Templates 获取所有模版文件夹 +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { + templates := make([]string, 0) + entries, err := os.ReadDir("resource") + if err != nil { + return nil, errors.Wrap(err, "读取模版文件夹失败!") + } + for i := 0; i < len(entries); i++ { + if entries[i].IsDir() { + if entries[i].Name() == "page" { + continue + } // page 为表单生成器 + if entries[i].Name() == "function" { + continue + } // function 为函数生成器 + if entries[i].Name() == "preview" { + continue + } // preview 为预览代码生成器的代码 + if entries[i].Name() == "mcp" { + continue + } // preview 为mcp生成器的代码 + templates = append(templates, entries[i].Name()) + } + } + return templates, nil +} + +func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCodePackage, info request.AutoCode, isPackage bool) (code map[string]string, asts map[string]ast.Ast, creates map[string]string, err error) { + code = make(map[string]string) + asts = make(map[string]ast.Ast) + creates = make(map[string]string) + templateDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", entity.Template) + templateDirs, err := os.ReadDir(templateDir) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", templateDir) + } + for i := 0; i < len(templateDirs); i++ { + second := filepath.Join(templateDir, templateDirs[i].Name()) + switch templateDirs[i].Name() { + case "server": + if !info.GenerateServer && !isPackage { + break + } + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + ext := filepath.Ext(secondDirs[j].Name()) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", three) + } + name := strings.TrimSuffix(secondDirs[j].Name(), ext) + if name == "main.go" || name == "plugin.go" { + 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, "plugin", "register.go"), + ImportPath: fmt.Sprintf(`"%s/plugin/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + PackageName: entity.PackageName, + } + asts[pluginInitialize.PluginPath+"=>"+pluginInitialize.Type.String()] = pluginInitialize + creates[three] = pluginInitialize.Path + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "router", "service": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + hasEnter := strings.Index(threeDirs[k].Name(), "enter") + router := strings.Index(threeDirs[k].Name(), "router") + service := strings.Index(threeDirs[k].Name(), "service") + if router == -1 && api == -1 && service == -1 && hasEnter == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, info.HumpPackageName+".go") + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isApi != -1 { + packageApiEnter := &ast.PackageEnter{ + Type: ast.TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, "api", "v1", entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + PackageName: entity.PackageName, + PackageStructName: "ApiGroup", + } + asts[packageApiEnter.Path+"=>"+packageApiEnter.Type.String()] = packageApiEnter + packageApiModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`"%s/service"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Api", + AppName: "ServiceGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + ModuleName: info.Abbreviation + "Service", + PackageName: "service", + ServiceName: info.StructName + "Service", + } + asts[packageApiModuleEnter.Path+"=>"+packageApiModuleEnter.Type.String()] = packageApiModuleEnter + creates[four] = packageApiModuleEnter.Path + } + if isRouter != -1 { + packageRouterEnter := &ast.PackageEnter{ + Type: ast.TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, secondDirs[j].Name(), entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName), + PackageName: entity.PackageName, + PackageStructName: "RouterGroup", + } + asts[packageRouterEnter.Path+"=>"+packageRouterEnter.Type.String()] = packageRouterEnter + packageRouterModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`api "%s/api/v1"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Router", + AppName: "ApiGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + ModuleName: info.Abbreviation + "Api", + PackageName: "api", + ServiceName: info.StructName + "Api", + } + creates[four] = packageRouterModuleEnter.Path + asts[packageRouterModuleEnter.Path+"=>"+packageRouterModuleEnter.Type.String()] = packageRouterModuleEnter + packageInitializeRouter := &ast.PackageInitializeRouter{ + Type: ast.TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: fmt.Sprintf(`"%s/router"`, global.GVA_CONFIG.AutoCode.Module), + AppName: "RouterGroupApp", + GroupName: utils.FirstUpper(entity.PackageName), + ModuleName: entity.PackageName + "Router", + PackageName: "router", + FunctionName: "Init" + info.StructName + "Router", + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + asts[packageInitializeRouter.Path+"=>"+packageInitializeRouter.Type.String()] = packageInitializeRouter + } + if isService != -1 { + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) + importPath := fmt.Sprintf(`"%s/service/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName) + packageServiceEnter := &ast.PackageEnter{ + Type: ast.TypePackageServiceEnter, + Path: path, + ImportPath: importPath, + StructName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + PackageName: entity.PackageName, + PackageStructName: "ServiceGroup", + } + asts[packageServiceEnter.Path+"=>"+packageServiceEnter.Type.String()] = packageServiceEnter + packageServiceModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + StructName: info.StructName + "Service", + } + asts[packageServiceModuleEnter.Path+"=>"+packageServiceModuleEnter.Type.String()] = packageServiceModuleEnter + creates[four] = packageServiceModuleEnter.Path + } + continue + } + code[four] = create + continue + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isRouter != -1 { + pluginRouterEnter := &ast.PluginEnter{ + Type: ast.TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/api"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "api" + info.StructName, + GroupName: "Api", + PackageName: "api", + ServiceName: info.StructName, + } + asts[pluginRouterEnter.Path+"=>"+pluginRouterEnter.Type.String()] = pluginRouterEnter + creates[four] = pluginRouterEnter.Path + } + if isApi != -1 { + pluginApiEnter := &ast.PluginEnter{ + Type: ast.TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/service"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "service" + info.StructName, + GroupName: "Service", + PackageName: "service", + ServiceName: info.StructName, + } + asts[pluginApiEnter.Path+"=>"+pluginApiEnter.Type.String()] = pluginApiEnter + creates[four] = pluginApiEnter.Path + } + if isService != -1 { + pluginServiceEnter := &ast.PluginEnter{ + Type: ast.TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + } + asts[pluginServiceEnter.Path+"=>"+pluginServiceEnter.Type.String()] = pluginServiceEnter + creates[four] = pluginServiceEnter.Path + } + continue + } // enter.go + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + code[four] = create + } + case "gen", "config", "initialize", "plugin", "response": + if entity.Template == "package" { + continue + } // package模板不需要生成gen, config, initialize + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + gen := strings.Index(threeDirs[k].Name(), "gen") + api := strings.Index(threeDirs[k].Name(), "api") + menu := strings.Index(threeDirs[k].Name(), "menu") + viper := strings.Index(threeDirs[k].Name(), "viper") + plugin := strings.Index(threeDirs[k].Name(), "plugin") + config := strings.Index(threeDirs[k].Name(), "config") + router := strings.Index(threeDirs[k].Name(), "router") + hasGorm := strings.Index(threeDirs[k].Name(), "gorm") + response := strings.Index(threeDirs[k].Name(), "response") + 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 || 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 { + pluginGen := &ast.PluginGen{ + Type: ast.TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginGen.Path+"=>"+pluginGen.Type.String()] = pluginGen + creates[four] = pluginGen.Path + } + if hasGorm != -1 { + pluginInitializeGorm := &ast.PluginInitializeGorm{ + Type: ast.TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginInitializeGorm.Path+"=>"+pluginInitializeGorm.Type.String()] = pluginInitializeGorm + creates[four] = pluginInitializeGorm.Path + } + if router != -1 { + pluginInitializeRouter := &ast.PluginInitializeRouter{ + Type: ast.TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/router"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + AppName: "Router", + GroupName: info.StructName, + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + } + asts[pluginInitializeRouter.Path+"=>"+pluginInitializeRouter.Type.String()] = pluginInitializeRouter + creates[four] = pluginInitializeRouter.Path + } + } + case "model": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + var fourDirs []os.DirEntry + fourDirs, err = os.ReadDir(four) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", four) + } + for l := 0; l < len(fourDirs); l++ { + if fourDirs[l].Name() == ".DS_Store" { + continue + } + five := filepath.Join(four, fourDirs[l].Name()) + if fourDirs[l].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", five) + } + ext := filepath.Ext(five) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", five) + } + hasRequest := strings.Index(fourDirs[l].Name(), "request") + if hasRequest == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", five) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), threeDirs[k].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, threeDirs[k].Name(), info.HumpPackageName+".go") + } + code[five] = create + } + continue + } + ext := filepath.Ext(threeDirs[k].Name()) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + hasModel := strings.Index(threeDirs[k].Name(), "model") + if hasModel == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + packageInitializeGorm := &ast.PackageInitializeGorm{ + Type: ast.TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: fmt.Sprintf(`"%s/model/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + Business: info.BusinessDB, + StructName: info.StructName, + PackageName: entity.PackageName, + IsNew: true, + } + code[four] = packageInitializeGorm.Path + asts[packageInitializeGorm.Path+"=>"+packageInitializeGorm.Type.String()] = packageInitializeGorm + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + } + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "web": + if !info.GenerateWeb && !isPackage { + break + } + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "form", "view", "table": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + form := strings.Index(threeDirs[k].Name(), "form") + view := strings.Index(threeDirs[k].Name(), "view") + table := strings.Index(threeDirs[k].Name(), "table") + if api == -1 && form == -1 && view == -1 && table == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + if view != -1 || table != -1 { + formPath := filepath.Join(three, "form.vue"+ext) + value, ok := code[formPath] + if ok { + value = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+"Form"+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[formPath] = value + } + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + } + code[four] = create + continue + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), "plugin", entity.PackageName, secondDirs[j].Name(), info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "readme.txt.tpl", "readme.txt.template": + continue + default: + if templateDirs[i].Name() == ".DS_Store" { + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", second) + } + } + return code, asts, creates, nil +} diff --git a/server/service/system/auto_code_package_test.go b/server/service/system/auto_code_package_test.go new file mode 100644 index 0000000..d2a5473 --- /dev/null +++ b/server/service/system/auto_code_package_test.go @@ -0,0 +1,108 @@ +package system + +import ( + "context" + "reflect" + "testing" + + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +func Test_autoCodePackage_Create(t *testing.T) { + type args struct { + ctx context.Context + info *request.SysAutoCodePackageCreate + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "package", + PackageName: "gva", + }, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "plugin", + PackageName: "gva", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &autoCodePackage{} + if err := a.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodePackage_templates(t *testing.T) { + type args struct { + ctx context.Context + entity model.SysAutoCodePackage + info request.AutoCode + isPackage bool + } + tests := []struct { + name string + args args + wantCode map[string]string + wantEnter map[string]map[string]string + wantErr bool + }{ + { + name: "测试1", + args: args{ + ctx: context.Background(), + entity: model.SysAutoCodePackage{ + Desc: "描述", + Label: "展示名", + Template: "plugin", + PackageName: "preview", + }, + info: request.AutoCode{ + Abbreviation: "user", + HumpPackageName: "user", + }, + isPackage: false, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodePackage{} + gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info, tt.args.isPackage) + if (err != nil) != tt.wantErr { + t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr) + return + } + for key, value := range gotCode { + t.Logf("\n") + t.Logf(key) + t.Logf(value) + t.Logf("\n") + } + t.Log(gotCreates) + if !reflect.DeepEqual(gotEnter, tt.wantEnter) { + t.Errorf("templates() gotEnter = %v, want %v", gotEnter, tt.wantEnter) + } + }) + } +} diff --git a/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go new file mode 100644 index 0000000..be3dff6 --- /dev/null +++ b/server/service/system/auto_code_plugin.go @@ -0,0 +1,512 @@ +package system + +import ( + "bytes" + "context" + "fmt" + goast "go/ast" + "go/parser" + "go/printer" + "go/token" + "io" + "mime/multipart" + "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) + +type autoCodePlugin struct{} + +// Install 插件安装 +func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) { + const GVAPLUGPINATH = "./gva-plug-temp/" + defer os.RemoveAll(GVAPLUGPINATH) + _, err = os.Stat(GVAPLUGPINATH) + if os.IsNotExist(err) { + os.Mkdir(GVAPLUGPINATH, os.ModePerm) + } + + src, err := file.Open() + if err != nil { + return -1, -1, err + } + defer src.Close() + + // 在临时目录创建目标文件 + // 使用完整路径拼接的好处:明确文件位置,避免路径混乱 + out, err := os.Create(GVAPLUGPINATH + file.Filename) + if err != nil { + return -1, -1, err + } + + // 将上传的文件内容复制到临时文件 + // 使用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) + var webIndex = -1 + var serverIndex = -1 + webPlugin := "" + serverPlugin := "" + serverPackage := "" + serverRootName := "" + + for i := range paths { + paths[i] = filepath.ToSlash(paths[i]) + pathArr := strings.Split(paths[i], "/") + ln := len(pathArr) + + if ln < 4 { + continue + } + 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]) + } + } + if len(serverPlugin) == 0 && len(webPlugin) == 0 { + zap.L().Error("非标准插件,请按照文档自动迁移使用") + return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用") + } + + 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 { + err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web) + if err != nil { + return webIndex, serverIndex, err + } + } + + return 1, 1, err +} + +func installation(path string, formPath string, toPath string) error { + arr := strings.Split(filepath.ToSlash(path), "/") + ln := len(arr) + if ln < 3 { + return errors.New("arr") + } + name := arr[ln-3] + + var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path) + var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin") + _, err := os.Stat(to + name) + if err == nil { + zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to)) + return errors.New(toPath + "已存在同名插件,请自行手动安装") + } + 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 { + if ok, _ := skipMacSpecialDocument(nil, path, ""); ok { + continue + } + np = append(np, path) + } + return np +} + +func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) { + if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") { + return true, nil + } + return false, nil +} + +func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { + if plugName == "" { + return "", errors.New("插件名称不能为空") + } + + // 防止路径穿越 + plugName = filepath.Clean(plugName) + + webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName) + // 创建一个新的zip文件 + + // 判断目录是否存在 + _, err = os.Stat(webPath) + if err != nil { + return "", errors.New("web路径不存在") + } + _, err = os.Stat(serverPath) + if err != nil { + return "", errors.New("server路径不存在") + } + + fileName := plugName + ".zip" + // 创建一个新的zip文件 + files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{ + webPath: plugName + "/web/plugin/" + plugName, + serverPath: plugName + "/server/plugin/" + plugName, + }) + + // create the output file we'll write to + out, err := os.Create(fileName) + if err != nil { + return + } + defer out.Close() + + // we can use the CompressedArchive type to gzip a tarball + // (compression is not required; you could use Tar directly) + format := archives.CompressedArchive{ + //Compression: archives.Gz{}, + Archival: archives.Zip{}, + } + + // create the archive + err = format.Archive(context.Background(), out, files) + if err != nil { + return + } + + return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil +} + +func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) { + menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go") + src, err := os.ReadFile(menuPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu") + var menus []system.SysBaseMenu + + parentMenu := []system.SysBaseMenu{ + { + ParentId: 0, + Path: menuInfo.PlugName + "Menu", + Name: menuInfo.PlugName + "Menu", + Hidden: false, + Component: "view/routerHolder.vue", + Sort: 0, + Meta: system.Meta{ + Title: menuInfo.ParentMenu, + Icon: "school", + }, + }, + } + + // 查询菜单及其关联的参数和按钮 + err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error + if err != nil { + return err + } + menus = append(parentMenu, menus...) + menuExpr := ast.CreateMenuStructAst(menus) + arrayAst.Elts = *menuExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(menuPath, bf.Bytes(), 0666) + return nil +} + +func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) { + apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go") + src, err := os.ReadFile(apiPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysApi") + var apis []system.SysApi + err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error + if err != nil { + return err + } + apisExpr := ast.CreateApiStructAst(apis) + arrayAst.Elts = *apisExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + 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) +} diff --git a/server/service/system/auto_code_template.go b/server/service/system/auto_code_template.go new file mode 100644 index 0000000..dffb376 --- /dev/null +++ b/server/service/system/auto_code_template.go @@ -0,0 +1,453 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + utilsAst "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var AutoCodeTemplate = new(autoCodeTemplate) + +type autoCodeTemplate struct{} + +func (s *autoCodeTemplate) checkPackage(Pkg string, template string) (err error) { + switch template { + case "package": + apiEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", Pkg, "enter.go") + _, err = os.Stat(apiEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少api/v1/%s/enter.go", Pkg) + } + serviceEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", Pkg, "enter.go") + _, err = os.Stat(serviceEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少service/%s/enter.go", Pkg) + } + routerEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", Pkg, "enter.go") + _, err = os.Stat(routerEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少router/%s/enter.go", Pkg) + } + case "plugin": + pluginEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", Pkg, "plugin.go") + _, err = os.Stat(pluginEnter) + if err != nil { + return fmt.Errorf("plugin结构异常,缺少plugin/%s/plugin.go", Pkg) + } + } + return nil +} + +// Create 创建生成自动化代码 +func (s *autoCodeTemplate) Create(ctx context.Context, info request.AutoCode) error { + history := info.History() + var autoPkg model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&autoPkg).Error + if err != nil { + return errors.Wrap(err, "查询包失败!") + } + err = s.checkPackage(info.Package, autoPkg.Template) + if err != nil { + return err + } + // 增加判断: 重复创建struct 或者重复的简称 + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) { + return errors.New("已经创建过此数据结构,请勿重复创建!") + } + + generate, templates, injections, err := s.generate(ctx, info, autoPkg) + if err != nil { + return err + } + for key, builder := range generate { + err = os.MkdirAll(filepath.Dir(key), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key) + } + err = os.WriteFile(key, []byte(builder.String()), 0666) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key) + } + } + + // 自动创建api + if info.AutoCreateApiToSql && !info.OnlyTemplate { + apis := info.Apis() + err := global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for _, v := range apis { + var api model.SysApi + var id uint + err := tx.Where("path = ? AND method = ?", v.Path, v.Method).First(&api).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + if err = tx.Create(&v).Error; err != nil { // 遇到错误时回滚事务 + return err + } + id = v.ID + } else { + id = api.ID + } + history.ApiIDs = append(history.ApiIDs, id) + } + return nil + }) + if err != nil { + return err + } + } + + // 自动创建menu + if info.AutoCreateMenuToSql { + var entity model.SysBaseMenu + var id uint + err := global.GVA_DB.WithContext(ctx).First(&entity, "name = ?", info.Abbreviation).Error + if err == nil { + id = entity.ID + } else { + entity = info.Menu(autoPkg.Template) + if info.AutoCreateBtnAuth && !info.OnlyTemplate { + entity.MenuBtn = []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "add", Desc: "新增"}, + {SysBaseMenuID: entity.ID, Name: "batchDelete", Desc: "批量删除"}, + {SysBaseMenuID: entity.ID, Name: "delete", Desc: "删除"}, + {SysBaseMenuID: entity.ID, Name: "edit", Desc: "编辑"}, + {SysBaseMenuID: entity.ID, Name: "info", Desc: "详情"}, + } + if info.HasExcel { + excelBtn := []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "exportTemplate", Desc: "导出模板"}, + {SysBaseMenuID: entity.ID, Name: "exportExcel", Desc: "导出Excel"}, + {SysBaseMenuID: entity.ID, Name: "importExcel", Desc: "导入Excel"}, + } + entity.MenuBtn = append(entity.MenuBtn, excelBtn...) + } + } + err = global.GVA_DB.WithContext(ctx).Create(&entity).Error + id = entity.ID + if err != nil { + return errors.Wrap(err, "创建菜单失败!") + } + } + history.MenuID = id + } + + if info.HasExcel { + dbName := info.BusinessDB + name := info.Package + "_" + info.StructName + tableName := info.TableName + fieldsMap := make(map[string]string, len(info.Fields)) + for _, field := range info.Fields { + if field.Excel { + fieldsMap[field.ColumnName] = field.FieldDesc + } + } + templateInfo, _ := json.Marshal(fieldsMap) + sysExportTemplate := model.SysExportTemplate{ + DBName: dbName, + Name: name, + TableName: tableName, + TemplateID: name, + TemplateInfo: string(templateInfo), + } + err = SysExportTemplateServiceApp.CreateSysExportTemplate(&sysExportTemplate) + if err != nil { + return err + } + history.ExportTemplateID = sysExportTemplate.ID + } + + // 创建历史记录 + history.Templates = templates + history.Injections = make(map[string]string, len(injections)) + for key, value := range injections { + bytes, _ := json.Marshal(value) + history.Injections[key] = string(bytes) + } + err = AutocodeHistory.Create(ctx, history) + if err != nil { + return err + } + return nil +} + +// Preview 预览自动化代码 +func (s *autoCodeTemplate) Preview(ctx context.Context, info request.AutoCode) (map[string]string, error) { + var entity model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&entity).Error + if err != nil { + return nil, errors.Wrap(err, "查询包失败!") + } + // 增加判断: 重复创建struct 或者重复的简称 + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd { + return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!") + } + + preview := make(map[string]string) + codes, _, _, err := s.generate(ctx, info, entity) + if err != nil { + return nil, err + } + for key, writer := range codes { + if len(key) > len(global.GVA_CONFIG.AutoCode.Root) { + key, _ = filepath.Rel(global.GVA_CONFIG.AutoCode.Root, key) + } + // 获取key的后缀 取消. + suffix := filepath.Ext(key)[1:] + var builder strings.Builder + builder.WriteString("```" + suffix + "\n\n") + builder.WriteString(writer.String()) + builder.WriteString("\n\n```") + preview[key] = builder.String() + } + return preview, nil +} + +func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, entity model.SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]utilsAst.Ast, error) { + templates, asts, _, err := AutoCodePackage.templates(ctx, entity, info, false) + if err != nil { + return nil, nil, nil, err + } + code := make(map[string]strings.Builder) + for key, create := range templates { + var files *template.Template + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]生成文件失败!", create) + } + code[create] = builder + } // 生成文件 + injections := make(map[string]utilsAst.Ast, len(asts)) + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + if keys[1] == utilsAst.TypePluginInitializeV2 { + continue + } + if info.OnlyTemplate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + if !info.AutoMigrate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + var builder strings.Builder + parse, _ := value.Parse("", &builder) + if parse != nil { + _ = value.Injection(parse) + err = value.Format("", &builder, parse) + if err != nil { + return nil, nil, nil, err + } + code[keys[0]] = builder + injections[keys[1]] = value + fmt.Println(keys[0], "注入成功!") + } + } + } + // 注入代码 + return code, templates, injections, nil +} + +func (s *autoCodeTemplate) AddFunc(info request.AutoFunc) error { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + err = s.addTemplateToFile("api.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("server.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("api.js", info) + if err != nil { + return err + } + return s.addTemplateToAst("router", info) +} + +func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]string, error) { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return nil, err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + + apiStr, err := s.getTemplateStr("api.go", info) + if err != nil { + return nil, err + } + serverStr, err := s.getTemplateStr("server.go", info) + if err != nil { + return nil, err + } + jsStr, err := s.getTemplateStr("api.js", info) + if err != nil { + return nil, err + } + return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil + +} + +func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) { + tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl") + files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath) + if err != nil { + return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + fmt.Println(err.Error()) + return "", errors.Wrapf(err, "[filpath:%s]生成文件失败!", tempPath) + } + return builder.String(), nil +} + +func (s *autoCodeTemplate) addTemplateToAst(t string, info request.AutoFunc) error { + tPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", info.Package, info.HumpPackageName+".go") + funcName := fmt.Sprintf("Init%sRouter", info.StructName) + + routerStr := "RouterWithoutAuth" + if info.IsAuth { + routerStr = "Router" + } + + stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName) + if info.IsPlugin { + tPath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "router", info.HumpPackageName+".go") + stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName) + funcName = "Init" + } + + src, err := os.ReadFile(tPath) + if err != nil { + return err + } + + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + return err + } + funcDecl := utilsAst.FindFunction(astFile, funcName) + stmtNode := utilsAst.CreateStmt(stmtStr) + + if info.IsAuth { + for i := 0; i < len(funcDecl.Body.List); i++ { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } else { + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } + + // 创建一个新的文件 + f, err := os.Create(tPath) + if err != nil { + return err + } + defer f.Close() + + if err := format.Node(f, fileSet, astFile); err != nil { + return err + } + return err +} + +func (s *autoCodeTemplate) addTemplateToFile(t string, info request.AutoFunc) error { + getTemplateStr, err := s.getTemplateStr(t, info) + if err != nil { + return err + } + var target string + + switch t { + case "api.go": + if info.IsAi && info.ApiFunc != "" { + getTemplateStr = info.ApiFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", info.Package, info.HumpPackageName+".go") + case "server.go": + if info.IsAi && info.ServerFunc != "" { + getTemplateStr = info.ServerFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", info.Package, info.HumpPackageName+".go") + case "api.js": + if info.IsAi && info.JsFunc != "" { + getTemplateStr = info.JsFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "api", info.Package, info.PackageName+".js") + } + if info.IsPlugin { + switch t { + case "api.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "api", info.HumpPackageName+".go") + case "server.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "service", info.HumpPackageName+".go") + case "api.js": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", info.Package, "api", info.PackageName+".js") + } + } + + // 打开文件,如果不存在则返回错误 + file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + defer file.Close() + + // 写入内容 + _, err = fmt.Fprintln(file, getTemplateStr) + if err != nil { + fmt.Printf("写入文件失败: %s\n", err.Error()) + return err + } + + return nil +} diff --git a/server/service/system/auto_code_template_test.go b/server/service/system/auto_code_template_test.go new file mode 100644 index 0000000..09d8191 --- /dev/null +++ b/server/service/system/auto_code_template_test.go @@ -0,0 +1,84 @@ +package system + +import ( + "context" + "encoding/json" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "reflect" + "testing" +) + +func Test_autoCodeTemplate_Create(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodeTemplate{} + if err := s.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodeTemplate_Preview(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testJson := `{"structName":"SysUser","tableName":"sys_users","packageName":"sysUsers","package":"gva","abbreviation":"sysUsers","description":"sysUsers表","businessDB":"","autoCreateApiToSql":true,"autoCreateMenuToSql":true,"autoMigrate":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"Uuid","fieldDesc":"用户UUID","fieldType":"string","dataType":"varchar","fieldJson":"uuid","primaryKey":false,"dataTypeLong":"191","columnName":"uuid","comment":"用户UUID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Username","fieldDesc":"用户登录名","fieldType":"string","dataType":"varchar","fieldJson":"username","primaryKey":false,"dataTypeLong":"191","columnName":"username","comment":"用户登录名","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Password","fieldDesc":"用户登录密码","fieldType":"string","dataType":"varchar","fieldJson":"password","primaryKey":false,"dataTypeLong":"191","columnName":"password","comment":"用户登录密码","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"NickName","fieldDesc":"用户昵称","fieldType":"string","dataType":"varchar","fieldJson":"nickName","primaryKey":false,"dataTypeLong":"191","columnName":"nick_name","comment":"用户昵称","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"SideMode","fieldDesc":"用户侧边主题","fieldType":"string","dataType":"varchar","fieldJson":"sideMode","primaryKey":false,"dataTypeLong":"191","columnName":"side_mode","comment":"用户侧边主题","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"HeaderImg","fieldDesc":"用户头像","fieldType":"string","dataType":"varchar","fieldJson":"headerImg","primaryKey":false,"dataTypeLong":"191","columnName":"header_img","comment":"用户头像","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"BaseColor","fieldDesc":"基础颜色","fieldType":"string","dataType":"varchar","fieldJson":"baseColor","primaryKey":false,"dataTypeLong":"191","columnName":"base_color","comment":"基础颜色","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"AuthorityId","fieldDesc":"用户角色ID","fieldType":"int","dataType":"bigint","fieldJson":"authorityId","primaryKey":false,"dataTypeLong":"20","columnName":"authority_id","comment":"用户角色ID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Phone","fieldDesc":"用户手机号","fieldType":"string","dataType":"varchar","fieldJson":"phone","primaryKey":false,"dataTypeLong":"191","columnName":"phone","comment":"用户手机号","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Email","fieldDesc":"用户邮箱","fieldType":"string","dataType":"varchar","fieldJson":"email","primaryKey":false,"dataTypeLong":"191","columnName":"email","comment":"用户邮箱","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Enable","fieldDesc":"用户是否被冻结 1正常 2冻结","fieldType":"int","dataType":"bigint","fieldJson":"enable","primaryKey":false,"dataTypeLong":"19","columnName":"enable","comment":"用户是否被冻结 1正常 2冻结","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}}],"humpPackageName":"sys_users"}` + err := json.Unmarshal([]byte(testJson), &tt.args.info) + if err != nil { + t.Error(err) + return + } + err = tt.args.info.Pretreatment() + if err != nil { + t.Error(err) + return + } + got, err := AutoCodeTemplate.Preview(tt.args.ctx, tt.args.info) + if (err != nil) != tt.wantErr { + t.Errorf("Preview() error = %+v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Preview() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/service/system/enter.go b/server/service/system/enter.go new file mode 100644 index 0000000..536fb1a --- /dev/null +++ b/server/service/system/enter.go @@ -0,0 +1,30 @@ +package system + +type ServiceGroup struct { + JwtService + ApiService + MenuService + UserService + CasbinService + InitDBService + AutoCodeService + BaseMenuService + AuthorityService + DictionaryService + SystemConfigService + OperationRecordService + DictionaryDetailService + AuthorityBtnService + SysExportTemplateService + SysParamsService + SysVersionService + SkillsService + AIWorkflowSession aiWorkflowSession + AutoCodePlugin autoCodePlugin + AutoCodePackage autoCodePackage + AutoCodeHistory autoCodeHistory + AutoCodeTemplate autoCodeTemplate + SysErrorService + LoginLogService + ApiTokenService +} diff --git a/server/service/system/jwt_black_list.go b/server/service/system/jwt_black_list.go new file mode 100644 index 0000000..6c34bbb --- /dev/null +++ b/server/service/system/jwt_black_list.go @@ -0,0 +1,52 @@ +package system + +import ( + "context" + + "go.uber.org/zap" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" +) + +type JwtService struct{} + +var JwtServiceApp = new(JwtService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: JsonInBlacklist +//@description: 拉黑jwt +//@param: jwtList model.JwtBlacklist +//@return: err error + +func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err error) { + err = global.GVA_DB.Create(&jwtList).Error + if err != nil { + return + } + global.BlackCache.SetDefault(jwtList.Jwt, struct{}{}) + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetRedisJWT +//@description: 从redis取jwt +//@param: userName string +//@return: redisJWT string, err error + +func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err error) { + redisJWT, err = global.GVA_REDIS.Get(context.Background(), userName).Result() + return redisJWT, err +} + +func LoadAll() { + var data []string + err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error + if err != nil { + global.GVA_LOG.Error("加载数据库jwt黑名单失败!", zap.Error(err)) + return + } + for i := 0; i < len(data); i++ { + global.BlackCache.SetDefault(data[i], struct{}{}) + } // jwt黑名单 加入 BlackCache 中 +} diff --git a/server/service/system/sys_api.go b/server/service/system/sys_api.go new file mode 100644 index 0000000..a77d1d5 --- /dev/null +++ b/server/service/system/sys_api.go @@ -0,0 +1,326 @@ +package system + +import ( + "errors" + "fmt" + "strings" + + "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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateApi +//@description: 新增基础api +//@param: api model.SysApi +//@return: err error + +type ApiService struct{} + +var ApiServiceApp = new(ApiService) + +func (apiService *ApiService) CreateApi(api system.SysApi) (err error) { + if !errors.Is(global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&system.SysApi{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同api") + } + return global.GVA_DB.Create(&api).Error +} + +func (apiService *ApiService) GetApiGroups() (groups []string, groupApiMap map[string]string, err error) { + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + groupApiMap = make(map[string]string, 0) + for i := range apis { + pathArr := strings.Split(apis[i].Path, "/") + newGroup := true + for i2 := range groups { + if groups[i2] == apis[i].ApiGroup { + newGroup = false + } + } + if newGroup { + groups = append(groups, apis[i].ApiGroup) + } + groupApiMap[pathArr[1]] = apis[i].ApiGroup + } + return +} + +func (apiService *ApiService) SyncApi() (newApis, deleteApis, ignoreApis []system.SysApi, err error) { + newApis = make([]system.SysApi, 0) + deleteApis = make([]system.SysApi, 0) + ignoreApis = make([]system.SysApi, 0) + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + var ignores []system.SysIgnoreApi + err = global.GVA_DB.Find(&ignores).Error + if err != nil { + return + } + + for i := range ignores { + ignoreApis = append(ignoreApis, system.SysApi{ + Path: ignores[i].Path, + Description: "", + ApiGroup: "", + Method: ignores[i].Method, + }) + } + + var cacheApis []system.SysApi + for i := range global.GVA_ROUTERS { + ignoresFlag := false + for j := range ignores { + if ignores[j].Path == global.GVA_ROUTERS[i].Path && ignores[j].Method == global.GVA_ROUTERS[i].Method { + ignoresFlag = true + } + } + if !ignoresFlag { + cacheApis = append(cacheApis, system.SysApi{ + Path: global.GVA_ROUTERS[i].Path, + Method: global.GVA_ROUTERS[i].Method, + }) + } + } + + //对比数据库中的api和内存中的api,如果数据库中的api不存在于内存中,则把api放入删除数组,如果内存中的api不存在于数据库中,则把api放入新增数组 + for i := range cacheApis { + var flag bool + // 如果存在于内存不存在于api数组中 + for j := range apis { + if cacheApis[i].Path == apis[j].Path && cacheApis[i].Method == apis[j].Method { + flag = true + } + } + if !flag { + newApis = append(newApis, system.SysApi{ + Path: cacheApis[i].Path, + Description: "", + ApiGroup: "", + Method: cacheApis[i].Method, + }) + } + } + + for i := range apis { + var flag bool + // 如果存在于api数组不存在于内存 + for j := range cacheApis { + if cacheApis[j].Path == apis[i].Path && cacheApis[j].Method == apis[i].Method { + flag = true + } + } + if !flag { + deleteApis = append(deleteApis, apis[i]) + } + } + return +} + +func (apiService *ApiService) IgnoreApi(ignoreApi system.SysIgnoreApi) (err error) { + if ignoreApi.Flag { + return global.GVA_DB.Create(&ignoreApi).Error + } + return global.GVA_DB.Unscoped().Delete(&ignoreApi, "path = ? AND method = ?", ignoreApi.Path, ignoreApi.Method).Error +} + +func (apiService *ApiService) EnterSyncApi(syncApis systemRes.SysSyncApis) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var txErr error + if len(syncApis.NewApis) > 0 { + txErr = tx.Create(&syncApis.NewApis).Error + if txErr != nil { + return txErr + } + } + for i := range syncApis.DeleteApis { + CasbinServiceApp.ClearCasbin(1, syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method) + txErr = tx.Delete(&system.SysApi{}, "path = ? AND method = ?", syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method).Error + if txErr != nil { + return txErr + } + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApi +//@description: 删除基础api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApi(api system.SysApi) (err error) { + var entity system.SysApi + err = global.GVA_DB.First(&entity, "id = ?", api.ID).Error // 根据id查询api记录 + if errors.Is(err, gorm.ErrRecordNotFound) { // api记录不存在 + return err + } + err = global.GVA_DB.Delete(&entity).Error + if err != nil { + return err + } + CasbinServiceApp.ClearCasbin(1, entity.Path, entity.Method) + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAPIInfoList +//@description: 分页获取数据, +//@param: api model.SysApi, info request.PageInfo, order string, desc bool +//@return: list interface{}, total int64, err error + +func (apiService *ApiService) GetAPIInfoList(api system.SysApi, info request.PageInfo, order string, desc bool) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysApi{}) + var apiList []system.SysApi + + if api.Path != "" { + db = db.Where("path LIKE ?", "%"+api.Path+"%") + } + + if api.Description != "" { + db = db.Where("description LIKE ?", "%"+api.Description+"%") + } + + if api.Method != "" { + db = db.Where("method = ?", api.Method) + } + + if api.ApiGroup != "" { + db = db.Where("api_group = ?", api.ApiGroup) + } + + err = db.Count(&total).Error + + if err != nil { + return apiList, total, err + } + + db = db.Limit(limit).Offset(offset) + OrderStr := "id desc" + if order != "" { + orderMap := make(map[string]bool, 5) + orderMap["id"] = true + orderMap["path"] = true + orderMap["api_group"] = true + orderMap["description"] = true + orderMap["method"] = true + if !orderMap[order] { + err = fmt.Errorf("非法的排序字段: %v", order) + return apiList, total, err + } + OrderStr = order + if desc { + OrderStr = order + " desc" + } + } + err = db.Order(OrderStr).Find(&apiList).Error + return apiList, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAllApis +//@description: 获取所有的api +//@return: apis []model.SysApi, err error + +func (apiService *ApiService) GetAllApis(authorityID uint) (apis []system.SysApi, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + err = global.GVA_DB.Order("id desc").Find(&apis).Error + if parentAuthorityID == 0 || !global.GVA_CONFIG.System.UseStrictAuth { + return + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(authorityID) + // 挑选 apis里面的path和method也在paths里面的api + var authApis []system.SysApi + for i := range apis { + for j := range paths { + if paths[j].Path == apis[i].Path && paths[j].Method == apis[i].Method { + authApis = append(authApis, apis[i]) + } + } + } + return authApis, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetApiById +//@description: 根据id获取api +//@param: id float64 +//@return: api model.SysApi, err error + +func (apiService *ApiService) GetApiById(id int) (api system.SysApi, err error) { + err = global.GVA_DB.First(&api, "id = ?", id).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateApi +//@description: 根据id更新api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) UpdateApi(api system.SysApi) (err error) { + var oldA system.SysApi + err = global.GVA_DB.First(&oldA, "id = ?", api.ID).Error + if oldA.Path != api.Path || oldA.Method != api.Method { + var duplicateApi system.SysApi + if ferr := global.GVA_DB.First(&duplicateApi, "path = ? AND method = ?", api.Path, api.Method).Error; ferr != nil { + if !errors.Is(ferr, gorm.ErrRecordNotFound) { + return ferr + } + } else { + if duplicateApi.ID != api.ID { + return errors.New("存在相同api路径") + } + } + + } + if err != nil { + return err + } + + err = CasbinServiceApp.UpdateCasbinApi(oldA.Path, api.Path, oldA.Method, api.Method) + if err != nil { + return err + } + + return global.GVA_DB.Save(&api).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApisByIds +//@description: 删除选中API +//@param: apis []model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApisByIds(ids request.IdsReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var apis []system.SysApi + err = tx.Find(&apis, "id in ?", ids.Ids).Error + if err != nil { + return err + } + err = tx.Delete(&[]system.SysApi{}, "id in ?", ids.Ids).Error + if err != nil { + return err + } + for _, sysApi := range apis { + CasbinServiceApp.ClearCasbin(1, sysApi.Path, sysApi.Method) + } + return err + }) +} diff --git a/server/service/system/sys_api_token.go b/server/service/system/sys_api_token.go new file mode 100644 index 0000000..b2ebdde --- /dev/null +++ b/server/service/system/sys_api_token.go @@ -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 +} diff --git a/server/service/system/sys_authority.go b/server/service/system/sys_authority.go new file mode 100644 index 0000000..1264707 --- /dev/null +++ b/server/service/system/sys_authority.go @@ -0,0 +1,412 @@ +package system + +import ( + "errors" + "strconv" + + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + + "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" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "gorm.io/gorm" +) + +var ErrRoleExistence = errors.New("存在相同角色id") + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateAuthority +//@description: 创建一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +type AuthorityService struct{} + +var AuthorityServiceApp = new(AuthorityService) + +func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + + if err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error; !errors.Is(err, gorm.ErrRecordNotFound) { + return auth, ErrRoleExistence + } + + e := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + if err = tx.Create(&auth).Error; err != nil { + return err + } + + auth.SysBaseMenus = systemReq.DefaultMenu() + if err = tx.Model(&auth).Association("SysBaseMenus").Replace(&auth.SysBaseMenus); err != nil { + return err + } + casbinInfos := systemReq.DefaultCasbin() + authorityId := strconv.Itoa(int(auth.AuthorityId)) + rules := [][]string{} + for _, v := range casbinInfos { + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + return CasbinServiceApp.AddPolicies(tx, rules) + }) + + return auth, e +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CopyAuthority +//@description: 复制一个角色 +//@param: copyInfo response.SysAuthorityCopyResponse +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) CopyAuthority(adminAuthorityID uint, copyInfo response.SysAuthorityCopyResponse) (authority system.SysAuthority, err error) { + var authorityBox system.SysAuthority + if !errors.Is(global.GVA_DB.Where("authority_id = ?", copyInfo.Authority.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) { + return authority, ErrRoleExistence + } + copyInfo.Authority.Children = []system.SysAuthority{} + menus, err := MenuServiceApp.GetMenuAuthority(&request.GetAuthorityId{AuthorityId: copyInfo.OldAuthorityId}) + if err != nil { + return + } + var baseMenu []system.SysBaseMenu + for _, v := range menus { + intNum := v.MenuId + v.SysBaseMenu.ID = uint(intNum) + baseMenu = append(baseMenu, v.SysBaseMenu) + } + copyInfo.Authority.SysBaseMenus = baseMenu + err = global.GVA_DB.Create(©Info.Authority).Error + if err != nil { + return + } + + var btns []system.SysAuthorityBtn + + err = global.GVA_DB.Find(&btns, "authority_id = ?", copyInfo.OldAuthorityId).Error + if err != nil { + return + } + if len(btns) > 0 { + for i := range btns { + btns[i].AuthorityId = copyInfo.Authority.AuthorityId + } + err = global.GVA_DB.Create(&btns).Error + + if err != nil { + return + } + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(copyInfo.OldAuthorityId) + err = CasbinServiceApp.UpdateCasbin(adminAuthorityID, copyInfo.Authority.AuthorityId, paths) + if err != nil { + _ = authorityService.DeleteAuthority(©Info.Authority) + } + return copyInfo.Authority, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateAuthority +//@description: 更改一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) UpdateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + var oldAuthority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&oldAuthority).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return system.SysAuthority{}, errors.New("查询角色数据失败") + } + err = global.GVA_DB.Model(&oldAuthority).Updates(&auth).Error + return auth, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteAuthority +//@description: 删除角色 +//@param: auth *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) DeleteAuthority(auth *system.SysAuthority) error { + if errors.Is(global.GVA_DB.Debug().Preload("Users").First(&auth).Error, gorm.ErrRecordNotFound) { + return errors.New("该角色不存在") + } + if len(auth.Users) != 0 { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysUser{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("parent_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色存在子角色不允许删除") + } + + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var err error + if err = tx.Preload("SysBaseMenus").Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(auth).Unscoped().Delete(auth).Error; err != nil { + return err + } + + if len(auth.SysBaseMenus) > 0 { + if err = tx.Model(auth).Association("SysBaseMenus").Delete(auth.SysBaseMenus); err != nil { + return err + } + // err = db.Association("SysBaseMenus").Delete(&auth) + } + if len(auth.DataAuthorityId) > 0 { + if err = tx.Model(auth).Association("DataAuthorityId").Delete(auth.DataAuthorityId); err != nil { + return err + } + } + + if err = tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", auth.AuthorityId).Error; err != nil { + return err + } + if err = tx.Where("authority_id = ?", auth.AuthorityId).Delete(&[]system.SysAuthorityBtn{}).Error; err != nil { + return err + } + + authorityId := strconv.Itoa(int(auth.AuthorityId)) + + if err = CasbinServiceApp.RemoveFilteredPolicy(tx, authorityId); err != nil { + return err + } + + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetAuthorityInfoList(authorityID uint) (list []system.SysAuthority, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + if err != nil { + return nil, err + } + var authorities []system.SysAuthority + db := global.GVA_DB.Model(&system.SysAuthority{}) + if global.GVA_CONFIG.System.UseStrictAuth { + // 当开启了严格树形结构后 + if *authority.ParentId == 0 { + // 只有顶级角色可以修改自己的权限和以下权限 + err = db.Preload("DataAuthorityId").Where("authority_id = ?", authorityID).Find(&authorities).Error + } else { + // 非顶级角色只能修改以下权限 + err = db.Debug().Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + } + } else { + err = db.Preload("DataAuthorityId").Where("parent_id = ?", "0").Find(&authorities).Error + } + + for k := range authorities { + err = authorityService.findChildrenAuthority(&authorities[k]) + } + return authorities, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetStructAuthorityList(authorityID uint) (list []uint, err error) { + var auth system.SysAuthority + _ = global.GVA_DB.First(&auth, "authority_id = ?", authorityID).Error + var authorities []system.SysAuthority + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + if len(authorities) > 0 { + for k := range authorities { + list = append(list, authorities[k].AuthorityId) + childrenList, err := authorityService.GetStructAuthorityList(authorities[k].AuthorityId) + if err == nil { + list = append(list, childrenList...) + } + } + } + if *auth.ParentId == 0 { + list = append(list, authorityID) + } + return list, err +} + +func (authorityService *AuthorityService) CheckAuthorityIDAuth(authorityID, targetID uint) (err error) { + if !global.GVA_CONFIG.System.UseStrictAuth { + return nil + } + authIDS, err := authorityService.GetStructAuthorityList(authorityID) + if err != nil { + return err + } + hasAuth := false + for _, v := range authIDS { + if v == targetID { + hasAuth = true + break + } + } + if !hasAuth { + return errors.New("您提交的角色ID不合法") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfo +//@description: 获取所有角色信息 +//@param: auth model.SysAuthority +//@return: sa system.SysAuthority, err error + +func (authorityService *AuthorityService) GetAuthorityInfo(auth system.SysAuthority) (sa system.SysAuthority, err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(&sa).Error + return sa, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetDataAuthority +//@description: 设置角色资源权限 +//@param: auth model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetDataAuthority(adminAuthorityID uint, auth system.SysAuthority) error { + var checkIDs []uint + checkIDs = append(checkIDs, auth.AuthorityId) + for i := range auth.DataAuthorityId { + checkIDs = append(checkIDs, auth.DataAuthorityId[i].AuthorityId) + } + + for i := range checkIDs { + err := authorityService.CheckAuthorityIDAuth(adminAuthorityID, checkIDs[i]) + if err != nil { + return err + } + } + + var s system.SysAuthority + global.GVA_DB.Preload("DataAuthorityId").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("DataAuthorityId").Replace(&auth.DataAuthorityId) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetMenuAuthority +//@description: 菜单与角色绑定 +//@param: auth *model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetMenuAuthority(auth *system.SysAuthority) error { + var s system.SysAuthority + global.GVA_DB.Preload("SysBaseMenus").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("SysBaseMenus").Replace(&auth.SysBaseMenus) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: findChildrenAuthority +//@description: 查询子角色 +//@param: authority *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) findChildrenAuthority(authority *system.SysAuthority) (err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authority.AuthorityId).Find(&authority.Children).Error + if len(authority.Children) > 0 { + for k := range authority.Children { + err = authorityService.findChildrenAuthority(&authority.Children[k]) + } + } + return err +} + +func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) (parentID uint, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + if err != nil { + return + } + return *authority.ParentId, nil +} + +// GetUserIdsByAuthorityId 获取拥有指定角色的所有用户ID +func (authorityService *AuthorityService) GetUserIdsByAuthorityId(authorityId uint) (userIds []uint, err error) { + var records []system.SysUserAuthority + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&records).Error + if err != nil { + return nil, err + } + for _, r := range records { + userIds = append(userIds, r.SysUserId) + } + return userIds, nil +} + +// SetRoleUsers 全量覆盖某角色关联的用户列表 +// 入参:角色ID + 目标用户ID列表,保存时将该角色的关联关系完全替换为传入列表 +func (authorityService *AuthorityService) SetRoleUsers(authorityId uint, userIds []uint) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 1. 查出当前拥有该角色的所有用户ID + var existingRecords []system.SysUserAuthority + if err := tx.Where("sys_authority_authority_id = ?", authorityId).Find(&existingRecords).Error; err != nil { + return err + } + + currentSet := make(map[uint]struct{}) + for _, r := range existingRecords { + currentSet[r.SysUserId] = struct{}{} + } + + targetSet := make(map[uint]struct{}) + for _, id := range userIds { + targetSet[id] = struct{}{} + } + + // 2. 删除该角色所有已有的用户关联 + if err := tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", authorityId).Error; err != nil { + return err + } + + // 3. 对被移除的用户:若该角色是其主角色,则将主角色切换为其剩余的其他角色 + for userId := range currentSet { + if _, ok := targetSet[userId]; ok { + continue // 仍在目标列表中,不处理 + } + var user system.SysUser + if err := tx.First(&user, "id = ?", userId).Error; err != nil { + continue + } + if user.AuthorityId == authorityId { + // 从剩余关联(已删除当前角色后)中找另一个角色作为主角色 + var another system.SysUserAuthority + if err := tx.Where("sys_user_id = ?", userId).First(&another).Error; err != nil { + // 没有其他角色,主角色保持不变,不做处理 + continue + } + if err := tx.Model(&system.SysUser{}).Where("id = ?", userId). + Update("authority_id", another.SysAuthorityAuthorityId).Error; err != nil { + return err + } + } + } + + // 4. 批量插入新的关联记录 + if len(userIds) > 0 { + newRecords := make([]system.SysUserAuthority, 0, len(userIds)) + for _, userId := range userIds { + newRecords = append(newRecords, system.SysUserAuthority{ + SysUserId: userId, + SysAuthorityAuthorityId: authorityId, + }) + } + if err := tx.Create(&newRecords).Error; err != nil { + return err + } + } + + return nil + }) +} diff --git a/server/service/system/sys_authority_btn.go b/server/service/system/sys_authority_btn.go new file mode 100644 index 0000000..1cc8f1f --- /dev/null +++ b/server/service/system/sys_authority_btn.go @@ -0,0 +1,60 @@ +package system + +import ( + "errors" + "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/model/system/response" + "gorm.io/gorm" +) + +type AuthorityBtnService struct{} + +var AuthorityBtnServiceApp = new(AuthorityBtnService) + +func (a *AuthorityBtnService) GetAuthorityBtn(req request.SysAuthorityBtnReq) (res response.SysAuthorityBtnRes, err error) { + var authorityBtn []system.SysAuthorityBtn + err = global.GVA_DB.Find(&authorityBtn, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return + } + var selected []uint + for _, v := range authorityBtn { + selected = append(selected, v.SysBaseMenuBtnID) + } + res.Selected = selected + return res, err +} + +func (a *AuthorityBtnService) SetAuthorityBtn(req request.SysAuthorityBtnReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var authorityBtn []system.SysAuthorityBtn + err = tx.Delete(&[]system.SysAuthorityBtn{}, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return err + } + for _, v := range req.Selected { + authorityBtn = append(authorityBtn, system.SysAuthorityBtn{ + AuthorityId: req.AuthorityId, + SysMenuID: req.MenuID, + SysBaseMenuBtnID: v, + }) + } + if len(authorityBtn) > 0 { + err = tx.Create(&authorityBtn).Error + } + if err != nil { + return err + } + return err + }) +} + +func (a *AuthorityBtnService) CanRemoveAuthorityBtn(ID string) (err error) { + fErr := global.GVA_DB.First(&system.SysAuthorityBtn{}, "sys_base_menu_btn_id = ?", ID).Error + if errors.Is(fErr, gorm.ErrRecordNotFound) { + return nil + } + return errors.New("此按钮正在被使用无法删除") +} diff --git a/server/service/system/sys_auto_code_interface.go b/server/service/system/sys_auto_code_interface.go new file mode 100644 index 0000000..7939f14 --- /dev/null +++ b/server/service/system/sys_auto_code_interface.go @@ -0,0 +1,55 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +type AutoCodeService struct{} + +type Database interface { + GetDB(businessDB string) (data []response.Db, err error) + GetTables(businessDB string, dbName string) (data []response.Table, err error) + GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) +} + +func (autoCodeService *AutoCodeService) Database(businessDB string) Database { + + if businessDB == "" { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + return AutoCodeMysql + case "pgsql": + return AutoCodePgsql + case "mssql": + return AutoCodeMssql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } else { + for _, info := range global.GVA_CONFIG.DBList { + if info.AliasName == businessDB { + switch info.Type { + case "mysql": + return AutoCodeMysql + case "mssql": + return AutoCodeMssql + case "pgsql": + return AutoCodePgsql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } + } + return AutoCodeMysql + } + +} diff --git a/server/service/system/sys_auto_code_mssql.go b/server/service/system/sys_auto_code_mssql.go new file mode 100644 index 0000000..31bdc96 --- /dev/null +++ b/server/service/system/sys_auto_code_mssql.go @@ -0,0 +1,83 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeMssql = new(autoCodeMssql) + +type autoCodeMssql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "select name AS 'database' from sys.databases;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + + sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName) + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf(` +SELECT + sc.name AS column_name, + st.name AS data_type, + sc.max_length AS data_type_long, + CASE + WHEN pk.object_id IS NOT NULL THEN 1 + ELSE 0 + END AS primary_key, + sc.column_id +FROM + %s.sys.columns sc +JOIN + sys.types st ON sc.user_type_id=st.user_type_id +LEFT JOIN + %s.sys.objects so ON so.name='%s' AND so.type='U' +LEFT JOIN + %s.sys.indexes si ON si.object_id = so.object_id AND si.is_primary_key = 1 +LEFT JOIN + %s.sys.index_columns sic ON sic.object_id = si.object_id AND sic.index_id = si.index_id AND sic.column_id = sc.column_id +LEFT JOIN + %s.sys.key_constraints pk ON pk.object_id = si.object_id +WHERE + st.is_user_defined=0 AND sc.object_id = so.object_id +ORDER BY + sc.column_id +`, dbName, dbName, tableName, dbName, dbName, dbName) + + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} diff --git a/server/service/system/sys_auto_code_mysql.go b/server/service/system/sys_auto_code_mysql.go new file mode 100644 index 0000000..c7f0f1b --- /dev/null +++ b/server/service/system/sys_auto_code_mysql.go @@ -0,0 +1,83 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeMysql = new(autoCodeMysql) + +type autoCodeMysql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_schema = ?` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + c.COLUMN_NAME column_name, + c.DATA_TYPE data_type, + CASE c.DATA_TYPE + WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'int' THEN c.NUMERIC_PRECISION + WHEN 'bigint' THEN c.NUMERIC_PRECISION + ELSE '' + END AS data_type_long, + c.COLUMN_COMMENT column_comment, + CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key, + c.ORDINAL_POSITION +FROM + INFORMATION_SCHEMA.COLUMNS c +LEFT JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu +ON + c.TABLE_SCHEMA = kcu.TABLE_SCHEMA + AND c.TABLE_NAME = kcu.TABLE_NAME + AND c.COLUMN_NAME = kcu.COLUMN_NAME + AND kcu.CONSTRAINT_NAME = 'PRIMARY' +WHERE + c.TABLE_NAME = ? + AND c.TABLE_SCHEMA = ? +ORDER BY + c.ORDINAL_POSITION;` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + } + + return entities, err +} diff --git a/server/service/system/sys_auto_code_oracle.go b/server/service/system/sys_auto_code_oracle.go new file mode 100644 index 0000000..fc3c43f --- /dev/null +++ b/server/service/system/sys_auto_code_oracle.go @@ -0,0 +1,72 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodeOracle = new(autoCodeOracle) + +type autoCodeOracle struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT lower(username) AS "database" FROM all_users` + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?` + + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + lower(a.COLUMN_NAME) as "column_name", + (CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type", + (CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long", + b.COMMENTS as "column_comment", + (CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key", + a.COLUMN_ID +FROM + all_tab_columns a +JOIN + all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME +LEFT JOIN + ( + SELECT + acc.OWNER, + acc.TABLE_NAME, + acc.COLUMN_NAME + FROM + all_cons_columns acc + JOIN + all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME + WHERE + ac.CONSTRAINT_TYPE = 'P' + ) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME +WHERE + lower(a.table_name) = ? + AND lower(a.OWNER) = ? +ORDER BY + a.COLUMN_ID +` + + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + return entities, err +} diff --git a/server/service/system/sys_auto_code_pgsql.go b/server/service/system/sys_auto_code_pgsql.go new file mode 100644 index 0000000..fae16fb --- /dev/null +++ b/server/service/system/sys_auto_code_pgsql.go @@ -0,0 +1,135 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" +) + +var AutoCodePgsql = new(autoCodePgsql) + +type autoCodePgsql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false` + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?` + + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, "public").Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + // todo 数据获取不全, 待完善sql + sql := ` +SELECT + psc.COLUMN_NAME AS COLUMN_NAME, + psc.udt_name AS data_type, + CASE + psc.udt_name + WHEN 'text' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'varchar' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'smallint' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'decimal' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'integer' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int4' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int8' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'bigint' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'timestamp' THEN + concat_ws ( '', '', psc.datetime_precision ) + ELSE '' + END AS data_type_long, + ( + SELECT + pd.description + FROM + pg_description pd + WHERE + (pd.objoid,pd.objsubid) in ( + SELECT pa.attrelid,pa.attnum + FROM + pg_attribute pa + WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE + pc.relname = psc.table_name + ) + and attname = psc.column_name + ) + ) AS column_comment, + ( + SELECT + COUNT(*) + FROM + pg_constraint + WHERE + contype = 'p' + AND conrelid = ( + SELECT + oid + FROM + pg_class + WHERE + relname = psc.table_name + ) + AND conkey::int[] @> ARRAY[( + SELECT + attnum::integer + FROM + pg_attribute + WHERE + attrelid = conrelid + AND attname = psc.column_name + )] + ) > 0 AS primary_key, + psc.ordinal_position +FROM + INFORMATION_SCHEMA.COLUMNS psc +WHERE + table_catalog = ? + AND table_schema = 'public' + AND TABLE_NAME = ? +ORDER BY + psc.ordinal_position; +` + var entities []response.Column + //sql = strings.ReplaceAll(sql, "@table_catalog", dbName) + //sql = strings.ReplaceAll(sql, "@table_name", tableName) + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, tableName).Scan(&entities).Error + return entities, err +} diff --git a/server/service/system/sys_auto_code_sqlite.go b/server/service/system/sys_auto_code_sqlite.go new file mode 100644 index 0000000..59bcfce --- /dev/null +++ b/server/service/system/sys_auto_code_sqlite.go @@ -0,0 +1,84 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "path/filepath" + "strings" +) + +var AutoCodeSqlite = new(autoCodeSqlite) + +type autoCodeSqlite struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "PRAGMA database_list;" + var databaseList []struct { + File string `gorm:"column:file"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&databaseList).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&databaseList).Error + } + for _, database := range databaseList { + if database.File != "" { + fileName := filepath.Base(database.File) + fileExt := filepath.Ext(fileName) + fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt) + + entities = append(entities, response.Db{fileNameWithoutExt}) + } + } + // entities = append(entities, response.Db{global.GVA_CONFIG.Sqlite.Dbname}) + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `SELECT name FROM sqlite_master WHERE type='table'` + tabelNames := []string{} + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&tabelNames).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&tabelNames).Error + } + for _, tabelName := range tabelNames { + entities = append(entities, response.Table{tabelName}) + } + return entities, err +} + +// GetColumn 获取指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName) + var columnInfos []struct { + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + Pk int `gorm:"column:pk"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&columnInfos).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&columnInfos).Error + } + for _, columnInfo := range columnInfos { + entities = append(entities, response.Column{ + ColumnName: columnInfo.Name, + DataType: columnInfo.Type, + PrimaryKey: columnInfo.Pk == 1, + }) + } + return entities, err +} diff --git a/server/service/system/sys_base_menu.go b/server/service/system/sys_base_menu.go new file mode 100644 index 0000000..3a6560f --- /dev/null +++ b/server/service/system/sys_base_menu.go @@ -0,0 +1,147 @@ +package system + +import ( + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "gorm.io/gorm" +) + +type BaseMenuService struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteBaseMenu +//@description: 删除基础路由 +//@param: id float64 +//@return: err error + +var BaseMenuServiceApp = new(BaseMenuService) + +func (baseMenuService *BaseMenuService) DeleteBaseMenu(id int) (err error) { + err = global.GVA_DB.First(&system.SysBaseMenu{}, "parent_id = ?", id).Error + if err == nil { + return errors.New("此菜单存在子菜单不可删除") + } + var menu system.SysBaseMenu + err = global.GVA_DB.First(&menu, id).Error + if err != nil { + return errors.New("记录不存在") + } + err = global.GVA_DB.First(&system.SysAuthority{}, "default_router = ?", menu.Name).Error + if err == nil { + return errors.New("此菜单有角色正在作为首页,不可删除") + } + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + err = tx.Delete(&system.SysBaseMenu{}, "id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + err = tx.Delete(&system.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + return nil + }) + +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateBaseMenu +//@description: 更新路由 +//@param: menu model.SysBaseMenu +//@return: err error + +func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) (err error) { + var oldMenu system.SysBaseMenu + upDateMap := make(map[string]interface{}) + upDateMap["keep_alive"] = menu.KeepAlive + upDateMap["transition_type"] = menu.TransitionType + upDateMap["close_tab"] = menu.CloseTab + upDateMap["default_menu"] = menu.DefaultMenu + upDateMap["parent_id"] = menu.ParentId + upDateMap["path"] = menu.Path + upDateMap["name"] = menu.Name + upDateMap["hidden"] = menu.Hidden + upDateMap["component"] = menu.Component + upDateMap["title"] = menu.Title + upDateMap["active_name"] = menu.ActiveName + upDateMap["icon"] = menu.Icon + upDateMap["sort"] = menu.Sort + + err = global.GVA_DB.Transaction(func(tx *gorm.DB) error { + tx.Where("id = ?", menu.ID).Find(&oldMenu) + if oldMenu.Name != menu.Name { + if !errors.Is(tx.Where("id <> ? AND name = ?", menu.ID, menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + global.GVA_LOG.Debug("存在相同name修改失败") + return errors.New("存在相同name修改失败") + } + } + txErr := tx.Unscoped().Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + txErr = tx.Unscoped().Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + if len(menu.Parameters) > 0 { + for k := range menu.Parameters { + menu.Parameters[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.Parameters).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + if len(menu.MenuBtn) > 0 { + for k := range menu.MenuBtn { + menu.MenuBtn[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.MenuBtn).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + txErr = tx.Model(&oldMenu).Updates(upDateMap).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + return nil + }) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuById +//@description: 返回当前选中menu +//@param: id float64 +//@return: menu system.SysBaseMenu, err error + +func (baseMenuService *BaseMenuService) GetBaseMenuById(id int) (menu system.SysBaseMenu, err error) { + err = global.GVA_DB.Preload("MenuBtn").Preload("Parameters").Where("id = ?", id).First(&menu).Error + return +} diff --git a/server/service/system/sys_casbin.go b/server/service/system/sys_casbin.go new file mode 100644 index 0000000..b281a4e --- /dev/null +++ b/server/service/system/sys_casbin.go @@ -0,0 +1,215 @@ +package system + +import ( + "errors" + "strconv" + + "gorm.io/gorm" + + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + _ "github.com/go-sql-driver/mysql" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbin +//@description: 更新casbin权限 +//@param: authorityId string, casbinInfos []request.CasbinInfo +//@return: error + +type CasbinService struct{} + +var CasbinServiceApp = new(CasbinService) + +func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID uint, casbinInfos []request.CasbinInfo) error { + + err := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, AuthorityID) + if err != nil { + return err + } + + if global.GVA_CONFIG.System.UseStrictAuth { + apis, e := ApiServiceApp.GetAllApis(adminAuthorityID) + if e != nil { + return e + } + + for i := range casbinInfos { + hasApi := false + for j := range apis { + if apis[j].Path == casbinInfos[i].Path && apis[j].Method == casbinInfos[i].Method { + hasApi = true + break + } + } + if !hasApi { + return errors.New("存在api不在权限列表中") + } + } + } + + authorityId := strconv.Itoa(int(AuthorityID)) + casbinService.ClearCasbin(0, authorityId) + rules := [][]string{} + //做权限去重处理 + deduplicateMap := make(map[string]bool) + for _, v := range casbinInfos { + key := authorityId + v.Path + v.Method + if _, ok := deduplicateMap[key]; !ok { + deduplicateMap[key] = true + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + } + if len(rules) == 0 { + return nil + } // 设置空权限无需调用 AddPolicies 方法 + e := utils.GetCasbin() + success, _ := e.AddPolicies(rules) + if !success { + return errors.New("存在相同api,添加失败,请联系管理员") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbinApi +//@description: API更新随动 +//@param: oldPath string, newPath string, oldMethod string, newMethod string +//@return: error + +func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath string, oldMethod string, newMethod string) error { + err := global.GVA_DB.Model(&gormadapter.CasbinRule{}).Where("v1 = ? AND v2 = ?", oldPath, oldMethod).Updates(map[string]interface{}{ + "v1": newPath, + "v2": newMethod, + }).Error + if err != nil { + return err + } + + e := utils.GetCasbin() + return e.LoadPolicy() +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetPolicyPathByAuthorityId +//@description: 获取权限列表 +//@param: authorityId string +//@return: pathMaps []request.CasbinInfo + +func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { + e := utils.GetCasbin() + authorityId := strconv.Itoa(int(AuthorityID)) + list, _ := e.GetFilteredPolicy(0, authorityId) + for _, v := range list { + pathMaps = append(pathMaps, request.CasbinInfo{ + Path: v[1], + Method: v[2], + }) + } + return pathMaps +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ClearCasbin +//@description: 清除匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { + e := utils.GetCasbin() + success, _ := e.RemoveFilteredPolicy(v, p...) + return success +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveFilteredPolicy +//@description: 使用数据库方法清理筛选的politicy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string +//@return: error + +func (casbinService *CasbinService) RemoveFilteredPolicy(db *gorm.DB, authorityId string) error { + return db.Delete(&gormadapter.CasbinRule{}, "v0 = ?", authorityId).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SyncPolicy +//@description: 同步目前数据库的policy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string, rules [][]string +//@return: error + +func (casbinService *CasbinService) SyncPolicy(db *gorm.DB, authorityId string, rules [][]string) error { + err := casbinService.RemoveFilteredPolicy(db, authorityId) + if err != nil { + return err + } + return casbinService.AddPolicies(db, rules) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddPolicies +//@description: 添加匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) error { + var casbinRules []gormadapter.CasbinRule + for i := range rules { + casbinRules = append(casbinRules, gormadapter.CasbinRule{ + Ptype: "p", + V0: rules[i][0], + V1: rules[i][1], + V2: rules[i][2], + }) + } + return db.Create(&casbinRules).Error +} + +func (casbinService *CasbinService) FreshCasbin() (err error) { + e := utils.GetCasbin() + err = e.LoadPolicy() + return err +} + +// GetAuthoritiesByApi 获取拥有指定API权限的所有角色ID +func (casbinService *CasbinService) GetAuthoritiesByApi(path, method string) (authorityIds []uint, err error) { + var rules []gormadapter.CasbinRule + err = global.GVA_DB.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Find(&rules).Error + if err != nil { + return nil, err + } + for _, r := range rules { + id, e := strconv.Atoi(r.V0) + if e == nil { + authorityIds = append(authorityIds, uint(id)) + } + } + return authorityIds, nil +} + +// SetApiAuthorities 全量覆盖某API关联的角色列表 +func (casbinService *CasbinService) SetApiAuthorities(path, method string, authorityIds []uint) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 1. 删除该API所有已有的角色关联 + if err := tx.Where("ptype = 'p' AND v1 = ? AND v2 = ?", path, method).Delete(&gormadapter.CasbinRule{}).Error; err != nil { + return err + } + // 2. 批量插入新的关联记录 + if len(authorityIds) > 0 { + newRules := make([]gormadapter.CasbinRule, 0, len(authorityIds)) + for _, authorityId := range authorityIds { + newRules = append(newRules, gormadapter.CasbinRule{ + Ptype: "p", + V0: strconv.Itoa(int(authorityId)), + V1: path, + V2: method, + }) + } + if err := tx.Create(&newRules).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go new file mode 100644 index 0000000..d5d068d --- /dev/null +++ b/server/service/system/sys_dictionary.go @@ -0,0 +1,297 @@ +package system + +import ( + "encoding/json" + "errors" + + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionary +//@description: 创建字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +type DictionaryService struct{} + +var DictionaryServiceApp = new(DictionaryService) + +func (dictionaryService *DictionaryService) CreateSysDictionary(sysDictionary system.SysDictionary) (err error) { + if (!errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound)) { + return errors.New("存在相同的type,不允许创建") + } + err = global.GVA_DB.Create(&sysDictionary).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionary +//@description: 删除字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary system.SysDictionary) (err error) { + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).Preload("SysDictionaryDetails").First(&sysDictionary).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("请不要搞事") + } + if err != nil { + return err + } + err = global.GVA_DB.Delete(&sysDictionary).Error + if err != nil { + return err + } + + if sysDictionary.SysDictionaryDetails != nil { + return global.GVA_DB.Where("sys_dictionary_id=?", sysDictionary.ID).Delete(sysDictionary.SysDictionaryDetails).Error + } + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionary +//@description: 更新字典数据 +//@param: sysDictionary *model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) { + var dict system.SysDictionary + sysDictionaryMap := map[string]interface{}{ + "Name": sysDictionary.Name, + "Type": sysDictionary.Type, + "Status": sysDictionary.Status, + "Desc": sysDictionary.Desc, + "ParentID": sysDictionary.ParentID, + } + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).First(&dict).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return errors.New("查询字典数据失败") + } + if dict.Type != sysDictionary.Type { + if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同的type,不允许创建") + } + } + + // 检查是否会形成循环引用 + if sysDictionary.ParentID != nil && *sysDictionary.ParentID != 0 { + if err := dictionaryService.checkCircularReference(sysDictionary.ID, *sysDictionary.ParentID); err != nil { + return err + } + } + + err = global.GVA_DB.Model(&dict).Updates(sysDictionaryMap).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionary +//@description: 根据id或者type获取字典单条数据 +//@param: Type string, Id uint +//@return: err error, sysDictionary model.SysDictionary + +func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uint, status *bool) (sysDictionary system.SysDictionary, err error) { + var flag = false + if status == nil { + flag = true + } else { + flag = *status + } + err = global.GVA_DB.Where("(type = ? OR id = ?) and status = ?", Type, Id, flag).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + return db.Where("status = ? and deleted_at is null", true).Order("sort") + }).First(&sysDictionary).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetSysDictionaryInfoList +//@description: 分页获取字典列表 +//@param: info request.SysDictionarySearch +//@return: err error, list interface{}, total int64 + +func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) { + var sysDictionarys []system.SysDictionary + query := global.GVA_DB.WithContext(c) + if req.Name != "" { + query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%") + } + // 预加载子字典 + query = query.Preload("Children") + err = query.Find(&sysDictionarys).Error + return sysDictionarys, err +} + +// checkCircularReference 检查是否会形成循环引用 +func (dictionaryService *DictionaryService) checkCircularReference(currentID uint, parentID uint) error { + if currentID == parentID { + return errors.New("不能将字典设置为自己的父级") + } + + // 递归检查父级链条 + var parent system.SysDictionary + err := global.GVA_DB.Where("id = ?", parentID).First(&parent).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // 父级不存在,允许设置 + } + return err + } + + // 如果父级还有父级,继续检查 + if parent.ParentID != nil && *parent.ParentID != 0 { + return dictionaryService.checkCircularReference(currentID, *parent.ParentID) + } + + 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 + }) +} diff --git a/server/service/system/sys_dictionary_detail.go b/server/service/system/sys_dictionary_detail.go new file mode 100644 index 0000000..56425d3 --- /dev/null +++ b/server/service/system/sys_dictionary_detail.go @@ -0,0 +1,392 @@ +package system + +import ( + "fmt" + "strconv" + + "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" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionaryDetail +//@description: 创建字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +type DictionaryDetailService struct{} + +var DictionaryDetailServiceApp = new(DictionaryDetailService) + +func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + + err = global.GVA_DB.Create(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionaryDetail +//@description: 删除字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 检查是否有子项 + var count int64 + err = global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("parent_id = ?", sysDictionaryDetail.ID).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("该字典详情下还有子项,无法删除") + } + + err = global.GVA_DB.Delete(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionaryDetail +//@description: 更新字典详情数据 +//@param: sysDictionaryDetail *model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetail(sysDictionaryDetail *system.SysDictionaryDetail) (err error) { + // 如果更新了父级ID,需要重新计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + + // 检查循环引用 + if dictionaryDetailService.checkCircularReference(sysDictionaryDetail.ID, *sysDictionaryDetail.ParentID) { + return fmt.Errorf("不能将字典详情设置为自己或其子项的父级") + } + + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + + err = global.GVA_DB.Save(sysDictionaryDetail).Error + if err != nil { + return err + } + + // 更新所有子项的层级和路径 + return dictionaryDetailService.updateChildrenLevelAndPath(sysDictionaryDetail.ID) +} + +// checkCircularReference 检查循环引用 +func (dictionaryDetailService *DictionaryDetailService) checkCircularReference(id, parentID uint) bool { + if id == parentID { + return true + } + + var parent system.SysDictionaryDetail + err := global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return false + } + + if parent.ParentID == nil { + return false + } + + return dictionaryDetailService.checkCircularReference(id, *parent.ParentID) +} + +// updateChildrenLevelAndPath 更新子项的层级和路径 +func (dictionaryDetailService *DictionaryDetailService) updateChildrenLevelAndPath(parentID uint) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", parentID).Find(&children).Error + if err != nil { + return err + } + + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return err + } + + for _, child := range children { + child.Level = parent.Level + 1 + if parent.Path == "" { + child.Path = strconv.Itoa(int(parent.ID)) + } else { + child.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + + err = global.GVA_DB.Save(&child).Error + if err != nil { + return err + } + + // 递归更新子项的子项 + err = dictionaryDetailService.updateChildrenLevelAndPath(child.ID) + if err != nil { + return err + } + } + + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetail +//@description: 根据id获取字典详情单条数据 +//@param: id uint +//@return: sysDictionaryDetail system.SysDictionaryDetail, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetail(id uint) (sysDictionaryDetail system.SysDictionaryDetail, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysDictionaryDetail).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetailInfoList +//@description: 分页获取字典详情列表 +//@param: info request.SysDictionaryDetailSearch +//@return: list interface{}, total int64, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetailInfoList(info request.SysDictionaryDetailSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}) + var sysDictionaryDetails []system.SysDictionaryDetail + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Label != "" { + db = db.Where("label LIKE ?", "%"+info.Label+"%") + } + if info.Value != "" { + db = db.Where("value = ?", info.Value) + } + if info.Status != nil { + db = db.Where("status = ?", info.Status) + } + if info.SysDictionaryID != 0 { + db = db.Where("sys_dictionary_id = ?", info.SysDictionaryID) + } + if info.ParentID != nil { + db = db.Where("parent_id = ?", *info.ParentID) + } + if info.Level != nil { + db = db.Where("level = ?", *info.Level) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("sort").Order("id").Find(&sysDictionaryDetails).Error + return sysDictionaryDetails, total, err +} + +// 按照字典id获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + err = global.GVA_DB.Find(&sysDictionaryDetails, "sys_dictionary_id = ?", dictionaryID).Error + return sysDictionaryDetails, err +} + +// GetDictionaryTreeList 获取字典树形结构列表 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + // 只获取顶级项目(parent_id为空) + err = global.GVA_DB.Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + +// loadChildren 递归加载子项 +func (dictionaryDetailService *DictionaryDetailService) loadChildren(detail *system.SysDictionaryDetail) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", detail.ID).Order("sort").Find(&children).Error + if err != nil { + return err + } + + for i := range children { + // 设置disabled属性:当status为false时,disabled为true + if children[i].Status != nil { + children[i].Disabled = !*children[i].Status + } else { + children[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&children[i]) + if err != nil { + return err + } + } + + detail.Children = children + return nil +} + +// GetDictionaryDetailsByParent 根据父级ID获取字典详情 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryDetailsByParent(req request.GetDictionaryDetailsByParentRequest) (list []system.SysDictionaryDetail, err error) { + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("sys_dictionary_id = ?", req.SysDictionaryID) + + if req.ParentID != nil { + db = db.Where("parent_id = ?", *req.ParentID) + } else { + db = db.Where("parent_id IS NULL") + } + + err = db.Order("sort").Find(&list).Error + if err != nil { + return list, err + } + + // 设置disabled属性 + for i := range list { + if list[i].Status != nil { + list[i].Disabled = !*list[i].Status + } else { + list[i].Disabled = false // 默认不禁用 + } + } + + // 如果需要包含子级数据,使用递归方式加载所有层级的子项 + if req.IncludeChildren { + for i := range list { + err = dictionaryDetailService.loadChildren(&list[i]) + if err != nil { + return list, err + } + } + } + + return list, err +} + +// 按照字典type获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.Find(&sysDictionaryDetails, "type = ?", t).Error + return sysDictionaryDetails, err +} + +// GetDictionaryTreeListByType 根据字典类型获取树形结构 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}). + Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id"). + Where("sys_dictionaries.type = ? AND sys_dictionary_details.parent_id IS NULL", t). + Order("sys_dictionary_details.sort") + + err = db.Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + +// 按照字典id+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue(dictionaryID uint, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetail system.SysDictionaryDetail + err = global.GVA_DB.First(&sysDictionaryDetail, "sys_dictionary_id = ? and value = ?", dictionaryID, value).Error + return sysDictionaryDetail, err +} + +// 按照字典type+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeValue(t string, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetails system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error + return sysDictionaryDetails, err +} + +// GetDictionaryPath 获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPath(id uint) (path []system.SysDictionaryDetail, err error) { + var detail system.SysDictionaryDetail + err = global.GVA_DB.First(&detail, id).Error + if err != nil { + return nil, err + } + + path = append(path, detail) + + if detail.ParentID != nil { + parentPath, err := dictionaryDetailService.GetDictionaryPath(*detail.ParentID) + if err != nil { + return nil, err + } + path = append(parentPath, path...) + } + + return path, nil +} + +// GetDictionaryPathByValue 根据值获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPathByValue(dictionaryID uint, value string) (path []system.SysDictionaryDetail, err error) { + detail, err := dictionaryDetailService.GetDictionaryInfoByValue(dictionaryID, value) + if err != nil { + return nil, err + } + + return dictionaryDetailService.GetDictionaryPath(detail.ID) +} diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go new file mode 100644 index 0000000..065b624 --- /dev/null +++ b/server/service/system/sys_error.go @@ -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 +} diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go new file mode 100644 index 0000000..6f449c9 --- /dev/null +++ b/server/service/system/sys_export_template.go @@ -0,0 +1,724 @@ +package system + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "mime/multipart" + "net/url" + "strconv" + "strings" + "time" + + "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" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/xuri/excelize/v2" + "gorm.io/gorm" +) + +type SysExportTemplateService struct { +} + +var SysExportTemplateServiceApp = new(SysExportTemplateService) + +// CreateSysExportTemplate 创建导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) CreateSysExportTemplate(sysExportTemplate *system.SysExportTemplate) (err error) { + err = global.GVA_DB.Create(sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplate 删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + err = global.GVA_DB.Delete(&sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplateByIds 批量删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplateByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysExportTemplate{}, "id in ?", ids.Ids).Error + return err +} + +// UpdateSysExportTemplate 更新导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) UpdateSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + conditions := sysExportTemplate.Conditions + e := tx.Delete(&[]system.Condition{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.Conditions = nil + + joins := sysExportTemplate.JoinTemplate + e = tx.Delete(&[]system.JoinTemplate{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.JoinTemplate = nil + + e = tx.Updates(&sysExportTemplate).Error + if e != nil { + return e + } + if len(conditions) > 0 { + for i := range conditions { + conditions[i].ID = 0 + } + e = tx.Create(&conditions).Error + } + if len(joins) > 0 { + for i := range joins { + joins[i].ID = 0 + } + e = tx.Create(&joins).Error + } + return e + }) +} + +// GetSysExportTemplate 根据id获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplate(id uint) (sysExportTemplate system.SysExportTemplate, err error) { + err = global.GVA_DB.Where("id = ?", id).Preload("JoinTemplate").Preload("Conditions").First(&sysExportTemplate).Error + return +} + +// GetSysExportTemplateInfoList 分页获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateInfoList(info systemReq.SysExportTemplateSearch) (list []system.SysExportTemplate, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysExportTemplate{}) + var sysExportTemplates []system.SysExportTemplate + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.TableName != "" { + db = db.Where("table_name = ?", info.TableName) + } + if info.TemplateID != "" { + db = db.Where("template_id = ?", info.TemplateID) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysExportTemplates).Error + return sysExportTemplates, total, err +} + +// ExportExcel 导出Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) { + var params = values.Get("params") + paramsValues, err := url.ParseQuery(params) + if err != nil { + return nil, "", fmt.Errorf("解析 params 参数失败: %v", err) + } + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return nil, "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + selects := strings.Join(selectKeyFmt, ", ") + var tableMap []map[string]interface{} + db := global.GVA_DB + if template.DBName != "" { + db = global.MustGetGlobalDBByDBName(template.DBName) + } + + // 如果有自定义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] + } + } + + // 执行原生 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 { + 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 condition.Operator == "IN" || condition.Operator == "NOT IN" { + sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) + } + + 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) + } + } + } + // 通过参数传入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) + } + orderStr = orderStr + " " + checkOrderArr[1] + } + db = db.Order(orderStr) + } + + err = db.Debug().Find(&tableMap).Error + if err != nil { + return nil, "", err + } + } + + var rows [][]string + rows = append(rows, tableTitle) + for _, exTable := range tableMap { + var row []string + for _, column := range columns { + column = strings.ReplaceAll(column, "\"", "") + column = strings.ReplaceAll(column, "`", "") + if len(template.JoinTemplate) > 0 { + columnAs := strings.Split(column, " as ") + if len(columnAs) > 1 { + column = strings.TrimSpace(strings.Split(column, " as ")[1]) + } else { + columnArr := strings.Split(column, ".") + if len(columnArr) > 1 { + column = strings.Split(column, ".")[1] + } + } + } + // 需要对时间类型特殊处理 + if t, ok := exTable[column].(time.Time); ok { + row = append(row, t.Format("2006-01-02 15:04:05")) + } else { + row = append(row, fmt.Sprintf("%v", exTable[column])) + } + } + rows = append(rows, row) + } + for i, row := range rows { + 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) + } + + if sErr != nil { + return nil, "", sErr + } + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + 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) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + + columns, err := utils.GetJSONKeys(template.TemplateInfo) + + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + for _, key := range columns { + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + for i := range tableTitle { + fErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(i+1), 1), tableTitle[i]) + if fErr != nil { + return nil, "", fErr + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + return file, template.Name, nil +} + +// 辅助函数:检查表是否有deleted_at列 +func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool { + var count int64 + global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count) + return count > 0 +} + +// ImportExcel 导入Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return err + } + + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + f, err := excelize.OpenReader(src) + if err != nil { + return err + } + + rows, err := f.GetRows("Sheet1") + if err != nil { + return err + } + if len(rows) < 2 { + return errors.New("Excel data is not enough.\nIt should contain title row and data") + } + + var templateInfoMap = make(map[string]string) + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return err + } + + 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 { + if template.ImportSQL != "" { + return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items) + } + 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 { + n-- + columnName = string(rune('A'+n%26)) + columnName + n /= 26 + } + return columnName +} diff --git a/server/service/system/sys_initdb.go b/server/service/system/sys_initdb.go new file mode 100644 index 0000000..825e0ac --- /dev/null +++ b/server/service/system/sys_initdb.go @@ -0,0 +1,189 @@ +package system + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "gorm.io/gorm" + "sort" +) + +const ( + Mysql = "mysql" + Pgsql = "pgsql" + Sqlite = "sqlite" + Mssql = "mssql" + InitSuccess = "\n[%v] --> 初始数据成功!\n" + InitDataExist = "\n[%v] --> %v 的初始数据已存在!\n" + InitDataFailed = "\n[%v] --> %v 初始数据失败! \nerr: %+v\n" + InitDataSuccess = "\n[%v] --> %v 初始数据成功!\n" +) + +const ( + InitOrderSystem = 10 + InitOrderInternal = 1000 + InitOrderExternal = 100000 +) + +var ( + ErrMissingDBContext = errors.New("missing db in context") + ErrMissingDependentContext = errors.New("missing dependent value in context") + ErrDBTypeMismatch = errors.New("db type mismatch") +) + +// SubInitializer 提供 source/*/init() 使用的接口,每个 initializer 完成一个初始化过程 +type SubInitializer interface { + InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义 + MigrateTable(ctx context.Context) (next context.Context, err error) + InitializeData(ctx context.Context) (next context.Context, err error) + TableCreated(ctx context.Context) bool + DataInserted(ctx context.Context) bool +} + +// TypedDBInitHandler 执行传入的 initializer +type TypedDBInitHandler interface { + EnsureDB(ctx context.Context, conf *request.InitDB) (context.Context, error) // 建库,失败属于 fatal error,因此让它 panic + WriteConfig(ctx context.Context) error // 回写配置 + InitTables(ctx context.Context, inits initSlice) error // 建表 handler + InitData(ctx context.Context, inits initSlice) error // 建数据 handler +} + +// orderedInitializer 组合一个顺序字段,以供排序 +type orderedInitializer struct { + order int + SubInitializer +} + +// initSlice 供 initializer 排序依赖时使用 +type initSlice []*orderedInitializer + +var ( + initializers initSlice + cache map[string]*orderedInitializer +) + +// RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用 +func RegisterInit(order int, i SubInitializer) { + if initializers == nil { + initializers = initSlice{} + } + if cache == nil { + cache = map[string]*orderedInitializer{} + } + name := i.InitializerName() + if _, existed := cache[name]; existed { + panic(fmt.Sprintf("Name conflict on %s", name)) + } + ni := orderedInitializer{order, i} + initializers = append(initializers, &ni) + cache[name] = &ni +} + +/* ---- * service * ---- */ + +type InitDBService struct{} + +// InitDB 创建数据库并初始化 总入口 +func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) { + ctx := context.TODO() + ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword) + if len(initializers) == 0 { + return errors.New("无可用初始化过程,请检查初始化是否已执行完成") + } + sort.Sort(&initializers) // 保证有依赖的 initializer 排在后面执行 + // Note: 若 initializer 只有单一依赖,可以写为 B=A+1, C=A+1; 由于 BC 之间没有依赖关系,所以谁先谁后并不影响初始化 + // 若存在多个依赖,可以写为 C=A+B, D=A+B+C, E=A+1; + // C必然>A|B,因此在AB之后执行,D必然>A|B|C,因此在ABC后执行,而E只依赖A,顺序与CD无关,因此E与CD哪个先执行并不影响 + var initHandler TypedDBInitHandler + switch conf.DBType { + case "mysql": + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + case "pgsql": + initHandler = NewPgsqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "pgsql") + case "sqlite": + initHandler = NewSqliteInitHandler() + ctx = context.WithValue(ctx, "dbtype", "sqlite") + case "mssql": + initHandler = NewMssqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mssql") + default: + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + } + ctx, err = initHandler.EnsureDB(ctx, &conf) + if err != nil { + return err + } + + db := ctx.Value("db").(*gorm.DB) + global.GVA_DB = db + + if err = initHandler.InitTables(ctx, initializers); err != nil { + return err + } + if err = initHandler.InitData(ctx, initializers); err != nil { + return err + } + + if err = initHandler.WriteConfig(ctx); err != nil { + return err + } + initializers = initSlice{} + cache = map[string]*orderedInitializer{} + return nil +} + +// createDatabase 创建数据库( EnsureDB() 中调用 ) +func createDatabase(dsn string, driver string, createSql string) error { + db, err := sql.Open(driver, dsn) + if err != nil { + return err + } + defer func(db *sql.DB) { + err = db.Close() + if err != nil { + fmt.Println(err) + } + }(db) + if err = db.Ping(); err != nil { + return err + } + _, err = db.Exec(createSql) + return err +} + +// createTables 创建表(默认 dbInitHandler.initTables 行为) +func createTables(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.TableCreated(next) { + continue + } + if n, err := init.MigrateTable(next); err != nil { + return err + } else { + next = n + } + } + return nil +} + +/* -- sortable interface -- */ + +func (a initSlice) Len() int { + return len(a) +} + +func (a initSlice) Less(i, j int) bool { + return a[i].order < a[j].order +} + +func (a initSlice) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} diff --git a/server/service/system/sys_initdb_mssql.go b/server/service/system/sys_initdb_mssql.go new file mode 100644 index 0000000..ad8d37c --- /dev/null +++ b/server/service/system/sys_initdb_mssql.go @@ -0,0 +1,92 @@ +package system + +import ( + "context" + "errors" + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/google/uuid" + "github.com/gookit/color" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + "path/filepath" +) + +type MssqlInitHandler struct{} + +func NewMssqlInitHandler() *MssqlInitHandler { + return &MssqlInitHandler{} +} + +// WriteConfig mssql回写配置 +func (h MssqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mssql) + if !ok { + return errors.New("mssql config invalid") + } + global.GVA_CONFIG.System.DbType = "mssql" + global.GVA_CONFIG.Mssql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mssql +func (h MssqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mssql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMssqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MssqlEmptyDsn() + + mssqlConfig := sqlserver.Config{ + DSN: dsn, // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + + var db *gorm.DB + + if db, err = gorm.Open(sqlserver.New(mssqlConfig), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return nil, err + } + + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MssqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MssqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mssql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mssql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mssql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mssql) + return nil +} diff --git a/server/service/system/sys_initdb_mysql.go b/server/service/system/sys_initdb_mysql.go new file mode 100644 index 0000000..2ef64d2 --- /dev/null +++ b/server/service/system/sys_initdb_mysql.go @@ -0,0 +1,97 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/gookit/color" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/google/uuid" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type MysqlInitHandler struct{} + +func NewMysqlInitHandler() *MysqlInitHandler { + return &MysqlInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h MysqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mysql) + if !ok { + return errors.New("mysql config invalid") + } + global.GVA_CONFIG.System.DbType = "mysql" + global.GVA_CONFIG.Mysql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mysql +func (h MysqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mysql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMysqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MysqlEmptyDsn() + createSql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;", c.Dbname) + if err = createDatabase(dsn, "mysql", createSql); err != nil { + return nil, err + } // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(mysql.New(mysql.Config{ + DSN: c.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: true, // 根据版本自动配置 + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MysqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MysqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mysql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mysql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mysql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mysql) + return nil +} diff --git a/server/service/system/sys_initdb_pgsql.go b/server/service/system/sys_initdb_pgsql.go new file mode 100644 index 0000000..fa0a65a --- /dev/null +++ b/server/service/system/sys_initdb_pgsql.go @@ -0,0 +1,101 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/gookit/color" + + "github.com/flipped-aurora/gin-vue-admin/server/utils" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/google/uuid" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type PgsqlInitHandler struct{} + +func NewPgsqlInitHandler() *PgsqlInitHandler { + return &PgsqlInitHandler{} +} + +// WriteConfig pgsql 回写配置 +func (h PgsqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Pgsql) + if !ok { + return errors.New("postgresql config invalid") + } + global.GVA_CONFIG.System.DbType = "pgsql" + global.GVA_CONFIG.Pgsql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 pg +func (h PgsqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "pgsql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToPgsqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.PgsqlEmptyDsn() + var createSql string + if conf.Template != "" { + createSql = fmt.Sprintf("CREATE DATABASE %s WITH TEMPLATE %s;", c.Dbname, conf.Template) + } else { + createSql = fmt.Sprintf("CREATE DATABASE %s;", c.Dbname) + } + if err = createDatabase(dsn, "pgx", createSql); err != nil { + return nil, err + } // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(postgres.New(postgres.Config{ + DSN: c.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h PgsqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h PgsqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for i := 0; i < len(inits); i++ { + if inits[i].DataInserted(next) { + color.Info.Printf(InitDataExist, Pgsql, inits[i].InitializerName()) + continue + } + if n, err := inits[i].InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Pgsql, inits[i].InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Pgsql, inits[i].InitializerName()) + } + } + color.Info.Printf(InitSuccess, Pgsql) + return nil +} diff --git a/server/service/system/sys_initdb_sqlite.go b/server/service/system/sys_initdb_sqlite.go new file mode 100644 index 0000000..a102ad3 --- /dev/null +++ b/server/service/system/sys_initdb_sqlite.go @@ -0,0 +1,88 @@ +package system + +import ( + "context" + "errors" + "github.com/glebarez/sqlite" + "github.com/google/uuid" + "github.com/gookit/color" + "gorm.io/gorm" + "path/filepath" + + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +type SqliteInitHandler struct{} + +func NewSqliteInitHandler() *SqliteInitHandler { + return &SqliteInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h SqliteInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Sqlite) + if !ok { + return errors.New("sqlite config invalid") + } + global.GVA_CONFIG.System.DbType = "sqlite" + global.GVA_CONFIG.Sqlite = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 sqlite +func (h SqliteInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "sqlite" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToSqliteConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.SqliteEmptyDsn() + + var db *gorm.DB + if db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h SqliteInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h SqliteInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Sqlite, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Sqlite, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Sqlite, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Sqlite) + return nil +} diff --git a/server/service/system/sys_login_log.go b/server/service/system/sys_login_log.go new file mode 100644 index 0000000..79fee93 --- /dev/null +++ b/server/service/system/sys_login_log.go @@ -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 +} diff --git a/server/service/system/sys_menu.go b/server/service/system/sys_menu.go new file mode 100644 index 0000000..c2156ef --- /dev/null +++ b/server/service/system/sys_menu.go @@ -0,0 +1,390 @@ +package system + +import ( + "errors" + "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" + "gorm.io/gorm" + "strconv" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getMenuTreeMap +//@description: 获取路由总树map +//@param: authorityId string +//@return: treeMap map[string][]system.SysMenu, err error + +type MenuService struct{} + +var MenuServiceApp = new(MenuService) + +func (menuService *MenuService) getMenuTreeMap(authorityId uint) (treeMap map[uint][]system.SysMenu, err error) { + var allMenus []system.SysMenu + var baseMenu []system.SysBaseMenu + var btns []system.SysAuthorityBtn + treeMap = make(map[uint][]system.SysMenu) + + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?)", MenuIds).Order("sort").Preload("Parameters").Find(&baseMenu).Error + if err != nil { + return + } + + for i := range baseMenu { + allMenus = append(allMenus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: authorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + + err = global.GVA_DB.Where("authority_id = ?", authorityId).Preload("SysBaseMenuBtn").Find(&btns).Error + if err != nil { + return + } + var btnMap = make(map[uint]map[string]uint) + for _, v := range btns { + if btnMap[v.SysMenuID] == nil { + btnMap[v.SysMenuID] = make(map[string]uint) + } + btnMap[v.SysMenuID][v.SysBaseMenuBtn.Name] = authorityId + } + for _, v := range allMenus { + v.Btns = btnMap[v.SysBaseMenu.ID] + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuTree +//@description: 获取动态菜单树 +//@param: authorityId string +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuTree(authorityId uint) (menus []system.SysMenu, err error) { + menuTree, err := menuService.getMenuTreeMap(authorityId) + menus = menuTree[0] + for i := 0; i < len(menus); i++ { + err = menuService.getChildrenList(&menus[i], menuTree) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getChildrenList +//@description: 获取子菜单 +//@param: menu *model.SysMenu, treeMap map[string][]model.SysMenu +//@return: err error + +func (menuService *MenuService) getChildrenList(menu *system.SysMenu, treeMap map[uint][]system.SysMenu) (err error) { + menu.Children = treeMap[menu.MenuId] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetInfoList +//@description: 获取路由分页 +//@return: list interface{}, total int64,err error + +func (menuService *MenuService) GetInfoList(authorityID uint) (list interface{}, err error) { + var menuList []system.SysBaseMenu + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menuList = treeMap[0] + for i := 0; i < len(menuList); i++ { + err = menuService.getBaseChildrenList(&menuList[i], treeMap) + } + return menuList, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseChildrenList +//@description: 获取菜单的子菜单 +//@param: menu *model.SysBaseMenu, treeMap map[string][]model.SysBaseMenu +//@return: err error + +func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, treeMap map[uint][]system.SysBaseMenu) (err error) { + menu.Children = treeMap[menu.ID] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getBaseChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddBaseMenu +//@description: 添加基础路由 +//@param: menu model.SysBaseMenu +//@return: error + +func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 检查name是否重复 + if !errors.Is(tx.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复name,请修改name") + } + + if menu.ParentId != 0 { + // 检查父菜单是否存在 + var parentMenu system.SysBaseMenu + if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("父菜单不存在") + } + return err + } + + // 检查父菜单下现有子菜单数量 + var existingChildrenCount int64 + err := tx.Model(&system.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error + if err != nil { + return err + } + + // 如果父菜单原本是叶子菜单(没有子菜单),现在要变成枝干菜单,需要清空其权限分配 + if existingChildrenCount == 0 { + // 检查父菜单是否被其他角色设置为首页 + var defaultRouterCount int64 + err := tx.Model(&system.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error + if err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限") + } + + // 清空父菜单的所有权限分配 + err = tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&system.SysAuthorityMenu{}).Error + if err != nil { + return err + } + } + } + + // 创建菜单 + return tx.Create(&menu).Error + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseMenuTreeMap +//@description: 获取路由总树map +//@return: treeMap map[string][]system.SysBaseMenu, err error + +func (menuService *MenuService) getBaseMenuTreeMap(authorityID uint) (treeMap map[uint][]system.SysBaseMenu, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + + var allMenus []system.SysBaseMenu + treeMap = make(map[uint][]system.SysBaseMenu) + db := global.GVA_DB.Order("sort").Preload("MenuBtn").Preload("Parameters") + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && parentAuthorityID != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityID).Find(&authorityMenus).Error + if err != nil { + return nil, err + } + var menuIds []string + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + db = db.Where("id in (?)", menuIds) + } + + err = db.Find(&allMenus).Error + for _, v := range allMenus { + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuTree +//@description: 获取基础路由树 +//@return: menus []system.SysBaseMenu, err error + +func (menuService *MenuService) GetBaseMenuTree(authorityID uint) (menus []system.SysBaseMenu, err error) { + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menus = treeMap[0] + for i := 0; i < len(menus); i++ { + err = menuService.getBaseChildrenList(&menus[i], treeMap) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddMenuAuthority +//@description: 为角色增加menu树 +//@param: menus []model.SysBaseMenu, authorityId string +//@return: err error + +func (menuService *MenuService) AddMenuAuthority(menus []system.SysBaseMenu, adminAuthorityID, authorityId uint) (err error) { + var auth system.SysAuthority + auth.AuthorityId = authorityId + auth.SysBaseMenus = menus + + err = AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, authorityId) + if err != nil { + return err + } + + var authority system.SysAuthority + _ = global.GVA_DB.First(&authority, "authority_id = ?", adminAuthorityID).Error + var menuIds []string + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && *authority.ParentId != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", adminAuthorityID).Find(&authorityMenus).Error + if err != nil { + return err + } + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + + for i := range menus { + hasMenu := false + for j := range menuIds { + idStr := strconv.Itoa(int(menus[i].ID)) + if idStr == menuIds[j] { + hasMenu = true + } + } + if !hasMenu { + return errors.New("添加失败,请勿跨级操作") + } + } + } + + err = AuthorityServiceApp.SetMenuAuthority(&auth) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuAuthority +//@description: 查看当前角色树 +//@param: info *request.GetAuthorityId +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (menus []system.SysMenu, err error) { + var baseMenu []system.SysBaseMenu + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", info.AuthorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?) ", MenuIds).Order("sort").Find(&baseMenu).Error + + for i := range baseMenu { + menus = append(menus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: info.AuthorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + return menus, err +} + +// GetAuthoritiesByMenuId 获取拥有指定菜单的所有角色ID +func (menuService *MenuService) GetAuthoritiesByMenuId(menuId uint) (authorityIds []uint, err error) { + var records []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_base_menu_id = ?", menuId).Find(&records).Error + if err != nil { + return nil, err + } + for _, r := range records { + id, e := strconv.Atoi(r.AuthorityId) + if e == nil { + authorityIds = append(authorityIds, uint(id)) + } + } + return authorityIds, nil +} + +// GetDefaultRouterAuthorityIds 获取将指定菜单设为首页的角色ID列表 +func (menuService *MenuService) GetDefaultRouterAuthorityIds(menuId uint) (authorityIds []uint, err error) { + var menu system.SysBaseMenu + err = global.GVA_DB.First(&menu, menuId).Error + if err != nil { + return nil, err + } + var authorities []system.SysAuthority + err = global.GVA_DB.Where("default_router = ?", menu.Name).Find(&authorities).Error + if err != nil { + return nil, err + } + for _, auth := range authorities { + authorityIds = append(authorityIds, auth.AuthorityId) + } + return authorityIds, nil +} + +// SetMenuAuthorities 全量覆盖某菜单关联的角色列表 +func (menuService *MenuService) SetMenuAuthorities(menuId uint, authorityIds []uint) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 1. 删除该菜单所有已有的角色关联 + if err := tx.Where("sys_base_menu_id = ?", menuId).Delete(&system.SysAuthorityMenu{}).Error; err != nil { + return err + } + // 2. 批量插入新的关联记录 + if len(authorityIds) > 0 { + menuIdStr := strconv.Itoa(int(menuId)) + newRecords := make([]system.SysAuthorityMenu, 0, len(authorityIds)) + for _, authorityId := range authorityIds { + newRecords = append(newRecords, system.SysAuthorityMenu{ + MenuId: menuIdStr, + AuthorityId: strconv.Itoa(int(authorityId)), + }) + } + if err := tx.Create(&newRecords).Error; err != nil { + return err + } + } + return nil + }) +} + +// UserAuthorityDefaultRouter 用户角色默认路由检查 +// +// Author [SliverHorn](https://github.com/SliverHorn) +func (menuService *MenuService) UserAuthorityDefaultRouter(user *system.SysUser) { + var menuIds []string + err := global.GVA_DB.Model(&system.SysAuthorityMenu{}).Where("sys_authority_authority_id = ?", user.AuthorityId).Pluck("sys_base_menu_id", &menuIds).Error + if err != nil { + return + } + var am system.SysBaseMenu + err = global.GVA_DB.First(&am, "name = ? and id in (?)", user.Authority.DefaultRouter, menuIds).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + user.Authority.DefaultRouter = "404" + } +} diff --git a/server/service/system/sys_operation_record.go b/server/service/system/sys_operation_record.go new file mode 100644 index 0000000..ef131db --- /dev/null +++ b/server/service/system/sys_operation_record.go @@ -0,0 +1,83 @@ +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" +) + +//@author: [granty1](https://github.com/granty1) +//@function: CreateSysOperationRecord +//@description: 创建记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +type OperationRecordService struct{} + +var OperationRecordServiceApp = new(OperationRecordService) + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysOperationRecordByIds +//@description: 批量删除记录 +//@param: ids request.IdsReq +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecordByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysOperationRecord{}, "id in (?)", ids.Ids).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: DeleteSysOperationRecord +//@description: 删除操作记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { + err = global.GVA_DB.Delete(&sysOperationRecord).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: GetSysOperationRecord +//@description: 根据id获取单条操作记录 +//@param: id uint +//@return: sysOperationRecord system.SysOperationRecord, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecord(id uint) (sysOperationRecord system.SysOperationRecord, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysOperationRecord).Error + return +} + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysOperationRecordInfoList +//@description: 分页获取操作记录列表 +//@param: info systemReq.SysOperationRecordSearch +//@return: list interface{}, total int64, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecordInfoList(info systemReq.SysOperationRecordSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysOperationRecord{}) + var sysOperationRecords []system.SysOperationRecord + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Method != "" { + db = db.Where("method = ?", info.Method) + } + if info.Path != "" { + db = db.Where("path LIKE ?", "%"+info.Path+"%") + } + if info.Status != 0 { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Order("id desc").Limit(limit).Offset(offset).Preload("User").Find(&sysOperationRecords).Error + return sysOperationRecords, total, err +} diff --git a/server/service/system/sys_params.go b/server/service/system/sys_params.go new file mode 100644 index 0000000..7391ec0 --- /dev/null +++ b/server/service/system/sys_params.go @@ -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/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type SysParamsService struct{} + +// CreateSysParams 创建参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) { + err = global.GVA_DB.Create(sysParams).Error + return err +} + +// DeleteSysParams 删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error + return err +} + +// DeleteSysParamsByIds 批量删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error + return err +} + +// UpdateSysParams 更新参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) { + err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error + return err +} + +// GetSysParams 根据ID获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParams(ID string) (sysParams system.SysParams, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysParams).Error + return +} + +// GetSysParamsInfoList 分页获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParamsInfoList(info systemReq.SysParamsSearch) (list []system.SysParams, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysParams{}) + var sysParamss []system.SysParams + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.Key != "" { + db = db.Where("key LIKE ?", "%"+info.Key+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysParamss).Error + return sysParamss, total, err +} + +// GetSysParam 根据key获取参数value +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParam(key string) (param system.SysParams, err error) { + err = global.GVA_DB.Where(system.SysParams{Key: key}).First(¶m).Error + return +} diff --git a/server/service/system/sys_skills.go b/server/service/system/sys_skills.go new file mode 100644 index 0000000..c055dda --- /dev/null +++ b/server/service/system/sys_skills.go @@ -0,0 +1,785 @@ +package system + +import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "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) Delete(_ context.Context, req request.SkillDeleteRequest) error { + if strings.TrimSpace(req.Tool) == "" { + return errors.New("工具类型不能为空") + } + if !isSafeName(req.Skill) { + return errors.New("技能名称不合法") + } + skillDir, err := s.skillDir(req.Tool, req.Skill) + if err != nil { + return err + } + info, err := os.Stat(skillDir) + if err != nil { + if os.IsNotExist(err) { + return errors.New("技能不存在") + } + return err + } + if !info.IsDir() { + return errors.New("技能目录异常") + } + return os.RemoveAll(skillDir) +} + +func (s *SkillsService) Package(_ context.Context, req request.SkillPackageRequest) (string, []byte, error) { + if strings.TrimSpace(req.Tool) == "" { + return "", nil, errors.New("工具类型不能为空") + } + if !isSafeName(req.Skill) { + return "", nil, errors.New("技能名称不合法") + } + + skillDir, err := s.skillDir(req.Tool, req.Skill) + if err != nil { + return "", nil, err + } + info, err := os.Stat(skillDir) + if err != nil { + if os.IsNotExist(err) { + return "", nil, errors.New("技能不存在") + } + return "", nil, err + } + if !info.IsDir() { + return "", nil, errors.New("技能目录异常") + } + + buf := bytes.NewBuffer(nil) + zw := zip.NewWriter(buf) + + walkErr := filepath.WalkDir(skillDir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + rel, err := filepath.Rel(skillDir, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + zipName := filepath.ToSlash(rel) + if d.IsDir() { + _, err = zw.Create(strings.TrimSuffix(zipName, "/") + "/") + return err + } + + fileInfo, err := d.Info() + if err != nil { + return err + } + header, err := zip.FileInfoHeader(fileInfo) + if err != nil { + return err + } + header.Name = zipName + header.Method = zip.Deflate + + writer, err := zw.CreateHeader(header) + if err != nil { + return err + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + _, err = writer.Write(content) + return err + }) + if walkErr != nil { + _ = zw.Close() + return "", nil, walkErr + } + + if err = zw.Close(); err != nil { + return "", nil, err + } + + return req.Skill + ".zip", buf.Bytes(), 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) DownloadOnlineSkill(_ context.Context, req request.DownloadOnlineSkillReq) error { + skillsDir, err := s.toolSkillsDir(req.Tool) + if err != nil { + return err + } + + body, err := json.Marshal(map[string]interface{}{ + "plugin_id": req.ID, + "version": req.Version, + }) + if err != nil { + return fmt.Errorf("构建下载请求失败: %w", err) + } + + downloadReq, err := http.NewRequest(http.MethodPost, "https://plugin.gin-vue-admin.com/api/shopPlugin/downloadSkill", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("构建下载请求失败: %w", err) + } + downloadReq.Header.Set("Content-Type", "application/json") + + downloadResp, err := http.DefaultClient.Do(downloadReq) + if err != nil { + return fmt.Errorf("下载技能失败: %w", err) + } + defer downloadResp.Body.Close() + + if downloadResp.StatusCode != http.StatusOK { + return fmt.Errorf("下载技能失败, HTTP状态码: %d", downloadResp.StatusCode) + } + + metaBody, err := io.ReadAll(downloadResp.Body) + if err != nil { + return fmt.Errorf("读取下载结果失败: %w", err) + } + + var meta struct { + Data struct { + URL string `json:"url"` + } `json:"data"` + } + if err = json.Unmarshal(metaBody, &meta); err != nil { + return fmt.Errorf("解析下载结果失败: %w", err) + } + + realDownloadURL := strings.TrimSpace(meta.Data.URL) + if realDownloadURL == "" { + return errors.New("下载结果缺少 url") + } + + zipResp, err := http.Get(realDownloadURL) + if err != nil { + return fmt.Errorf("下载压缩包失败: %w", err) + } + defer zipResp.Body.Close() + + if zipResp.StatusCode != http.StatusOK { + return fmt.Errorf("下载压缩包失败, HTTP状态码: %d", zipResp.StatusCode) + } + + tmpFile, err := os.CreateTemp("", "gva-skill-*.zip") + if err != nil { + return fmt.Errorf("创建临时文件失败: %w", err) + } + tmpPath := tmpFile.Name() + defer os.Remove(tmpPath) + + if _, err = io.Copy(tmpFile, zipResp.Body); err != nil { + tmpFile.Close() + return fmt.Errorf("保存技能包失败: %w", err) + } + tmpFile.Close() + + if err = extractZipToDir(tmpPath, skillsDir); err != nil { + return fmt.Errorf("解压技能包失败: %w", err) + } + + return nil +} + +func extractZipToDir(zipPath, destDir string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + name := filepath.FromSlash(f.Name) + if strings.Contains(name, "..") { + continue + } + + target := filepath.Join(destDir, name) + if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)) { + continue + } + + if f.FileInfo().IsDir() { + if err := os.MkdirAll(target, os.ModePerm); err != nil { + return err + } + continue + } + + if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + rc.Close() + return err + } + + _, err = io.Copy(out, rc) + rc.Close() + out.Close() + if 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) + }) +} diff --git a/server/service/system/sys_system.go b/server/service/system/sys_system.go new file mode 100644 index 0000000..a4415f0 --- /dev/null +++ b/server/service/system/sys_system.go @@ -0,0 +1,62 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/config" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSystemConfig +//@description: 读取配置文件 +//@return: conf config.Server, err error + +type SystemConfigService struct{} + +var SystemConfigServiceApp = new(SystemConfigService) + +func (systemConfigService *SystemConfigService) GetSystemConfig() (conf config.Server, err error) { + return global.GVA_CONFIG, nil +} + +// @description set system config, +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSystemConfig +//@description: 设置配置文件 +//@param: system model.System +//@return: err error + +func (systemConfigService *SystemConfigService) SetSystemConfig(system system.System) (err error) { + cs := utils.StructToMap(system.Config) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + err = global.GVA_VP.WriteConfig() + return err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetServerInfo +//@description: 获取服务器信息 +//@return: server *utils.Server, err error + +func (systemConfigService *SystemConfigService) GetServerInfo() (server *utils.Server, err error) { + var s utils.Server + s.Os = utils.InitOS() + if s.Cpu, err = utils.InitCPU(); err != nil { + global.GVA_LOG.Error("func utils.InitCPU() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Ram, err = utils.InitRAM(); err != nil { + global.GVA_LOG.Error("func utils.InitRAM() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Disk, err = utils.InitDisk(); err != nil { + global.GVA_LOG.Error("func utils.InitDisk() Failed", zap.String("err", err.Error())) + return &s, err + } + + return &s, nil +} diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go new file mode 100644 index 0000000..c02906e --- /dev/null +++ b/server/service/system/sys_user.go @@ -0,0 +1,336 @@ +package system + +import ( + "errors" + "fmt" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + + "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/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Register +//@description: 用户注册 +//@param: u model.SysUser +//@return: userInter system.SysUser, err error + +type UserService struct{} + +var UserServiceApp = new(UserService) + +func (userService *UserService) Register(u system.SysUser) (userInter system.SysUser, err error) { + var user system.SysUser + if !errors.Is(global.GVA_DB.Where("username = ?", u.Username).First(&user).Error, gorm.ErrRecordNotFound) { // 判断用户名是否注册 + return userInter, errors.New("用户名已注册") + } + // 否则 附加uuid 密码hash加密 注册 + u.Password = utils.BcryptHash(u.Password) + u.UUID = uuid.New() + err = global.GVA_DB.Create(&u).Error + return u, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: Login +//@description: 用户登录 +//@param: u *model.SysUser +//@return: err error, userInter *model.SysUser + +func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysUser, err error) { + if nil == global.GVA_DB { + return nil, fmt.Errorf("db not init") + } + + var user system.SysUser + err = global.GVA_DB.Where("username = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error + if err == nil { + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return nil, errors.New("密码错误") + } + MenuServiceApp.UserAuthorityDefaultRouter(&user) + } + return &user, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ChangePassword +//@description: 修改用户密码 +//@param: u *model.SysUser, newPassword string +//@return: err error + +func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (err error) { + var user system.SysUser + err = global.GVA_DB.Select("id, password").Where("id = ?", u.ID).First(&user).Error + if err != nil { + return err + } + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return errors.New("原密码错误") + } + pwd := utils.BcryptHash(newPassword) + err = global.GVA_DB.Model(&user).Update("password", pwd).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetUserInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: err error, list interface{}, total int64 + +func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysUser{}) + var userList []system.SysUser + + if info.NickName != "" { + db = db.Where("nick_name LIKE ?", "%"+info.NickName+"%") + } + if info.Phone != "" { + db = db.Where("phone LIKE ?", "%"+info.Phone+"%") + } + if info.Username != "" { + db = db.Where("username LIKE ?", "%"+info.Username+"%") + } + if info.Email != "" { + db = db.Where("email LIKE ?", "%"+info.Email+"%") + } + + err = db.Count(&total).Error + if err != nil { + return + } + + orderStr := "id desc" + if info.OrderKey != "" { + allowedOrders := map[string]bool{ + "id": true, + "username": true, + "nick_name": true, + "phone": true, + "email": true, + } + if allowedOrders[info.OrderKey] { + orderStr = info.OrderKey + if info.Desc { + orderStr = info.OrderKey + " desc" + } + } + } + + err = db.Limit(limit).Offset(offset).Order(orderStr).Preload("Authorities").Preload("Authority").Find(&userList).Error + return userList, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthority +//@description: 设置一个用户的权限 +//@param: uuid uuid.UUID, authorityId string +//@return: err error + +func (userService *UserService) SetUserAuthority(id uint, authorityId uint) (err error) { + + assignErr := global.GVA_DB.Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityId).First(&system.SysUserAuthority{}).Error + if errors.Is(assignErr, gorm.ErrRecordNotFound) { + return errors.New("该用户无此角色") + } + + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityId).First(&authority).Error + if err != nil { + return err + } + var authorityMenu []system.SysAuthorityMenu + var authorityMenuIDs []string + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&authorityMenu).Error + if err != nil { + return err + } + + for i := range authorityMenu { + authorityMenuIDs = append(authorityMenuIDs, authorityMenu[i].MenuId) + } + + var authorityMenus []system.SysBaseMenu + err = global.GVA_DB.Preload("Parameters").Where("id in (?)", authorityMenuIDs).Find(&authorityMenus).Error + if err != nil { + return err + } + hasMenu := false + for i := range authorityMenus { + if authorityMenus[i].Name == authority.DefaultRouter { + hasMenu = true + break + } + } + if !hasMenu { + return errors.New("找不到默认路由,无法切换本角色") + } + + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", id).Update("authority_id", authorityId).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthorities +//@description: 设置一个用户的权限 +//@param: id uint, authorityIds []string +//@return: err error + +func (userService *UserService) SetUserAuthorities(adminAuthorityID, id uint, authorityIds []uint) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var user system.SysUser + TxErr := tx.Where("id = ?", id).First(&user).Error + if TxErr != nil { + global.GVA_LOG.Debug(TxErr.Error()) + return errors.New("查询用户数据失败") + } + TxErr = tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error + if TxErr != nil { + return TxErr + } + var useAuthority []system.SysUserAuthority + for _, v := range authorityIds { + e := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, v) + if e != nil { + return e + } + useAuthority = append(useAuthority, system.SysUserAuthority{ + SysUserId: id, SysAuthorityAuthorityId: v, + }) + } + TxErr = tx.Create(&useAuthority).Error + if TxErr != nil { + return TxErr + } + TxErr = tx.Model(&user).Update("authority_id", authorityIds[0]).Error + if TxErr != nil { + return TxErr + } + // 返回 nil 提交事务 + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteUser +//@description: 删除用户 +//@param: id float64 +//@return: err error + +func (userService *UserService) DeleteUser(id int) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("id = ?", id).Delete(&system.SysUser{}).Error; err != nil { + return err + } + if err := tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error; err != nil { + return err + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserInfo +//@description: 设置用户信息 +//@param: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetUserInfo(req system.SysUser) error { + return global.GVA_DB.Model(&system.SysUser{}). + Select("updated_at", "nick_name", "header_img", "phone", "email", "enable"). + Where("id=?", req.ID). + Updates(map[string]interface{}{ + "updated_at": time.Now(), + "nick_name": req.NickName, + "header_img": req.HeaderImg, + "phone": req.Phone, + "email": req.Email, + "enable": req.Enable, + }).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfInfo +//@description: 设置用户信息 +//@param: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetSelfInfo(req system.SysUser) error { + return global.GVA_DB.Model(&system.SysUser{}). + Where("id=?", req.ID). + Updates(req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfSetting +//@description: 设置用户配置 +//@param: req datatypes.JSON, uid uint +//@return: err error + +func (userService *UserService) SetSelfSetting(req common.JSONMap, uid uint) error { + return global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", uid).Update("origin_setting", req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetUserInfo +//@description: 获取用户信息 +//@param: uuid uuid.UUID +//@return: err error, user system.SysUser + +func (userService *UserService) GetUserInfo(uuid uuid.UUID) (user system.SysUser, err error) { + var reqUser system.SysUser + err = global.GVA_DB.Preload("Authorities").Preload("Authority").First(&reqUser, "uuid = ?", uuid).Error + if err != nil { + return reqUser, err + } + MenuServiceApp.UserAuthorityDefaultRouter(&reqUser) + return reqUser, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserById +//@description: 通过id获取用户信息 +//@param: id int +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserById(id int) (user *system.SysUser, err error) { + var u system.SysUser + err = global.GVA_DB.Where("id = ?", id).First(&u).Error + return &u, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserByUuid +//@description: 通过uuid获取用户信息 +//@param: uuid string +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserByUuid(uuid string) (user *system.SysUser, err error) { + var u system.SysUser + if err = global.GVA_DB.Where("uuid = ?", uuid).First(&u).Error; err != nil { + return &u, errors.New("用户不存在") + } + return &u, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ResetPassword +//@description: 修改用户密码 +//@param: ID uint +//@return: err error + +func (userService *UserService) ResetPassword(ID uint, password string) (err error) { + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash(password)).Error + return err +} diff --git a/server/service/system/sys_version.go b/server/service/system/sys_version.go new file mode 100644 index 0000000..68d5ae2 --- /dev/null +++ b/server/service/system/sys_version.go @@ -0,0 +1,230 @@ +package system + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "gorm.io/gorm" +) + +type SysVersionService struct{} + +// CreateSysVersion 创建版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) { + err = global.GVA_DB.Create(sysVersion).Error + return err +} + +// DeleteSysVersion 删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error + return err +} + +// DeleteSysVersionByIds 批量删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error + return err +} + +// GetSysVersion 根据ID获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error + return +} + +// GetSysVersionInfoList 分页获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysVersion{}) + var sysVersions []system.SysVersion + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.VersionName != nil && *info.VersionName != "" { + db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%") + } + if info.VersionCode != nil && *info.VersionCode != "" { + db = db.Where("version_code = ?", *info.VersionCode) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysVersions).Error + return sysVersions, total, err +} +func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} + +// GetMenusByIds 根据ID列表获取菜单数据 +func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + return +} + +// GetApisByIds 根据ID列表获取API数据 +func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) { + err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error + return +} + +// GetDictionariesByIds 根据ID列表获取字典数据 +func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error + return +} + +// ImportMenus 导入菜单数据 +func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 递归创建菜单 + return sysVersionService.createMenusRecursively(tx, menus, 0) + }) +} + +// createMenusRecursively 递归创建菜单 +func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error { + for _, menu := range menus { + // 检查菜单是否已存在 + var existingMenu system.SysBaseMenu + if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil { + // 菜单已存在,使用现有菜单ID继续处理子菜单 + if len(menu.Children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil { + return err + } + } + continue + } + + // 保存参数和按钮数据,稍后处理 + parameters := menu.Parameters + menuBtns := menu.MenuBtn + children := menu.Children + + // 创建新菜单(不包含关联数据) + newMenu := system.SysBaseMenu{ + ParentId: parentId, + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + if err := tx.Create(&newMenu).Error; err != nil { + return err + } + + // 创建参数 + if len(parameters) > 0 { + for _, param := range parameters { + newParam := system.SysBaseMenuParameter{ + SysBaseMenuID: newMenu.ID, + Type: param.Type, + Key: param.Key, + Value: param.Value, + } + if err := tx.Create(&newParam).Error; err != nil { + return err + } + } + } + + // 创建菜单按钮 + if len(menuBtns) > 0 { + for _, btn := range menuBtns { + newBtn := system.SysBaseMenuBtn{ + SysBaseMenuID: newMenu.ID, + Name: btn.Name, + Desc: btn.Desc, + } + if err := tx.Create(&newBtn).Error; err != nil { + return err + } + } + } + + // 递归处理子菜单 + if len(children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil { + return err + } + } + } + return nil +} + +// ImportApis 导入API数据 +func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + // 检查API是否已存在 + var existingApi system.SysApi + if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil { + // API已存在,跳过 + continue + } + + // 创建新API + newApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + + if err := tx.Create(&newApi).Error; err != nil { + return err + } + } + return nil + }) +} + +// ImportDictionaries 导入字典数据 +func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + // 检查字典是否已存在 + var existingDict system.SysDictionary + if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil { + // 字典已存在,跳过 + continue + } + + // 创建新字典 + newDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + SysDictionaryDetails: dict.SysDictionaryDetails, + } + + if err := tx.Create(&newDict).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/server/source/example/file_upload_download.go b/server/source/example/file_upload_download.go new file mode 100644 index 0000000..0f6f7a7 --- /dev/null +++ b/server/source/example/file_upload_download.go @@ -0,0 +1,65 @@ +package example + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/model/example" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderExaFile = system.InitOrderInternal + 1 + +type initExaFileMysql struct{} + +// auto run +func init() { + system.RegisterInit(initOrderExaFile, &initExaFileMysql{}) +} + +func (i *initExaFileMysql) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&example.ExaFileUploadAndDownload{}) +} + +func (i *initExaFileMysql) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&example.ExaFileUploadAndDownload{}) +} + +func (i *initExaFileMysql) InitializerName() string { + return example.ExaFileUploadAndDownload{}.TableName() +} + +func (i *initExaFileMysql) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []example.ExaFileUploadAndDownload{ + {Name: "10.png", Url: "https://qmplusimg.henrongyi.top/gvalogo.png", Tag: "png", Key: "158787308910.png"}, + {Name: "logo.png", Url: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png", Tag: "png", Key: "1587973709logo.png"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, example.ExaFileUploadAndDownload{}.TableName()+"表数据初始化失败!") + } + return ctx, nil +} + +func (i *initExaFileMysql) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + lookup := example.ExaFileUploadAndDownload{Name: "logo.png", Key: "1587973709logo.png"} + if errors.Is(db.First(&lookup, &lookup).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/api.go b/server/source/system/api.go new file mode 100644 index 0000000..a1330c3 --- /dev/null +++ b/server/source/system/api.go @@ -0,0 +1,265 @@ +package system + +import ( + "context" + + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApi struct{} + +const initOrderApi = system.InitOrderSystem + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApi, &initApi{}) +} + +func (i *initApi) InitializerName() string { + return sysModel.SysApi{}.TableName() +} + +func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysApi{}) +} + +func (i *initApi) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysApi{}) +} + +func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + 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: "获取用户列表"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setUserInfo", Description: "设置用户信息"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfInfo", Description: "设置自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "GET", Path: "/user/getUserInfo", Description: "获取自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthorities", Description: "设置权限组"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/changePassword", Description: "修改密码(建议选择)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthority", Description: "修改用户角色(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/resetPassword", Description: "重置用户密码"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfSetting", Description: "用户界面配置"}, + + {ApiGroup: "api", Method: "POST", Path: "/api/createApi", Description: "创建api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/deleteApi", Description: "删除Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/updateApi", Description: "更新Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiList", Description: "获取api列表"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getAllApis", Description: "获取所有api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiById", Description: "获取api详细信息"}, + {ApiGroup: "api", Method: "DELETE", Path: "/api/deleteApisByIds", Description: "批量删除api"}, + {ApiGroup: "api", Method: "GET", Path: "/api/syncApi", Description: "获取待同步API"}, + {ApiGroup: "api", Method: "GET", Path: "/api/getApiGroups", Description: "获取路由组"}, + {ApiGroup: "api", Method: "POST", Path: "/api/enterSyncApi", Description: "确认同步API"}, + {ApiGroup: "api", Method: "POST", Path: "/api/ignoreApi", Description: "忽略API"}, + + {ApiGroup: "角色", Method: "POST", Path: "/authority/copyAuthority", Description: "拷贝角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/createAuthority", Description: "创建角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/deleteAuthority", Description: "删除角色"}, + {ApiGroup: "角色", Method: "PUT", Path: "/authority/updateAuthority", Description: "更新角色信息"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/getAuthorityList", Description: "获取角色列表"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/setDataAuthority", Description: "设置角色资源权限"}, + {ApiGroup: "角色", Method: "GET", Path: "/authority/getUsersByAuthority", Description: "获取角色关联用户ID列表"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/setRoleUsers", Description: "全量覆盖角色关联用户"}, + + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/updateCasbin", Description: "更改角色api权限"}, + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/getPolicyPathByAuthorityId", Description: "获取权限列表"}, + + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addBaseMenu", Description: "新增菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenu", Description: "获取菜单树(必选)"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/deleteBaseMenu", Description: "删除菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/updateBaseMenu", Description: "更新菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuById", Description: "根据id获取菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuList", Description: "分页获取基础menu列表"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuTree", Description: "获取用户动态路由"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuAuthority", Description: "获取指定角色menu"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addMenuAuthority", Description: "增加menu和角色关联关系"}, + + {ApiGroup: "分片上传", Method: "GET", Path: "/fileUploadAndDownload/findFile", Description: "寻找目标文件(秒传)"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinue", Description: "断点续传"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinueFinish", Description: "断点续传完成"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/removeChunk", Description: "上传完成移除文件"}, + + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/upload", Description: "文件上传(建议选择)"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/deleteFile", Description: "删除文件"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/editFileName", Description: "文件名或者备注编辑"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/getFileList", Description: "获取上传文件列表"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/importURL", Description: "导入URL"}, + + {ApiGroup: "系统服务", Method: "POST", Path: "/system/getServerInfo", Description: "获取服务器信息"}, + {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/deleteSkill", 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: "skills", Method: "POST", Path: "/skills/packageSkill", Description: "打包技能"}, + + {ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"}, + {ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"}, + {ApiGroup: "客户", Method: "DELETE", Path: "/customer/customer", Description: "删除客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customer", Description: "获取单一客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customerList", Description: "获取客户列表"}, + + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getDB", Description: "获取所有数据库"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getTables", Description: "获取数据库表"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/createTemp", Description: "自动化代码"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/preview", Description: "预览自动化代码"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpStatus", Description: "获取 MCP 独立服务状态"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpStart", Description: "启动 MCP 独立服务"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpStop", Description: "停用 MCP 独立服务"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 管理"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/saveAIWorkflowSession", Description: "保存AI需求工作流会话"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/getAIWorkflowSessionList", Description: "获取AI需求工作流会话列表"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/getAIWorkflowSessionDetail", Description: "获取AI需求工作流会话详情"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/deleteAIWorkflowSession", Description: "删除AI需求工作流会话"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/dumpAIWorkflowMarkdown", Description: "AI需求工作流Markdown落盘"}, + + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/createPackage", Description: "配置模板"}, + {ApiGroup: "模板配置", Method: "GET", Path: "/autoCode/getTemplates", Description: "获取模板文件"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/getPackage", Description: "获取所有模板"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/delPackage", Description: "删除模板"}, + + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getMeta", Description: "获取meta信息"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/rollback", Description: "回滚自动生成代码"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getSysHistory", Description: "查询回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/delSysHistory", Description: "删除回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/addFunc", Description: "增加模板方法"}, + + {ApiGroup: "系统字典详情", Method: "PUT", Path: "/sysDictionaryDetail/updateSysDictionaryDetail", Description: "更新字典内容"}, + {ApiGroup: "系统字典详情", Method: "POST", Path: "/sysDictionaryDetail/createSysDictionaryDetail", Description: "新增字典内容"}, + {ApiGroup: "系统字典详情", Method: "DELETE", Path: "/sysDictionaryDetail/deleteSysDictionaryDetail", Description: "删除字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/findSysDictionaryDetail", Description: "根据ID获取字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getSysDictionaryDetailList", Description: "获取字典内容列表"}, + + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeList", Description: "获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeListByType", Description: "根据分类获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryDetailsByParent", Description: "根据父级ID获取字典详情"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryPath", Description: "获取字典详情的完整路径"}, + + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/createSysDictionary", Description: "新增字典"}, + {ApiGroup: "系统字典", Method: "DELETE", Path: "/sysDictionary/deleteSysDictionary", Description: "删除字典"}, + {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获取操作记录"}, + {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/getSysOperationRecordList", Description: "获取操作记录列表"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecord", Description: "删除操作记录"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecordByIds", Description: "批量删除操作历史"}, + + {ApiGroup: "断点续传(插件版)", Method: "POST", Path: "/simpleUploader/upload", Description: "插件版分片上传"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/checkFileMd5", Description: "文件完整度验证"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/mergeFileMd5", Description: "上传完成合并文件"}, + + {ApiGroup: "email", Method: "POST", Path: "/email/emailTest", Description: "发送测试邮件"}, + {ApiGroup: "email", Method: "POST", Path: "/email/sendEmail", Description: "发送邮件"}, + + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/setAuthorityBtn", Description: "设置按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/getAuthorityBtn", Description: "获取已有按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/canRemoveAuthorityBtn", Description: "删除按钮"}, + + {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/createSysExportTemplate", Description: "新增导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplate", Description: "删除导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplateByIds", Description: "批量删除导出模板"}, + {ApiGroup: "导出模板", Method: "PUT", Path: "/sysExportTemplate/updateSysExportTemplate", Description: "更新导出模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/findSysExportTemplate", Description: "根据ID获取导出模板"}, + {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: "/sysParams/createSysParams", Description: "新建参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParams", Description: "删除参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParamsByIds", Description: "批量删除参数"}, + {ApiGroup: "参数管理", Method: "PUT", Path: "/sysParams/updateSysParams", Description: "更新参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/findSysParams", Description: "根据ID获取参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParamsList", Description: "获取参数列表"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParam", Description: "获取参数列表"}, + {ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"}, + + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApi) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/authorityBtn/canRemoveAuthorityBtn", "POST"). + First(&sysModel.SysApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/api_ignore.go b/server/source/system/api_ignore.go new file mode 100644 index 0000000..e18aa7c --- /dev/null +++ b/server/source/system/api_ignore.go @@ -0,0 +1,78 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApiIgnore struct{} + +const initOrderApiIgnore = initOrderApi + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApiIgnore, &initApiIgnore{}) +} + +func (i *initApiIgnore) InitializerName() string { + return sysModel.SysIgnoreApi{}.TableName() +} + +func (i *initApiIgnore) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysIgnoreApi{ + {Method: "GET", Path: "/swagger/*any"}, + {Method: "GET", Path: "/api/freshCasbin"}, + {Method: "GET", Path: "/uploads/file/*filepath"}, + {Method: "GET", Path: "/health"}, + {Method: "HEAD", Path: "/uploads/file/*filepath"}, + {Method: "POST", Path: "/autoCode/llmAuto"}, + {Method: "POST", Path: "/autoCode/llmAutoSSE"}, + {Method: "POST", Path: "/system/reloadSystem"}, + {Method: "POST", Path: "/base/login"}, + {Method: "POST", Path: "/base/captcha"}, + {Method: "POST", Path: "/init/initdb"}, + {Method: "POST", Path: "/init/checkdb"}, + {Method: "GET", Path: "/info/getInfoDataSource"}, + {Method: "GET", Path: "/info/getInfoPublic"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysIgnoreApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApiIgnore) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/swagger/*any", "GET"). + First(&sysModel.SysIgnoreApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/authorities_menus.go b/server/source/system/authorities_menus.go new file mode 100644 index 0000000..9aa0f51 --- /dev/null +++ b/server/source/system/authorities_menus.go @@ -0,0 +1,121 @@ +package system + +import ( + "context" + + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenuAuthority = initOrderMenu + initOrderAuthority + +type initMenuAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenuAuthority, &initMenuAuthority{}) +} + +func (i *initMenuAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + return ctx, nil // do nothing +} + +func (i *initMenuAuthority) TableCreated(ctx context.Context) bool { + return false // always replace +} + +func (i *initMenuAuthority) InitializerName() string { + return "sys_menu_authorities" +} + +func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + initAuth := &initAuthority{} + authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") + } + + allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) + if !ok { + return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") + } + next = ctx + + // 构建菜单ID映射,方便快速查找 + menuMap := make(map[uint]sysModel.SysBaseMenu) + for _, menu := range allMenus { + menuMap[menu.ID] = menu + } + + // 为不同角色分配不同权限 + // 1. 超级管理员角色(888) - 拥有所有菜单权限 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil { + return next, errors.Wrap(err, "为超级管理员分配菜单失败") + } + + // 2. 普通用户角色(8881) - 仅拥有基础功能菜单 + // 仅选择部分父级菜单及其子菜单 + var menu8881 []sysModel.SysBaseMenu + + // 添加仪表盘、关于我们和个人信息菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") { + menu8881 = append(menu8881, menu) + } + } + + if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { + return next, errors.Wrap(err, "为普通用户分配菜单失败") + } + + // 3. 测试角色(9528) - 拥有部分菜单权限 + var menu9528 []sysModel.SysBaseMenu + + // 添加所有父级菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 { + menu9528 = append(menu9528, menu) + } + } + + // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单 + for _, menu := range allMenus { + parentName := "" + if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" { + parentName = menuMap[menu.ParentId].Name + } + + if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example" || parentName == "common") { + menu9528 = append(menu9528, menu) + } + } + + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil { + return next, errors.Wrap(err, "为测试角色分配菜单失败") + } + + return next, nil +} + +func (i *initMenuAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + auth := &sysModel.SysAuthority{} + if ret := db.Model(auth). + Where("authority_id = ?", 9528).Preload("SysBaseMenus").Find(auth); ret != nil { + if ret.Error != nil { + return false + } + return len(auth.SysBaseMenus) > 0 + } + return false +} diff --git a/server/source/system/authority.go b/server/source/system/authority.go new file mode 100644 index 0000000..1157a19 --- /dev/null +++ b/server/source/system/authority.go @@ -0,0 +1,88 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderAuthority = initOrderCasbin + 1 + +type initAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderAuthority, &initAuthority{}) +} + +func (i *initAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysAuthority{}) +} + +func (i *initAuthority) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysAuthority{}) +} + +func (i *initAuthority) InitializerName() string { + return sysModel.SysAuthority{}.TableName() +} + +func (i *initAuthority) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysAuthority{ + {AuthorityId: 888, AuthorityName: "普通用户", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 9528, AuthorityName: "测试角色", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 8881, AuthorityName: "普通用户子角色", ParentId: utils.Pointer[uint](888), DefaultRouter: "dashboard"}, + } + + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", sysModel.SysAuthority{}.TableName()) + } + // data authority + if err := db.Model(&entities[0]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 888}, + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[0]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + if err := db.Model(&entities[1]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[1]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("authority_id = ?", "8881"). + First(&sysModel.SysAuthority{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go new file mode 100644 index 0000000..370e1d7 --- /dev/null +++ b/server/source/system/casbin.go @@ -0,0 +1,360 @@ +package system + +import ( + "context" + + adapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderCasbin = initOrderApiIgnore + 1 + +type initCasbin struct{} + +// auto run +func init() { + system.RegisterInit(initOrderCasbin, &initCasbin{}) +} + +func (i *initCasbin) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&adapter.CasbinRule{}) +} + +func (i *initCasbin) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&adapter.CasbinRule{}) +} + +func (i *initCasbin) InitializerName() string { + var entity adapter.CasbinRule + return entity.TableName() +} + +func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + 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"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApisByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/api/syncApi", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/getApiGroups", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/enterSyncApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/ignoreApi", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/authority/copyAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/updateAuthority", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/setDataAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/getUsersByAuthority", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/authority/setRoleUsers", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuById", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/user/getUserInfo", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/user/setUserInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/deleteUser", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthorities", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/resetPassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfSetting", V2: "PUT"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/findFile", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinueFinish", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinue", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/removeChunk", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/system/getSystemConfig", V2: "POST"}, + {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/deleteSkill", 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: "/skills/packageSkill", 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"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/customer/customerList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/autoCode/getDB", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getMeta", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/preview", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTables", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getColumn", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/rollback", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTemplates", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpStatus", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpStart", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpStop", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/saveAIWorkflowSession", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getAIWorkflowSessionList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getAIWorkflowSessionDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/deleteAIWorkflowSession", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/dumpAIWorkflowMarkdown", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/createSysDictionaryDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getSysDictionaryDetailList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/deleteSysDictionaryDetail", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeListByType", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryDetailsByParent", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryPath", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionary/findSysDictionary", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/updateSysDictionary", V2: "PUT"}, + {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"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/createSysOperationRecord", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/getSysOperationRecordList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecord", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecordByIds", V2: "DELETE"}, + + {Ptype: "p", V0: "888", V1: "/email/emailTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/email/sendEmail", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/simpleUploader/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/checkFileMd5", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/mergeFileMd5", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/authorityBtn/setAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/getAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/canRemoveAuthorityBtn", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/createSysExportTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplate", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplateByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/updateSysExportTemplate", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/findSysExportTemplate", V2: "GET"}, + {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: "/sysParams/createSysParams", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParams", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParamsByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/updateSysParams", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysParams/findSysParams", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParamsList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParam", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/getCategoryList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysVersion/findSysVersion", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"}, + + {Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/setDataAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/getUsersByAuthority", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/authority/setRoleUsers", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserInfo", V2: "GET"}, + + {Ptype: "p", V0: "9528", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getAllApis", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/setDataAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/getUsersByAuthority", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/authority/setRoleUsers", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "9528", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/mcpStatus", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/mcpStart", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/mcpStop", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/saveAIWorkflowSession", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/getAIWorkflowSessionList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/getAIWorkflowSessionDetail", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/deleteAIWorkflowSession", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/dumpAIWorkflowMarkdown", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "Casbin 表 ("+i.InitializerName()+") 数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initCasbin) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where(adapter.CasbinRule{Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}). + First(&adapter.CasbinRule{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/dictionary.go b/server/source/system/dictionary.go new file mode 100644 index 0000000..a0cf8e0 --- /dev/null +++ b/server/source/system/dictionary.go @@ -0,0 +1,71 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDict = initOrderCasbin + 1 + +type initDict struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDict, &initDict{}) +} + +func (i *initDict) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionary{}) +} + +func (i *initDict) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionary{}) +} + +func (i *initDict) InitializerName() string { + return sysModel.SysDictionary{}.TableName() +} + +func (i *initDict) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + True := true + entities := []sysModel.SysDictionary{ + {Name: "性别", Type: "gender", Status: &True, Desc: "性别字典"}, + {Name: "数据库int类型", Type: "int", Status: &True, Desc: "int类型对应的数据库类型"}, + {Name: "数据库时间日期类型", Type: "time.Time", Status: &True, Desc: "数据库时间日期类型"}, + {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"}, + {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"}, + {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"}, + } + + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionary{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initDict) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("type = ?", "bool").First(&sysModel.SysDictionary{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/dictionary_detail.go b/server/source/system/dictionary_detail.go new file mode 100644 index 0000000..8a2bd2b --- /dev/null +++ b/server/source/system/dictionary_detail.go @@ -0,0 +1,121 @@ +package system + +import ( + "context" + "fmt" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDictDetail = initOrderDict + 1 + +type initDictDetail struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDictDetail, &initDictDetail{}) +} + +func (i *initDictDetail) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionaryDetail{}) +} + +func (i *initDictDetail) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionaryDetail{}) +} + +func (i *initDictDetail) InitializerName() string { + return sysModel.SysDictionaryDetail{}.TableName() +} + +func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + dicts, ok := ctx.Value(new(initDict).InitializerName()).([]sysModel.SysDictionary) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, + fmt.Sprintf("未找到 %s 表初始化数据", sysModel.SysDictionary{}.TableName())) + } + True := true + dicts[0].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "男", Value: "1", Status: &True, Sort: 1}, + {Label: "女", Value: "2", Status: &True, Sort: 2}, + } + + dicts[1].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "smallint", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "mediumint", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "int", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "bigint", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "int2", Value: "5", Status: &True, Extend: "pgsql", Sort: 5}, + {Label: "int4", Value: "6", Status: &True, Extend: "pgsql", Sort: 6}, + {Label: "int6", Value: "7", Status: &True, Extend: "pgsql", Sort: 7}, + {Label: "int8", Value: "8", Status: &True, Extend: "pgsql", Sort: 8}, + } + + dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "date", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "timestamp", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5}, + } + dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "float", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3}, + {Label: "smallserial", Value: "4", Status: &True, Extend: "pgsql", Sort: 4}, + } + + dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "char", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "text", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "blob", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "mediumblob", Value: "6", Status: &True, Extend: "mysql", Sort: 6}, + {Label: "mediumtext", Value: "7", Status: &True, Extend: "mysql", Sort: 7}, + {Label: "longblob", Value: "8", Status: &True, Extend: "mysql", Sort: 8}, + {Label: "longtext", Value: "9", Status: &True, Extend: "mysql", Sort: 9}, + } + + dicts[5].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "tinyint", Value: "1", Extend: "mysql", Status: &True}, + {Label: "bool", Value: "2", Extend: "pgsql", Status: &True}, + } + for _, dict := range dicts { + if err := db.Model(&dict).Association("SysDictionaryDetails"). + Replace(dict.SysDictionaryDetails); err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionaryDetail{}.TableName()+"表数据初始化失败!") + } + } + return ctx, nil +} + +func (i *initDictDetail) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var dict sysModel.SysDictionary + if err := db.Preload("SysDictionaryDetails"). + First(&dict, &sysModel.SysDictionary{Name: "数据库bool类型"}).Error; err != nil { + return false + } + return len(dict.SysDictionaryDetails) > 0 && dict.SysDictionaryDetails[0].Label == "tinyint" +} diff --git a/server/source/system/excel_template.go b/server/source/system/excel_template.go new file mode 100644 index 0000000..20bf16d --- /dev/null +++ b/server/source/system/excel_template.go @@ -0,0 +1,75 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initExcelTemplate struct{} + +const initOrderExcelTemplate = initOrderDictDetail + 1 + +// auto run +func init() { + system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{}) +} + +func (i *initExcelTemplate) InitializerName() string { + return "sys_export_templates" +} + +func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + entities := []sysModel.SysExportTemplate{ + { + Name: "api", + TableName: "sys_apis", + TemplateID: "api", + TemplateInfo: `{ +"path":"路径", +"method":"方法(大写)", +"description":"方法介绍", +"api_group":"方法分组" +}`, + }, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initExcelTemplate) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/menu.go b/server/source/system/menu.go new file mode 100644 index 0000000..814a1bf --- /dev/null +++ b/server/source/system/menu.go @@ -0,0 +1,135 @@ +package system + +import ( + "context" + + . "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenu = initOrderAuthority + 1 + +type initMenu struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenu, &initMenu{}) +} + +func (i *initMenu) InitializerName() string { + return SysBaseMenu{}.TableName() +} + +func (i *initMenu) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate( + &SysBaseMenu{}, + &SysBaseMenuParameter{}, + &SysBaseMenuBtn{}, + ) +} + +func (i *initMenu) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + m := db.Migrator() + return m.HasTable(&SysBaseMenu{}) && + m.HasTable(&SysBaseMenuParameter{}) && + m.HasTable(&SysBaseMenuBtn{}) +} + +func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + // 定义所有菜单 + allMenus := []SysBaseMenu{ + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}}, + {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "common", Name: "common", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "公共能力", Icon: "folder-opened"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "编程辅助", Icon: "tools"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, + } + + // 先创建父级菜单(ParentId = 0 的菜单) + if err = db.Create(&allMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!") + } + + // 建立菜单映射 - 通过Name查找已创建的菜单及其ID + menuNameMap := make(map[string]uint) + for _, menu := range allMenus { + menuNameMap[menu.Name] = menu.ID + } + + // 定义子菜单,并设置正确的ParentId + childMenus := []SysBaseMenu{ + // superAdmin子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 8, Meta: Meta{Title: "系统配置", Icon: "operation"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 9, Meta: Meta{Title: "API Token", Icon: "key"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 10, Meta: Meta{Title: "登录日志", Icon: "monitor"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 11, Meta: Meta{Title: "版本管理", Icon: "server"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 12, Meta: Meta{Title: "错误日志", Icon: "warn"}}, + + // common子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["common"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 1, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, + + // example子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, + + // systemTools子菜单 + {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: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, + {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: "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: "aiWorkflow", Name: "aiWorkflow", Component: "view/systemTools/aiWrokflow/index.vue", Sort: 4, Meta: Meta{Title: "AI需求工作流", Icon: "magic-stick", KeepAlive: true}}, + {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: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 6, Meta: Meta{Title: "Mcp Tools管理", Icon: "partly-cloudy"}}, + {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: "skills", Name: "skills", Component: "view/systemTools/skills/index.vue", Sort: 8, Meta: Meta{Title: "Skills管理", Icon: "document"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 9, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, + } + + // 创建子菜单 + if err = db.Create(&childMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!") + } + + // 组合所有菜单作为返回结果 + allEntities := append(allMenus, childMenus...) + next = context.WithValue(ctx, i.InitializerName(), allEntities) + return next, nil +} + +func (i *initMenu) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ?", "autoPkg").First(&SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/user.go b/server/source/system/user.go new file mode 100644 index 0000000..d5c20d5 --- /dev/null +++ b/server/source/system/user.go @@ -0,0 +1,106 @@ +package system + +import ( + "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/google/uuid" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderUser = initOrderAuthority + 1 + +type initUser struct{} + +// auto run +func init() { + system.RegisterInit(initOrderUser, &initUser{}) +} + +func (i *initUser) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysUser{}) +} + +func (i *initUser) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysUser{}) +} + +func (i *initUser) InitializerName() string { + return sysModel.SysUser{}.TableName() +} + +func (i *initUser) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + ap := ctx.Value("adminPassword") + apStr, ok := ap.(string) + if !ok { + apStr = "123456" + } + + password := utils.BcryptHash(apStr) + adminPassword := utils.BcryptHash(apStr) + + entities := []sysModel.SysUser{ + { + UUID: uuid.New(), + Username: "admin", + Password: adminPassword, + NickName: "Mr.奇淼", + HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg", + AuthorityId: 888, + Phone: "17611111111", + Email: "333333333@qq.com", + }, + { + UUID: uuid.New(), + Username: "a303176530", + Password: password, + NickName: "用户1", + HeaderImg: "https://qmplusimg.henrongyi.top/1572075907logo.png", + AuthorityId: 9528, + Phone: "17611111111", + Email: "333333333@qq.com"}, + } + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysUser{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + authorityEntities, ok := ctx.Value(new(initAuthority).InitializerName()).([]sysModel.SysAuthority) + if !ok { + return next, errors.Wrap(system.ErrMissingDependentContext, "创建 [用户-权限] 关联失败, 未找到权限表初始化数据") + } + if err = db.Model(&entities[0]).Association("Authorities").Replace(authorityEntities); err != nil { + return next, err + } + if err = db.Model(&entities[1]).Association("Authorities").Replace(authorityEntities[:1]); err != nil { + return next, err + } + return next, err +} + +func (i *initUser) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var record sysModel.SysUser + if errors.Is(db.Where("username = ?", "a303176530"). + Preload("Authorities").First(&record).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return len(record.Authorities) > 0 && record.Authorities[0].AuthorityId == 888 +} diff --git a/server/task/clearTable.go b/server/task/clearTable.go new file mode 100644 index 0000000..8d1e2f2 --- /dev/null +++ b/server/task/clearTable.go @@ -0,0 +1,51 @@ +package task + +import ( + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "time" + + "gorm.io/gorm" +) + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: ClearTable +//@description: 清理数据库表数据 +//@param: db(数据库对象) *gorm.DB, tableName(表名) string, compareField(比较字段) string, interval(间隔) string +//@return: error + +func ClearTable(db *gorm.DB) error { + var ClearTableDetail []common.ClearDB + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "sys_operation_records", + CompareField: "created_at", + Interval: "2160h", + }) + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "jwt_blacklists", + CompareField: "created_at", + Interval: "168h", + }) + + if db == nil { + return errors.New("db Cannot be empty") + } + + for _, detail := range ClearTableDetail { + duration, err := time.ParseDuration(detail.Interval) + if err != nil { + return err + } + if duration < 0 { + return errors.New("parse duration < 0") + } + err = db.Debug().Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", detail.TableName, detail.CompareField), time.Now().Add(-duration)).Error + if err != nil { + return err + } + } + return nil +} diff --git a/server/utils/ast/ast.go b/server/utils/ast/ast.go new file mode 100644 index 0000000..a2b4728 --- /dev/null +++ b/server/utils/ast/ast.go @@ -0,0 +1,409 @@ +package ast + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "go/ast" + "go/parser" + "go/token" + "log" +) + +// AddImport 增加 import 方法 +func AddImport(astNode ast.Node, imp string) { + impStr := fmt.Sprintf("\"%s\"", imp) + ast.Inspect(astNode, func(node ast.Node) bool { + if genDecl, ok := node.(*ast.GenDecl); ok { + if genDecl.Tok == token.IMPORT { + for i := range genDecl.Specs { + if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok { + if impNode.Path.Value == impStr { + return false + } + } + } + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: impStr, + }, + }) + } + } + return true + }) +} + +// FindFunction 查询特定function方法 +func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl { + var funcDeclP *ast.FuncDecl + ast.Inspect(astNode, func(node ast.Node) bool { + if funcDecl, ok := node.(*ast.FuncDecl); ok { + if funcDecl.Name.String() == FunctionName { + funcDeclP = funcDecl + return false + } + } + return true + }) + return funcDeclP +} + +// FindArray 查询特定数组方法 +func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit { + var assignStmt *ast.CompositeLit + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Rhs { + if exprType, ok := expr.(*ast.CompositeLit); ok { + if arrayType, ok := exprType.Type.(*ast.ArrayType); ok { + sel, ok1 := arrayType.Elt.(*ast.SelectorExpr) + x, ok2 := sel.X.(*ast.Ident) + if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName { + assignStmt = exprType + return false + } + } + } + } + } + return true + }) + return assignStmt +} + +func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr { + var menuElts []ast.Expr + for i := range menus { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ParentId"}, + Value: &ast.BasicLit{Kind: token.INT, Value: "0"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Hidden"}, + Value: &ast.Ident{Name: "false"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Component"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Component)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Sort"}, + Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", menus[i].Sort)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Meta"}, + Value: &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "Meta"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Title"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Title)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Icon"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Icon)}, + }, + }, + }, + }, + } + + // 添加菜单参数 + if len(menus[i].Parameters) > 0 { + var paramElts []ast.Expr + for _, param := range menus[i].Parameters { + paramElts = append(paramElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Type"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Type)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Key"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Key)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Value"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Value)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Parameters"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + }, + Elts: paramElts, + }, + }) + } + + // 添加菜单按钮 + if len(menus[i].MenuBtn) > 0 { + var btnElts []ast.Expr + for _, btn := range menus[i].MenuBtn { + btnElts = append(btnElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Desc"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Desc)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "MenuBtn"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + }, + Elts: btnElts, + }, + }) + } + + menuElts = append(menuElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &menuElts +} + +func CreateApiStructAst(apis []system.SysApi) *[]ast.Expr { + var apiElts []ast.Expr + for i := range apis { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Description"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Description)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ApiGroup"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].ApiGroup)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Method"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Method)}, + }, + } + apiElts = append(apiElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &apiElts +} + +// CheckImport 检查是否存在Import +func CheckImport(file *ast.File, importPath string) bool { + for _, imp := range file.Imports { + // Remove quotes around the import path + path := imp.Path.Value[1 : len(imp.Path.Value)-1] + + if path == importPath { + return true + } + } + + return false +} + +func clearPosition(astNode ast.Node) { + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.Ident: + // 清除位置信息 + node.NamePos = token.NoPos + case *ast.CallExpr: + // 清除位置信息 + node.Lparen = token.NoPos + node.Rparen = token.NoPos + case *ast.BasicLit: + // 清除位置信息 + node.ValuePos = token.NoPos + case *ast.SelectorExpr: + // 清除位置信息 + node.Sel.NamePos = token.NoPos + case *ast.BinaryExpr: + node.OpPos = token.NoPos + case *ast.UnaryExpr: + node.OpPos = token.NoPos + case *ast.StarExpr: + node.Star = token.NoPos + } + return true + }) +} + +func CreateStmt(statement string) *ast.ExprStmt { + expr, err := parser.ParseExpr(statement) + if err != nil { + log.Fatal(err) + } + clearPosition(expr) + return &ast.ExprStmt{X: expr} +} + +func IsBlockStmt(node ast.Node) bool { + _, ok := node.(*ast.BlockStmt) + return ok +} + +func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool { + exists := false + ast.Inspect(block, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Lhs { + if ident, ok := expr.(*ast.Ident); ok && ident.Name == varName { + exists = true + return false + } + } + } + return true + }) + 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 +} diff --git a/server/utils/ast/ast_auto_enter.go b/server/utils/ast/ast_auto_enter.go new file mode 100644 index 0000000..382f554 --- /dev/null +++ b/server/utils/ast/ast_auto_enter.go @@ -0,0 +1,47 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +func ImportForAutoEnter(path string, funcName string, code string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + ast.Inspect(astFile, func(node ast.Node) bool { + if typeSpec, ok := node.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == funcName { + if st, ok := typeSpec.Type.(*ast.StructType); ok { + for i := range st.Fields.List { + if t, ok := st.Fields.List[i].Type.(*ast.Ident); ok { + if t.Name == code { + return false + } + } + } + sn := &ast.Field{ + Type: &ast.Ident{Name: code}, + } + st.Fields.List = append(st.Fields.List, sn) + } + } + } + return true + }) + var out []byte + bf := bytes.NewBuffer(out) + err = printer.Fprint(bf, fileSet, astFile) + if err != nil { + return + } + _ = os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/server/utils/ast/ast_enter.go b/server/utils/ast/ast_enter.go new file mode 100644 index 0000000..7a5c727 --- /dev/null +++ b/server/utils/ast/ast_enter.go @@ -0,0 +1,181 @@ +package ast + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "log" + "os" + "strconv" + "strings" +) + +type Visitor struct { + ImportCode string + StructName string + PackageName string + GroupName string +} + +func (vi *Visitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.GenDecl: + // 查找有没有import context包 + // Notice:没有考虑没有import任何包的情况 + if n.Tok == token.IMPORT && vi.ImportCode != "" { + vi.addImport(n) + // 不需要再遍历子树 + return nil + } + if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" { + vi.addStruct(n) + return nil + } + case *ast.FuncDecl: + if n.Name.Name == "Routers" { + vi.addFuncBodyVar(n) + return nil + } + + } + return vi +} + +func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor { + for i := range genDecl.Specs { + switch n := genDecl.Specs[i].(type) { + case *ast.TypeSpec: + if strings.Index(n.Name.Name, "Group") > -1 { + switch t := n.Type.(type) { + case *ast.StructType: + f := &ast.Field{ + Names: []*ast.Ident{ + { + Name: vi.StructName, + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.StructName, + }, + }, + }, + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: vi.PackageName, + }, + Sel: &ast.Ident{ + Name: vi.GroupName, + }, + }, + } + t.Fields.List = append(t.Fields.List, f) + } + } + } + } + return vi +} + +func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor { + // 是否已经import + hasImported := false + for _, v := range genDecl.Specs { + importSpec := v.(*ast.ImportSpec) + // 如果已经包含 + if importSpec.Path.Value == strconv.Quote(vi.ImportCode) { + hasImported = true + } + } + if !hasImported { + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: strconv.Quote(vi.ImportCode), + }, + }) + } + return vi +} + +func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor { + hasVar := false + for _, v := range funDecl.Body.List { + switch varSpec := v.(type) { + case *ast.AssignStmt: + for i := range varSpec.Lhs { + switch nn := varSpec.Lhs[i].(type) { + case *ast.Ident: + if nn.Name == vi.PackageName+"Router" { + hasVar = true + } + } + } + } + } + if !hasVar { + assignStmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: vi.PackageName + "Router", + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.PackageName + "Router", + }, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "router", + }, + Sel: &ast.Ident{ + Name: "RouterGroupApp", + }, + }, + Sel: &ast.Ident{ + Name: cases.Title(language.English).String(vi.PackageName), + }, + }, + }, + } + funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1]) + index := 1 + copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:]) + funDecl.Body.List[index] = assignStmt + } + return vi +} + +func ImportReference(filepath, importCode, structName, packageName, groupName string) error { + fSet := token.NewFileSet() + fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments) + if err != nil { + return err + } + importCode = strings.TrimSpace(importCode) + v := &Visitor{ + ImportCode: importCode, + StructName: structName, + PackageName: packageName, + GroupName: groupName, + } + if importCode == "" { + ast.Print(fSet, fParser) + } + + ast.Walk(v, fParser) + + var output []byte + buffer := bytes.NewBuffer(output) + err = format.Node(buffer, fSet, fParser) + if err != nil { + log.Fatal(err) + } + // 写回数据 + return os.WriteFile(filepath, buffer.Bytes(), 0o600) +} diff --git a/server/utils/ast/ast_gorm.go b/server/utils/ast/ast_gorm.go new file mode 100644 index 0000000..4340721 --- /dev/null +++ b/server/utils/ast/ast_gorm.go @@ -0,0 +1,166 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +// AddRegisterTablesAst 自动为 gorm.go 注册一个自动迁移 +func AddRegisterTablesAst(path, funcName, pk, varName, dbName, model string) { + modelPk := fmt.Sprintf("github.com/flipped-aurora/gin-vue-admin/server/model/%s", pk) + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + AddImport(astFile, modelPk) + FuncNode := FindFunction(astFile, funcName) + if FuncNode != nil { + ast.Print(fileSet, FuncNode) + } + addDBVar(FuncNode.Body, varName, dbName) + addAutoMigrate(FuncNode.Body, varName, pk, model) + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(path, bf.Bytes(), 0666) +} + +// 增加一个 db库变量 +func addDBVar(astBody *ast.BlockStmt, varName, dbName string) { + if dbName == "" { + return + } + dbStr := fmt.Sprintf("\"%s\"", dbName) + + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if ident.Name == varName { + return + } + } + } + } + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: varName, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: dbStr, + }, + }, + }, + }, + } + astBody.List = append([]ast.Stmt{assignNode}, astBody.List...) +} + +// 为db库变量增加 AutoMigrate 方法 +func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) { + if dbname == "" { + dbname = "db" + } + flag := true + ast.Inspect(astBody, func(node ast.Node) bool { + // 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑 + switch n := node.(type) { + case *ast.CallExpr: + // 判断是否找到了AutoMigrate语句 + if s, ok := n.Fun.(*ast.SelectorExpr); ok { + if x, ok := s.X.(*ast.Ident); ok { + if s.Sel.Name == "AutoMigrate" && x.Name == dbname { + flag = false + if !NeedAppendModel(n, pk, model) { + return false + } + // 判断已经找到了AutoMigrate语句 + n.Args = append(n.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }) + return false + } + } + } + } + return true + //然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条 + }) + + if flag { + exprStmt := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: dbname, + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + Args: []ast.Expr{ + &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }, + }, + }} + astBody.List = append(astBody.List, exprStmt) + } +} + +// NeedAppendModel 为automigrate增加实参 +func NeedAppendModel(callNode ast.Node, pk string, model string) bool { + flag := true + ast.Inspect(callNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.SelectorExpr: + if x, ok := n.X.(*ast.Ident); ok { + if n.Sel.Name == model && x.Name == pk { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/server/utils/ast/ast_init_test.go b/server/utils/ast/ast_init_test.go new file mode 100644 index 0000000..ec6bd9b --- /dev/null +++ b/server/utils/ast/ast_init_test.go @@ -0,0 +1,11 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" +) + +func init() { + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../") + global.GVA_CONFIG.AutoCode.Server = "server" +} diff --git a/server/utils/ast/ast_rollback.go b/server/utils/ast/ast_rollback.go new file mode 100644 index 0000000..daa8422 --- /dev/null +++ b/server/utils/ast/ast_rollback.go @@ -0,0 +1,173 @@ +package ast + +import ( + "bytes" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" +) + +func RollBackAst(pk, model string) { + RollGormBack(pk, model) + RollRouterBack(pk, model) +} + +func RollGormBack(pk, model string) { + + // 首先分析存在多少个ttt作为调用方的node块 + // 如果多个 仅仅删除对应块即可 + // 如果单个 那么还需要剔除import + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + var n *ast.CallExpr + var k int = -1 + var pkNum = 0 + ast.Inspect(astFile, func(node ast.Node) bool { + if node, ok := node.(*ast.CallExpr); ok { + for i := range node.Args { + pkOK := false + modelOK := false + ast.Inspect(node.Args[i], func(item ast.Node) bool { + if ii, ok := item.(*ast.Ident); ok { + if ii.Name == pk { + pkOK = true + pkNum++ + } + if ii.Name == model { + modelOK = true + } + } + if pkOK && modelOK { + n = node + k = i + } + return true + }) + } + } + return true + }) + if k > -1 { + n.Args = append(append([]ast.Expr{}, n.Args[:k]...), n.Args[k+1:]...) + } + if pkNum == 1 { + var imI int = -1 + var gp *ast.GenDecl + ast.Inspect(astFile, func(node ast.Node) bool { + if gen, ok := node.(*ast.GenDecl); ok { + for i := range gen.Specs { + if imspec, ok := gen.Specs[i].(*ast.ImportSpec); ok { + if imspec.Path.Value == "\"github.com/flipped-aurora/gin-vue-admin/server/model/"+pk+"\"" { + gp = gen + imI = i + return false + } + } + } + } + return true + }) + + if imI > -1 { + gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...) + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) + +} + +func RollRouterBack(pk, model string) { + + // 首先抓到所有的代码块结构 {} + // 分析结构中是否存在一个变量叫做 pk+Router + // 然后获取到代码块指针 对内部需要回滚的代码进行剔除 + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + + var block *ast.BlockStmt + var routerStmt *ast.FuncDecl + + ast.Inspect(astFile, func(node ast.Node) bool { + if n, ok := node.(*ast.FuncDecl); ok { + if n.Name.Name == "initBizRouter" { + routerStmt = n + } + } + + if n, ok := node.(*ast.BlockStmt); ok { + ast.Inspect(n, func(bNode ast.Node) bool { + if in, ok := bNode.(*ast.Ident); ok { + if in.Name == pk+"Router" { + block = n + return false + } + } + return true + }) + return true + } + return true + }) + var k int + for i := range block.List { + if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok { + ast.Inspect(stmtNode, func(node ast.Node) bool { + if n, ok := node.(*ast.Ident); ok { + if n.Name == "Init"+model+"Router" { + k = i + return false + } + } + return true + }) + } + } + + block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...) + + if len(block.List) == 1 { + // 说明这个块就没任何意义了 + block.List = nil + } + + for i, n := range routerStmt.Body.List { + if n, ok := n.(*ast.BlockStmt); ok { + if n.List == nil { + routerStmt.Body.List = append(append([]ast.Stmt{}, routerStmt.Body.List[:i]...), routerStmt.Body.List[i+1:]...) + i-- + } + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/server/utils/ast/ast_router.go b/server/utils/ast/ast_router.go new file mode 100644 index 0000000..86356b8 --- /dev/null +++ b/server/utils/ast/ast_router.go @@ -0,0 +1,135 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "strings" +) + +func AppendNodeToList(stmts []ast.Stmt, stmt ast.Stmt, index int) []ast.Stmt { + return append(stmts[:index], append([]ast.Stmt{stmt}, stmts[index:]...)...) +} + +func AddRouterCode(path, funcName, pk, model string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, parser.ParseComments) + + if err != nil { + fmt.Println(err) + } + + FuncNode := FindFunction(astFile, funcName) + + pkName := strings.ToUpper(pk[:1]) + pk[1:] + routerName := fmt.Sprintf("%sRouter", pk) + modelName := fmt.Sprintf("Init%sRouter", model) + var bloctPre *ast.BlockStmt + for i := len(FuncNode.Body.List) - 1; i >= 0; i-- { + if block, ok := FuncNode.Body.List[i].(*ast.BlockStmt); ok { + bloctPre = block + } + } + ast.Print(fileSet, FuncNode) + if ok, b := needAppendRouter(FuncNode, pk); ok { + routerNode := + &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: routerName}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: "router"}, + Sel: &ast.Ident{Name: "RouterGroupApp"}, + }, + Sel: &ast.Ident{Name: pkName}, + }, + }, + }, + }, + } + + FuncNode.Body.List = AppendNodeToList(FuncNode.Body.List, routerNode, len(FuncNode.Body.List)-1) + bloctPre = routerNode + } else { + bloctPre = b + } + + if needAppendInit(FuncNode, routerName, modelName) { + bloctPre.List = append(bloctPre.List, + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: routerName}, + Sel: &ast.Ident{Name: modelName}, + }, + Args: []ast.Expr{ + &ast.Ident{ + Name: "privateGroup", + }, + &ast.Ident{ + Name: "publicGroup", + }, + }, + }, + }) + } + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.WriteFile(path, bf.Bytes(), 0666) +} + +func needAppendRouter(funcNode ast.Node, pk string) (bool, *ast.BlockStmt) { + flag := true + var block *ast.BlockStmt + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.BlockStmt: + for i := range n.List { + if assignNode, ok := n.List[i].(*ast.AssignStmt); ok { + if identNode, ok := assignNode.Lhs[0].(*ast.Ident); ok { + if identNode.Name == fmt.Sprintf("%sRouter", pk) { + flag = false + block = n + return false + } + } + } + } + + } + return true + }) + return flag, block +} + +func needAppendInit(funcNode ast.Node, routerName string, modelName string) bool { + flag := true + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := funcNode.(type) { + case *ast.CallExpr: + if selectNode, ok := n.Fun.(*ast.SelectorExpr); ok { + x, xok := selectNode.X.(*ast.Ident) + if xok && x.Name == routerName && selectNode.Sel.Name == modelName { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/server/utils/ast/ast_test.go b/server/utils/ast/ast_test.go new file mode 100644 index 0000000..001f530 --- /dev/null +++ b/server/utils/ast/ast_test.go @@ -0,0 +1,32 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" + "testing" +) + +func TestAst(t *testing.T) { + filename := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go") + fileSet := token.NewFileSet() + file, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) + if err != nil { + t.Error(err) + return + } + err = ast.Print(fileSet, file) + if err != nil { + t.Error(err) + return + } + err = printer.Fprint(os.Stdout, token.NewFileSet(), file) + if err != nil { + panic(err) + } + +} diff --git a/server/utils/ast/ast_type.go b/server/utils/ast/ast_type.go new file mode 100644 index 0000000..43285c9 --- /dev/null +++ b/server/utils/ast/ast_type.go @@ -0,0 +1,53 @@ +package ast + +type Type string + +func (r Type) String() string { + return string(r) +} + +func (r Type) Group() string { + switch r { + case TypePackageApiEnter: + return "ApiGroup" + case TypePackageRouterEnter: + return "RouterGroup" + case TypePackageServiceEnter: + return "ServiceGroup" + case TypePackageApiModuleEnter: + return "ApiGroup" + case TypePackageRouterModuleEnter: + return "RouterGroup" + case TypePackageServiceModuleEnter: + return "ServiceGroup" + case TypePluginApiEnter: + return "api" + case TypePluginRouterEnter: + return "router" + case TypePluginServiceEnter: + return "service" + default: + return "" + } +} + +const ( + TypePackageApiEnter = "PackageApiEnter" // server/api/v1/enter.go + TypePackageRouterEnter = "PackageRouterEnter" // server/router/enter.go + TypePackageServiceEnter = "PackageServiceEnter" // server/service/enter.go + TypePackageApiModuleEnter = "PackageApiModuleEnter" // server/api/v1/{package}/enter.go + TypePackageRouterModuleEnter = "PackageRouterModuleEnter" // server/router/{package}/enter.go + TypePackageServiceModuleEnter = "PackageServiceModuleEnter" // server/service/{package}/enter.go + TypePackageInitializeGorm = "PackageInitializeGorm" // server/initialize/gorm_biz.go + TypePackageInitializeRouter = "PackageInitializeRouter" // server/initialize/router_biz.go + 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/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 + TypePluginInitializeGorm = "PluginInitializeGorm" // server/plugin/{package}/initialize/gorm.go + TypePluginInitializeMenu = "PluginInitializeMenu" // server/plugin/{package}/initialize/menu.go + TypePluginInitializeRouter = "PluginInitializeRouter" // server/plugin/{package}/initialize/router.go +) diff --git a/server/utils/ast/extract_func.go b/server/utils/ast/extract_func.go new file mode 100644 index 0000000..4b880c1 --- /dev/null +++ b/server/utils/ast/extract_func.go @@ -0,0 +1,62 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" +) + +// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码 +// 返回:方法名、完整源码、起止行号 +func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) { + // 读取源文件 + src, readErr := os.ReadFile(filePath) + if readErr != nil { + err = fmt.Errorf("read file failed: %w", readErr) + return + } + + // 解析 AST + fset := token.NewFileSet() + file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments) + if parseErr != nil { + err = fmt.Errorf("parse file failed: %w", parseErr) + return + } + + // 在 AST 中定位包含指定行号的函数声明 + var target *ast.FuncDecl + ast.Inspect(file, func(n ast.Node) bool { + fd, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + s := fset.Position(fd.Pos()).Line + e := fset.Position(fd.End()).Line + if line >= s && line <= e { + target = fd + startLine = s + endLine = e + return false + } + return true + }) + + if target == nil { + err = fmt.Errorf("no function encloses line %d in %s", line, filePath) + return + } + + // 使用字节偏移精确提取源码片段(包含注释与原始格式) + start := fset.Position(target.Pos()).Offset + end := fset.Position(target.End()).Offset + if start < 0 || end > len(src) || start >= end { + err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src)) + return + } + source = string(src[start:end]) + name = target.Name.Name + return +} \ No newline at end of file diff --git a/server/utils/ast/import.go b/server/utils/ast/import.go new file mode 100644 index 0000000..5de18a3 --- /dev/null +++ b/server/utils/ast/import.go @@ -0,0 +1,94 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" + "strings" +) + +type Import struct { + Base + ImportPath string // 导包路径 +} + +func NewImport(importPath string) *Import { + return &Import{ImportPath: importPath} +} + +func (a *Import) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + return a.Base.Parse(filename, writer) +} + +func (a *Import) Rollback(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + if len(v1.Specs) == 0 { + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } // 如果没有import声明,就删除, 如果不删除则会出现import() + break + } + } + } + } + return nil +} + +func (a *Import) Injection(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + var has bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + has = true + break + } + } + if !has { + spec := &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + } + v1.Specs = append(v1.Specs, spec) + return nil + } + } + } + if !has { + decls := file.Decls + file.Decls = make([]ast.Decl, 0, len(file.Decls)+1) + decl := &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{ + &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + }, + }, + } + file.Decls = append(file.Decls, decl) + file.Decls = append(file.Decls, decls...) + } // 如果没有import声明,就创建一个, 主要要放在第一个 + return nil +} + +func (a *Import) Format(filename string, writer io.Writer, file *ast.File) error { + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/interfaces.go b/server/utils/ast/interfaces.go new file mode 100644 index 0000000..33ecc47 --- /dev/null +++ b/server/utils/ast/interfaces.go @@ -0,0 +1,17 @@ +package ast + +import ( + "go/ast" + "io" +) + +type Ast interface { + // Parse 解析文件/代码 + Parse(filename string, writer io.Writer) (file *ast.File, err error) + // Rollback 回滚 + Rollback(file *ast.File) error + // Injection 注入 + Injection(file *ast.File) error + // Format 格式化输出 + Format(filename string, writer io.Writer, file *ast.File) error +} diff --git a/server/utils/ast/interfaces_base.go b/server/utils/ast/interfaces_base.go new file mode 100644 index 0000000..9c80874 --- /dev/null +++ b/server/utils/ast/interfaces_base.go @@ -0,0 +1,81 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/pkg/errors" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + "path" + "path/filepath" + "strings" +) + +type Base struct { + FileSet *token.FileSet +} + +func (a *Base) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + a.FileSet = token.NewFileSet() + if writer != nil { + file, err = parser.ParseFile(a.FileSet, filename, nil, parser.ParseComments) + } else { + file, err = parser.ParseFile(a.FileSet, filename, writer, parser.ParseComments) + } + if err != nil { + return nil, errors.Wrapf(err, "[filepath:%s]打开/解析文件失败!", filename) + } + return file, nil +} + +func (a *Base) Rollback(file *ast.File) error { + return nil +} + +func (a *Base) Injection(file *ast.File) error { + return nil +} + +func (a *Base) Format(filename string, writer io.Writer, file *ast.File) error { + fileSet := a.FileSet + if fileSet == nil { + fileSet = token.NewFileSet() + } + if writer == nil { + open, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, 0666) + defer open.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]打开文件失败!", filename) + } + writer = open + } + err := format.Node(writer, fileSet, file) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]注入失败!", filename) + } + return nil +} + +// RelativePath 绝对路径转相对路径 +func (a *Base) RelativePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + hasServer := strings.Index(filePath, server) + if hasServer != -1 { + filePath = strings.TrimPrefix(filePath, server) + keys := strings.Split(filePath, string(filepath.Separator)) + filePath = path.Join(keys...) + } + return filePath +} + +// AbsolutePath 相对路径转绝对路径 +func (a *Base) AbsolutePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(filePath, "/") + filePath = filepath.Join(keys...) + filePath = filepath.Join(server, filePath) + return filePath +} diff --git a/server/utils/ast/package_enter.go b/server/utils/ast/package_enter.go new file mode 100644 index 0000000..f4b6305 --- /dev/null +++ b/server/utils/ast/package_enter.go @@ -0,0 +1,85 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageEnter 模块化入口 +type PackageEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + PackageStructName string // 包结构体名称 +} + +func (a *PackageEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageEnter) Rollback(file *ast.File) error { + // 无需回滚 + return nil +} + +func (a *PackageEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + return true + } + + for _, spec := range genDecl.Specs { + typeSpec, specok := spec.(*ast.TypeSpec) + if !specok || typeSpec.Name.Name != a.Type.Group() { + continue + } + + structType, structTypeOK := typeSpec.Type.(*ast.StructType) + if !structTypeOK { + continue + } + + for _, field := range structType.Fields.List { + if len(field.Names) == 1 && field.Names[0].Name == a.StructName { + return true + } + } + + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.PackageStructName}, + }, + } + structType.Fields.List = append(structType.Fields.List, field) + return false + } + + return true + }) + return nil +} + +func (a *PackageEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/package_enter_test.go b/server/utils/ast/package_enter_test.go new file mode 100644 index 0000000..3cf4ab4 --- /dev/null +++ b/server/utils/ast/package_enter_test.go @@ -0,0 +1,154 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup回滚", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleRouterGroup回滚", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup回滚", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup注入", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + }, + { + name: "测试ExampleRouterGroup注入", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup注入", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_initialize_gorm.go b/server/utils/ast/package_initialize_gorm.go new file mode 100644 index 0000000..594f714 --- /dev/null +++ b/server/utils/ast/package_initialize_gorm.go @@ -0,0 +1,196 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeGorm 包初始化gorm +type PackageInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + Business string // 业务库 gva => gva, 不要传"gva" + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PackageInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeGorm) Rollback(file *ast.File) error { + packageNameNum := 0 + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 删除结构体参数 + for i := 0; i < len(callExpr.Args); i++ { + if com, comok := callExpr.Args[i].(*ast.CompositeLit); comok { + if selector, exprok := com.Type.(*ast.SelectorExpr); exprok { + if x, identok := selector.X.(*ast.Ident); identok { + if x.Name == a.PackageName { + packageNameNum++ + if selector.Sel.Name == a.StructName { + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + i-- + } + } + } + } + } + } + return true + }) + + if packageNameNum == 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + return nil +} + +func (a *PackageInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + bizModelDecl := FindFunction(file, "bizModel") + if bizModelDecl != nil { + a.addDbVar(bizModelDecl.Body) + } + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 添加结构体参数 + callExpr.Args = append(callExpr.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: ast.NewIdent(a.PackageName), + Sel: ast.NewIdent(a.StructName), + }, + }) + return true + }) + return nil +} + +func (a *PackageInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +// 创建businessDB变量 +func (a *PackageInitializeGorm) addDbVar(astBody *ast.BlockStmt) { + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if (a.Business == "" && ident.Name == "db") || ident.Name == a.Business+"Db" { + return + } + } + } + } + + // 添加 businessDb := global.GetGlobalDBByDBName("business") 变量 + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: a.Business + "Db", + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: fmt.Sprintf("\"%s\"", a.Business), + }, + }, + }, + }, + } + + // 添加 businessDb.AutoMigrate() 方法 + autoMigrateCall := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: a.Business + "Db", + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + }, + } + + returnNode := astBody.List[len(astBody.List)-1] + astBody.List = append(astBody.List[:len(astBody.List)-1], assignNode, autoMigrateCall, returnNode) +} diff --git a/server/utils/ast/package_initialize_gorm_test.go b/server/utils/ast/package_initialize_gorm_test.go new file mode 100644 index 0000000..af5cef9 --- /dev/null +++ b/server/utils/ast/package_initialize_gorm_test.go @@ -0,0 +1,171 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_initialize_router.go b/server/utils/ast/package_initialize_router.go new file mode 100644 index 0000000..9fe4429 --- /dev/null +++ b/server/utils/ast/package_initialize_router.go @@ -0,0 +1,150 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeRouter 包初始化路由 +// ModuleName := PackageName.AppName.GroupName +// ModuleName.FunctionName(RouterGroupName) +type PackageInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + FunctionName string // 函数名 + RouterGroupName string // 路由分组名称 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PackageInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + exprNum := 0 + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + for ii, stmt := range funcDecl.Body.List[i].(*ast.BlockStmt).List { + // 检查语句是否为 *ast.ExprStmt + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + // 检查表达式是否为 *ast.CallExpr + callExpr, ok := exprStmt.X.(*ast.CallExpr) + if !ok { + continue + } + // 检查是否调用了我们正在寻找的函数 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + // 检查调用的函数是否为 systemRouter.InitApiRouter + ident, ok := selExpr.X.(*ast.Ident) + //只要存在调用则+1 + if ok && ident.Name == a.ModuleName { + exprNum++ + } + //判断是否为目标结构 + if !ok || ident.Name != a.ModuleName || selExpr.Sel.Name != a.FunctionName { + continue + } + exprNum-- + // 从语句列表中移除。 + funcDecl.Body.List[i].(*ast.BlockStmt).List = append(funcDecl.Body.List[i].(*ast.BlockStmt).List[:ii], funcDecl.Body.List[i].(*ast.BlockStmt).List[ii+1:]...) + // 如果不再存在任何调用,则删除导入和变量。 + if exprNum == 0 { + funcDecl.Body.List = append(funcDecl.Body.List[:i], funcDecl.Body.List[i+1:]...) + } + break + } + break + } + } + } + + return nil +} + +func (a *PackageInitializeRouter) Injection(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + hasRouter := false + var varBlock *ast.BlockStmt + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + hasRouter = true + varBlock = funcDecl.Body.List[i].(*ast.BlockStmt) + break + } + } + } + if !hasRouter { + stmt := a.CreateAssignStmt() + varBlock = &ast.BlockStmt{ + List: []ast.Stmt{ + stmt, + }, + } + } + routerStmt := CreateStmt(fmt.Sprintf("%s.%s(%s,%s)", a.ModuleName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName)) + varBlock.List = append(varBlock.List, routerStmt) + if !hasRouter { + funcDecl.Body.List = append(funcDecl.Body.List, varBlock) + } + return nil +} + +func (a *PackageInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +func (a *PackageInitializeRouter) CreateAssignStmt() *ast.AssignStmt { + //创建左侧变量 + ident := &ast.Ident{ + Name: a.ModuleName, + } + + //创建右侧的赋值语句 + selector := &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + } + + // 创建一个组合的赋值语句 + stmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ident}, + Tok: token.DEFINE, + Rhs: []ast.Expr{selector}, + } + + return stmt +} diff --git a/server/utils/ast/package_initialize_router_test.go b/server/utils/ast/package_initialize_router_test.go new file mode 100644 index 0000000..5a23dbb --- /dev/null +++ b/server/utils/ast/package_initialize_router_test.go @@ -0,0 +1,158 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 InitCustomerRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + + { + name: "测试 InitCustomerRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_module_enter.go b/server/utils/ast/package_module_enter.go new file mode 100644 index 0000000..881fb3f --- /dev/null +++ b/server/utils/ast/package_module_enter.go @@ -0,0 +1,180 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageModuleEnter 模块化入口 +// ModuleName := PackageName.AppName.GroupName.ServiceName +type PackageModuleEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PackageModuleEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageModuleEnter) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + v3.Fields.List = append(v3.Fields.List[:k], v3.Fields.List[k+1:]...) + } + } + } + continue + } + if a.Type == TypePackageServiceModuleEnter { + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + if i == len(file.Decls) { + file.Decls = append(file.Decls[:i-1]) + break + } // 空的var(), 如果不删除则会影响的注入变量, 因为识别不到*ast.ValueSpec + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } + } + } + } + return nil +} + +func (a *PackageModuleEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var hasValue bool + var hasVariables bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok == token.VAR { + hasVariables = true + } + for j := 0; j < len(v1.Specs); j++ { + if a.Type == TypePackageServiceModuleEnter { + hasValue = true + } + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + var hasStruct bool + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + hasStruct = true + } + } + if !hasStruct { + field := &ast.Field{Type: &ast.Ident{Name: a.StructName}} + v3.Fields.List = append(v3.Fields.List, field) + } + } + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + hasVariables = true + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + hasValue = true + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + hasVariables = false + } // 说明是空var() + if hasVariables && !hasValue { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + v1.Specs = append(v1.Specs, spec) + hasValue = true + } + } + } + } + if !hasValue && !hasVariables { + decl := &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{ + &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + }, + }, + } + file.Decls = append(file.Decls, decl) + } + return nil +} + +func (a *PackageModuleEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/package_module_enter_test.go b/server/utils/ast/package_module_enter_test.go new file mode 100644 index 0000000..0015e35 --- /dev/null +++ b/server/utils/ast/package_module_enter_test.go @@ -0,0 +1,185 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPackageModuleEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 回滚", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 回滚", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageModuleEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "github.com/flipped-aurora/gin-vue-admin/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 注入", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 注入", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_enter.go b/server/utils/ast/plugin_enter.go new file mode 100644 index 0000000..df5bba4 --- /dev/null +++ b/server/utils/ast/plugin_enter.go @@ -0,0 +1,167 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PluginEnter 插件化入口 +// ModuleName := PackageName.GroupName.ServiceName +type PluginEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + StructCamelName string // 结构体小驼峰名称 + ModuleName string // 模块名称 + GroupName string // 分组名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PluginEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginEnter) Rollback(file *ast.File) error { + //回滚结构体内内容 + var structType *ast.StructType + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + structType = s + for i, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + s.Fields.List = append(s.Fields.List[:i], s.Fields.List[i+1:]...) + return false + } + } + } + } + return true + }) + + if len(structType.Fields.List) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //回滚变量内容 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for i, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + genDecl.Specs = append(genDecl.Specs[:i], genDecl.Specs[i+1:]...) + return false + } + } + } + } + } + return true + }) + + return nil +} + +func (a *PluginEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + + has := false + hasVar := false + var firstStruct *ast.StructType + var varSpec *ast.GenDecl + //寻找是否存在结构且定位 + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + firstStruct = s + for _, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + has = true + return false + } + } + } + } + return true + }) + + if !has { + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.Ident{Name: a.StructCamelName}, + } + firstStruct.Fields.List = append(firstStruct.Fields.List, field) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //寻找是否存在变量且定位 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for _, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + varSpec = genDecl + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + hasVar = true + return false + } + } + } + } + } + return true + }) + + if !hasVar { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + varSpec.Specs = append(varSpec.Specs, spec) + } + + return nil +} + +func (a *PluginEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_enter_test.go b/server/utils/ast/plugin_enter_test.go new file mode 100644 index 0000000..60b8dfc --- /dev/null +++ b/server/utils/ast/plugin_enter_test.go @@ -0,0 +1,200 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserApi 注入", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserRouter 注入", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + { + name: "测试 gva的User 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserRouter 回滚", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserApi 回滚", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 回滚", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_gen.go b/server/utils/ast/plugin_gen.go new file mode 100644 index 0000000..ed7d04f --- /dev/null +++ b/server/utils/ast/plugin_gen.go @@ -0,0 +1,189 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +type PluginGen struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 +} + +func (a *PluginGen) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} +func (a *PluginGen) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + } + if k >= len(v3.Args) { + break + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + if len(v3.Args) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + var has bool + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + if !has { + if a.IsNew { + arg := &ast.CallExpr{ + Fun: &ast.Ident{Name: "\n\t\tnew"}, + Args: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + }, + } + v3.Args = append(v3.Args, arg) + v3.Args = append(v3.Args, &ast.BasicLit{ + Kind: token.STRING, + Value: "\n", + }) + break + } + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + v3.Args = append(v3.Args, arg) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_gen_test.go b/server/utils/ast/plugin_gen_test.go new file mode 100644 index 0000000..1b9c790 --- /dev/null +++ b/server/utils/ast/plugin_gen_test.go @@ -0,0 +1,127 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginGenModel_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginGenModel_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_gorm.go b/server/utils/ast/plugin_initialize_gorm.go new file mode 100644 index 0000000..e342251 --- /dev/null +++ b/server/utils/ast/plugin_initialize_gorm.go @@ -0,0 +1,111 @@ +package ast + +import ( + "go/ast" + "io" +) + +type PluginInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PluginInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeGorm) Rollback(file *ast.File) error { + var needRollBackImport bool + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, seok := callExpr.Fun.(*ast.SelectorExpr) + if !seok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + if len(callExpr.Args) <= 1 { + needRollBackImport = true + } + // 删除指定的参数 + for i, arg := range callExpr.Args { + compLit, cok := arg.(*ast.CompositeLit) + if !cok { + continue + } + + cselExpr, sok := compLit.Type.(*ast.SelectorExpr) + if !sok { + continue + } + + ident, idok := cselExpr.X.(*ast.Ident) + if idok && ident.Name == a.PackageName && cselExpr.Sel.Name == a.StructName { + // 删除参数 + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + break + } + } + + return true + }) + + if needRollBackImport { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var call *ast.CallExpr + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if ok && selExpr.Sel.Name == "AutoMigrate" { + call = callExpr + return false + } + + return true + }) + + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + + call.Args = append(call.Args, arg) + return nil +} + +func (a *PluginInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_gorm_test.go b/server/utils/ast/plugin_initialize_gorm_test.go new file mode 100644 index 0000000..ebfc12a --- /dev/null +++ b/server/utils/ast/plugin_initialize_gorm_test.go @@ -0,0 +1,138 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + { + name: "测试 new(model.SysUsers) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "SysUser", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_router.go b/server/utils/ast/plugin_initialize_router.go new file mode 100644 index 0000000..6550789 --- /dev/null +++ b/server/utils/ast/plugin_initialize_router.go @@ -0,0 +1,124 @@ +package ast + +import ( + "fmt" + "go/ast" + "io" +) + +// PluginInitializeRouter 插件初始化路由 +// PackageName.AppName.GroupName.FunctionName() +type PluginInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + ImportGlobalPath string // 导包全局变量路径 + ImportMiddlewarePath string // 导包中间件路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + PackageName string // 包名 + FunctionName string // 函数名 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PluginInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "Router") + delI := 0 + routerNum := 0 + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + stmt, ok := funcDecl.Body.List[i].(*ast.ExprStmt) + if !ok { + continue + } + + callExpr, ok := stmt.X.(*ast.CallExpr) + if !ok { + continue + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + + if ok { + if iExpr, ieok := ident.X.(*ast.SelectorExpr); ieok { + if iden, idok := iExpr.X.(*ast.Ident); idok { + if iden.Name == "router" { + routerNum++ + } + } + } + if ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + // 删除语句 + delI = i + } + } + } + + funcDecl.Body.List = append(funcDecl.Body.List[:delI], funcDecl.Body.List[delI+1:]...) + + if routerNum <= 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeRouter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + funcDecl := FindFunction(file, "Router") + + var exists bool + + ast.Inspect(funcDecl, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + if ok && ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + exists = true + return false + } + return true + }) + + if !exists { + stmtStr := fmt.Sprintf("%s.%s.%s.%s(%s, %s)", a.PackageName, a.AppName, a.GroupName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName) + stmt := CreateStmt(stmtStr) + funcDecl.Body.List = append(funcDecl.Body.List, stmt) + } + return nil +} + +func (a *PluginInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_router_test.go b/server/utils/ast/plugin_initialize_router_test.go new file mode 100644 index 0000000..4dffd7f --- /dev/null +++ b/server/utils/ast/plugin_initialize_router_test.go @@ -0,0 +1,155 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 回滚", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"github.com/flipped-aurora/gin-vue-admin/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_v2.go b/server/utils/ast/plugin_initialize_v2.go new file mode 100644 index 0000000..974f513 --- /dev/null +++ b/server/utils/ast/plugin_initialize_v2.go @@ -0,0 +1,82 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" + "strconv" + "strings" +) + +type PluginInitializeV2 struct { + Base + Type Type // 类型 + Path string // 文件路径 + PluginPath string // 插件路径 + RelativePath string // 相对路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 +} + +func (a *PluginInitializeV2) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.PluginPath + a.RelativePath = a.Base.RelativePath(a.PluginPath) + return a.Base.Parse(filename, writer) + } + a.PluginPath = a.Base.AbsolutePath(a.RelativePath) + filename = a.PluginPath + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeV2) Injection(file *ast.File) error { + 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 +} + +func (a *PluginInitializeV2) Rollback(file *ast.File) error { + return nil +} + +func (a *PluginInitializeV2) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.PluginPath + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_v2_test.go b/server/utils/ast/plugin_initialize_v2_test.go new file mode 100644 index 0000000..ceb2a74 --- /dev/null +++ b/server/utils/ast/plugin_initialize_v2_test.go @@ -0,0 +1,100 @@ +package ast + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "path/filepath" + "testing" +) + +func TestPluginInitialize_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 注册注入", + fields: fields{ + Type: TypePluginInitializeV2, + 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, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + } + file, err := a.Parse("", nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format("", nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitialize_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + PluginName string + StructName string + PackageName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 回滚", + fields: fields{ + Type: TypePluginInitializeV2, + 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, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + StructName: "Plugin", + PackageName: "gva", + } + file, err := a.Parse("", nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format("", nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go new file mode 100644 index 0000000..2845aea --- /dev/null +++ b/server/utils/autocode/template_funcs.go @@ -0,0 +1,713 @@ +package autocode + +import ( + "fmt" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "slices" + "strings" + "text/template" +) + +// GetTemplateFuncMap 返回模板函数映射,用于在模板中使用 +func GetTemplateFuncMap() template.FuncMap { + return template.FuncMap{ + "title": strings.Title, + "GenerateField": GenerateField, + "GenerateSearchField": GenerateSearchField, + "GenerateSearchConditions": GenerateSearchConditions, + "GenerateSearchFormItem": GenerateSearchFormItem, + "GenerateTableColumn": GenerateTableColumn, + "GenerateFormItem": GenerateFormItem, + "GenerateDescriptionItem": GenerateDescriptionItem, + "GenerateDefaultFormValue": GenerateDefaultFormValue, + } +} + +// 渲染Model中的字段 +func GenerateField(field systemReq.AutoCodeField) string { + // 构建gorm标签 + gormTag := `` + + if field.FieldIndexType != "" { + gormTag += field.FieldIndexType + ";" + } + + if field.PrimaryKey { + gormTag += "primarykey;" + } + + if field.DefaultValue != "" { + gormTag += fmt.Sprintf("default:%s;", field.DefaultValue) + } + + if field.Comment != "" { + gormTag += fmt.Sprintf("comment:%s;", field.Comment) + } + + gormTag += "column:" + field.ColumnName + ";" + + // 对于int类型,根据DataTypeLong决定具体的Go类型,不使用size标签 + if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" { + gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong) + } + + requireTag := ` binding:"required"` + "`" + + // 根据字段类型构建不同的字段定义 + var result string + switch field.FieldType { + case "enum": + result = fmt.Sprintf(`%s string `+"`"+`json:"%s" form:"%s" gorm:"%stype:enum(%s);"`+"`", + field.FieldName, field.FieldJson, field.FieldJson, gormTag, field.DataTypeLong) + case "picture", "video": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s string `+"`"+`%s`+"`"+``, field.FieldName, tagContent) + case "file", "pictures", "array": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``, + field.FieldName, tagContent) + case "richtext": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``, + field.FieldName, tagContent) + case "json": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"object"`+"`"+``, + field.FieldName, tagContent) + default: + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + // 对于int类型,根据DataTypeLong决定具体的Go类型 + var fieldType string + if field.FieldType == "int" { + switch field.DataTypeLong { + case "1", "2", "3": + fieldType = "int8" + case "4", "5": + fieldType = "int16" + case "6", "7", "8", "9", "10": + fieldType = "int32" + case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20": + fieldType = "int64" + default: + fieldType = "int64" + } + } else { + fieldType = field.FieldType + } + + result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``, + field.FieldName, fieldType, tagContent) + } + + if field.Require { + result = result[0:len(result)-1] + requireTag + } + + // 添加字段描述 + if field.FieldDesc != "" { + result += fmt.Sprintf(" //%s", field.FieldDesc) + } + + return result +} + +// 格式化搜索条件语句 +func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string { + var conditions []string + + for _, field := range fields { + if field.FieldSearchType == "" { + continue + } + + var condition string + + if slices.Contains([]string{"enum", "pictures", "picture", "video", "json", "richtext", "array"}, field.FieldType) { + if field.FieldType == "enum" { + if field.FieldSearchType == "LIKE" { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s LIKE ?", "%%"+ info.%s+"%%") + }`, + field.FieldName, field.ColumnName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s %s ?", info.%s) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName) + } + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + // TODO 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + }`, field.FieldName) + } + + } else if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + if field.FieldType == "time.Time" { + condition = fmt.Sprintf(` + if len(info.%sRange) == 2 { + db = db.Where("%s %s ? AND ? ", info.%sRange[0], info.%sRange[1]) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.Start%s != nil && info.End%s != nil { + db = db.Where("%s %s ? AND ? ", *info.Start%s, *info.End%s) + }`, + field.FieldName, field.FieldName, field.ColumnName, + field.FieldSearchType, field.FieldName, field.FieldName) + } + } else { + nullCheck := "info." + field.FieldName + " != nil" + if field.FieldType == "string" { + condition = fmt.Sprintf(` + if %s && *info.%s != "" {`, nullCheck, field.FieldName) + } else { + condition = fmt.Sprintf(` + if %s {`, nullCheck) + } + + if field.FieldSearchType == "LIKE" { + condition += fmt.Sprintf(` + db = db.Where("%s LIKE ?", "%%"+ *info.%s+"%%") + }`, + field.ColumnName, field.FieldName) + } else { + condition += fmt.Sprintf(` + db = db.Where("%s %s ?", *info.%s) + }`, + field.ColumnName, field.FieldSearchType, field.FieldName) + } + } + + conditions = append(conditions, condition) + } + + return strings.Join(conditions, "") +} + +// 格式化前端搜索条件 +func GenerateSearchFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 根据字段属性生成不同的输入类型 + if field.FieldType == "bool" { + result += fmt.Sprintf(` +`, field.FieldJson) + result += ` +` + result += ` +` + result += ` +` + } else if field.DictType != "" { + multipleAttr := "" + if field.FieldType == "array" { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr) + } else if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else if field.FieldType == "float64" || field.FieldType == "int" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += fmt.Sprintf(` +`, field.FieldName) + result += ` — +` + result += fmt.Sprintf(` +`, field.FieldName) + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + } else if field.FieldType == "time.Time" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += ` +` + result += fmt.Sprintf(``, field.FieldJson) + } else { + result += fmt.Sprintf(``, field.FieldJson) + } + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + // 关闭表单项 + result += `` + + return result +} + +// GenerateTableColumn generates HTML for table column based on field properties +func GenerateTableColumn(field systemReq.AutoCodeField) string { + // Add sortable attribute if needed + sortAttr := "" + if field.Sort { + sortAttr = " sortable" + } + + // Handle different field types + if field.CheckDataSource { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.DictType != "" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "bool" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "time.Time" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "picture" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "pictures" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "video" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "richtext" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "file" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "json" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "array" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else { + return fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + } +} + +func GenerateFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 处理不同字段类型 + if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = " multiple" + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else { + switch field.FieldType { + case "bool": + result += fmt.Sprintf(` +`, + field.FieldJson) + + case "string": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable) + } else { + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + } + + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "json": + result += fmt.Sprintf(` // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.%s 后端会按照json的类型进行存取 +`, field.FieldJson) + result += fmt.Sprintf(` {{ formData.%s }} +`, field.FieldJson) + + case "array": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + case "int": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + + case "time.Time": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "float64": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "enum": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DataTypeLong) + result += ` +` + + case "picture": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "pictures": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "video": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "file": + result += fmt.Sprintf(` +`, field.FieldJson) + } + } + + // 关闭表单项 + result += `` + + return result +} + +func GenerateDescriptionItem(field systemReq.AutoCodeField) string { + // 开始构建描述项 + result := fmt.Sprintf(` +`, field.FieldDesc) + + if field.CheckDataSource { + result += ` +` + } else if field.FieldType != "picture" && field.FieldType != "pictures" && + field.FieldType != "file" && field.FieldType != "array" && + field.FieldType != "richtext" { + result += fmt.Sprintf(` {{ detailForm.%s }} +`, field.FieldJson) + } else { + switch field.FieldType { + case "picture": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "array": + result += fmt.Sprintf(` +`, field.FieldJson) + case "pictures": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + case "file": + result += fmt.Sprintf(`
+`, field.FieldJson) + result += ` +` + result += ` +` + result += ` {{ item.name }} +` + result += ` +` + result += `
+` + } + } + + // 关闭描述项 + result += `
` + + return result +} + +func GenerateDefaultFormValue(field systemReq.AutoCodeField) string { + // 根据字段类型确定默认值 + var defaultValue string + + switch field.FieldType { + case "bool": + defaultValue = "false" + case "string", "richtext": + defaultValue = "''" + case "int": + if field.DataSource != nil { // 检查数据源是否存在 + defaultValue = "undefined" + } else { + defaultValue = "0" + } + case "time.Time": + defaultValue = "new Date()" + case "float64": + defaultValue = "0" + case "picture", "video": + defaultValue = "\"\"" + case "pictures", "file", "array": + defaultValue = "[]" + case "json": + defaultValue = "{}" + default: + defaultValue = "null" + } + + // 返回格式化后的默认值字符串 + return fmt.Sprintf(`%s: %s,`, field.FieldJson, defaultValue) +} + +// GenerateSearchField 根据字段属性生成搜索结构体中的字段定义 +func GenerateSearchField(field systemReq.AutoCodeField) string { + var result string + + if field.FieldSearchType == "" { + return "" // 如果没有搜索类型,返回空字符串 + } + + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + // 生成范围搜索字段 + // time 的情况 + if field.FieldType == "time.Time" { + result = fmt.Sprintf("%sRange []time.Time `json:\"%sRange\" form:\"%sRange[]\"`", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + startField := fmt.Sprintf("Start%s *%s `json:\"start%s\" form:\"start%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + endField := fmt.Sprintf("End%s *%s `json:\"end%s\" form:\"end%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + result = startField + "\n" + endField + } + } else { + // 生成普通搜索字段 + if field.FieldType == "enum" || field.FieldType == "picture" || + field.FieldType == "pictures" || field.FieldType == "video" || + field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" || field.FieldType == "file" { + result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + result = fmt.Sprintf("%s *%s `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldType, field.FieldJson, field.FieldJson) + } + } + + return result +} diff --git a/server/utils/breakpoint_continue.go b/server/utils/breakpoint_continue.go new file mode 100644 index 0000000..66368e2 --- /dev/null +++ b/server/utils/breakpoint_continue.go @@ -0,0 +1,121 @@ +package utils + +import ( + "errors" + "os" + "strconv" + "strings" +) + +// 前端传来文件片与当前片为什么文件的第几片 +// 后端拿到以后比较次分片是否上传 或者是否为不完全片 +// 前端发送每片多大 +// 前端告知是否为最后一片且是否完成 + +const ( + breakpointDir = "./breakpointDir/" + finishDir = "./fileDir/" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: BreakPointContinue +//@description: 断点续传 +//@param: content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string +//@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 { + return path, err + } + pathC, err := makeFileContent(content, fileName, path, contentNumber) + return pathC, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CheckMd5 +//@description: 检查Md5 +//@param: content []byte, chunkMd5 string +//@return: CanUpload bool + +func CheckMd5(content []byte, chunkMd5 string) (CanUpload bool) { + fileMd5 := MD5V(content) + if fileMd5 == chunkMd5 { + return true // 可以继续上传 + } else { + return false // 切片不完整,废弃 + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片内容 +//@param: content []byte, fileName string, FileDir string, contentNumber int +//@return: string, error + +func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(FileDir, "..") { + return "", errors.New("文件名或路径不合法") + } + path := FileDir + fileName + "_" + strconv.Itoa(contentNumber) + f, err := os.Create(path) + if err != nil { + return path, err + } + defer f.Close() + _, err = f.Write(content) + if err != nil { + return path, err + } + + return path, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片文件 +//@param: fileName string, FileMd5 string +//@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 + } + _ = os.MkdirAll(finishDir, os.ModePerm) + fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644) + if err != nil { + return finishDir + fileName, err + } + defer fd.Close() + for k := range rd { + content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k)) + _, err = fd.Write(content) + if err != nil { + _ = os.Remove(finishDir + fileName) + return finishDir + fileName, err + } + } + return finishDir + fileName, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveChunk +//@description: 移除切片 +//@param: FileMd5 string +//@return: error + +func RemoveChunk(FileMd5 string) error { + if strings.Contains(FileMd5, "..") { + return errors.New("路径不合法") + } + err := os.RemoveAll(breakpointDir + FileMd5) + return err +} diff --git a/server/utils/captcha/redis.go b/server/utils/captcha/redis.go new file mode 100644 index 0000000..ffb4dbf --- /dev/null +++ b/server/utils/captcha/redis.go @@ -0,0 +1,61 @@ +package captcha + +import ( + "context" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +func NewDefaultRedisStore() *RedisStore { + return &RedisStore{ + Expiration: time.Second * 180, + PreKey: "CAPTCHA_", + Context: context.TODO(), + } +} + +type RedisStore struct { + Expiration time.Duration + PreKey string + Context context.Context +} + +func (rs *RedisStore) UseWithCtx(ctx context.Context) *RedisStore { + if ctx == nil { + rs.Context = ctx + } + return rs +} + +func (rs *RedisStore) Set(id string, value string) error { + err := global.GVA_REDIS.Set(rs.Context, rs.PreKey+id, value, rs.Expiration).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreSetError!", zap.Error(err)) + return err + } + return nil +} + +func (rs *RedisStore) Get(key string, clear bool) string { + val, err := global.GVA_REDIS.Get(rs.Context, key).Result() + if err != nil { + global.GVA_LOG.Error("RedisStoreGetError!", zap.Error(err)) + return "" + } + if clear { + err := global.GVA_REDIS.Del(rs.Context, key).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreClearError!", zap.Error(err)) + return "" + } + } + return val +} + +func (rs *RedisStore) Verify(id, answer string, clear bool) bool { + key := rs.PreKey + id + v := rs.Get(key, clear) + return v == answer +} diff --git a/server/utils/casbin_util.go b/server/utils/casbin_util.go new file mode 100644 index 0000000..62d44fc --- /dev/null +++ b/server/utils/casbin_util.go @@ -0,0 +1,52 @@ +package utils + +import ( + "sync" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +// GetCasbin 获取casbin实例 +func GetCasbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/server/utils/claims.go b/server/utils/claims.go new file mode 100644 index 0000000..f697967 --- /dev/null +++ b/server/utils/claims.go @@ -0,0 +1,148 @@ +package utils + +import ( + "net" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func ClearToken(c *gin.Context) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", "", -1, "/", "", false, false) + } else { + c.SetCookie("x-token", "", -1, "/", host, false, false) + } +} + +func SetToken(c *gin.Context, token string, maxAge int) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", token, maxAge, "/", "", false, false) + } else { + c.SetCookie("x-token", token, maxAge, "/", host, false, false) + } +} + +func GetToken(c *gin.Context) string { + token := c.Request.Header.Get("x-token") + if token == "" { + j := NewJWT() + token, _ = c.Cookie("x-token") + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在x-token且claims是否为规定结构") + return token + } + SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix())) + } + return token +} + +func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) { + token := GetToken(c) + j := NewJWT() + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") + } + return claims, err +} + +// GetUserID 从Gin的Context中获取从jwt解析出来的用户ID +func GetUserID(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.BaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.BaseClaims.ID + } +} + +// GetUserUuid 从Gin的Context中获取从jwt解析出来的用户UUID +func GetUserUuid(c *gin.Context) uuid.UUID { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return uuid.UUID{} + } else { + return cl.UUID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.UUID + } +} + +// GetUserAuthorityId 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserAuthorityId(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.AuthorityId + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.AuthorityId + } +} + +// GetUserInfo 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserInfo(c *gin.Context) *systemReq.CustomClaims { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return nil + } else { + return cl + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse + } +} + +// GetUserName 从Gin的Context中获取从jwt解析出来的用户名 +func GetUserName(c *gin.Context) string { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return "" + } else { + return cl.Username + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.Username + } +} + +func LoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) { + j := NewJWT() + claims = j.CreateClaims(systemReq.BaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + NickName: user.GetNickname(), + Username: user.GetUsername(), + AuthorityId: user.GetAuthorityId(), + }) + token, err = j.CreateToken(claims) + return +} diff --git a/server/utils/directory.go b/server/utils/directory.go new file mode 100644 index 0000000..d419fee --- /dev/null +++ b/server/utils/directory.go @@ -0,0 +1,124 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: PathExists +//@description: 文件目录是否存在 +//@param: path string +//@return: bool, error + +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateDir +//@description: 批量创建文件夹 +//@param: dirs ...string +//@return: err error + +func CreateDir(dirs ...string) (err error) { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + global.GVA_LOG.Debug("create directory" + v) + if err := os.MkdirAll(v, os.ModePerm); err != nil { + global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err)) + return err + } + } + } + return err +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: FileMove +//@description: 文件移动供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileMove(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + return os.Rename(src, dst) +} + +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: TrimSpace +//@description: 去除结构体空格 +//@param: target interface (target: 目标结构体,传入必须是指针类型) +//@return: null + +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} diff --git a/server/utils/fmt_plus.go b/server/utils/fmt_plus.go new file mode 100644 index 0000000..1257a65 --- /dev/null +++ b/server/utils/fmt_plus.go @@ -0,0 +1,125 @@ +package utils + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "math/rand" + "reflect" + "strings" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: StructToMap +//@description: 利用反射将结构体转化为map +//@param: obj interface{} +//@return: map[string]interface{} + +func StructToMap(obj interface{}) map[string]interface{} { + obj1 := reflect.TypeOf(obj) + obj2 := reflect.ValueOf(obj) + + data := make(map[string]interface{}) + for i := 0; i < obj1.NumField(); i++ { + if obj1.Field(i).Tag.Get("mapstructure") != "" { + data[obj1.Field(i).Tag.Get("mapstructure")] = obj2.Field(i).Interface() + } else { + data[obj1.Field(i).Name] = obj2.Field(i).Interface() + } + } + return data +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ArrayToString +//@description: 将数组格式化为字符串 +//@param: array []interface{} +//@return: string + +func ArrayToString(array []interface{}) string { + return strings.Replace(strings.Trim(fmt.Sprint(array), "[]"), " ", ",", -1) +} + +func Pointer[T any](in T) (out *T) { + return &in +} + +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +// MaheHump 将字符串转换为驼峰命名 +func MaheHump(s string) string { + words := strings.Split(s, "-") + + for i := 1; i < len(words); i++ { + words[i] = strings.Title(words[i]) + } + + return strings.Join(words, "") +} + +// HumpToUnderscore 将驼峰命名转换为下划线分割模式 +func HumpToUnderscore(s string) string { + var result strings.Builder + + for i, char := range s { + if i > 0 && char >= 'A' && char <= 'Z' { + // 在大写字母前添加下划线 + result.WriteRune('_') + result.WriteRune(char - 'A' + 'a') // 转小写 + } else { + result.WriteRune(char) + } + } + + return strings.ToLower(result.String()) +} + +// RandomString 随机字符串 +func RandomString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + b := make([]rune, n) + for i := range b { + b[i] = letters[RandomInt(0, len(letters))] + } + return string(b) +} + +func RandomInt(min, max int) int { + return min + rand.Intn(max-min) +} + +// BuildTree 用于构建一个树形结构 +func BuildTree[T common.TreeNode[T]](nodes []T) []T { + nodeMap := make(map[int]T) + // 创建一个基本map + for i := range nodes { + nodeMap[nodes[i].GetID()] = nodes[i] + } + + for i := range nodes { + if nodes[i].GetParentID() != 0 { + parent := nodeMap[nodes[i].GetParentID()] + parent.SetChildren(nodes[i]) + } + } + + var rootNodes []T + + for i := range nodeMap { + if nodeMap[i].GetParentID() == 0 { + rootNodes = append(rootNodes, nodeMap[i]) + } + } + return rootNodes +} diff --git a/server/utils/hash.go b/server/utils/hash.go new file mode 100644 index 0000000..9c3564b --- /dev/null +++ b/server/utils/hash.go @@ -0,0 +1,31 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "golang.org/x/crypto/bcrypt" +) + +// BcryptHash 使用 bcrypt 对密码进行加密 +func BcryptHash(password string) string { + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes) +} + +// BcryptCheck 对比明文密码和数据库的哈希值 +func BcryptCheck(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: MD5V +//@description: md5加密 +//@param: str []byte +//@return: string + +func MD5V(str []byte, b ...byte) string { + h := md5.New() + h.Write(str) + return hex.EncodeToString(h.Sum(b)) +} diff --git a/server/utils/human_duration.go b/server/utils/human_duration.go new file mode 100644 index 0000000..0cdb055 --- /dev/null +++ b/server/utils/human_duration.go @@ -0,0 +1,29 @@ +package utils + +import ( + "strconv" + "strings" + "time" +) + +func ParseDuration(d string) (time.Duration, error) { + d = strings.TrimSpace(d) + dr, err := time.ParseDuration(d) + if err == nil { + return dr, nil + } + if strings.Contains(d, "d") { + index := strings.Index(d, "d") + + hour, _ := strconv.Atoi(d[:index]) + dr = time.Hour * 24 * time.Duration(hour) + ndr, err := time.ParseDuration(d[index+1:]) + if err != nil { + return dr, nil + } + return dr + ndr, nil + } + + dv, err := strconv.ParseInt(d, 10, 64) + return time.Duration(dv), err +} diff --git a/server/utils/human_duration_test.go b/server/utils/human_duration_test.go new file mode 100644 index 0000000..8a5294b --- /dev/null +++ b/server/utils/human_duration_test.go @@ -0,0 +1,49 @@ +package utils + +import ( + "testing" + "time" +) + +func TestParseDuration(t *testing.T) { + type args struct { + d string + } + tests := []struct { + name string + args args + want time.Duration + wantErr bool + }{ + { + name: "5h20m", + args: args{"5h20m"}, + want: time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d5h20m", + args: args{"1d5h20m"}, + want: 24*time.Hour + time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d", + args: args{"1d"}, + want: 24 * time.Hour, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseDuration(tt.args.d) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseDuration() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/utils/json.go b/server/utils/json.go new file mode 100644 index 0000000..8c4118c --- /dev/null +++ b/server/utils/json.go @@ -0,0 +1,34 @@ +package utils + +import ( + "encoding/json" + "strings" +) + +func GetJSONKeys(jsonStr string) (keys []string, err error) { + // 使用json.Decoder,以便在解析过程中记录键的顺序 + dec := json.NewDecoder(strings.NewReader(jsonStr)) + t, err := dec.Token() + if err != nil { + return nil, err + } + // 确保数据是一个对象 + if t != json.Delim('{') { + return nil, err + } + for dec.More() { + t, err = dec.Token() + if err != nil { + return nil, err + } + keys = append(keys, t.(string)) + + // 解析值 + var value interface{} + err = dec.Decode(&value) + if err != nil { + return nil, err + } + } + return keys, nil +} diff --git a/server/utils/json_test.go b/server/utils/json_test.go new file mode 100644 index 0000000..f21a679 --- /dev/null +++ b/server/utils/json_test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "testing" +) + +func TestGetJSONKeys(t *testing.T) { + var jsonStr = ` + { + "Name": "test", + "TableName": "test", + "TemplateID": "test", + "TemplateInfo": "test", + "Limit": 0 +}` + keys, err := GetJSONKeys(jsonStr) + if err != nil { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if len(keys) != 5 { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if keys[0] != "Name" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[1] != "TableName" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[2] != "TemplateID" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[3] != "TemplateInfo" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[4] != "Limit" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + + fmt.Println(keys) +} diff --git a/server/utils/jwt.go b/server/utils/jwt.go new file mode 100644 index 0000000..b4e6b3b --- /dev/null +++ b/server/utils/jwt.go @@ -0,0 +1,105 @@ +package utils + +import ( + "context" + "errors" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + jwt "github.com/golang-jwt/jwt/v5" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenValid = errors.New("未知错误") + TokenExpired = errors.New("token已过期") + TokenNotValidYet = errors.New("token尚未激活") + TokenMalformed = errors.New("这不是一个token") + TokenSignatureInvalid = errors.New("无效签名") + TokenInvalid = errors.New("无法处理此token") +) + +func NewJWT() *JWT { + return &JWT{ + []byte(global.GVA_CONFIG.JWT.SigningKey), + } +} + +func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims { + bf, _ := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + ep, _ := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims := request.CustomClaims{ + BaseClaims: baseClaims, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, // 受众 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateToken 创建一个token +func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 +func (j *JWT) CreateTokenByOldToken(oldToken string, claims request.CustomClaims) (string, error) { + v, err, _ := global.GVA_Concurrency_Control.Do("JWT:"+oldToken, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// ParseToken 解析 token +func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenValid +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} diff --git a/server/utils/plugin/plugin.go b/server/utils/plugin/plugin.go new file mode 100644 index 0000000..a59d5b5 --- /dev/null +++ b/server/utils/plugin/plugin.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +const ( + OnlyFuncName = "Plugin" +) + +// Plugin 插件模式接口化 +type Plugin interface { + // Register 注册路由 + Register(group *gin.RouterGroup) + + // RouterPath 用户返回注册路由 + RouterPath() string +} diff --git a/server/utils/plugin/v2/plugin.go b/server/utils/plugin/v2/plugin.go new file mode 100644 index 0000000..4dac0ab --- /dev/null +++ b/server/utils/plugin/v2/plugin.go @@ -0,0 +1,11 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +// Plugin 插件模式接口化v2 +type Plugin interface { + // Register 注册路由 + Register(group *gin.Engine) +} diff --git a/server/utils/plugin/v2/registry.go b/server/utils/plugin/v2/registry.go new file mode 100644 index 0000000..4ec5fce --- /dev/null +++ b/server/utils/plugin/v2/registry.go @@ -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 +} diff --git a/server/utils/request/http.go b/server/utils/request/http.go new file mode 100644 index 0000000..5d46f22 --- /dev/null +++ b/server/utils/request/http.go @@ -0,0 +1,127 @@ +package request + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/url" + "time" +) + +func HttpRequest( + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any) (*http.Response, error) { + return doJSONRequest(context.Background(), 0, urlStr, method, headers, params, data) +} + +// HttpRequestWithTimeout 发送HTTP请求,支持自定义超时时间 +// timeout 参数可选,单位为时间.Duration,默认值为 10 分钟 +func HttpRequestWithTimeout( + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any, + timeout ...time.Duration) (*http.Response, error) { + t := 10 * time.Minute + if len(timeout) > 0 && timeout[0] > 0 { + t = timeout[0] + } + return doJSONRequest(context.Background(), t, urlStr, method, headers, params, data) +} + +// HttpRequestWithContextAndTimeout 发送HTTP请求,支持自定义超时时间和上下文 +func HttpRequestWithContextAndTimeout( + ctx context.Context, + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any, + timeout ...time.Duration) (*http.Response, error) { + t := 10 * time.Minute + if len(timeout) > 0 { + if timeout[0] < 0 { + t = 0 // 负值表示不设置超时(用于 SSE 等流式场景) + } else if timeout[0] > 0 { + t = timeout[0] + } + } + return doJSONRequest(ctx, t, urlStr, method, headers, params, data) +} + +func doJSONRequest( + ctx context.Context, + timeout time.Duration, + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any) (*http.Response, error) { + if ctx == nil { + ctx = context.Background() + } + + // URL + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + // 添加查询参数 + query := u.Query() + for k, v := range params { + query.Set(k, v) + } + u.RawQuery = query.Encode() + + // 将数据编码为JSON + buf := new(bytes.Buffer) + if data != nil { + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + buf = bytes.NewBuffer(b) + } + + // 创建请求 + req, err := http.NewRequestWithContext(ctx, method, u.String(), buf) + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + if data != nil { + req.Header.Set("Content-Type", "application/json") + } + + client := &http.Client{} + if timeout > 0 { + client.Timeout = timeout + } + + // 当请求 SSE 流时,禁用 Transport 层的自动 gzip 压缩 + // 避免 gzip 解压缓冲导致 SSE 事件无法实时到达 + if req.Header.Get("Accept") == "text/event-stream" { + client.Transport = &http.Transport{ + DisableCompression: true, + } + } + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + // 返回响应,调用方负责关闭 + return resp, nil +} diff --git a/server/utils/server.go b/server/utils/server.go new file mode 100644 index 0000000..8c14cd7 --- /dev/null +++ b/server/utils/server.go @@ -0,0 +1,126 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "runtime" + "time" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +const ( + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB +) + +type Server struct { + Os Os `json:"os"` + Cpu Cpu `json:"cpu"` + Ram Ram `json:"ram"` + Disk []Disk `json:"disk"` +} + +type Os struct { + GOOS string `json:"goos"` + NumCPU int `json:"numCpu"` + Compiler string `json:"compiler"` + GoVersion string `json:"goVersion"` + NumGoroutine int `json:"numGoroutine"` +} + +type Cpu struct { + Cpus []float64 `json:"cpus"` + Cores int `json:"cores"` +} + +type Ram struct { + UsedMB int `json:"usedMb"` + TotalMB int `json:"totalMb"` + UsedPercent int `json:"usedPercent"` +} + +type Disk struct { + MountPoint string `json:"mountPoint"` + UsedMB int `json:"usedMb"` + UsedGB int `json:"usedGb"` + TotalMB int `json:"totalMb"` + TotalGB int `json:"totalGb"` + UsedPercent int `json:"usedPercent"` +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: OS信息 +//@return: o Os, err error + +func InitOS() (o Os) { + o.GOOS = runtime.GOOS + o.NumCPU = runtime.NumCPU() + o.Compiler = runtime.Compiler + o.GoVersion = runtime.Version() + o.NumGoroutine = runtime.NumGoroutine() + return o +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: CPU信息 +//@return: c Cpu, err error + +func InitCPU() (c Cpu, err error) { + if cores, err := cpu.Counts(false); err != nil { + return c, err + } else { + c.Cores = cores + } + if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil { + return c, err + } else { + c.Cpus = cpus + } + return c, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitRAM +//@description: RAM信息 +//@return: r Ram, err error + +func InitRAM() (r Ram, err error) { + if u, err := mem.VirtualMemory(); err != nil { + return r, err + } else { + r.UsedMB = int(u.Used) / MB + r.TotalMB = int(u.Total) / MB + r.UsedPercent = int(u.UsedPercent) + } + return r, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitDisk +//@description: 硬盘信息 +//@return: d Disk, err error + +func InitDisk() (d []Disk, err error) { + for i := range global.GVA_CONFIG.DiskList { + mp := global.GVA_CONFIG.DiskList[i].MountPoint + if u, err := disk.Usage(mp); err != nil { + return d, err + } else { + d = append(d, Disk{ + MountPoint: mp, + UsedMB: int(u.Used) / MB, + UsedGB: int(u.Used) / GB, + TotalMB: int(u.Total) / MB, + TotalGB: int(u.Total) / GB, + UsedPercent: int(u.UsedPercent), + }) + } + } + return d, nil +} diff --git a/server/utils/stacktrace/stacktrace.go b/server/utils/stacktrace/stacktrace.go new file mode 100644 index 0000000..dbae2c9 --- /dev/null +++ b/server/utils/stacktrace/stacktrace.go @@ -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 +} \ No newline at end of file diff --git a/server/utils/system_events.go b/server/utils/system_events.go new file mode 100644 index 0000000..736ea74 --- /dev/null +++ b/server/utils/system_events.go @@ -0,0 +1,34 @@ +package utils + +import ( + "sync" +) + +// SystemEvents 定义系统级事件处理 +type SystemEvents struct { + reloadHandlers []func() error + mu sync.RWMutex +} + +// 全局事件管理器 +var GlobalSystemEvents = &SystemEvents{} + +// RegisterReloadHandler 注册系统重载处理函数 +func (e *SystemEvents) RegisterReloadHandler(handler func() error) { + e.mu.Lock() + defer e.mu.Unlock() + e.reloadHandlers = append(e.reloadHandlers, handler) +} + +// TriggerReload 触发所有注册的重载处理函数 +func (e *SystemEvents) TriggerReload() error { + e.mu.RLock() + defer e.mu.RUnlock() + + for _, handler := range e.reloadHandlers { + if err := handler(); err != nil { + return err + } + } + return nil +} diff --git a/server/utils/timer/timed_task.go b/server/utils/timer/timed_task.go new file mode 100644 index 0000000..06865d2 --- /dev/null +++ b/server/utils/timer/timed_task.go @@ -0,0 +1,229 @@ +package timer + +import ( + "github.com/robfig/cron/v3" + "sync" +) + +type Timer interface { + // 寻找所有Cron + FindCronList() map[string]*taskManager + // 添加Task 方法形式以秒的形式加入 + AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 + // 添加Task 接口形式以秒的形式加入 + AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过函数的方法添加任务 + AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发 + AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 获取对应taskName的cron 可能会为空 + FindCron(cronName string) (*taskManager, bool) + // 指定cron开始执行 + StartCron(cronName string) + // 指定cron停止执行 + StopCron(cronName string) + // 查找指定cron下的指定task + FindTask(cronName string, taskName string) (*task, bool) + // 根据id删除指定cron下的指定task + RemoveTask(cronName string, id int) + // 根据taskName删除指定cron下的指定task + RemoveTaskByName(cronName string, taskName string) + // 清理掉指定cronName + Clear(cronName string) + // 停止所有的cron + Close() +} + +type task struct { + EntryID cron.EntryID + Spec string + TaskName string +} + +type taskManager struct { + corn *cron.Cron + tasks map[cron.EntryID]*task +} + +// timer 定时任务管理 +type timer struct { + cronList map[string]*taskManager + sync.Mutex +} + +// AddTaskByFunc 通过函数的方法添加任务 +func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJob 通过接口的方法添加任务 +func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJobWithSeconds 通过接口的方法添加任务 +func (t *timer) AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// FindCron 获取对应cronName的cron 可能会为空 +func (t *timer) FindCron(cronName string) (*taskManager, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + return v, ok +} + +// FindTask 获取对应cronName的cron 可能会为空 +func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + if !ok { + return nil, ok + } + for _, t2 := range v.tasks { + if t2.TaskName == taskName { + return t2, true + } + } + return nil, false +} + +// FindCronList 获取所有的任务列表 +func (t *timer) FindCronList() map[string]*taskManager { + t.Lock() + defer t.Unlock() + return t.cronList +} + +// StartCron 开始任务 +func (t *timer) StartCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Start() + } +} + +// StopCron 停止任务 +func (t *timer) StopCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + } +} + +// RemoveTask 从cronName 删除指定任务 +func (t *timer) RemoveTask(cronName string, id int) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Remove(cron.EntryID(id)) + delete(v.tasks, cron.EntryID(id)) + } +} + +// RemoveTaskByName 从cronName 使用taskName 删除指定任务 +func (t *timer) RemoveTaskByName(cronName string, taskName string) { + fTask, ok := t.FindTask(cronName, taskName) + if !ok { + return + } + t.RemoveTask(cronName, int(fTask.EntryID)) +} + +// Clear 清除任务 +func (t *timer) Clear(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + delete(t.cronList, cronName) + } +} + +// Close 释放资源 +func (t *timer) Close() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Stop() + } +} + +func NewTimerTask() Timer { + return &timer{cronList: make(map[string]*taskManager)} +} diff --git a/server/utils/timer/timed_task_test.go b/server/utils/timer/timed_task_test.go new file mode 100644 index 0000000..9f2c02c --- /dev/null +++ b/server/utils/timer/timed_task_test.go @@ -0,0 +1,72 @@ +package timer + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var job = mockJob{} + +type mockJob struct{} + +func (job mockJob) Run() { + mockFunc() +} + +func mockFunc() { + time.Sleep(time.Second) + fmt.Println("1s...") +} + +func TestNewTimerTask(t *testing.T) { + tm := NewTimerTask() + _tm := tm.(*timer) + + { + _, err := tm.AddTaskByFunc("func", "@every 1s", mockFunc, "测试mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["func"] + if !ok { + t.Error("no find func") + } + } + + { + _, err := tm.AddTaskByJob("job", "@every 1s", job, "测试job mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["job"] + if !ok { + t.Error("no find job") + } + } + + { + _, ok := tm.FindCron("func") + if !ok { + t.Error("no find func") + } + _, ok = tm.FindCron("job") + if !ok { + t.Error("no find job") + } + _, ok = tm.FindCron("none") + if ok { + t.Error("find none") + } + } + { + tm.Clear("func") + _, ok := tm.FindCron("func") + if ok { + t.Error("find func") + } + } + { + a := tm.FindCronList() + b, c := tm.FindCron("job") + fmt.Println(a, b, c) + } +} diff --git a/server/utils/validator.go b/server/utils/validator.go new file mode 100644 index 0000000..a56dac0 --- /dev/null +++ b/server/utils/validator.go @@ -0,0 +1,294 @@ +package utils + +import ( + "errors" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Rules map[string][]string + +type RulesMap map[string]Rules + +var CustomizeMap = make(map[string]Rules) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RegisterRule +//@description: 注册自定义规则方案建议在路由初始化层即注册 +//@param: key string, rule Rules +//@return: err error + +func RegisterRule(key string, rule Rules) (err error) { + if CustomizeMap[key] != nil { + return errors.New(key + "已注册,无法重复注册") + } else { + CustomizeMap[key] = rule + return nil + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: NotEmpty +//@description: 非空 不能为其对应类型的0值 +//@return: string + +func NotEmpty() string { + return "notEmpty" +} + +// @author: [zooqkl](https://github.com/zooqkl) +// @function: RegexpMatch +// @description: 正则校验 校验输入项是否满足正则表达式 +// @param: rule string +// @return: string + +func RegexpMatch(rule string) string { + return "regexp=" + rule +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Lt +//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Lt(mark string) string { + return "lt=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Le +//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Le(mark string) string { + return "le=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Eq +//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Eq(mark string) string { + return "eq=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ne +//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ne(mark string) string { + return "ne=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ge +//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ge(mark string) string { + return "ge=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Gt +//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Gt(mark string) string { + return "gt=" + mark +} + +// +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Verify +//@description: 校验方法 +//@param: st interface{}, roleMap Rules(入参实例,规则map) +//@return: err error + +func Verify(st interface{}, roleMap Rules) (err error) { + compareMap := map[string]bool{ + "lt": true, + "le": true, + "eq": true, + "ne": true, + "ge": true, + "gt": true, + } + + typ := reflect.TypeOf(st) + val := reflect.ValueOf(st) // 获取reflect.Type类型 + + kd := val.Kind() // 获取到st对应的类别 + if kd != reflect.Struct { + return errors.New("expect struct") + } + num := val.NumField() + // 遍历结构体的所有字段 + for i := 0; i < num; i++ { + tagVal := typ.Field(i) + val := val.Field(i) + if tagVal.Type.Kind() == reflect.Struct { + if err = Verify(val.Interface(), roleMap); err != nil { + return err + } + } + if len(roleMap[tagVal.Name]) > 0 { + for _, v := range roleMap[tagVal.Name] { + switch { + case v == "notEmpty": + if isBlank(val) { + return errors.New(tagVal.Name + "值不能为空") + } + case strings.Split(v, "=")[0] == "regexp": + if !regexpMatch(strings.Split(v, "=")[1], val.String()) { + return errors.New(tagVal.Name + "格式校验不通过") + } + case compareMap[strings.Split(v, "=")[0]]: + if !compareVerify(val, v) { + return errors.New(tagVal.Name + "长度或值不在合法范围," + v) + } + } + } + } + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compareVerify +//@description: 长度和数字的校验方法 根据类型自动校验 +//@param: value reflect.Value, VerifyStr string +//@return: bool + +func compareVerify(value reflect.Value, VerifyStr string) bool { + switch value.Kind() { + case reflect.String: + return compare(len([]rune(value.String())), VerifyStr) + case reflect.Slice, reflect.Array: + return compare(value.Len(), VerifyStr) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return compare(value.Uint(), VerifyStr) + case reflect.Float32, reflect.Float64: + return compare(value.Float(), VerifyStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return compare(value.Int(), VerifyStr) + default: + return false + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: isBlank +//@description: 非空校验 +//@param: value reflect.Value +//@return: bool + +func isBlank(value reflect.Value) bool { + switch value.Kind() { + case reflect.String, reflect.Slice: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compare +//@description: 比较函数 +//@param: value interface{}, VerifyStr string +//@return: bool + +func compare(value interface{}, VerifyStr string) bool { + VerifyStrArr := strings.Split(VerifyStr, "=") + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Int() < VInt + case VerifyStrArr[0] == "le": + return val.Int() <= VInt + case VerifyStrArr[0] == "eq": + return val.Int() == VInt + case VerifyStrArr[0] == "ne": + return val.Int() != VInt + case VerifyStrArr[0] == "ge": + return val.Int() >= VInt + case VerifyStrArr[0] == "gt": + return val.Int() > VInt + default: + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + VInt, VErr := strconv.Atoi(VerifyStrArr[1]) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Uint() < uint64(VInt) + case VerifyStrArr[0] == "le": + return val.Uint() <= uint64(VInt) + case VerifyStrArr[0] == "eq": + return val.Uint() == uint64(VInt) + case VerifyStrArr[0] == "ne": + return val.Uint() != uint64(VInt) + case VerifyStrArr[0] == "ge": + return val.Uint() >= uint64(VInt) + case VerifyStrArr[0] == "gt": + return val.Uint() > uint64(VInt) + default: + return false + } + case reflect.Float32, reflect.Float64: + VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Float() < VFloat + case VerifyStrArr[0] == "le": + return val.Float() <= VFloat + case VerifyStrArr[0] == "eq": + return val.Float() == VFloat + case VerifyStrArr[0] == "ne": + return val.Float() != VFloat + case VerifyStrArr[0] == "ge": + return val.Float() >= VFloat + case VerifyStrArr[0] == "gt": + return val.Float() > VFloat + default: + return false + } + default: + return false + } +} + +func regexpMatch(rule, matchStr string) bool { + return regexp.MustCompile(rule).MatchString(matchStr) +} diff --git a/server/utils/validator_test.go b/server/utils/validator_test.go new file mode 100644 index 0000000..bdacb8b --- /dev/null +++ b/server/utils/validator_test.go @@ -0,0 +1,37 @@ +package utils + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "testing" +) + +type PageInfoTest struct { + PageInfo request.PageInfo + Name string +} + +func TestVerify(t *testing.T) { + PageInfoVerify := Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "Name": {NotEmpty()}} + var testInfo PageInfoTest + testInfo.Name = "test" + testInfo.PageInfo.Page = 0 + testInfo.PageInfo.PageSize = 0 + err := Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能捕捉0值") + } + testInfo.Name = "" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能正常检测name为空") + } + testInfo.Name = "test" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err != nil { + t.Error("校验失败,未能正常通过检测") + } +} diff --git a/server/utils/verify.go b/server/utils/verify.go new file mode 100644 index 0000000..cc2cb78 --- /dev/null +++ b/server/utils/verify.go @@ -0,0 +1,19 @@ +package utils + +var ( + IdVerify = Rules{"ID": []string{NotEmpty()}} + ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}} + MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}} + MenuMetaVerify = Rules{"Title": {NotEmpty()}} + LoginVerify = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}} + RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}} + PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}} + CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}} + AutoCodeVerify = Rules{"Abbreviation": {NotEmpty()}, "StructName": {NotEmpty()}, "PackageName": {NotEmpty()}} + AutoPackageVerify = Rules{"PackageName": {NotEmpty()}} + AuthorityVerify = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}} + AuthorityIdVerify = Rules{"AuthorityId": {NotEmpty()}} + OldAuthorityVerify = Rules{"OldAuthorityId": {NotEmpty()}} + ChangePasswordVerify = Rules{"Password": {NotEmpty()}, "NewPassword": {NotEmpty()}} + SetUserAuthorityVerify = Rules{"AuthorityId": {NotEmpty()}} +) diff --git a/server/utils/zip.go b/server/utils/zip.go new file mode 100644 index 0000000..ef35d10 --- /dev/null +++ b/server/utils/zip.go @@ -0,0 +1,53 @@ +package utils + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// 解压 +func Unzip(zipFile string, destDir string) ([]string, error) { + zipReader, err := zip.OpenReader(zipFile) + var paths []string + if err != nil { + return []string{}, err + } + defer zipReader.Close() + + for _, f := range zipReader.File { + if strings.Contains(f.Name, "..") { + return []string{}, fmt.Errorf("%s 文件名不合法", f.Name) + } + fpath := filepath.Join(destDir, f.Name) + paths = append(paths, fpath) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + } else { + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return []string{}, err + } + + inFile, err := f.Open() + if err != nil { + return []string{}, err + } + defer inFile.Close() + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return []string{}, err + } + defer outFile.Close() + + _, err = io.Copy(outFile, inFile) + if err != nil { + return []string{}, err + } + } + } + return paths, nil +} diff --git a/web-admin/.env.development b/web-admin/.env.development new file mode 100644 index 0000000..0dece1a --- /dev/null +++ b/web-admin/.env.development @@ -0,0 +1 @@ +VITE_BASE_API=/api diff --git a/web-admin/.env.production b/web-admin/.env.production new file mode 100644 index 0000000..0dece1a --- /dev/null +++ b/web-admin/.env.production @@ -0,0 +1 @@ +VITE_BASE_API=/api diff --git a/web-admin/.gitignore b/web-admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web-admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web-admin/README.md b/web-admin/README.md new file mode 100644 index 0000000..bdb0281 --- /dev/null +++ b/web-admin/README.md @@ -0,0 +1,47 @@ +# web-admin + +新的 React 管理后台。 + +## 技术选型 + +| 方案 | 作用 | 选择原因 | +| --- | --- | --- | +| React 19 + Vite | 前端运行与构建 | 启动快,适合独立新后台落地 | +| Ant Design | 表格、表单、抽屉、树控件 | 现阶段后台治理页密度高,组件成熟,交互成本低 | +| Zustand | 会话状态 | 结构轻,适合 token、用户、菜单这类状态 | +| React Query | 请求缓存基础设施 | 先接入,后续复杂页面可以逐步改成 query 模式 | + +## 启动 + +```bash +npm install +npm run dev +``` + +默认开发地址:`http://localhost:8081` + +默认后端代理:`http://127.0.0.1:8888` + +## 已接入页面 + +- 登录 +- 仪表盘 +- 用户管理 +- 角色管理 +- 菜单管理 +- API 管理 +- 字典管理 +- 参数管理 +- 登录日志 +- 操作历史 +- 系统配置 +- API Token +- 错误日志 +- 个人中心 +- 服务器状态 + +## 盘点文档 + +接口和功能梳理见: + +- [`docs/system-inventory.md`](/Users/lee-mac/Desktop/rmk-new/web-admin/docs/system-inventory.md) diff --git a/web-admin/docs/system-inventory.md b/web-admin/docs/system-inventory.md new file mode 100644 index 0000000..6ac1a7a --- /dev/null +++ b/web-admin/docs/system-inventory.md @@ -0,0 +1,191 @@ +# 系统接口与功能梳理 + +## 菜单结构 + +后端默认菜单初始化文件:`server/source/system/menu.go` + +### 总览 + +- `dashboard` 仪表盘 +- `about` 关于系统 +- `person` 个人中心 +- `state` 服务器状态 + +### 超级管理员 + +- `authority` 角色管理 +- `menu` 菜单管理 +- `api` API 管理 +- `user` 用户管理 +- `dictionary` 字典管理 +- `operation` 操作历史 +- `sysParams` 参数管理 +- `system` 系统配置 +- `apiToken` API Token +- `loginLog` 登录日志 +- `sysVersion` 版本管理 +- `sysError` 错误日志 + +### 示例模块 + +- `breakpoint` 断点续传 +- `customer` 客户列表示例 + +### 公共模块 + +- `upload` 媒体库 + +### 编程辅助 + +- `autoPkg` 模板配置 +- `autoCode` 代码生成器 +- `autoCodeAdmin` 自动化代码管理 +- `formCreate` 表单生成器 +- `aiWorkflow` AI 需求工作流 +- `exportTemplate` 导出模板 +- `mcpTest` Mcp Tools 管理 +- `mcpTool` Mcp Tools 模板 +- `skills` Skills 管理 +- `picture` AI 页面绘制 +- `autoCodeEdit/:id` 隐藏编辑路由 + +## 主要接口分组 + +后端默认接口初始化文件:`server/source/system/api.go` + +### 认证与会话 + +- `POST /base/login` +- `POST /base/captcha` +- `GET /user/getUserInfo` +- `POST /jwt/jsonInBlacklist` +- `POST /user/setUserAuthority` + +### 用户管理 + +- `POST /user/getUserList` +- `POST /user/admin_register` +- `PUT /user/setUserInfo` +- `POST /user/resetPassword` +- `PUT /user/setSelfInfo` +- `POST /user/changePassword` + +### 角色与权限 + +- `POST /authority/getAuthorityList` +- `POST /authority/createAuthority` +- `PUT /authority/updateAuthority` +- `POST /authority/deleteAuthority` +- `POST /authority/copyAuthority` +- `GET /authority/getUsersByAuthority` +- `POST /authority/setRoleUsers` +- `POST /menu/getMenuAuthority` +- `POST /menu/addMenuAuthority` +- `POST /casbin/getPolicyPathByAuthorityId` +- `POST /casbin/updateCasbin` + +### 菜单管理 + +- `POST /menu/getBaseMenuTree` +- `POST /menu/getMenuList` +- `POST /menu/addBaseMenu` +- `POST /menu/updateBaseMenu` +- `POST /menu/deleteBaseMenu` +- `POST /menu/getBaseMenuById` +- `GET /menu/getMenuRoles` +- `POST /menu/setMenuRoles` + +### API 管理 + +- `POST /api/getApiList` +- `POST /api/getAllApis` +- `POST /api/createApi` +- `POST /api/updateApi` +- `POST /api/deleteApi` +- `POST /api/getApiById` +- `GET /api/freshCasbin` +- `GET /api/getApiRoles` +- `POST /api/setApiRoles` + +### 字典与参数 + +- `GET /sysDictionary/getSysDictionaryList` +- `POST /sysDictionary/createSysDictionary` +- `PUT /sysDictionary/updateSysDictionary` +- `DELETE /sysDictionary/deleteSysDictionary` +- `GET /sysDictionaryDetail/getDictionaryTreeList` +- `POST /sysDictionaryDetail/createSysDictionaryDetail` +- `PUT /sysDictionaryDetail/updateSysDictionaryDetail` +- `DELETE /sysDictionaryDetail/deleteSysDictionaryDetail` +- `GET /sysParams/getSysParamsList` +- `POST /sysParams/createSysParams` +- `PUT /sysParams/updateSysParams` +- `DELETE /sysParams/deleteSysParams` + +### 审计与运维 + +- `GET /sysLoginLog/getLoginLogList` +- `DELETE /sysLoginLog/deleteLoginLog` +- `DELETE /sysLoginLog/deleteLoginLogByIds` +- `GET /sysOperationRecord/getSysOperationRecordList` +- `DELETE /sysOperationRecord/deleteSysOperationRecord` +- `DELETE /sysOperationRecord/deleteSysOperationRecordByIds` +- `POST /system/getSystemConfig` +- `POST /system/setSystemConfig` +- `POST /system/reloadSystem` +- `POST /system/getServerInfo` +- `POST /sysApiToken/getApiTokenList` +- `POST /sysApiToken/createApiToken` +- `POST /sysApiToken/deleteApiToken` +- `GET /sysError/getSysErrorList` +- `PUT /sysError/updateSysError` +- `DELETE /sysError/deleteSysError` +- `GET /sysError/getSysErrorSolution` + +## React 新后台覆盖情况 + +### 已做成可用页 + +- 登录 +- 仪表盘 +- 用户管理 +- 角色管理 +- 菜单管理 +- API 管理 +- 字典管理 +- 参数管理 +- 登录日志 +- 操作历史 +- 系统配置 +- API Token +- 错误日志 +- 个人中心 +- 服务器状态 + +### 已做成模块入口页 + +- 关于系统 +- 版本管理 +- 媒体库 +- 断点续传 +- 客户列表示例 +- 模板配置 +- 代码生成器 +- 自动化代码管理 +- 表单生成器 +- AI 需求工作流 +- 导出模板 +- Mcp Tools 管理 +- Mcp Tools 模板 +- Skills 管理 +- AI 页面绘制 +- 插件系统 +- 插件安装 +- 打包插件 +- 邮件插件 +- 公告管理 + +## 说明 + +- 新后台没有修改后端协议,仍然复用原有 token、菜单、权限和接口格式。 +- 当前仍有部分研发辅助类重模块保留为入口页,原因不是无法实现,而是这些模块交互面太大,适合单独拆阶段继续重构。 diff --git a/web-admin/eslint.config.js b/web-admin/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/web-admin/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/web-admin/index.html b/web-admin/index.html new file mode 100644 index 0000000..31aa859 --- /dev/null +++ b/web-admin/index.html @@ -0,0 +1,13 @@ + + + + + + + web-admin + + +
+ + + diff --git a/web-admin/package-lock.json b/web-admin/package-lock.json new file mode 100644 index 0000000..32d3439 --- /dev/null +++ b/web-admin/package-lock.json @@ -0,0 +1,4732 @@ +{ + "name": "web-admin", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-admin", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.1.1", + "@tanstack/react-query": "^5.96.2", + "antd": "^6.3.5", + "axios": "^1.14.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.20", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz", + "integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.14.0.tgz", + "integrity": "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.2.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.4.tgz", + "integrity": "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.0.tgz", + "integrity": "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.6.2", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.8.0.tgz", + "integrity": "sha512-Dr41bFevLB5NgVaJhEUmNvbEf+ynAhim6W98ZW2xvCsdFISc2TYP4ZvCVdie3eaZdum2kieVcvpNHu+UrzAAHA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.10.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", + "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", + "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/textarea": "~1.1.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", + "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", + "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz", + "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.1.tgz", + "integrity": "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz", + "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.1.tgz", + "integrity": "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", + "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/textarea": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", + "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.3.0.tgz", + "integrity": "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.2.4.tgz", + "integrity": "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.8.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.8.0.tgz", + "integrity": "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.2.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", + "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.0.tgz", + "integrity": "sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", + "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.96.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.2.tgz", + "integrity": "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.96.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.2.tgz", + "integrity": "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.96.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.5.tgz", + "integrity": "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.1.1", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.28.4", + "@rc-component/cascader": "~1.14.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.8.4", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.8.0", + "@rc-component/image": "~1.8.0", + "@rc-component/input": "~1.1.2", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.6.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.3.2", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~1.2.0", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.9.1", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.6.15", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.9.1", + "@rc-component/tabs": "~1.7.0", + "@rc-component/textarea": "~1.1.2", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.3.0", + "@rc-component/tree": "~1.2.4", + "@rc-component/tree-select": "~1.8.0", + "@rc-component/trigger": "^3.9.0", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.10.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz", + "integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/web-admin/package.json b/web-admin/package.json new file mode 100644 index 0000000..446f1d1 --- /dev/null +++ b/web-admin/package.json @@ -0,0 +1,39 @@ +{ + "name": "web-admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.1.1", + "@tanstack/react-query": "^5.96.2", + "antd": "^6.3.5", + "axios": "^1.14.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.20", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.14.0", + "recharts": "^3.8.1", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vite": "^8.0.4" + } +} diff --git a/web-admin/public/favicon.svg b/web-admin/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web-admin/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-admin/public/icons.svg b/web-admin/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web-admin/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-admin/src/App.tsx b/web-admin/src/App.tsx new file mode 100644 index 0000000..88138a2 --- /dev/null +++ b/web-admin/src/App.tsx @@ -0,0 +1,198 @@ +import { useEffect, useMemo, useState } from 'react' +import { HashRouter, Navigate, Outlet, Route, Routes, useLocation, useNavigate } from 'react-router-dom' +import { Alert, Result, Spin } from 'antd' +import { authApi, menuApi } from '@/lib/api' +import { buildFullMenus, findDefaultRoute, flattenMenus, isExternalMenu } from '@/lib/menu' +import { useAuthStore } from '@/store/auth' +import { LoginPage } from '@/features/auth/LoginPage' +import { AdminShell } from '@/features/layout/AdminShell' +import { DashboardPage } from '@/features/dashboard/DashboardPage' +import { ModuleLandingPage } from '@/features/discovery/ModuleLandingPage' +import { ProfilePage } from '@/features/person/ProfilePage' +import { ServerStatePage } from '@/features/server/ServerStatePage' +import { UserManagementPage } from '@/features/users/UserManagementPage' +import { RoleManagementPage } from '@/features/roles/RoleManagementPage' +import { MenuManagementPage } from '@/features/menus/MenuManagementPage' +import { ApiManagementPage } from '@/features/apis/ApiManagementPage' +import { DictionaryManagementPage } from '@/features/dictionaries/DictionaryManagementPage' +import { ParamsManagementPage } from '@/features/params/ParamsManagementPage' +import { LoginLogPage } from '@/features/logs/LoginLogPage' +import { OperationLogPage } from '@/features/logs/OperationLogPage' +import { SystemConfigPage } from '@/features/system/SystemConfigPage' +import { ApiTokenPage } from '@/features/tokens/ApiTokenPage' +import { ErrorLogPage } from '@/features/errors/ErrorLogPage' + +type AppRoute = { + path: string + menuName: string + element: React.ReactNode +} + +const appRoutes: AppRoute[] = [ + { path: 'dashboard', menuName: 'dashboard', element: }, + { path: 'about', menuName: 'about', element: }, + { path: 'admin', menuName: 'superAdmin', element: }, + { path: 'admin/authority', menuName: 'authority', element: }, + { path: 'admin/menu', menuName: 'menu', element: }, + { path: 'admin/api', menuName: 'api', element: }, + { path: 'admin/user', menuName: 'user', element: }, + { path: 'admin/dictionary', menuName: 'dictionary', element: }, + { path: 'admin/operation', menuName: 'operation', element: }, + { path: 'admin/sysParams', menuName: 'sysParams', element: }, + { path: 'admin/system', menuName: 'system', element: }, + { path: 'admin/apiToken', menuName: 'apiToken', element: }, + { path: 'admin/loginLog', menuName: 'loginLog', element: }, + { path: 'admin/sysVersion', menuName: 'sysVersion', element: }, + { path: 'admin/sysError', menuName: 'sysError', element: }, + { path: 'common', menuName: 'common', element: }, + { path: 'common/upload', menuName: 'upload', element: }, + { path: 'example', menuName: 'example', element: }, + { path: 'example/breakpoint', menuName: 'breakpoint', element: }, + { path: 'example/customer', menuName: 'customer', element: }, + { path: 'systemTools', menuName: 'systemTools', element: }, + { path: 'systemTools/autoPkg', menuName: 'autoPkg', element: }, + { path: 'systemTools/autoCode', menuName: 'autoCode', element: }, + { path: 'systemTools/autoCodeAdmin', menuName: 'autoCodeAdmin', element: }, + { path: 'systemTools/formCreate', menuName: 'formCreate', element: }, + { path: 'systemTools/aiWorkflow', menuName: 'aiWorkflow', element: }, + { path: 'systemTools/exportTemplate', menuName: 'exportTemplate', element: }, + { path: 'systemTools/mcpTest', menuName: 'mcpTest', element: }, + { path: 'systemTools/mcpTool', menuName: 'mcpTool', element: }, + { path: 'systemTools/skills', menuName: 'skills', element: }, + { path: 'systemTools/picture', menuName: 'picture', element: }, + { path: 'systemTools/autoCodeEdit/:id', menuName: 'autoCodeEdit', element: }, + { path: 'person', menuName: 'person', element: }, + { path: 'state', menuName: 'state', element: }, +] + +function BootstrapGate() { + const navigate = useNavigate() + const location = useLocation() + const token = useAuthStore((state) => state.token) + const menus = useAuthStore((state) => state.menus) + const setUser = useAuthStore((state) => state.setUser) + const setMenus = useAuthStore((state) => state.setMenus) + const clearSession = useAuthStore((state) => state.clearSession) + const [loading, setLoading] = useState(true) + const [loadError, setLoadError] = useState(null) + + useEffect(() => { + let ignore = false + + const bootstrap = async () => { + if (!token) { + navigate('/login', { replace: true, state: { redirectTo: location.pathname } }) + return + } + + try { + setLoading(true) + const [userInfo, menuRes] = await Promise.all([authApi.getUserInfo(), menuApi.getMenu()]) + if (ignore) { + return + } + + setUser(userInfo.data.userInfo) + setMenus(buildFullMenus(menuRes.data.menus)) + setLoadError(null) + } catch (error) { + if (ignore) { + return + } + const message = error instanceof Error ? error.message : '会话恢复失败' + clearSession() + setLoadError(message) + navigate('/login', { replace: true }) + } finally { + if (!ignore) { + setLoading(false) + } + } + } + + bootstrap() + + return () => { + ignore = true + } + }, [clearSession, location.pathname, navigate, setMenus, setUser, token]) + + if (!token) { + return null + } + + if (loading && menus.length === 0) { + return ( +
+ + 正在恢复管理台会话... +
+ ) + } + + if (loadError) { + return ( +
+ +
+ ) + } + + return +} + +function RouteGuard({ route }: { route: AppRoute }) { + const menus = useAuthStore((state) => state.menus) + const allowedPaths = useMemo( + () => + new Set( + flattenMenus(menus) + .filter((item) => !isExternalMenu(item)) + .map((item) => item.fullPath), + ), + [menus], + ) + + if (!allowedPaths.has(`/${route.path}`)) { + return ( + + ) + } + + return <>{route.element} +} + +function LayoutFrame() { + const menus = useAuthStore((state) => state.menus) + const user = useAuthStore((state) => state.user) + const defaultPath = findDefaultRoute(menus, user?.authority?.defaultRouter) + + return ( + + + } /> + {appRoutes.map((route) => ( + } /> + ))} + } /> + + + ) +} + +export default function App() { + return ( + + + } /> + }> + } /> + + + + ) +} diff --git a/web-admin/src/assets/hero.png b/web-admin/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/web-admin/src/assets/hero.png differ diff --git a/web-admin/src/assets/react.svg b/web-admin/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web-admin/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-admin/src/assets/vite.svg b/web-admin/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web-admin/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web-admin/src/features/apis/ApiManagementPage.tsx b/web-admin/src/features/apis/ApiManagementPage.tsx new file mode 100644 index 0000000..af04fc1 --- /dev/null +++ b/web-admin/src/features/apis/ApiManagementPage.tsx @@ -0,0 +1,318 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { + Button, + Card, + Col, + Drawer, + Form, + Input, + Modal, + Row, + Select, + Space, + Table, + Tag, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { apiRegistryApi, authorityApi } from '@/lib/api' +import { flattenAuthorities } from '@/lib/tree' +import type { ApiRecord, Authority } from '@/types/system' + +const methodOptions = ['GET', 'POST', 'PUT', 'DELETE'] + +export function ApiManagementPage() { + const [searchForm] = Form.useForm() + const [editForm] = Form.useForm() + const [apis, setApis] = useState([]) + const [roles, setRoles] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [total, setTotal] = useState(0) + const [modalOpen, setModalOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [editingApi, setEditingApi] = useState(null) + const [drawerOpen, setDrawerOpen] = useState(false) + const [activeApi, setActiveApi] = useState(null) + const [selectedRoles, setSelectedRoles] = useState([]) + const [savingRoles, setSavingRoles] = useState(false) + + const roleOptions = useMemo( + () => + flattenAuthorities(roles).map((item) => ({ + label: `${' '.repeat(item.depth)}${item.authorityName} (${item.authorityId})`, + value: item.authorityId, + })), + [roles], + ) + + const apiGroupOptions = useMemo( + () => + Array.from(new Set(apis.map((item) => item.apiGroup))) + .filter(Boolean) + .map((group) => ({ + label: group, + value: group, + })), + [apis], + ) + + const reloadApis = useCallback(async () => { + setLoading(true) + try { + const [apiRes, roleRes] = await Promise.all([ + apiRegistryApi.getApiList({ + page, + pageSize, + ...searchForm.getFieldsValue(), + }), + authorityApi.getAuthorityList(), + ]) + setApis(apiRes.data.list) + setTotal(apiRes.data.total) + setRoles(roleRes.data) + } finally { + setLoading(false) + } + }, [page, pageSize, searchForm]) + + useEffect(() => { + reloadApis() + }, [reloadApis]) + + const openCreate = () => { + setEditingApi(null) + editForm.resetFields() + editForm.setFieldsValue({ method: 'POST' }) + setModalOpen(true) + } + + const openEdit = async (record: ApiRecord) => { + const response = await apiRegistryApi.getApiById(record.ID) + setEditingApi(response.data.api) + editForm.setFieldsValue(response.data.api) + setModalOpen(true) + } + + const saveApi = async () => { + const values = await editForm.validateFields() + setSaving(true) + try { + if (editingApi) { + await apiRegistryApi.updateApi({ + ID: editingApi.ID, + ...values, + }) + message.success('接口已更新') + } else { + await apiRegistryApi.createApi(values) + message.success('接口已创建') + } + setModalOpen(false) + reloadApis() + } finally { + setSaving(false) + } + } + + const deleteApi = (record: ApiRecord) => { + Modal.confirm({ + title: `删除接口 ${record.path}`, + okButtonProps: { danger: true }, + onOk: async () => { + await apiRegistryApi.deleteApi({ ID: record.ID }) + message.success('接口已删除') + reloadApis() + }, + }) + } + + const openRoleDrawer = async (record: ApiRecord) => { + const response = await apiRegistryApi.getApiRoles(record.path, record.method) + setActiveApi(record) + setSelectedRoles(response.data) + setDrawerOpen(true) + } + + const saveRoles = async () => { + if (!activeApi) { + return + } + setSavingRoles(true) + try { + await apiRegistryApi.setApiRoles({ + path: activeApi.path, + method: activeApi.method, + authorityIds: selectedRoles, + }) + message.success('接口角色关系已更新') + setDrawerOpen(false) + } finally { + setSavingRoles(false) + } + } + + const columns: ColumnsType = [ + { title: 'ID', dataIndex: 'ID', width: 80 }, + { title: '路径', dataIndex: 'path', width: 260 }, + { title: '分组', dataIndex: 'apiGroup', width: 160 }, + { title: '描述', dataIndex: 'description', width: 240 }, + { + title: '方法', + dataIndex: 'method', + width: 100, + render: (value: string) => {value}, + }, + { + title: '操作', + key: 'actions', + width: 220, + fixed: 'right', + render: (_, record) => ( + + + + + + ), + }, + ] + + return ( +
+ +
+
+ + API 管理 + + + 当前页覆盖接口目录、接口元数据和接口角色关系。 + +
+ + + + +
+
{ setPage(1); reloadApis() }}> + + + + + + + + + + + + + + ({ label: item, value: item }))} /> + + + + + + + +
+
+ + { + setPage(nextPage) + setPageSize(nextPageSize) + }, + }} + /> + + + setModalOpen(false)} + onOk={saveApi} + confirmLoading={saving} + > +
+ + + + + + + + + + + + + + ) +} diff --git a/web-admin/src/features/auth/LoginPage.tsx b/web-admin/src/features/auth/LoginPage.tsx new file mode 100644 index 0000000..ceb6fd9 --- /dev/null +++ b/web-admin/src/features/auth/LoginPage.tsx @@ -0,0 +1,183 @@ +import { useEffect, useMemo, useState } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { Alert, Button, Card, Form, Input, Typography, message } from 'antd' +import { authApi, menuApi } from '@/lib/api' +import { buildFullMenus, findDefaultRoute } from '@/lib/menu' +import { useAuthStore } from '@/store/auth' +import type { CaptchaInfo } from '@/types/system' + +type LoginForm = { + username: string + password: string + captcha?: string +} + +function normalizeCaptchaSrc(picPath?: string) { + if (!picPath) { + return '' + } + + if (picPath.startsWith('data:image')) { + return picPath + } + + return `data:image/png;base64,${picPath}` +} + +export function LoginPage() { + const navigate = useNavigate() + const location = useLocation() + const applySession = useAuthStore((state) => state.applySession) + const setMenus = useAuthStore((state) => state.setMenus) + const setUser = useAuthStore((state) => state.setUser) + const [form] = Form.useForm() + const [captcha, setCaptcha] = useState(null) + const [submitting, setSubmitting] = useState(false) + const [loadCaptchaError, setLoadCaptchaError] = useState(null) + + const redirectTarget = useMemo(() => { + const state = location.state as { redirectTo?: string } | null + return state?.redirectTo + }, [location.state]) + + const fetchCaptcha = async () => { + try { + const response = await authApi.getCaptcha() + setCaptcha(response.data) + setLoadCaptchaError(null) + } catch (error) { + const messageText = error instanceof Error ? error.message : '验证码加载失败' + setLoadCaptchaError(messageText) + } + } + + useEffect(() => { + fetchCaptcha() + }, []) + + const submit = async () => { + const values = await form.validateFields() + setSubmitting(true) + try { + const loginResponse = await authApi.login({ + username: values.username, + password: values.password, + captcha: values.captcha, + captchaId: captcha?.captchaId, + }) + + applySession({ + token: loginResponse.data.token, + user: loginResponse.data.user, + }) + + const [userInfo, menuRes] = await Promise.all([authApi.getUserInfo(), menuApi.getMenu()]) + const menus = buildFullMenus(menuRes.data.menus) + setUser(userInfo.data.userInfo) + setMenus(menus) + message.success('登录成功') + + const fallback = findDefaultRoute(menus, userInfo.data.userInfo.authority?.defaultRouter) || '/dashboard' + navigate(redirectTarget || fallback, { replace: true }) + } catch { + form.setFieldValue('captcha', '') + fetchCaptcha() + } finally { + setSubmitting(false) + } + } + + return ( +
+
+ Gin-Vue-Admin · React 重设计版 +
+ + 把现有后端协议 +
+ 接成一套更清晰的管理后台 +
+ + 当前登录页兼容原有验证码与 JWT 流程。登录成功后会直接拉取真实菜单树,按当前角色动态生成可访问页面。 + +
+
+
+ + + 登录管理后台 + + + 首版默认沿用原系统认证协议,避免后端改造。 + + {loadCaptchaError ? ( + + 重试 + + } + style={{ marginBottom: 16 }} + /> + ) : null} + + + + + + + + {captcha?.openCaptcha ? ( + +
+ +
+ captcha +
+
+
+ ) : null} + + +
+
+
+ ) +} diff --git a/web-admin/src/features/dashboard/DashboardPage.tsx b/web-admin/src/features/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..4bb47d6 --- /dev/null +++ b/web-admin/src/features/dashboard/DashboardPage.tsx @@ -0,0 +1,123 @@ +import { Card, Col, List, Progress, Row, Statistic, Tag, Typography } from 'antd' +import { moduleList } from '@/features/discovery/systemInventory' +import { flattenMenus } from '@/lib/menu' +import { useAuthStore } from '@/store/auth' + +export function DashboardPage() { + const menus = useAuthStore((state) => state.menus) + const user = useAuthStore((state) => state.user) + + const flatMenus = flattenMenus(menus) + const implementedCount = moduleList.filter((item) => item.status === 'ready').length + const partialCount = moduleList.filter((item) => item.status === 'partial').length + const allowedModules = flatMenus.length + const allEndpoints = moduleList.reduce((total, item) => total + item.endpoints.length, 0) + + return ( +
+ +
+
+ + React 管理后台重构中 + + + {user?.nickName || user?.userName || '管理员'},欢迎回来 + + + 新后台已经基于真实菜单树和后端接口协议接管路由结构。当前仪表盘会直接反映已有菜单权限、模块覆盖进度和需要继续迁移的能力。 + + +
+ + + + + + + + + + + + + 重构覆盖率 + + {item}} + /> + + + +
+
+ 模块总数 + + {moduleList.length} + +
+
+ 已接入 + + {implementedCount} + +
+
+ 待继续 + + {partialCount} + +
+
+ 已登记接口触点 + + {allEndpoints} + +
+
+ +
+ {moduleList.map((item) => ( +
+
+ + {item.title} + + +
+ + {item.summary} + + {item.group} + {item.endpoints.length} 个接口触点 +
+ ))} +
+
+ + !item.hidden)} + renderItem={(item) => ( + + + + )} + /> + + + ) +} diff --git a/web-admin/src/features/dictionaries/DictionaryManagementPage.tsx b/web-admin/src/features/dictionaries/DictionaryManagementPage.tsx new file mode 100644 index 0000000..4b1fee8 --- /dev/null +++ b/web-admin/src/features/dictionaries/DictionaryManagementPage.tsx @@ -0,0 +1,334 @@ +import { useCallback, useEffect, useState } from 'react' +import { + Button, + Card, + Col, + Form, + Input, + InputNumber, + Modal, + Row, + Select, + Space, + Switch, + Table, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { dictionaryApi } from '@/lib/api' +import type { Dictionary, DictionaryDetail } from '@/types/system' + +export function DictionaryManagementPage() { + const [dictForm] = Form.useForm() + const [detailForm] = Form.useForm() + const [dictionaries, setDictionaries] = useState([]) + const [activeDictionary, setActiveDictionary] = useState(null) + const [details, setDetails] = useState([]) + const [loading, setLoading] = useState(false) + const [dictModalOpen, setDictModalOpen] = useState(false) + const [detailModalOpen, setDetailModalOpen] = useState(false) + const [editingDictionary, setEditingDictionary] = useState(null) + const [editingDetail, setEditingDetail] = useState(null) + + const reloadDictionaries = useCallback(async () => { + setLoading(true) + try { + const response = await dictionaryApi.getDictionaryList() + const list = response.data + setDictionaries(list) + if (!activeDictionary && list.length) { + setActiveDictionary(list[0]) + } + } finally { + setLoading(false) + } + }, [activeDictionary]) + + const reloadDetails = useCallback(async (dictionary?: Dictionary | null) => { + const target = dictionary || activeDictionary + if (!target) { + setDetails([]) + return + } + + const response = await dictionaryApi.getDictionaryTree(target.ID) + setDetails(response.data.list) + }, [activeDictionary]) + + useEffect(() => { + reloadDictionaries() + }, [reloadDictionaries]) + + useEffect(() => { + reloadDetails(activeDictionary) + }, [activeDictionary, reloadDetails]) + + const openCreateDictionary = () => { + setEditingDictionary(null) + dictForm.resetFields() + dictForm.setFieldsValue({ status: true }) + setDictModalOpen(true) + } + + const openEditDictionary = (record: Dictionary) => { + setEditingDictionary(record) + dictForm.setFieldsValue({ + name: record.name, + type: record.type, + desc: record.desc, + status: record.status ?? true, + parentID: record.parentID, + }) + setDictModalOpen(true) + } + + const saveDictionary = async () => { + const values = await dictForm.validateFields() + const payload = { + ID: editingDictionary?.ID, + ...values, + } + + if (editingDictionary) { + await dictionaryApi.updateDictionary(payload) + message.success('字典已更新') + } else { + await dictionaryApi.createDictionary(payload) + message.success('字典已创建') + } + setDictModalOpen(false) + reloadDictionaries() + } + + const deleteDictionary = (record: Dictionary) => { + Modal.confirm({ + title: `删除字典 ${record.name}`, + okButtonProps: { danger: true }, + onOk: async () => { + await dictionaryApi.deleteDictionary({ ID: record.ID }) + message.success('字典已删除') + if (activeDictionary?.ID === record.ID) { + setActiveDictionary(null) + } + reloadDictionaries() + }, + }) + } + + const openCreateDetail = () => { + if (!activeDictionary) { + message.warning('请先选择字典') + return + } + setEditingDetail(null) + detailForm.resetFields() + detailForm.setFieldsValue({ + sysDictionaryID: activeDictionary.ID, + status: true, + sort: 1, + }) + setDetailModalOpen(true) + } + + const openEditDetail = (record: DictionaryDetail) => { + setEditingDetail(record) + detailForm.setFieldsValue({ + label: record.label, + value: record.value, + extend: record.extend, + status: record.status ?? true, + sort: record.sort, + parentID: record.parentID, + }) + setDetailModalOpen(true) + } + + const saveDetail = async () => { + if (!activeDictionary) { + return + } + const values = await detailForm.validateFields() + const payload = { + ID: editingDetail?.ID, + sysDictionaryID: activeDictionary.ID, + ...values, + } + if (editingDetail) { + await dictionaryApi.updateDictionaryDetail(payload) + message.success('字典项已更新') + } else { + await dictionaryApi.createDictionaryDetail(payload) + message.success('字典项已创建') + } + setDetailModalOpen(false) + reloadDetails() + } + + const deleteDetail = (record: DictionaryDetail) => { + Modal.confirm({ + title: `删除字典项 ${record.label}`, + okButtonProps: { danger: true }, + onOk: async () => { + await dictionaryApi.deleteDictionaryDetail({ ID: record.ID }) + message.success('字典项已删除') + reloadDetails() + }, + }) + } + + const detailColumns: ColumnsType = [ + { title: '展示值', dataIndex: 'label', width: 180 }, + { title: '字典值', dataIndex: 'value', width: 180 }, + { title: '扩展值', dataIndex: 'extend', width: 180 }, + { title: '排序', dataIndex: 'sort', width: 80 }, + { + title: '状态', + width: 100, + render: (_, record) => (record.status ? '启用' : '停用'), + }, + { + title: '操作', + key: 'actions', + width: 180, + render: (_, record) => ( + + + + + ), + }, + ] + + return ( +
+ +
+
+ + 字典管理 + + + 当前页覆盖字典主表和树形字典项管理。 + +
+ + + + +
+ +
+ + + {dictionaries.map((item) => ( + setActiveDictionary(item)} + style={{ + borderColor: activeDictionary?.ID === item.ID ? '#d16f3f' : undefined, + }} + extra={ + + + + + } + > + {item.name} +
{item.type}
+
+ ))} +
+
+ + + +
+ + + + + + setDictModalOpen(false)} + onOk={saveDictionary} + > +
+ + + + + + + + + + + + + +
+ + setDetailModalOpen(false)} + onOk={saveDetail} + > +
+ + + + + + + + + + + + + + + + +
+ + ) +} diff --git a/web-admin/src/features/discovery/ModuleLandingPage.tsx b/web-admin/src/features/discovery/ModuleLandingPage.tsx new file mode 100644 index 0000000..29dad49 --- /dev/null +++ b/web-admin/src/features/discovery/ModuleLandingPage.tsx @@ -0,0 +1,69 @@ +import { Card, List, Tag, Typography } from 'antd' +import { moduleCatalog } from './systemInventory' + +const statusLabel = { + ready: '已接入', + partial: '待继续', + planned: '待迁移', +} + +type Props = { + moduleName: string +} + +export function ModuleLandingPage({ moduleName }: Props) { + const moduleInfo = moduleCatalog[moduleName] + + if (!moduleInfo) { + return ( +
+ + 未登记模块 + + 当前路由已经进入新后台,但模块注册表还没有补充它的功能描述。 + + +
+ ) + } + + return ( +
+ +
+
+ + {moduleInfo.title} + + + {moduleInfo.summary} + +
+ + {statusLabel[moduleInfo.status]} + +
+
+
+ + {item}} + /> + + + ( + + {item} + + )} + locale={{ emptyText: '当前页面没有直接绑定接口。' }} + /> + +
+
+ ) +} diff --git a/web-admin/src/features/discovery/systemInventory.ts b/web-admin/src/features/discovery/systemInventory.ts new file mode 100644 index 0000000..0db652b --- /dev/null +++ b/web-admin/src/features/discovery/systemInventory.ts @@ -0,0 +1,303 @@ +import type { ModuleDescriptor } from '@/types/system' + +export const moduleCatalog: Record = { + dashboard: { + name: 'dashboard', + title: '仪表盘', + group: '总览', + status: 'ready', + summary: '展示系统结构、路由覆盖情况和后端模块分布。', + features: ['菜单覆盖总览', 'API 分组概览', '已重构模块状态', '快捷入口'], + endpoints: ['/menu/getMenu', '/user/getUserInfo', '/system/getServerInfo'], + }, + about: { + name: 'about', + title: '关于系统', + group: '总览', + status: 'partial', + summary: '用于展示项目来源、版本背景和运维提示。', + features: ['项目说明', '技术栈梳理', '外部链接入口'], + endpoints: [], + }, + superAdmin: { + name: 'superAdmin', + title: '超级管理员', + group: '平台治理', + status: 'partial', + summary: '聚合用户、角色、菜单、接口、字典和参数等平台治理功能。', + features: ['权限治理', '配置管理', '审计日志', 'Token 管理'], + endpoints: ['/authority/getAuthorityList', '/menu/getBaseMenuTree', '/api/getApiList'], + }, + authority: { + name: 'authority', + title: '角色管理', + group: '平台治理', + status: 'ready', + summary: '管理角色树、默认首页、菜单权限、API 权限和角色用户关系。', + features: ['角色树', '菜单授权', 'API 授权', '角色分配用户'], + endpoints: ['/authority/getAuthorityList', '/menu/addMenuAuthority', '/casbin/updateCasbin', '/authority/setRoleUsers'], + }, + menu: { + name: 'menu', + title: '菜单管理', + group: '平台治理', + status: 'ready', + summary: '维护后台菜单树、路由属性、按钮定义和角色关联。', + features: ['菜单树编辑', '路由路径维护', '角色分配', '按钮权限入口'], + endpoints: ['/menu/getBaseMenuTree', '/menu/addBaseMenu', '/menu/updateBaseMenu', '/menu/setMenuRoles'], + }, + api: { + name: 'api', + title: 'API 管理', + group: '平台治理', + status: 'ready', + summary: '管理接口目录、接口角色授权和 Casbin 刷新。', + features: ['接口列表', '接口增删改', '角色授权', '接口分组'], + endpoints: ['/api/getApiList', '/api/createApi', '/api/updateApi', '/api/setApiRoles'], + }, + user: { + name: 'user', + title: '用户管理', + group: '平台治理', + status: 'ready', + summary: '管理账号、启停状态、角色集合、密码和头像等资料。', + features: ['用户列表', '新建用户', '重置密码', '角色切换'], + endpoints: ['/user/getUserList', '/user/admin_register', '/user/setUserInfo', '/user/resetPassword'], + }, + dictionary: { + name: 'dictionary', + title: '字典管理', + group: '平台治理', + status: 'ready', + summary: '管理字典主表与树形字典项,支持导入导出。', + features: ['字典列表', '树形详情', '导入导出', '状态控制'], + endpoints: ['/sysDictionary/getSysDictionaryList', '/sysDictionary/exportSysDictionary', '/sysDictionaryDetail/getDictionaryTreeList'], + }, + operation: { + name: 'operation', + title: '操作历史', + group: '审计', + status: 'ready', + summary: '查看系统请求日志、响应体和异常信息。', + features: ['操作审计', '请求明细', '批量删除'], + endpoints: ['/sysOperationRecord/getSysOperationRecordList', '/sysOperationRecord/deleteSysOperationRecordByIds'], + }, + sysParams: { + name: 'sysParams', + title: '参数管理', + group: '平台治理', + status: 'ready', + summary: '管理运行期参数键值,供前后端按键读取。', + features: ['参数列表', '键值维护', '说明管理'], + endpoints: ['/sysParams/getSysParamsList', '/sysParams/createSysParams', '/sysParams/updateSysParams'], + }, + system: { + name: 'system', + title: '系统配置', + group: '运维', + status: 'ready', + summary: '编辑服务配置文件并触发系统重载。', + features: ['配置读取', '配置保存', '服务重载'], + endpoints: ['/system/getSystemConfig', '/system/setSystemConfig', '/system/reloadSystem'], + }, + apiToken: { + name: 'apiToken', + title: 'API Token', + group: '运维', + status: 'ready', + summary: '为用户签发临时 JWT,并追踪失效状态和过期时间。', + features: ['Token 列表', '签发 Token', '作废 Token'], + endpoints: ['/sysApiToken/getApiTokenList', '/sysApiToken/createApiToken', '/sysApiToken/deleteApiToken'], + }, + loginLog: { + name: 'loginLog', + title: '登录日志', + group: '审计', + status: 'ready', + summary: '审计登录成功与失败事件,定位登录问题。', + features: ['登录结果', '失败原因', '设备信息'], + endpoints: ['/sysLoginLog/getLoginLogList', '/sysLoginLog/deleteLoginLogByIds'], + }, + sysVersion: { + name: 'sysVersion', + title: '版本管理', + group: '运维', + status: 'partial', + summary: '围绕版本导出、导入和回滚的发布管理模块。', + features: ['版本列表', '版本导出', '版本同步'], + endpoints: ['/sysVersion/getSysVersionList', '/sysVersion/exportVersion', '/sysVersion/importVersion'], + }, + sysError: { + name: 'sysError', + title: '错误日志', + group: '审计', + status: 'ready', + summary: '管理系统错误记录和 AI 处理状态。', + features: ['错误列表', '状态流转', 'AI 处理触发'], + endpoints: ['/sysError/getSysErrorList', '/sysError/getSysErrorSolution', '/sysError/updateSysError'], + }, + common: { + name: 'common', + title: '公共能力', + group: '公共模块', + status: 'partial', + summary: '承载多端复用的公共功能,不再挂在示例目录下。', + features: ['公共上传能力', '后续 app 复用入口'], + endpoints: ['/fileUploadAndDownload/getFileList', '/attachmentCategory/getCategoryList'], + }, + example: { + name: 'example', + title: '示例文件', + group: '业务示例', + status: 'partial', + summary: '保留和业务演示直接相关的示例页面。', + features: ['断点续传', '客户资源示例'], + endpoints: ['/customer/customerList'], + }, + upload: { + name: 'upload', + title: '媒体库', + group: '公共模块', + status: 'partial', + summary: '管理上传文件和资源分类,后续可直接复用到 app 模块。', + features: ['文件列表', '文件上传', '分类管理'], + endpoints: ['/fileUploadAndDownload/getFileList', '/attachmentCategory/getCategoryList'], + }, + breakpoint: { + name: 'breakpoint', + title: '断点续传', + group: '业务示例', + status: 'partial', + summary: '展示大文件切片上传流程。', + features: ['文件校验', '分片上传', '合并完成'], + endpoints: ['/fileUploadAndDownload/findFile', '/fileUploadAndDownload/breakpointContinue'], + }, + customer: { + name: 'customer', + title: '客户列表', + group: '业务示例', + status: 'partial', + summary: '示例业务资源表,用于展示普通 CRUD 模式。', + features: ['客户列表', '增删改查'], + endpoints: ['/customer/customerList', '/customer/customer'], + }, + systemTools: { + name: 'systemTools', + title: '编程辅助', + group: '研发辅助', + status: 'partial', + summary: '聚合代码生成、模板配置、技能管理和 AI 工作流。', + features: ['代码生成器', '模板配置', 'Skills 管理', 'AI 页面绘制'], + endpoints: ['/autoCode/getDB', '/skills/getSkillList'], + }, + autoPkg: { + name: 'autoPkg', + title: '模板配置', + group: '研发辅助', + status: 'partial', + summary: '维护自动代码生成模板。', + features: ['模板列表', '模板文件'], + endpoints: ['/autoCode/createPackage', '/autoCode/getPackage', '/autoCode/getTemplates'], + }, + autoCode: { + name: 'autoCode', + title: '代码生成器', + group: '研发辅助', + status: 'partial', + summary: '自动生成表单、接口、服务和路由代码。', + features: ['库表发现', '代码预览', '代码生成'], + endpoints: ['/autoCode/getDB', '/autoCode/getTables', '/autoCode/preview', '/autoCode/createTemp'], + }, + autoCodeAdmin: { + name: 'autoCodeAdmin', + title: '自动化代码管理', + group: '研发辅助', + status: 'partial', + summary: '管理自动化代码任务和函数模板。', + features: ['任务管理', '模板方法'], + endpoints: ['/autoCode/getMeta', '/autoCode/addFunc'], + }, + formCreate: { + name: 'formCreate', + title: '表单生成器', + group: '研发辅助', + status: 'partial', + summary: '拖拽式表单构建入口。', + features: ['表单设计'], + endpoints: [], + }, + aiWorkflow: { + name: 'aiWorkflow', + title: 'AI 需求工作流', + group: '研发辅助', + status: 'partial', + summary: '管理 AI 会话和需求工作流结果。', + features: ['会话列表', '会话详情', 'Markdown 落盘'], + endpoints: ['/autoCode/getAIWorkflowSessionList', '/autoCode/getAIWorkflowSessionDetail'], + }, + exportTemplate: { + name: 'exportTemplate', + title: '导出模板', + group: '研发辅助', + status: 'partial', + summary: '维护 Excel 导入导出模板。', + features: ['模板管理', 'SQL 预览'], + endpoints: ['/sysExportTemplate/getSysExportTemplateList', '/sysExportTemplate/previewSQL'], + }, + mcpTest: { + name: 'mcpTest', + title: 'Mcp Tools 管理', + group: '研发辅助', + status: 'partial', + summary: '围绕 MCP Tool 的状态、测试和服务控制。', + features: ['服务状态', '测试调用'], + endpoints: ['/autoCode/mcpStatus', '/autoCode/mcpTest', '/autoCode/mcpList'], + }, + mcpTool: { + name: 'mcpTool', + title: 'Mcp Tools 模板', + group: '研发辅助', + status: 'partial', + summary: '生成 MCP Tool 模板代码。', + features: ['模板生成'], + endpoints: ['/autoCode/mcp'], + }, + skills: { + name: 'skills', + title: 'Skills 管理', + group: '研发辅助', + status: 'partial', + summary: '管理技能定义、脚本、资源、模板和全局约束。', + features: ['技能列表', '脚本资源管理', '技能打包'], + endpoints: ['/skills/getSkillList', '/skills/saveSkill', '/skills/packageSkill'], + }, + picture: { + name: 'picture', + title: 'AI 页面绘制', + group: '研发辅助', + status: 'partial', + summary: '基于 AI 描述生成页面草图。', + features: ['图像工作台'], + endpoints: ['/autoCode/llmAuto'], + }, + person: { + name: 'person', + title: '个人中心', + group: '个人', + status: 'ready', + summary: '维护当前登录人的资料与密码。', + features: ['资料更新', '密码修改'], + endpoints: ['/user/setSelfInfo', '/user/changePassword'], + }, + state: { + name: 'state', + title: '服务器状态', + group: '运维', + status: 'ready', + summary: '读取后端运行状态与资源信息。', + features: ['原始状态树', '运行信息'], + endpoints: ['/system/getServerInfo'], + }, +} + +export const moduleList = Object.values(moduleCatalog) diff --git a/web-admin/src/features/errors/ErrorLogPage.tsx b/web-admin/src/features/errors/ErrorLogPage.tsx new file mode 100644 index 0000000..da19c6a --- /dev/null +++ b/web-admin/src/features/errors/ErrorLogPage.tsx @@ -0,0 +1,184 @@ +import { useCallback, useEffect, useState } from 'react' +import { + Button, + Card, + Form, + Input, + Modal, + Select, + Space, + Table, + Tag, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { sysErrorApi } from '@/lib/api' +import { formatDate } from '@/lib/date' +import type { SysErrorRecord } from '@/types/system' + +export function ErrorLogPage() { + const [editForm] = Form.useForm() + const [rows, setRows] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [total, setTotal] = useState(0) + const [editingRow, setEditingRow] = useState(null) + + const reloadRows = useCallback(async () => { + setLoading(true) + try { + const response = await sysErrorApi.getErrorList({ + page, + pageSize, + }) + setRows(response.data.list) + setTotal(response.data.total) + } finally { + setLoading(false) + } + }, [page, pageSize]) + + useEffect(() => { + reloadRows() + }, [reloadRows]) + + const saveRow = async () => { + const values = await editForm.validateFields() + if (!editingRow) { + return + } + await sysErrorApi.updateError({ + ...editingRow, + ...values, + }) + message.success('错误日志已更新') + setEditingRow(null) + reloadRows() + } + + const columns: ColumnsType = [ + { + title: '时间', + width: 180, + render: (_, record) => formatDate(record.CreatedAt), + }, + { + title: '来源', + dataIndex: 'form', + width: 180, + }, + { + title: '等级', + dataIndex: 'level', + width: 100, + }, + { + title: '状态', + width: 120, + render: (_, record) => {record.status || '未处理'}, + }, + { + title: '错误内容', + dataIndex: 'info', + }, + { + title: '解决方案', + dataIndex: 'solution', + }, + { + title: '操作', + width: 220, + render: (_, record) => ( + + + + + + ), + }, + ] + + return ( +
+ +
+
+ + 错误日志 + + + 当前页提供错误列表、状态更新和 AI 处理触发入口。 + +
+
+
+ +
{ + setPage(nextPage) + setPageSize(nextPageSize) + }, + }} + /> + + + setEditingRow(null)} onOk={saveRow}> +
+ + ({ + label: `${item.authorityName} (${item.authorityId})`, + value: item.authorityId, + }))} + style={{ minWidth: 220 }} + onChange={handleRoleSwitch} + /> + ) : null} + navigate('/person'), + }, + { + key: 'logout', + label: '退出登录', + danger: true, + onClick: handleLogout, + }, + ], + }} + > + + + + + + {children} + + + + ) +} diff --git a/web-admin/src/features/menus/MenuManagementPage.tsx b/web-admin/src/features/menus/MenuManagementPage.tsx new file mode 100644 index 0000000..3e051fe --- /dev/null +++ b/web-admin/src/features/menus/MenuManagementPage.tsx @@ -0,0 +1,337 @@ +import { useEffect, useMemo, useState } from 'react' +import { + Alert, + Button, + Card, + Drawer, + Form, + Input, + InputNumber, + Modal, + Select, + Space, + Switch, + Table, + Tag, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { authorityApi, menuApi } from '@/lib/api' +import { flattenAuthorities, flattenMenusForOptions } from '@/lib/tree' +import type { Authority, MenuNode } from '@/types/system' + +export function MenuManagementPage() { + const [form] = Form.useForm() + const [menus, setMenus] = useState([]) + const [roles, setRoles] = useState([]) + const [loading, setLoading] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [editingMenu, setEditingMenu] = useState(null) + const [drawerOpen, setDrawerOpen] = useState(false) + const [activeMenu, setActiveMenu] = useState(null) + const [selectedRoles, setSelectedRoles] = useState([]) + const [defaultRouterRoles, setDefaultRouterRoles] = useState([]) + const [savingRoles, setSavingRoles] = useState(false) + + const roleOptions = useMemo( + () => + flattenAuthorities(roles).map((item) => ({ + label: `${' '.repeat(item.depth)}${item.authorityName} (${item.authorityId})`, + value: item.authorityId, + })), + [roles], + ) + + const menuOptions = useMemo( + () => + flattenMenusForOptions(menus).map((item) => ({ + label: `${' '.repeat(item.depth)}${item.meta.title}`, + value: item.ID, + })), + [menus], + ) + + const reloadMenus = async () => { + setLoading(true) + try { + const [menuRes, roleRes] = await Promise.all([menuApi.getBaseMenuTree(), authorityApi.getAuthorityList()]) + setMenus(menuRes.data.menus) + setRoles(roleRes.data) + } finally { + setLoading(false) + } + } + + useEffect(() => { + reloadMenus() + }, []) + + const openCreate = (parentId = 0) => { + setEditingMenu(null) + form.resetFields() + form.setFieldsValue({ + parentId, + sort: 1, + hidden: false, + keepAlive: false, + closeTab: false, + }) + setModalOpen(true) + } + + const openEdit = async (record: MenuNode) => { + const response = await menuApi.getBaseMenuById(record.ID) + const menu = response.data.menu + setEditingMenu(menu) + form.setFieldsValue({ + parentId: menu.parentId, + title: menu.meta.title, + name: menu.name, + path: menu.path, + component: menu.component, + icon: menu.meta.icon, + sort: menu.sort, + hidden: menu.hidden, + keepAlive: menu.meta.keepAlive, + closeTab: menu.meta.closeTab, + }) + setModalOpen(true) + } + + const saveMenu = async () => { + const values = await form.validateFields() + const payload = { + ID: editingMenu?.ID, + parentId: values.parentId || 0, + name: values.name, + path: values.path, + component: values.component, + sort: values.sort, + hidden: Boolean(values.hidden), + meta: { + title: values.title, + icon: values.icon, + keepAlive: Boolean(values.keepAlive), + closeTab: Boolean(values.closeTab), + }, + } + + setSaving(true) + try { + if (editingMenu) { + await menuApi.updateBaseMenu(payload) + message.success('菜单已更新') + } else { + await menuApi.addBaseMenu(payload) + message.success('菜单已创建') + } + setModalOpen(false) + reloadMenus() + } finally { + setSaving(false) + } + } + + const deleteMenu = (record: MenuNode) => { + Modal.confirm({ + title: `删除菜单 ${record.meta.title}`, + content: '如果该菜单已被角色使用,删除前请先调整菜单授权。', + okButtonProps: { danger: true }, + onOk: async () => { + await menuApi.deleteBaseMenu(record.ID) + message.success('菜单已删除') + reloadMenus() + }, + }) + } + + const openRoleDrawer = async (record: MenuNode) => { + const response = await menuApi.getMenuRoles(record.ID) + setActiveMenu(record) + setSelectedRoles(response.data.authorityIds) + setDefaultRouterRoles(response.data.defaultRouterAuthorityIds) + setDrawerOpen(true) + } + + const saveRoles = async () => { + if (!activeMenu) { + return + } + setSavingRoles(true) + try { + await menuApi.setMenuRoles({ + menuId: activeMenu.ID, + authorityIds: selectedRoles, + }) + message.success('菜单角色关系已更新') + setDrawerOpen(false) + } finally { + setSavingRoles(false) + } + } + + const columns: ColumnsType = [ + { title: 'ID', dataIndex: 'ID', width: 80 }, + { + title: '展示名称', + width: 180, + render: (_, record) => record.meta.title, + }, + { title: '路由 Name', dataIndex: 'name', width: 140 }, + { title: '路由 Path', dataIndex: 'path', width: 180 }, + { title: '组件路径', dataIndex: 'component', width: 280 }, + { + title: '状态', + width: 180, + render: (_, record) => ( + + {record.hidden ? '隐藏' : '显示'} + {record.meta.keepAlive ? KeepAlive : null} + + ), + }, + { + title: '操作', + key: 'actions', + width: 260, + fixed: 'right', + render: (_, record) => ( + + + + + + + ), + }, + ] + + return ( +
+ +
+
+ + 菜单管理 + + + 当前页直接维护后端基础菜单树,保证新 React 后台与原权限模型保持一致。 + +
+ +
+
+ + + setModalOpen(false)} + onOk={saveMenu} + confirmLoading={saving} + width={760} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setDrawerOpen(false)} + title={activeMenu ? `菜单角色分配 · ${activeMenu.meta.title}` : '菜单角色分配'} + width={520} + extra={ + + } + > + {defaultRouterRoles.length ? ( + + ) : null} + + + + + + + + + + + + + + + + +
{ + setPage(nextPage) + setPageSize(nextPageSize) + }, + }} + /> + + + setModalOpen(false)} + onOk={saveParam} + > +
+ + + + + + + + + + + + + +
+ + ) +} diff --git a/web-admin/src/features/person/ProfilePage.tsx b/web-admin/src/features/person/ProfilePage.tsx new file mode 100644 index 0000000..fabf5b7 --- /dev/null +++ b/web-admin/src/features/person/ProfilePage.tsx @@ -0,0 +1,123 @@ +import { useState } from 'react' +import { Avatar, Button, Card, Col, Form, Input, Row, Typography, message } from 'antd' +import { authApi } from '@/lib/api' +import { useAuthStore } from '@/store/auth' + +export function ProfilePage() { + const user = useAuthStore((state) => state.user) + const setUser = useAuthStore((state) => state.setUser) + const [profileForm] = Form.useForm() + const [passwordForm] = Form.useForm() + const [savingProfile, setSavingProfile] = useState(false) + const [savingPassword, setSavingPassword] = useState(false) + + const submitProfile = async () => { + const values = await profileForm.validateFields() + setSavingProfile(true) + try { + await authApi.setSelfInfo({ + ID: user?.ID, + nickName: values.nickName, + phone: values.phone, + email: values.email, + headerImg: values.headerImg, + }) + setUser({ + ...(user || {}), + nickName: values.nickName, + phone: values.phone, + email: values.email, + headerImg: values.headerImg, + } as typeof user) + message.success('个人资料已更新') + } finally { + setSavingProfile(false) + } + } + + const submitPassword = async () => { + const values = await passwordForm.validateFields() + setSavingPassword(true) + try { + await authApi.changePassword({ + password: values.password, + newPassword: values.newPassword, + }) + passwordForm.resetFields() + message.success('密码已修改') + } finally { + setSavingPassword(false) + } + } + + return ( +
+ +
+
+ + 个人中心 + + + 当前页面使用后端自身资料接口,不依赖额外菜单配置。 + +
+ + {user?.nickName?.[0] || user?.userName?.[0]} + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + +
+ + + + ) +} diff --git a/web-admin/src/features/roles/RoleManagementPage.tsx b/web-admin/src/features/roles/RoleManagementPage.tsx new file mode 100644 index 0000000..1e75690 --- /dev/null +++ b/web-admin/src/features/roles/RoleManagementPage.tsx @@ -0,0 +1,397 @@ +import { useEffect, useMemo, useState } from 'react' +import { + Button, + Card, + Drawer, + Form, + Input, + Modal, + Select, + Space, + Table, + Tabs, + Tag, + Tree, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import type { DataNode } from 'antd/es/tree' +import { apiRegistryApi, authorityApi, casbinApi, menuApi, userApi } from '@/lib/api' +import { collectCheckedLeafMenus, flattenAuthorities } from '@/lib/tree' +import type { ApiRecord, Authority, MenuNode, UserInfo } from '@/types/system' + +type PermissionTab = 'menus' | 'apis' | 'users' + +function mapMenusToTree(menus: MenuNode[]): DataNode[] { + return menus.map((menu) => ({ + key: menu.ID, + title: menu.meta.title, + children: mapMenusToTree(menu.children || []), + })) +} + +function mapApisToTree(apis: ApiRecord[]): DataNode[] { + const grouped = apis.reduce>((accumulator, api) => { + if (!accumulator[api.apiGroup]) { + accumulator[api.apiGroup] = [] + } + accumulator[api.apiGroup].push(api) + return accumulator + }, {}) + + return Object.entries(grouped).map(([group, items]) => ({ + key: `group:${group}`, + title: `${group} 组`, + children: items.map((item) => ({ + key: `${item.path}::${item.method}`, + title: `${item.method} ${item.path} · ${item.description}`, + })), + })) +} + +export function RoleManagementPage() { + const [form] = Form.useForm() + const [roles, setRoles] = useState([]) + const [menuTree, setMenuTree] = useState([]) + const [apis, setApis] = useState([]) + const [users, setUsers] = useState([]) + const [roleModalOpen, setRoleModalOpen] = useState(false) + const [drawerOpen, setDrawerOpen] = useState(false) + const [savingRole, setSavingRole] = useState(false) + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState('menus') + const [editingRole, setEditingRole] = useState(null) + const [copySource, setCopySource] = useState(null) + const [activeRole, setActiveRole] = useState(null) + const [menuChecked, setMenuChecked] = useState([]) + const [defaultRouter, setDefaultRouter] = useState() + const [apiChecked, setApiChecked] = useState([]) + const [userIds, setUserIds] = useState([]) + const [savingPermission, setSavingPermission] = useState(false) + + const roleOptions = useMemo( + () => + flattenAuthorities(roles).map((item) => ({ + label: `${' '.repeat(item.depth)}${item.authorityName} (${item.authorityId})`, + value: item.authorityId, + })), + [roles], + ) + + const reloadRoles = async () => { + setLoading(true) + try { + const response = await authorityApi.getAuthorityList() + setRoles(response.data) + } finally { + setLoading(false) + } + } + + useEffect(() => { + reloadRoles() + }, []) + + const openCreate = (parentId?: number) => { + setEditingRole(null) + setCopySource(null) + form.resetFields() + form.setFieldsValue({ parentId: parentId ?? 0 }) + setRoleModalOpen(true) + } + + const openEdit = (record: Authority) => { + setEditingRole(record) + setCopySource(null) + form.setFieldsValue({ + authorityId: record.authorityId, + authorityName: record.authorityName, + parentId: record.parentId ?? 0, + }) + setRoleModalOpen(true) + } + + const openCopy = (record: Authority) => { + setEditingRole(null) + setCopySource(record) + form.setFieldsValue({ + authorityId: undefined, + authorityName: `${record.authorityName}-副本`, + parentId: record.parentId ?? 0, + }) + setRoleModalOpen(true) + } + + const saveRole = async () => { + const values = await form.validateFields() + setSavingRole(true) + try { + if (copySource) { + await authorityApi.copyAuthority({ + oldAuthorityId: copySource.authorityId, + authority: { + authorityId: values.authorityId, + authorityName: values.authorityName, + parentId: values.parentId, + }, + }) + message.success('角色已复制') + } else if (editingRole) { + await authorityApi.updateAuthority({ + authorityId: editingRole.authorityId, + authorityName: values.authorityName, + parentId: values.parentId, + defaultRouter: editingRole.defaultRouter, + }) + message.success('角色已更新') + } else { + await authorityApi.createAuthority({ + authorityId: values.authorityId, + authorityName: values.authorityName, + parentId: values.parentId, + }) + message.success('角色已创建') + } + + setRoleModalOpen(false) + reloadRoles() + } finally { + setSavingRole(false) + } + } + + const deleteRole = (record: Authority) => { + Modal.confirm({ + title: `删除角色 ${record.authorityName}`, + content: '删除前请确认没有用户正在依赖该角色。', + okButtonProps: { danger: true }, + onOk: async () => { + await authorityApi.deleteAuthority(record.authorityId) + message.success('角色已删除') + reloadRoles() + }, + }) + } + + const openPermissionDrawer = async (record: Authority) => { + setActiveRole(record) + setDrawerOpen(true) + const [menuRes, checkedMenusRes, apiRes, policyRes, userRes, roleUserRes] = await Promise.all([ + menuApi.getBaseMenuTree(), + menuApi.getMenuAuthority(record.authorityId), + apiRegistryApi.getAllApis(), + casbinApi.getPolicyPathByAuthorityId(record.authorityId), + userApi.getUserList({ page: 1, pageSize: 999 }), + authorityApi.getUsersByAuthorityId(record.authorityId), + ]) + + setMenuTree(menuRes.data.menus) + setMenuChecked( + checkedMenusRes.data.menus.map((item) => Number((item as unknown as { menuId?: number; ID: number }).menuId ?? item.ID)), + ) + setDefaultRouter(record.defaultRouter) + setApis(apiRes.data.apis) + setApiChecked(policyRes.data.paths.map((item) => `${item.path}::${item.method}`)) + setUsers(userRes.data.list) + setUserIds(roleUserRes.data) + } + + const saveCurrentPermission = async () => { + if (!activeRole) { + return + } + setSavingPermission(true) + try { + if (activeTab === 'menus') { + const checkedMenus = collectCheckedLeafMenus(menuTree, new Set(menuChecked)) + await menuApi.addMenuAuthority({ + authorityId: activeRole.authorityId, + menus: checkedMenus, + }) + await authorityApi.updateAuthority({ + authorityId: activeRole.authorityId, + authorityName: activeRole.authorityName, + parentId: activeRole.parentId, + defaultRouter, + }) + message.success('菜单权限已保存') + } else if (activeTab === 'apis') { + const selectedApis = apis + .filter((item) => apiChecked.includes(`${item.path}::${item.method}`)) + .map((item) => ({ path: item.path, method: item.method })) + await casbinApi.updateCasbin({ + authorityId: activeRole.authorityId, + casbinInfos: selectedApis, + }) + message.success('API 权限已保存') + } else { + await authorityApi.setRoleUsers({ + authorityId: activeRole.authorityId, + userIds, + }) + message.success('角色用户关系已保存') + } + reloadRoles() + } finally { + setSavingPermission(false) + } + } + + const menuOptions = useMemo( + () => + collectCheckedLeafMenus(menuTree, new Set(menuChecked)).map((menu) => ({ + label: menu.meta.title, + value: menu.name, + })), + [menuChecked, menuTree], + ) + + const columns: ColumnsType = [ + { + title: '角色 ID', + dataIndex: 'authorityId', + width: 120, + }, + { + title: '角色名称', + dataIndex: 'authorityName', + width: 200, + }, + { + title: '默认首页', + dataIndex: 'defaultRouter', + width: 140, + render: (value: string | undefined) => value || '-', + }, + { + title: '操作', + key: 'actions', + width: 320, + render: (_, record) => ( + + + + + + + + ), + }, + ] + + return ( +
+ +
+
+ + 角色管理 + + + 当前页接通角色树、菜单授权、API 授权和角色用户绑定。 + +
+ +
+
+ + + setRoleModalOpen(false)} + onOk={saveRole} + confirmLoading={savingRole} + > +
+ + + + + + + +
+ + setDrawerOpen(false)} + width={900} + title={activeRole ? `角色权限 · ${activeRole.authorityName}` : '角色权限'} + extra={ + + } + > + setActiveTab(key as PermissionTab)}> + + + ({ + label: `${user.nickName} (${user.userName})`, + value: user.ID, + }))} + onChange={(value) => setUserIds(value)} + /> +
+ {userIds.length} 个用户已绑定到当前角色 +
+
+
+
+ + ) +} diff --git a/web-admin/src/features/server/ServerStatePage.tsx b/web-admin/src/features/server/ServerStatePage.tsx new file mode 100644 index 0000000..7a5d8bb --- /dev/null +++ b/web-admin/src/features/server/ServerStatePage.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from 'react' +import { Card, Descriptions, Skeleton, Typography } from 'antd' +import { inventoryApi } from '@/lib/api' + +function renderValue(value: unknown) { + if (value === null || value === undefined) { + return '-' + } + + if (typeof value === 'object') { + return ( +
+        {JSON.stringify(value, null, 2)}
+      
+ ) + } + + return String(value) +} + +export function ServerStatePage() { + const [server, setServer] = useState | null>(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let ignore = false + + inventoryApi + .getServerInfo() + .then((response) => { + if (!ignore) { + setServer(response.data.server) + } + }) + .finally(() => { + if (!ignore) { + setLoading(false) + } + }) + + return () => { + ignore = true + } + }, []) + + return ( +
+ + + 服务器状态 + + + 该页面直接读取 `/system/getServerInfo` 的结构化结果,不预设字段,优先保证信息完整。 + + + + {loading ? ( + + ) : ( + + {Object.entries(server || {}).map(([key, value]) => ( + + {renderValue(value)} + + ))} + + )} + +
+ ) +} diff --git a/web-admin/src/features/system/SystemConfigPage.tsx b/web-admin/src/features/system/SystemConfigPage.tsx new file mode 100644 index 0000000..f280a4c --- /dev/null +++ b/web-admin/src/features/system/SystemConfigPage.tsx @@ -0,0 +1,130 @@ +import { useEffect, useMemo, useState } from 'react' +import { Button, Card, Col, Row, Space, Statistic, Tabs, Typography, message } from 'antd' +import { systemApi } from '@/lib/api' + +export function SystemConfigPage() { + const [config, setConfig] = useState>({}) + const [text, setText] = useState('') + const [loading, setLoading] = useState(false) + + const summary = useMemo(() => { + const system = (config.system || {}) as Record + return { + addr: system.addr || '-', + dbType: system['db-type'] || '-', + routerPrefix: system['router-prefix'] || '/', + strictAuth: String(system['use-strict-auth'] ?? false), + } + }, [config]) + + const reloadConfig = async () => { + setLoading(true) + try { + const response = await systemApi.getSystemConfig() + setConfig(response.data.config) + setText(JSON.stringify(response.data.config, null, 2)) + } finally { + setLoading(false) + } + } + + useEffect(() => { + reloadConfig() + }, []) + + const saveConfig = async () => { + const parsed = JSON.parse(text) as Record + await systemApi.setSystemConfig(parsed) + message.success('配置已保存') + setConfig(parsed) + } + + return ( +
+ +
+
+ + 系统配置 + + + 当前页以原始 JSON 为主视图,优先保证配置项覆盖完整,不丢字段。 + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + setText(event.target.value)} + style={{ + width: '100%', + minHeight: 540, + borderRadius: 16, + border: '1px solid rgba(16, 37, 66, 0.12)', + padding: 16, + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', + fontSize: 14, + }} + /> + ), + }, + { + key: 'preview', + label: '结构预览', + children: ( +
+                  {JSON.stringify(config, null, 2)}
+                
+ ), + }, + ]} + /> +
+ + ) +} diff --git a/web-admin/src/features/tokens/ApiTokenPage.tsx b/web-admin/src/features/tokens/ApiTokenPage.tsx new file mode 100644 index 0000000..646dd4d --- /dev/null +++ b/web-admin/src/features/tokens/ApiTokenPage.tsx @@ -0,0 +1,237 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { + Alert, + Button, + Card, + Col, + Form, + Input, + Modal, + Row, + Select, + Space, + Table, + Tag, + Typography, + message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { apiTokenApi, userApi } from '@/lib/api' +import { formatDate } from '@/lib/date' +import type { ApiTokenRecord, UserInfo } from '@/types/system' + +export function ApiTokenPage() { + const [searchForm] = Form.useForm() + const [issueForm] = Form.useForm() + const [rows, setRows] = useState([]) + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [total, setTotal] = useState(0) + const [modalOpen, setModalOpen] = useState(false) + const [issuedToken, setIssuedToken] = useState('') + + const selectedUser = useMemo( + () => users.find((item) => item.ID === issueForm.getFieldValue('userId')), + [issueForm, users], + ) + + const reloadRows = useCallback(async () => { + setLoading(true) + try { + const response = await apiTokenApi.getTokenList({ + page, + pageSize, + ...searchForm.getFieldsValue(), + }) + setRows(response.data.list) + setTotal(response.data.total) + } finally { + setLoading(false) + } + }, [page, pageSize, searchForm]) + + const loadUsers = useCallback(async () => { + const response = await userApi.getUserList({ page: 1, pageSize: 999 }) + setUsers(response.data.list) + }, []) + + useEffect(() => { + reloadRows() + loadUsers() + }, [loadUsers, reloadRows]) + + const issueToken = async () => { + const values = await issueForm.validateFields() + const response = await apiTokenApi.createToken(values) + setIssuedToken(response.data.token) + setModalOpen(false) + issueForm.resetFields() + message.success('Token 已签发') + reloadRows() + } + + const columns: ColumnsType = [ + { title: 'ID', dataIndex: 'ID', width: 80 }, + { + title: '用户', + width: 220, + render: (_, record) => `${record.user?.nickName || '-'} (${record.user?.userName || '-'})`, + }, + { title: '角色 ID', dataIndex: 'authorityId', width: 100 }, + { + title: '状态', + width: 100, + render: (_, record) => {record.status ? '有效' : '已作废'}, + }, + { + title: '过期时间', + width: 180, + render: (_, record) => formatDate(record.expiresAt), + }, + { title: '备注', dataIndex: 'remark' }, + { + title: '操作', + width: 120, + render: (_, record) => ( + + ), + }, + ] + + return ( +
+ +
+
+ + API Token + + + 为指定用户和角色签发临时访问 JWT。 + +
+ +
+
{ setPage(1); reloadRows() }}> + +
+ + + + + + +
{ + setPage(nextPage) + setPageSize(nextPageSize) + }, + }} + /> + + + setModalOpen(false)} onOk={issueToken}> + + + ({ + label: `${authority.authorityName} (${authority.authorityId})`, + value: authority.authorityId, + }))} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{ + setPage(nextPage) + setPageSize(nextPageSize) + }, + }} + /> + + + setModalOpen(false)} + onOk={saveUser} + confirmLoading={saving} + width={720} + > +
+ +
+ + + + + {!editingUser ? ( + + + + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + +