diff --git a/.aone_copilot/rules/project_rules.md b/.aone_copilot/rules/project_rules.md index f63d5a377..aa14f9580 100644 --- a/.aone_copilot/rules/project_rules.md +++ b/.aone_copilot/rules/project_rules.md @@ -194,6 +194,8 @@ web/ - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** @@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重 - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 + - **必须**优先使用UnoCSS原子化类名进行样式设计 + - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 + - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** @@ -622,6 +627,130 @@ src/plugin/[插件名]/ --- +## **⚠️ 前端工具库使用规范(强制)** + +> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** + +`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: + +### **工具文件清单** + +#### `request.js` — HTTP 请求封装(核心) +- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 +- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** +- 用法:`import service from '@/utils/request'` + +#### `date.js` — 日期格式化 +- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` +- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 +- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** +- 用法:`import { formatTimeToStr } from '@/utils/date'` + +#### `format.js` — 数据展示格式化(综合工具) +- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 +- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 +- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label +- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 +- `getDictFunc(type)` — 异步获取指定类型的字典数据 +- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 +- `onDownloadFile(url)` — 触发文件下载 +- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) +- `CreateUUID()` — 生成 UUID v4 字符串 +- `getBaseUrl()` — 获取当前环境的 API BaseURL +- **以上所有格式化场景优先使用此文件中的工具函数** +- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` + +#### `dictionary.js` — 字典数据获取 +- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 +- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** +- 用法:`import { getDict } from '@/utils/dictionary'` + +#### `stringFun.js` — 字符串处理 +- `toUpperCase(str)` — 首字母转大写 +- `toLowerCase(str)` — 首字母转小写 +- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` +- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` +- **进行命名格式转换时必须使用此工具,禁止使用正则手写** +- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` + +#### `params.js` — 系统参数获取 +- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 +- **获取系统配置参数时,优先使用此工具** +- 用法:`import { getParams } from '@/utils/params'` + +#### `bus.js` — 全局事件总线 +- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 +- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** +- 用法:`import { emitter } from '@/utils/bus'` + +#### `closeThisPage.js` — 关闭当前标签页 +- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) +- **在需要程序化关闭当前页面时,必须使用此工具** +- 用法:`import { closeThisPage } from '@/utils/closeThisPage'` + +#### `downloadImg.js` — 图片下载 +- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 +- **需要下载图片时,优先使用此工具** +- 用法:`import { downloadImage } from '@/utils/downloadImg'` + +#### `image.js` — 图片压缩 +- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 +- **上传图片前需要做压缩处理时,使用此工具** +- 用法:`import ImageCompress from '@/utils/image'` + +#### `event.js` — DOM 事件监听管理 +- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 +- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 +- **手动操作 DOM 事件时,使用此工具以确保安全性** +- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` + +#### `env.js` — 环境判断 +- `isDev` — 是否为开发环境(Boolean) +- `isProd` — 是否为生产环境(Boolean) +- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** +- 用法:`import { isDev, isProd } from '@/utils/env'` + +#### `doc.js` — 外部文档跳转 +- `toDoc(url)` — 在新标签页打开指定 URL +- 用法:`import { toDoc } from '@/utils/doc'` + +#### `fmtRouterTitle.js` — 路由标题格式化 +- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) +- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` + +#### `page.js` — 页面标题生成 +- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) +- 用法:`import getPageTitle from '@/utils/page'` + +#### `asyncRouter.js` — 异步路由处理 +- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 +- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** +- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` + +#### `btnAuth.js` — 按钮权限 +- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 +- **实现按钮级别权限控制时,必须使用此 Hook** +- 用法:`import { useBtnAuth } from '@/utils/btnAuth'` + +### **使用强制要求** + +| 场景 | 必须使用的工具 | +|------|----------------| +| 发送 HTTP 请求 | `@/utils/request` | +| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | +| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | +| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | +| 生成 UUID | `@/utils/format` 中的 `CreateUUID` | +| 驼峰/下划线命名转换 | `@/utils/stringFun` | +| 获取系统参数 | `@/utils/params` 中的 `getParams` | +| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | +| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | +| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | +| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | +| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | + +--- + ## **前后端协作规范** ### **接口协作规范** diff --git a/.claude/rules/project_rules.md b/.claude/rules/project_rules.md index f63d5a377..aa14f9580 100644 --- a/.claude/rules/project_rules.md +++ b/.claude/rules/project_rules.md @@ -194,6 +194,8 @@ web/ - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** @@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重 - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 + - **必须**优先使用UnoCSS原子化类名进行样式设计 + - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 + - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** @@ -622,6 +627,130 @@ src/plugin/[插件名]/ --- +## **⚠️ 前端工具库使用规范(强制)** + +> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** + +`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: + +### **工具文件清单** + +#### `request.js` — HTTP 请求封装(核心) +- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 +- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** +- 用法:`import service from '@/utils/request'` + +#### `date.js` — 日期格式化 +- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` +- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 +- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** +- 用法:`import { formatTimeToStr } from '@/utils/date'` + +#### `format.js` — 数据展示格式化(综合工具) +- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 +- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 +- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label +- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 +- `getDictFunc(type)` — 异步获取指定类型的字典数据 +- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 +- `onDownloadFile(url)` — 触发文件下载 +- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) +- `CreateUUID()` — 生成 UUID v4 字符串 +- `getBaseUrl()` — 获取当前环境的 API BaseURL +- **以上所有格式化场景优先使用此文件中的工具函数** +- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` + +#### `dictionary.js` — 字典数据获取 +- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 +- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** +- 用法:`import { getDict } from '@/utils/dictionary'` + +#### `stringFun.js` — 字符串处理 +- `toUpperCase(str)` — 首字母转大写 +- `toLowerCase(str)` — 首字母转小写 +- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` +- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` +- **进行命名格式转换时必须使用此工具,禁止使用正则手写** +- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` + +#### `params.js` — 系统参数获取 +- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 +- **获取系统配置参数时,优先使用此工具** +- 用法:`import { getParams } from '@/utils/params'` + +#### `bus.js` — 全局事件总线 +- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 +- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** +- 用法:`import { emitter } from '@/utils/bus'` + +#### `closeThisPage.js` — 关闭当前标签页 +- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) +- **在需要程序化关闭当前页面时,必须使用此工具** +- 用法:`import { closeThisPage } from '@/utils/closeThisPage'` + +#### `downloadImg.js` — 图片下载 +- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 +- **需要下载图片时,优先使用此工具** +- 用法:`import { downloadImage } from '@/utils/downloadImg'` + +#### `image.js` — 图片压缩 +- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 +- **上传图片前需要做压缩处理时,使用此工具** +- 用法:`import ImageCompress from '@/utils/image'` + +#### `event.js` — DOM 事件监听管理 +- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 +- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 +- **手动操作 DOM 事件时,使用此工具以确保安全性** +- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` + +#### `env.js` — 环境判断 +- `isDev` — 是否为开发环境(Boolean) +- `isProd` — 是否为生产环境(Boolean) +- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** +- 用法:`import { isDev, isProd } from '@/utils/env'` + +#### `doc.js` — 外部文档跳转 +- `toDoc(url)` — 在新标签页打开指定 URL +- 用法:`import { toDoc } from '@/utils/doc'` + +#### `fmtRouterTitle.js` — 路由标题格式化 +- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) +- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` + +#### `page.js` — 页面标题生成 +- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) +- 用法:`import getPageTitle from '@/utils/page'` + +#### `asyncRouter.js` — 异步路由处理 +- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 +- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** +- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` + +#### `btnAuth.js` — 按钮权限 +- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 +- **实现按钮级别权限控制时,必须使用此 Hook** +- 用法:`import { useBtnAuth } from '@/utils/btnAuth'` + +### **使用强制要求** + +| 场景 | 必须使用的工具 | +|------|----------------| +| 发送 HTTP 请求 | `@/utils/request` | +| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | +| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | +| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | +| 生成 UUID | `@/utils/format` 中的 `CreateUUID` | +| 驼峰/下划线命名转换 | `@/utils/stringFun` | +| 获取系统参数 | `@/utils/params` 中的 `getParams` | +| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | +| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | +| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | +| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | +| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | + +--- + ## **前后端协作规范** ### **接口协作规范** diff --git a/.codex/rules/project_rules.md b/.codex/rules/project_rules.md index f63d5a377..aa14f9580 100644 --- a/.codex/rules/project_rules.md +++ b/.codex/rules/project_rules.md @@ -194,6 +194,8 @@ web/ - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** @@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重 - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 + - **必须**优先使用UnoCSS原子化类名进行样式设计 + - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 + - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** @@ -622,6 +627,130 @@ src/plugin/[插件名]/ --- +## **⚠️ 前端工具库使用规范(强制)** + +> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** + +`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: + +### **工具文件清单** + +#### `request.js` — HTTP 请求封装(核心) +- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 +- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** +- 用法:`import service from '@/utils/request'` + +#### `date.js` — 日期格式化 +- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` +- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 +- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** +- 用法:`import { formatTimeToStr } from '@/utils/date'` + +#### `format.js` — 数据展示格式化(综合工具) +- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 +- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 +- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label +- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 +- `getDictFunc(type)` — 异步获取指定类型的字典数据 +- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 +- `onDownloadFile(url)` — 触发文件下载 +- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) +- `CreateUUID()` — 生成 UUID v4 字符串 +- `getBaseUrl()` — 获取当前环境的 API BaseURL +- **以上所有格式化场景优先使用此文件中的工具函数** +- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` + +#### `dictionary.js` — 字典数据获取 +- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 +- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** +- 用法:`import { getDict } from '@/utils/dictionary'` + +#### `stringFun.js` — 字符串处理 +- `toUpperCase(str)` — 首字母转大写 +- `toLowerCase(str)` — 首字母转小写 +- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` +- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` +- **进行命名格式转换时必须使用此工具,禁止使用正则手写** +- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` + +#### `params.js` — 系统参数获取 +- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 +- **获取系统配置参数时,优先使用此工具** +- 用法:`import { getParams } from '@/utils/params'` + +#### `bus.js` — 全局事件总线 +- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 +- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** +- 用法:`import { emitter } from '@/utils/bus'` + +#### `closeThisPage.js` — 关闭当前标签页 +- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) +- **在需要程序化关闭当前页面时,必须使用此工具** +- 用法:`import { closeThisPage } from '@/utils/closeThisPage'` + +#### `downloadImg.js` — 图片下载 +- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 +- **需要下载图片时,优先使用此工具** +- 用法:`import { downloadImage } from '@/utils/downloadImg'` + +#### `image.js` — 图片压缩 +- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 +- **上传图片前需要做压缩处理时,使用此工具** +- 用法:`import ImageCompress from '@/utils/image'` + +#### `event.js` — DOM 事件监听管理 +- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 +- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 +- **手动操作 DOM 事件时,使用此工具以确保安全性** +- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` + +#### `env.js` — 环境判断 +- `isDev` — 是否为开发环境(Boolean) +- `isProd` — 是否为生产环境(Boolean) +- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** +- 用法:`import { isDev, isProd } from '@/utils/env'` + +#### `doc.js` — 外部文档跳转 +- `toDoc(url)` — 在新标签页打开指定 URL +- 用法:`import { toDoc } from '@/utils/doc'` + +#### `fmtRouterTitle.js` — 路由标题格式化 +- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) +- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` + +#### `page.js` — 页面标题生成 +- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) +- 用法:`import getPageTitle from '@/utils/page'` + +#### `asyncRouter.js` — 异步路由处理 +- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 +- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** +- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` + +#### `btnAuth.js` — 按钮权限 +- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 +- **实现按钮级别权限控制时,必须使用此 Hook** +- 用法:`import { useBtnAuth } from '@/utils/btnAuth'` + +### **使用强制要求** + +| 场景 | 必须使用的工具 | +|------|----------------| +| 发送 HTTP 请求 | `@/utils/request` | +| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | +| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | +| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | +| 生成 UUID | `@/utils/format` 中的 `CreateUUID` | +| 驼峰/下划线命名转换 | `@/utils/stringFun` | +| 获取系统参数 | `@/utils/params` 中的 `getParams` | +| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | +| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | +| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | +| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | +| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | + +--- + ## **前后端协作规范** ### **接口协作规范** diff --git a/.cursor/rules/project_rules.md b/.cursor/rules/project_rules.md index f63d5a377..aa14f9580 100644 --- a/.cursor/rules/project_rules.md +++ b/.cursor/rules/project_rules.md @@ -194,6 +194,8 @@ web/ - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** @@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重 - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 + - **必须**优先使用UnoCSS原子化类名进行样式设计 + - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 + - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** @@ -622,6 +627,130 @@ src/plugin/[插件名]/ --- +## **⚠️ 前端工具库使用规范(强制)** + +> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** + +`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: + +### **工具文件清单** + +#### `request.js` — HTTP 请求封装(核心) +- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 +- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** +- 用法:`import service from '@/utils/request'` + +#### `date.js` — 日期格式化 +- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` +- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 +- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** +- 用法:`import { formatTimeToStr } from '@/utils/date'` + +#### `format.js` — 数据展示格式化(综合工具) +- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 +- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 +- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label +- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 +- `getDictFunc(type)` — 异步获取指定类型的字典数据 +- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 +- `onDownloadFile(url)` — 触发文件下载 +- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) +- `CreateUUID()` — 生成 UUID v4 字符串 +- `getBaseUrl()` — 获取当前环境的 API BaseURL +- **以上所有格式化场景优先使用此文件中的工具函数** +- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` + +#### `dictionary.js` — 字典数据获取 +- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 +- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** +- 用法:`import { getDict } from '@/utils/dictionary'` + +#### `stringFun.js` — 字符串处理 +- `toUpperCase(str)` — 首字母转大写 +- `toLowerCase(str)` — 首字母转小写 +- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` +- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` +- **进行命名格式转换时必须使用此工具,禁止使用正则手写** +- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` + +#### `params.js` — 系统参数获取 +- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 +- **获取系统配置参数时,优先使用此工具** +- 用法:`import { getParams } from '@/utils/params'` + +#### `bus.js` — 全局事件总线 +- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 +- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** +- 用法:`import { emitter } from '@/utils/bus'` + +#### `closeThisPage.js` — 关闭当前标签页 +- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) +- **在需要程序化关闭当前页面时,必须使用此工具** +- 用法:`import { closeThisPage } from '@/utils/closeThisPage'` + +#### `downloadImg.js` — 图片下载 +- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 +- **需要下载图片时,优先使用此工具** +- 用法:`import { downloadImage } from '@/utils/downloadImg'` + +#### `image.js` — 图片压缩 +- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 +- **上传图片前需要做压缩处理时,使用此工具** +- 用法:`import ImageCompress from '@/utils/image'` + +#### `event.js` — DOM 事件监听管理 +- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 +- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 +- **手动操作 DOM 事件时,使用此工具以确保安全性** +- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` + +#### `env.js` — 环境判断 +- `isDev` — 是否为开发环境(Boolean) +- `isProd` — 是否为生产环境(Boolean) +- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** +- 用法:`import { isDev, isProd } from '@/utils/env'` + +#### `doc.js` — 外部文档跳转 +- `toDoc(url)` — 在新标签页打开指定 URL +- 用法:`import { toDoc } from '@/utils/doc'` + +#### `fmtRouterTitle.js` — 路由标题格式化 +- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) +- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` + +#### `page.js` — 页面标题生成 +- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) +- 用法:`import getPageTitle from '@/utils/page'` + +#### `asyncRouter.js` — 异步路由处理 +- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 +- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** +- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` + +#### `btnAuth.js` — 按钮权限 +- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 +- **实现按钮级别权限控制时,必须使用此 Hook** +- 用法:`import { useBtnAuth } from '@/utils/btnAuth'` + +### **使用强制要求** + +| 场景 | 必须使用的工具 | +|------|----------------| +| 发送 HTTP 请求 | `@/utils/request` | +| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | +| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | +| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | +| 生成 UUID | `@/utils/format` 中的 `CreateUUID` | +| 驼峰/下划线命名转换 | `@/utils/stringFun` | +| 获取系统参数 | `@/utils/params` 中的 `getParams` | +| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | +| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | +| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | +| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | +| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | + +--- + ## **前后端协作规范** ### **接口协作规范** diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index f63d5a377..aa14f9580 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -194,6 +194,8 @@ web/ - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + - 以上三个字段返回给前端并未做驼峰处理,json内依然是 `ID`, `CreatedAt`, `UpdatedAt` + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 - **⚠️ 重要提醒:数据类型一致性** @@ -495,6 +497,9 @@ API函数的Swagger注释不仅用于生成API文档,也是前端开发的重 - **必须**进行响应式数据管理 - **必须**处理加载状态和错误状态 - **必须**遵循Element Plus组件规范 + - **必须**优先使用UnoCSS原子化类名进行样式设计 + - **必须**优先el-drawer组件进行编辑,新增,步骤等操作 + - **必须**使用el-drawer和el-dialog组件是后一定携带,destroy-on-close属性,确保组件销毁,避免内存泄漏和状态污染 #### **4. 状态管理 (`src/pinia/`)** @@ -622,6 +627,130 @@ src/plugin/[插件名]/ --- +## **⚠️ 前端工具库使用规范(强制)** + +> **核心原则:在开发任何前端功能时,必须优先检查并使用 `src/utils/` 目录下已封装好的工具函数,严禁重复造轮子。** + +`src/utils/` 目录提供了项目级别的通用工具集,涵盖 HTTP 请求、日期处理、格式转换、字符串操作、图片处理等多个方面。以下是各工具文件的功能说明: + +### **工具文件清单** + +#### `request.js` — HTTP 请求封装(核心) +- 基于 Axios 封装的统一 HTTP 请求实例,内置全局 Loading 状态管理、JWT Token 自动注入、统一错误处理和响应拦截 +- **所有 API 请求必须且只能通过此模块发送,禁止直接使用 axios** +- 用法:`import service from '@/utils/request'` + +#### `date.js` — 日期格式化 +- 扩展了 `Date.prototype.Format` 方法,支持自定义格式如 `yyyy-MM-dd hh:mm:ss` +- 导出 `formatTimeToStr(times, pattern)` 将时间戳或日期对象格式化为字符串 +- **需要格式化日期时,优先使用此工具,禁止自行手写日期格式化逻辑** +- 用法:`import { formatTimeToStr } from '@/utils/date'` + +#### `format.js` — 数据展示格式化(综合工具) +- `formatBoolean(bool)` — 将布尔值转为 "是"/"否" 中文展示 +- `formatDate(time)` — 将时间转为 `yyyy-MM-dd hh:mm:ss` 格式字符串 +- `filterDict(value, options)` — 在字典选项数组(支持多级树形)中根据 value 查找对应的 label +- `filterDataSource(dataSource, value)` — 在数据源(支持多级树形)中根据 value 查找 label,支持数组批量查找 +- `getDictFunc(type)` — 异步获取指定类型的字典数据 +- `ReturnArrImg(arr)` — 将图片路径(单个或数组)转为完整 URL,自动补全服务器前缀 +- `onDownloadFile(url)` — 触发文件下载 +- `setBodyPrimaryColor(primaryColor, darkMode)` — 动态设置主题色相关的 CSS 变量(支持亮/暗模式) +- `CreateUUID()` — 生成 UUID v4 字符串 +- `getBaseUrl()` — 获取当前环境的 API BaseURL +- **以上所有格式化场景优先使用此文件中的工具函数** +- 用法:`import { formatBoolean, formatDate, filterDict, CreateUUID, ... } from '@/utils/format'` + +#### `dictionary.js` — 字典数据获取 +- `getDict(type, options)` — 异步获取字典数据,支持 `depth`(深度)和 `value`(指定节点)参数,内置 Pinia store 缓存,避免重复请求 +- **凡是需要字典下拉数据、字典树形数据的场景,必须使用此工具** +- 用法:`import { getDict } from '@/utils/dictionary'` + +#### `stringFun.js` — 字符串处理 +- `toUpperCase(str)` — 首字母转大写 +- `toLowerCase(str)` — 首字母转小写 +- `toSQLLine(str)` — 驼峰命名转下划线(snake_case),如 `userName` → `user_name` +- `toHump(name)` — 下划线命名转驼峰,如 `user_name` → `userName` +- **进行命名格式转换时必须使用此工具,禁止使用正则手写** +- 用法:`import { toUpperCase, toSQLLine, toHump } from '@/utils/stringFun'` + +#### `params.js` — 系统参数获取 +- `getParams(key)` — 异步从 Pinia store 中获取系统参数,内置缓存 +- **获取系统配置参数时,优先使用此工具** +- 用法:`import { getParams } from '@/utils/params'` + +#### `bus.js` — 全局事件总线 +- 基于 `mitt` 封装的全局事件总线实例 `emitter`,用于跨组件通信 +- **跨层级组件通信优先使用此事件总线,避免滥用 Pinia** +- 用法:`import { emitter } from '@/utils/bus'` + +#### `closeThisPage.js` — 关闭当前标签页 +- `closeThisPage()` — 触发关闭当前多标签页的操作(通过事件总线发送 `closeThisPage` 事件) +- **在需要程序化关闭当前页面时,必须使用此工具** +- 用法:`import { closeThisPage } from '@/utils/closeThisPage'` + +#### `downloadImg.js` — 图片下载 +- `downloadImage(imgsrc, name)` — 通过 Canvas 将图片转为 base64 后触发下载,支持跨域 +- **需要下载图片时,优先使用此工具** +- 用法:`import { downloadImage } from '@/utils/downloadImg'` + +#### `image.js` — 图片压缩 +- 导出 `ImageCompress` 类,支持图片等比压缩至指定最大宽高,并可限制文件大小 +- **上传图片前需要做压缩处理时,使用此工具** +- 用法:`import ImageCompress from '@/utils/image'` + +#### `event.js` — DOM 事件监听管理 +- `addEventListen(target, event, handler, capture)` — 安全地添加 DOM 事件监听 +- `removeEventListen(target, event, handler, capture)` — 安全地移除 DOM 事件监听 +- **手动操作 DOM 事件时,使用此工具以确保安全性** +- 用法:`import { addEventListen, removeEventListen } from '@/utils/event'` + +#### `env.js` — 环境判断 +- `isDev` — 是否为开发环境(Boolean) +- `isProd` — 是否为生产环境(Boolean) +- **需要区分运行环境时,使用此工具,禁止直接读取 `import.meta.env`** +- 用法:`import { isDev, isProd } from '@/utils/env'` + +#### `doc.js` — 外部文档跳转 +- `toDoc(url)` — 在新标签页打开指定 URL +- 用法:`import { toDoc } from '@/utils/doc'` + +#### `fmtRouterTitle.js` — 路由标题格式化 +- `fmtTitle(title, route)` — 解析路由标题中的动态参数插值(如 `${id}` 替换为路由 params/query 值) +- 用法:`import { fmtTitle } from '@/utils/fmtRouterTitle'` + +#### `page.js` — 页面标题生成 +- `getPageTitle(pageTitle, route)` — 根据页面标题和路由生成完整的浏览器 Tab 标题(格式:`页面名 - 应用名`) +- 用法:`import getPageTitle from '@/utils/page'` + +#### `asyncRouter.js` — 异步路由处理 +- `asyncRouterHandle(asyncRouter)` — 将后端返回的路由配置(字符串 component 路径)动态转换为 Vue 组件的 import 函数,支持 `view/` 和 `plugin/` 目录 +- **动态路由相关逻辑已由此工具处理,不需要也不应该手动实现** +- 用法:`import { asyncRouterHandle } from '@/utils/asyncRouter'` + +#### `btnAuth.js` — 按钮权限 +- `useBtnAuth()` — Composition API Hook,返回当前路由挂载的按钮权限对象(来自 `route.meta.btns`),用于控制操作按钮的显示 +- **实现按钮级别权限控制时,必须使用此 Hook** +- 用法:`import { useBtnAuth } from '@/utils/btnAuth'` + +### **使用强制要求** + +| 场景 | 必须使用的工具 | +|------|----------------| +| 发送 HTTP 请求 | `@/utils/request` | +| 格式化日期时间 | `@/utils/date` 或 `@/utils/format` 中的 `formatDate` | +| 获取字典数据 | `@/utils/dictionary` 中的 `getDict` | +| 布尔值/字典值展示转换 | `@/utils/format` 中的 `formatBoolean` / `filterDict` | +| 生成 UUID | `@/utils/format` 中的 `CreateUUID` | +| 驼峰/下划线命名转换 | `@/utils/stringFun` | +| 获取系统参数 | `@/utils/params` 中的 `getParams` | +| 按钮权限判断 | `@/utils/btnAuth` 中的 `useBtnAuth` | +| 跨组件事件通信 | `@/utils/bus` 中的 `emitter` | +| 图片下载 | `@/utils/downloadImg` 中的 `downloadImage` | +| 图片上传压缩 | `@/utils/image` 中的 `ImageCompress` | +| 关闭当前 Tab 页 | `@/utils/closeThisPage` 中的 `closeThisPage` | + +--- + ## **前后端协作规范** ### **接口协作规范** diff --git a/server/api/v1/system/sys_api.go b/server/api/v1/system/sys_api.go index 7c34f2cdb..97a07b05e 100644 --- a/server/api/v1/system/sys_api.go +++ b/server/api/v1/system/sys_api.go @@ -321,3 +321,61 @@ func (s *SystemApiApi) FreshCasbin(c *gin.Context) { } response.OkWithMessage("刷新成功", c) } + +// GetApiRoles +// @Tags SysApi +// @Summary 获取拥有指定API权限的角色ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param path query string true "API路径" +// @Param method query string true "请求方法" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" +// @Router /api/getApiRoles [get] +func (s *SystemApiApi) GetApiRoles(c *gin.Context) { + path := c.Query("path") + method := c.Query("method") + if path == "" || method == "" { + response.FailWithMessage("API路径和请求方法不能为空", c) + return + } + authorityIds, err := casbinService.GetAuthoritiesByApi(path, method) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if authorityIds == nil { + authorityIds = []uint{} + } + response.OkWithDetailed(authorityIds, "获取成功", c) +} + +// SetApiRoles +// @Tags SysApi +// @Summary 全量覆盖某API关联的角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetApiAuthorities true "API路径、请求方法和角色ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /api/setApiRoles [post] +func (s *SystemApiApi) SetApiRoles(c *gin.Context) { + var req systemReq.SetApiAuthorities + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.Path == "" || req.Method == "" { + response.FailWithMessage("API路径和请求方法不能为空", c) + return + } + if err := casbinService.SetApiAuthorities(req.Path, req.Method, req.AuthorityIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + // 刷新casbin缓存使策略立即生效 + _ = casbinService.FreshCasbin() + response.OkWithMessage("设置成功", c) +} diff --git a/server/api/v1/system/sys_authority.go b/server/api/v1/system/sys_authority.go index b34fc3a0b..1a4c10159 100644 --- a/server/api/v1/system/sys_authority.go +++ b/server/api/v1/system/sys_authority.go @@ -4,6 +4,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" "github.com/flipped-aurora/gin-vue-admin/server/utils" @@ -200,3 +201,57 @@ func (a *AuthorityApi) SetDataAuthority(c *gin.Context) { } response.OkWithMessage("设置成功", c) } + +// GetUsersByAuthority +// @Tags Authority +// @Summary 获取拥有指定角色的用户ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param authorityId query uint true "角色ID" +// @Success 200 {object} response.Response{data=[]uint,msg=string} "获取成功" +// @Router /authority/getUsersByAuthority [get] +func (a *AuthorityApi) GetUsersByAuthority(c *gin.Context) { + var req systemReq.SetRoleUsers + if err := c.ShouldBindQuery(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + userIds, err := authorityService.GetUserIdsByAuthorityId(req.AuthorityId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if userIds == nil { + userIds = []uint{} + } + response.OkWithDetailed(userIds, "获取成功", c) +} + +// SetRoleUsers +// @Tags Authority +// @Summary 全量覆盖某角色关联的用户列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetRoleUsers true "角色ID和用户ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /authority/setRoleUsers [post] +func (a *AuthorityApi) SetRoleUsers(c *gin.Context) { + var req systemReq.SetRoleUsers + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.AuthorityId == 0 { + response.FailWithMessage("角色ID不能为空", c) + return + } + if err := authorityService.SetRoleUsers(req.AuthorityId, req.UserIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} diff --git a/server/api/v1/system/sys_menu.go b/server/api/v1/system/sys_menu.go index 30859621c..08027082a 100644 --- a/server/api/v1/system/sys_menu.go +++ b/server/api/v1/system/sys_menu.go @@ -244,6 +244,76 @@ func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) { response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c) } +// GetMenuRoles +// @Tags AuthorityMenu +// @Summary 获取拥有指定菜单的角色ID列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param menuId query uint true "菜单ID" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" +// @Router /menu/getMenuRoles [get] +func (a *AuthorityMenuApi) GetMenuRoles(c *gin.Context) { + var req systemReq.SetMenuAuthorities + if err := c.ShouldBindQuery(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.MenuId == 0 { + response.FailWithMessage("菜单ID不能为空", c) + return + } + authorityIds, err := menuService.GetAuthoritiesByMenuId(req.MenuId) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if authorityIds == nil { + authorityIds = []uint{} + } + defaultRouterAuthorityIds, err := menuService.GetDefaultRouterAuthorityIds(req.MenuId) + if err != nil { + global.GVA_LOG.Error("获取首页角色失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + if defaultRouterAuthorityIds == nil { + defaultRouterAuthorityIds = []uint{} + } + response.OkWithDetailed(gin.H{ + "authorityIds": authorityIds, + "defaultRouterAuthorityIds": defaultRouterAuthorityIds, + }, "获取成功", c) +} + +// SetMenuRoles +// @Tags AuthorityMenu +// @Summary 全量覆盖某菜单关联的角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body systemReq.SetMenuAuthorities true "菜单ID和角色ID列表" +// @Success 200 {object} response.Response{msg=string} "设置成功" +// @Router /menu/setMenuRoles [post] +func (a *AuthorityMenuApi) SetMenuRoles(c *gin.Context) { + var req systemReq.SetMenuAuthorities + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if req.MenuId == 0 { + response.FailWithMessage("菜单ID不能为空", c) + return + } + if err := menuService.SetMenuAuthorities(req.MenuId, req.AuthorityIds); err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} + // GetMenuList // @Tags Menu // @Summary 分页获取基础menu列表 diff --git a/server/api/v1/system/sys_skills.go b/server/api/v1/system/sys_skills.go index 056cddfc1..b0f24f59b 100644 --- a/server/api/v1/system/sys_skills.go +++ b/server/api/v1/system/sys_skills.go @@ -1,6 +1,8 @@ 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" @@ -55,6 +57,17 @@ func (s *SkillsApi) SaveSkill(c *gin.Context) { 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) @@ -217,3 +230,34 @@ func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) { } 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/global/version.go b/server/global/version.go index 544d42350..1fa4bf5fe 100644 --- a/server/global/version.go +++ b/server/global/version.go @@ -4,7 +4,7 @@ package global // 目前只有Version正式使用 其余为预留 const ( // Version 当前版本号 - Version = "v2.8.9" + Version = "v2.9.0" // AppName 应用名称 AppName = "Gin-Vue-Admin" // Description 应用描述 diff --git a/server/go.mod b/server/go.mod index ed64e32fb..42326d060 100644 --- a/server/go.mod +++ b/server/go.mod @@ -6,7 +6,11 @@ toolchain go1.24.2 require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible - github.com/aws/aws-sdk-go v1.55.6 + github.com/aws/aws-sdk-go-v2 v1.41.2 + github.com/aws/aws-sdk-go-v2/config v1.32.10 + github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/gorm-adapter/v3 v3.32.0 github.com/dzwvip/gorm-oracle v0.1.2 @@ -46,6 +50,7 @@ require ( golang.org/x/crypto v0.37.0 golang.org/x/sync v0.13.0 golang.org/x/text v0.24.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.5 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.11 @@ -61,6 +66,21 @@ require ( github.com/STARRY-S/zip v0.2.1 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect + github.com/aws/smithy-go v1.24.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect @@ -108,7 +128,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -178,7 +197,6 @@ require ( golang.org/x/tools v0.29.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/hints v1.1.2 // indirect gorm.io/plugin/dbresolver v1.5.3 // indirect modernc.org/fileutil v1.3.0 // indirect diff --git a/server/go.sum b/server/go.sum index 7d93cd51d..875014e5e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -54,8 +54,46 @@ github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3 h1:+mQ8NQBh7B7c2FBtppRnwkrmuwFON1XQQ+5yblomZKk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.3/go.mod h1:u67RKh3BRmS4FYLH+rN3N4T5fqpd9m2ttAwBJYEdosU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= +github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -273,10 +311,6 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -782,7 +816,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/initialize/router.go b/server/initialize/router.go index 83438a9ac..5455f4c28 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -111,7 +111,7 @@ func Routers() *gin.Engine { systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志 systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志 systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发 - systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器 + systemRouter.InitSkillsRouter(PrivateGroup,PublicGroup) // Skills 定义器 exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 diff --git a/server/main.go b/server/main.go index 11bea40f5..9f5385100 100644 --- a/server/main.go +++ b/server/main.go @@ -21,7 +21,7 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.9 +// @version v2.9.0 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header diff --git a/server/model/system/request/sys_api.go b/server/model/system/request/sys_api.go index 0be8ee72d..5567ee863 100644 --- a/server/model/system/request/sys_api.go +++ b/server/model/system/request/sys_api.go @@ -12,3 +12,10 @@ type SearchApiParams struct { OrderKey string `json:"orderKey"` // 排序 Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true } + +// SetApiAuthorities 通过API路径和方法全量覆盖关联角色列表 +type SetApiAuthorities struct { + Path string `json:"path" form:"path"` // API路径 + Method string `json:"method" form:"method"` // 请求方法 + AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表 +} diff --git a/server/model/system/request/sys_menu.go b/server/model/system/request/sys_menu.go index 275f037bc..75d4a5c5b 100644 --- a/server/model/system/request/sys_menu.go +++ b/server/model/system/request/sys_menu.go @@ -11,6 +11,12 @@ type AddMenuAuthorityInfo struct { AuthorityId uint `json:"authorityId"` // 角色ID } +// SetMenuAuthorities 通过菜单ID全量覆盖关联角色列表 +type SetMenuAuthorities struct { + MenuId uint `json:"menuId" form:"menuId"` // 菜单ID + AuthorityIds []uint `json:"authorityIds" form:"authorityIds"` // 角色ID列表 +} + func DefaultMenu() []system.SysBaseMenu { return []system.SysBaseMenu{{ GVA_MODEL: global.GVA_MODEL{ID: 1}, diff --git a/server/model/system/request/sys_skills.go b/server/model/system/request/sys_skills.go index e9bb3066a..773605fc6 100644 --- a/server/model/system/request/sys_skills.go +++ b/server/model/system/request/sys_skills.go @@ -11,6 +11,16 @@ type SkillDetailRequest struct { 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"` @@ -62,3 +72,9 @@ type SkillGlobalConstraintSaveRequest struct { 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 index 2cd4cf9f0..795077438 100644 --- a/server/model/system/request/sys_user.go +++ b/server/model/system/request/sys_user.go @@ -66,4 +66,12 @@ type GetUserList struct { NickName string `json:"nickName" form:"nickName"` Phone string `json:"phone" form:"phone"` Email string `json:"email" form:"email"` + OrderKey string `json:"orderKey" form:"orderKey"` // 排序 + Desc bool `json:"desc" form:"desc"` // 排序方式:升序false(默认)|降序true +} + +// SetRoleUsers 通过角色ID全量覆盖关联用户列表 +type SetRoleUsers struct { + AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID + UserIds []uint `json:"userIds" form:"userIds"` // 用户ID列表 } diff --git a/server/router/system/sys_api.go b/server/router/system/sys_api.go index c98785e94..0134a25a5 100644 --- a/server/router/system/sys_api.go +++ b/server/router/system/sys_api.go @@ -22,10 +22,12 @@ func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.Router 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_authority.go b/server/router/system/sys_authority.go index 9bef92ff1..67a2880f1 100644 --- a/server/router/system/sys_authority.go +++ b/server/router/system/sys_authority.go @@ -16,8 +16,10 @@ func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) { 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.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表 + authorityRouterWithoutRecord.GET("getUsersByAuthority", authorityApi.GetUsersByAuthority) // 获取角色关联用户ID列表 } } diff --git a/server/router/system/sys_menu.go b/server/router/system/sys_menu.go index 09584f4f7..69fb65cae 100644 --- a/server/router/system/sys_menu.go +++ b/server/router/system/sys_menu.go @@ -15,6 +15,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { 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) // 获取菜单树 @@ -22,6 +23,7 @@ func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { 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_skills.go b/server/router/system/sys_skills.go index 9529e66aa..d9f21bf0f 100644 --- a/server/router/system/sys_skills.go +++ b/server/router/system/sys_skills.go @@ -4,13 +4,15 @@ import "github.com/gin-gonic/gin" type SkillsRouter struct{} -func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) { +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) @@ -25,5 +27,9 @@ func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) { 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/service/system/sys_authority.go b/server/service/system/sys_authority.go index 66805e486..126470703 100644 --- a/server/service/system/sys_authority.go +++ b/server/service/system/sys_authority.go @@ -331,3 +331,82 @@ func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) } 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_casbin.go b/server/service/system/sys_casbin.go index 264c36aff..b281a4e2b 100644 --- a/server/service/system/sys_casbin.go +++ b/server/service/system/sys_casbin.go @@ -171,3 +171,45 @@ func (casbinService *CasbinService) FreshCasbin() (err error) { 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_menu.go b/server/service/system/sys_menu.go index 0a411129b..c2156ef2a 100644 --- a/server/service/system/sys_menu.go +++ b/server/service/system/sys_menu.go @@ -314,6 +314,65 @@ func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) ( 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) diff --git a/server/service/system/sys_skills.go b/server/service/system/sys_skills.go index 090fc927a..c055ddae2 100644 --- a/server/service/system/sys_skills.go +++ b/server/service/system/sys_skills.go @@ -1,10 +1,15 @@ package system import ( + "archive/zip" + "bytes" "context" + "encoding/json" "errors" "fmt" + "io" "io/fs" + "net/http" "os" "path/filepath" "sort" @@ -158,6 +163,107 @@ func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) er 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("技能名称不合法") @@ -279,6 +385,136 @@ func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.Skil 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 { diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go index 630f04cf8..c02906ea0 100644 --- a/server/service/system/sys_user.go +++ b/server/service/system/sys_user.go @@ -109,7 +109,25 @@ func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (lis if err != nil { return } - err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload("Authority").Find(&userList).Error + + 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 } diff --git a/server/source/system/api.go b/server/source/system/api.go index 9ab2e4271..026947305 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -85,6 +85,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {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: "获取权限列表"}, @@ -118,6 +120,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {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: "保存技能脚本"}, @@ -132,6 +135,7 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {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: "创建客户"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index e265f90a9..3132f0e45 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -74,6 +74,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {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"}, @@ -120,6 +122,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {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"}, @@ -134,6 +137,7 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {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"}, @@ -256,6 +260,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {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"}, @@ -297,6 +303,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {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"}, diff --git a/server/utils/upload/aws_s3.go b/server/utils/upload/aws_s3.go index 41a45664e..734fd58c1 100644 --- a/server/utils/upload/aws_s3.go +++ b/server/utils/upload/aws_s3.go @@ -1,18 +1,19 @@ package upload import ( + "context" "errors" "fmt" "mime/multipart" + "strings" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/flipped-aurora/gin-vue-admin/server/global" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" "go.uber.org/zap" ) @@ -21,13 +22,13 @@ type AwsS3 struct{} //@author: [WqyJh](https://github.com/WqyJh) //@object: *AwsS3 //@function: UploadFile -//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket +//@description: Upload file to Aws S3 using aws-sdk-go-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html //@param: file *multipart.FileHeader //@return: string, string, error func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { - session := newSession() - uploader := s3manager.NewUploader(session) + client := newS3Client() + uploader := manager.NewUploader(client) fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey @@ -38,10 +39,10 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { } defer f.Close() // 创建文件 defer 关闭 - _, err := uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket), - Key: aws.String(filename), - Body: f, + _, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket), + Key: aws.String(filename), + Body: f, ContentType: aws.String(file.Header.Get("Content-Type")), }) if err != nil { @@ -55,44 +56,59 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { //@author: [WqyJh](https://github.com/WqyJh) //@object: *AwsS3 //@function: DeleteFile -//@description: Delete file from Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-delete-bucket-item -//@param: file *multipart.FileHeader -//@return: string, string, error +//@description: Delete file from Aws S3 using aws-sdk-go-v2. See https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/s3-example-basic-bucket-operations.html +//@param: key string +//@return: error func (*AwsS3) DeleteFile(key string) error { - session := newSession() - svc := s3.New(session) + client := newS3Client() filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key bucket := global.GVA_CONFIG.AwsS3.Bucket - _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + _, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }) if err != nil { - global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) - return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function client.DeleteObject() failed, err:" + err.Error()) } - _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + waiter := s3.NewObjectNotExistsWaiter(client) + _ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), - }) + }, 30*time.Second) + return nil } -// newSession Create S3 session -func newSession() *session.Session { - sess, _ := session.NewSession(&aws.Config{ - Region: aws.String(global.GVA_CONFIG.AwsS3.Region), - Endpoint: aws.String(global.GVA_CONFIG.AwsS3.Endpoint), //minio在这里设置地址,可以兼容 - S3ForcePathStyle: aws.Bool(global.GVA_CONFIG.AwsS3.S3ForcePathStyle), - DisableSSL: aws.Bool(global.GVA_CONFIG.AwsS3.DisableSSL), - Credentials: credentials.NewStaticCredentials( - global.GVA_CONFIG.AwsS3.SecretID, - global.GVA_CONFIG.AwsS3.SecretKey, +// newS3Client creates an S3 v2 client with static credentials and optional custom endpoint. +// minio在这里设置Endpoint地址,可以兼容 +func newS3Client() *s3.Client { + cfg := global.GVA_CONFIG.AwsS3 + + awsCfg, _ := config.LoadDefaultConfig(context.TODO(), + config.WithRegion(cfg.Region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.SecretID, + cfg.SecretKey, "", - ), + )), + ) + + return s3.NewFromConfig(awsCfg, func(o *s3.Options) { + if cfg.Endpoint != "" { + endpoint := cfg.Endpoint + if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { + if cfg.DisableSSL { + endpoint = "http://" + endpoint + } else { + endpoint = "https://" + endpoint + } + } + o.BaseEndpoint = aws.String(endpoint) + } + o.UsePathStyle = cfg.S3ForcePathStyle }) - return sess } diff --git a/server/utils/upload/cloudflare_r2.go b/server/utils/upload/cloudflare_r2.go index a68d212bc..dc752d4ea 100644 --- a/server/utils/upload/cloudflare_r2.go +++ b/server/utils/upload/cloudflare_r2.go @@ -1,16 +1,17 @@ package upload import ( + "context" "errors" "fmt" "mime/multipart" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/flipped-aurora/gin-vue-admin/server/global" "go.uber.org/zap" ) @@ -18,8 +19,8 @@ import ( type CloudflareR2 struct{} func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, fileName string, err error) { - session := c.newSession() - client := s3manager.NewUploader(session) + client := c.newR2Client() + uploader := manager.NewUploader(client) fileKey := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) fileName = fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.Path, fileKey) @@ -30,56 +31,55 @@ func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, f } defer f.Close() // 创建文件 defer 关闭 - input := &s3manager.UploadInput{ + _, err = uploader.Upload(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(global.GVA_CONFIG.CloudflareR2.Bucket), Key: aws.String(fileName), Body: f, - } - - _, err = client.Upload(input) + }) if err != nil { global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) return "", "", err } - return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, - fileName), - fileKey, - nil + return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, fileName), fileKey, nil } func (c *CloudflareR2) DeleteFile(key string) error { - session := newSession() - svc := s3.New(session) + client := c.newR2Client() filename := global.GVA_CONFIG.CloudflareR2.Path + "/" + key bucket := global.GVA_CONFIG.CloudflareR2.Bucket - _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + _, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), }) if err != nil { - global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) - return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + global.GVA_LOG.Error("function client.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function client.DeleteObject() failed, err:" + err.Error()) } - _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + waiter := s3.NewObjectNotExistsWaiter(client) + _ = waiter.Wait(context.TODO(), &s3.HeadObjectInput{ Bucket: aws.String(bucket), Key: aws.String(filename), - }) + }, 30*time.Second) + return nil } -func (*CloudflareR2) newSession() *session.Session { - endpoint := fmt.Sprintf("%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID) +func (*CloudflareR2) newR2Client() *s3.Client { + endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID) - return session.Must(session.NewSession(&aws.Config{ - Region: aws.String("auto"), - Endpoint: aws.String(endpoint), - Credentials: credentials.NewStaticCredentials( + cfg, _ := config.LoadDefaultConfig(context.TODO(), + config.WithRegion("auto"), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( global.GVA_CONFIG.CloudflareR2.AccessKeyID, global.GVA_CONFIG.CloudflareR2.SecretAccessKey, "", - ), - })) + )), + ) + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(endpoint) + }) } diff --git a/web/src/api/api.js b/web/src/api/api.js index 7eed8fc2b..8ccbbaf93 100644 --- a/web/src/api/api.js +++ b/web/src/api/api.js @@ -174,3 +174,33 @@ export const enterSyncApi = (data) => { data }) } + +/** + * 获取拥有指定API权限的角色ID列表 + * @param {string} path API路径 + * @param {string} method 请求方法 + * @returns {Promise} 角色ID数组 + */ +export const getApiRoles = (path, method) => { + return service({ + url: '/api/getApiRoles', + method: 'get', + params: { path, method } + }) +} + +/** + * 全量覆盖某API关联的角色列表 + * @param {Object} data + * @param {string} data.path API路径 + * @param {string} data.method 请求方法 + * @param {number[]} data.authorityIds 角色ID列表 + * @returns {Promise} + */ +export const setApiRoles = (data) => { + return service({ + url: '/api/setApiRoles', + method: 'post', + data + }) +} diff --git a/web/src/api/authority.js b/web/src/api/authority.js index 040127320..919a728ce 100644 --- a/web/src/api/authority.js +++ b/web/src/api/authority.js @@ -83,3 +83,31 @@ export const updateAuthority = (data) => { data }) } + +/** + * 获取拥有指定角色的用户ID列表 + * @param {number} authorityId 角色ID + * @returns {Promise} 用户ID数组 + */ +export const getUsersByAuthorityId = (authorityId) => { + return service({ + url: '/authority/getUsersByAuthority', + method: 'get', + params: { authorityId } + }) +} + +/** + * 全量覆盖某角色关联的用户列表 + * @param {Object} data + * @param {number} data.authorityId 角色ID + * @param {number[]} data.userIds 用户ID列表 + * @returns {Promise} + */ +export const setRoleUsers = (data) => { + return service({ + url: '/authority/setRoleUsers', + method: 'post', + data + }) +} diff --git a/web/src/api/menu.js b/web/src/api/menu.js index 163b5a697..9e7398d2b 100644 --- a/web/src/api/menu.js +++ b/web/src/api/menu.js @@ -111,3 +111,31 @@ export const getBaseMenuById = (data) => { data }) } + +/** + * 获取拥有指定菜单的角色ID列表 + * @param {number} menuId 菜单ID + * @returns {Promise} 角色ID数组 + */ +export const getMenuRoles = (menuId) => { + return service({ + url: '/menu/getMenuRoles', + method: 'get', + params: { menuId } + }) +} + +/** + * 全量覆盖某菜单关联的角色列表 + * @param {Object} data + * @param {number} data.menuId 菜单ID + * @param {number[]} data.authorityIds 角色ID列表 + * @returns {Promise} + */ +export const setMenuRoles = (data) => { + return service({ + url: '/menu/setMenuRoles', + method: 'post', + data + }) +} diff --git a/web/src/api/skills.js b/web/src/api/skills.js index 3425a7ce7..801c52ef6 100644 --- a/web/src/api/skills.js +++ b/web/src/api/skills.js @@ -31,6 +31,14 @@ export const saveSkill = (data) => { }) } +export const deleteSkill = (data) => { + return service({ + url: '/skills/deleteSkill', + method: 'post', + data + }) +} + export const createSkillScript = (data) => { return service({ url: '/skills/createScript', @@ -142,3 +150,20 @@ export const saveGlobalConstraint = (data) => { data }) } + +export const packageSkill = (data) => { + return service({ + url: '/skills/packageSkill', + method: 'post', + data, + responseType: 'blob' + }) +} + +export const downloadOnlineSkill = (data) => { + return service({ + url: '/skills/downloadOnlineSkill', + method: 'post', + data + }) +} diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 318e8113f..404237155 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -60,6 +60,7 @@ "/src/view/superAdmin/params/sysParams.vue": "SysParams", "/src/view/superAdmin/user/user.vue": "User", "/src/view/system/state.vue": "State", + "/src/view/systemTools/apiToken/index.vue": "Index", "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/index.vue": "AutoCode", @@ -72,7 +73,9 @@ "/src/view/systemTools/formCreate/index.vue": "FormGenerator", "/src/view/systemTools/index.vue": "System", "/src/view/systemTools/installPlugin/index.vue": "Index", + "/src/view/systemTools/loginLog/index.vue": "Index", "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", + "/src/view/systemTools/skills/index.vue": "Skills", "/src/view/systemTools/sysError/sysError.vue": "SysError", "/src/view/systemTools/system/system.vue": "Config", "/src/view/systemTools/version/version.vue": "SysVersion", diff --git a/web/src/view/dashboard/components/card.vue b/web/src/view/dashboard/components/card.vue index c87c15fab..dbc4964f8 100644 --- a/web/src/view/dashboard/components/card.vue +++ b/web/src/view/dashboard/components/card.vue @@ -1,6 +1,6 @@ -