🎉 初始化项目

This commit is contained in:
2026-02-10 17:48:27 +08:00
parent f3da9c506a
commit db934ebed7
1575 changed files with 348967 additions and 0 deletions

View File

@@ -0,0 +1,773 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
---
#### **角色与目标**
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**熟练使用Golang、Vue3、Gin、GORM等技术栈。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
---
### **🚀 重要提示GVA Helper MCP 支持**
**在开始任何GVA开发工作之前请务必注意以下重要工作流程**
1. **MCP支持**: GVA框架本身支持MCPModel Context Protocol提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
gin-vue-admin 采用前后端分离架构:
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用
- **部署 (deploy/)**Docker、Kubernetes 等部署配置
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API
│ └──example/ # 示例模块API
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由
│ └──example/ # 示例路由
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务
│ └── example/ # 示例服务
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── cattery/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── cattery/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层Model, Service, API, Router都有其唯一职责**严禁跨层调用**。例如API层绝不能直接操作数据库必须通过Service层。Service层绝不能直接处理`gin.Context`
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed``response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 必须为字段添加清晰的 `json``gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**
- 当数据模型中使用指针类型(如 `*string``*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**从指针到非指针需要检查nil值从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体DTOs
- **必须**为字段添加 `json``form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码`gin.Context`**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx``request.XxxSearch`),并返回处理结果和 `error`
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则并将HTTP请求路径映射到具体的API处理函数上同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法返回该插件所有API的根路径例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "git.echol.cn/loser/st/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档也是前端开发的重要参考请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块API、组件、页面、状态都有其唯一职责**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用提供统一的接口服务
- **结构**: 按业务模块创建API文件如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名kebab-case短横线命名
- 组件名PascalCase大驼峰
- 变量名camelCase小驼峰
- 常量名UPPER_SNAKE_CASE大写下划线
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式ISO 8601标准
- **⚠️ 数据类型一致性**
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**后端Go中的指针类型在JSON序列化时会自动处理nil值前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范建议AI在开发gin-vue-admin项目时
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制

View File

@@ -0,0 +1,864 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
---
#### **角色与目标**
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**熟练使用Golang、Vue3、Gin、GORM等技术栈。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
---
### **🚀 重要提示GVA Helper MCP 支持**
**在开始任何GVA开发工作之前请务必注意以下重要工作流程**
1. **MCP支持**: GVA框架本身支持MCPModel Context Protocol提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
本项目基于gin-vue-admin框架采用前后端分离架构
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务(整体后端)
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用(管理后台前端)
- **前端 (web-app/)**:基于 Vue 3 + Vite 的单页面应用(用户端前端)
- **部署 (deploy/)**Docker、Kubernetes 等部署配置
### **重要代码组织规则**
**后端代码组织server/**
- `server/api/v1/app/` - 所有用户端API接口必须放在此目录下
- `server/service/app/` - 所有用户端服务层代码必须放在此目录下
- `server/router/app/` - 所有用户端路由定义必须放在此目录下
- `server/api/v1/system/` - 管理后台API接口放在此目录下
- `server/service/system/` - 管理后台服务层代码放在此目录下
- `server/router/system/` - 管理后台路由定义放在此目录下
**前端代码组织**
- `web/` - 管理后台前端,供管理员使用,用于系统管理和数据统计
- `web-app/` - 用户端前端,供普通用户使用,用于业务操作
**命名约定**
- 用户端相关的所有后端代码文件必须组织在 `app` 子目录中
- 这样可以清晰地区分管理端和用户端的代码,便于维护和扩展
- 避免将用户端和管理端代码混在一起
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API管理后台
│ ├── example/ # 示例模块API
│ └── app/ # 用户端API重要所有用户端API放这里
│ └── enter.go # 用户端API入口
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由(管理后台)
│ ├── example/ # 示例路由
│ └── app/ # 用户端路由(重要:所有用户端路由放这里)
│ └── enter.go # 用户端路由入口
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务(管理后台)
│ ├── example/ # 示例服务
│ └── app/ # 用户端服务(重要:所有用户端服务放这里)
│ └── enter.go # 用户端服务入口
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构**
#### **管理后台前端 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义管理后台
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── drama/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件(管理后台)
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── drama/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
#### **用户端前端 (web-app/)**
```
web-app/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义用户端
│ │ └── drama/ # Drama业务API
│ ├── assets/ # 资源文件
│ ├── components/ # 组件
│ ├── views/ # 页面组件(用户端)
│ │ ├── login/ # 登录页
│ │ ├── home/ # 首页
│ │ └── drama/ # Drama业务页面
│ ├── router/ # 路由配置
│ ├── pinia/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
**【重要】代码组织强制规则**
1. **用户端代码组织**
- 所有用户端相关的API代码必须放在 `server/api/v1/app/` 目录下
- 所有用户端相关的Service代码必须放在 `server/service/app/` 目录下
- 所有用户端相关的Router代码必须放在 `server/router/app/` 目录下
- 例如Drama模块用户端API应放在 `server/api/v1/app/drama/`
2. **管理后台代码组织**
- 管理后台相关的API代码放在 `server/api/v1/system/` 或其他业务模块目录
- 管理后台相关的Service代码放在 `server/service/system/` 或其他业务模块目录
- 管理后台相关的Router代码放在 `server/router/system/` 或其他业务模块目录
3. **严格遵守**
- 绝不能将用户端代码和管理端代码混在一起
- 必须按照上述目录结构组织代码
- 这是项目的强制性规范必须100%遵守
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层Model, Service, API, Router都有其唯一职责**严禁跨层调用**。例如API层绝不能直接操作数据库必须通过Service层。Service层绝不能直接处理`gin.Context`
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed``response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 必须为字段添加清晰的 `json``gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**
- 当数据模型中使用指针类型(如 `*string``*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**从指针到非指针需要检查nil值从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体DTOs
- **必须**为字段添加 `json``form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码`gin.Context`**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx``request.XxxSearch`),并返回处理结果和 `error`
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则并将HTTP请求路径映射到具体的API处理函数上同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法返回该插件所有API的根路径例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "git.echol.cn/loser/st/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档也是前端开发的重要参考请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块API、组件、页面、状态都有其唯一职责**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用提供统一的接口服务
- **结构**: 按业务模块创建API文件如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端代码组织规则**
**【重要】前端代码强制规则**
1. **用户端前端 (web-app/)**
- 用户端所有页面放在 `web-app/src/views/` 目录下
- 用户端所有API调用放在 `web-app/src/api/` 目录下
- 用户端所有组件放在 `web-app/src/components/` 目录下
- 面向普通用户的业务功能开发
2. **管理后台前端 (web/)**
- 管理后台页面放在 `web/src/view/` 目录下
- 管理后台API调用放在 `web/src/api/` 目录下
- 管理后台组件放在 `web/src/components/` 目录下
- 面向管理员的管理功能开发
3. **开发原则**
- 用户端和管理端前端代码完全分离
- 不同的项目目录,不同的依赖管理
- 可以独立部署和开发
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名kebab-case短横线命名
- 组件名PascalCase大驼峰
- 变量名camelCase小驼峰
- 常量名UPPER_SNAKE_CASE大写下划线
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式ISO 8601标准
- **⚠️ 数据类型一致性**
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**后端Go中的指针类型在JSON序列化时会自动处理nil值前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范建议AI在开发gin-vue-admin项目时
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制

864
.cursor/project_rules.md Normal file
View File

@@ -0,0 +1,864 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
---
#### **角色与目标**
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**熟练使用Golang、Vue3、Gin、GORM等技术栈。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
---
### **🚀 重要提示GVA Helper MCP 支持**
**在开始任何GVA开发工作之前请务必注意以下重要工作流程**
1. **MCP支持**: GVA框架本身支持MCPModel Context Protocol提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
本项目基于gin-vue-admin框架采用前后端分离架构
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务(整体后端)
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用(管理后台前端)
- **前端 (web-app/)**:基于 Vue 3 + Vite 的单页面应用(用户端前端)
- **部署 (deploy/)**Docker、Kubernetes 等部署配置
### **重要代码组织规则**
**后端代码组织server/**
- `server/api/v1/app/` - 所有用户端API接口必须放在此目录下
- `server/service/app/` - 所有用户端服务层代码必须放在此目录下
- `server/router/app/` - 所有用户端路由定义必须放在此目录下
- `server/api/v1/system/` - 管理后台API接口放在此目录下
- `server/service/system/` - 管理后台服务层代码放在此目录下
- `server/router/system/` - 管理后台路由定义放在此目录下
**前端代码组织**
- `web/` - 管理后台前端,供管理员使用,用于系统管理和数据统计
- `web-app/` - 用户端前端,供普通用户使用,用于业务操作
**命名约定**
- 用户端相关的所有后端代码文件必须组织在 `app` 子目录中
- 这样可以清晰地区分管理端和用户端的代码,便于维护和扩展
- 避免将用户端和管理端代码混在一起
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API管理后台
│ ├── example/ # 示例模块API
│ └── app/ # 用户端API重要所有用户端API放这里
│ └── enter.go # 用户端API入口
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由(管理后台)
│ ├── example/ # 示例路由
│ └── app/ # 用户端路由(重要:所有用户端路由放这里)
│ └── enter.go # 用户端路由入口
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务(管理后台)
│ ├── example/ # 示例服务
│ └── app/ # 用户端服务(重要:所有用户端服务放这里)
│ └── enter.go # 用户端服务入口
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构**
#### **管理后台前端 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义管理后台
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── drama/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件(管理后台)
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── drama/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
#### **用户端前端 (web-app/)**
```
web-app/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义用户端
│ │ └── drama/ # Drama业务API
│ ├── assets/ # 资源文件
│ ├── components/ # 组件
│ ├── views/ # 页面组件(用户端)
│ │ ├── login/ # 登录页
│ │ ├── home/ # 首页
│ │ └── drama/ # Drama业务页面
│ ├── router/ # 路由配置
│ ├── pinia/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
**【重要】代码组织强制规则**
1. **用户端代码组织**
- 所有用户端相关的API代码必须放在 `server/api/v1/app/` 目录下
- 所有用户端相关的Service代码必须放在 `server/service/app/` 目录下
- 所有用户端相关的Router代码必须放在 `server/router/app/` 目录下
- 例如Drama模块用户端API应放在 `server/api/v1/app/drama/`
2. **管理后台代码组织**
- 管理后台相关的API代码放在 `server/api/v1/system/` 或其他业务模块目录
- 管理后台相关的Service代码放在 `server/service/system/` 或其他业务模块目录
- 管理后台相关的Router代码放在 `server/router/system/` 或其他业务模块目录
3. **严格遵守**
- 绝不能将用户端代码和管理端代码混在一起
- 必须按照上述目录结构组织代码
- 这是项目的强制性规范必须100%遵守
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层Model, Service, API, Router都有其唯一职责**严禁跨层调用**。例如API层绝不能直接操作数据库必须通过Service层。Service层绝不能直接处理`gin.Context`
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed``response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 必须为字段添加清晰的 `json``gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**
- 当数据模型中使用指针类型(如 `*string``*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**从指针到非指针需要检查nil值从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体DTOs
- **必须**为字段添加 `json``form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码`gin.Context`**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx``request.XxxSearch`),并返回处理结果和 `error`
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则并将HTTP请求路径映射到具体的API处理函数上同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法返回该插件所有API的根路径例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "git.echol.cn/loser/st/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档也是前端开发的重要参考请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块API、组件、页面、状态都有其唯一职责**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用提供统一的接口服务
- **结构**: 按业务模块创建API文件如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端代码组织规则**
**【重要】前端代码强制规则**
1. **用户端前端 (web-app/)**
- 用户端所有页面放在 `web-app/src/views/` 目录下
- 用户端所有API调用放在 `web-app/src/api/` 目录下
- 用户端所有组件放在 `web-app/src/components/` 目录下
- 面向普通用户的业务功能开发
2. **管理后台前端 (web/)**
- 管理后台页面放在 `web/src/view/` 目录下
- 管理后台API调用放在 `web/src/api/` 目录下
- 管理后台组件放在 `web/src/components/` 目录下
- 面向管理员的管理功能开发
3. **开发原则**
- 用户端和管理端前端代码完全分离
- 不同的项目目录,不同的依赖管理
- 可以独立部署和开发
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名kebab-case短横线命名
- 组件名PascalCase大驼峰
- 变量名camelCase小驼峰
- 常量名UPPER_SNAKE_CASE大写下划线
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式ISO 8601标准
- **⚠️ 数据类型一致性**
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**后端Go中的指针类型在JSON序列化时会自动处理nil值前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范建议AI在开发gin-vue-admin项目时
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制

View File

@@ -0,0 +1,864 @@
### 功能描述以及必要性描述
---
name: gin-vue-admin
description: |
gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。
前端技术栈:
- Vue 3.5.7 + Composition API
- Vite 6.2.3 构建工具
- Pinia 2.2.2 状态管理
- Element Plus 2.10.2 UI组件库
- UnoCSS 66.4.2 原子化CSS框架
- Vue Router 4.4.3 路由管理
- Axios 1.8.2 HTTP客户端
- ECharts 5.5.1 数据可视化
- @vueuse/core Vue组合式API工具集
后端技术栈:
- Go 1.23 + Gin 1.10.0 Web框架
- GORM 1.25.12 ORM框架
- Casbin 2.103.0 权限管理
- Viper 1.19.0 配置管理
- Zap 1.27.0 日志系统
- Redis 9.7.0 缓存
- JWT 5.2.2 认证授权
- 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库
- 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务
核心特性:
- 完整的RBAC权限控制系统
- 代码自动生成功能
- 丰富的中间件支持
- 插件化架构设计
- Swagger API文档
---
#### **角色与目标**
你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**熟练使用Golang、Vue3、Gin、GORM等技术栈。
你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
---
### **🚀 重要提示GVA Helper MCP 支持**
**在开始任何GVA开发工作之前请务必注意以下重要工作流程**
1. **MCP支持**: GVA框架本身支持MCPModel Context Protocol提供了强大的开发辅助能力
2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手专门为GVA框架开发提供支持
3. **开发流程**:
- **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导**
- **第二步**: 在获得GVA Helper的专业建议和代码示例后再进行具体的开发操作
- **第三步**: 遵循GVA Helper提供的最佳实践和代码规范
4. **优势**: 通过GVA Helper可以获得
- 最新的GVA框架特性和最佳实践
- 符合项目规范的代码模板
- 避免常见的开发陷阱和错误
- 确保代码质量和一致性
**请始终记住GVA Helper → 获得支持 → 开始开发**
---
### **核心开发指令:绝不可违背的原则**
## **项目结构说明**
### **整体架构**
本项目基于gin-vue-admin框架采用前后端分离架构
- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务(整体后端)
- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用(管理后台前端)
- **前端 (web-app/)**:基于 Vue 3 + Vite 的单页面应用(用户端前端)
- **部署 (deploy/)**Docker、Kubernetes 等部署配置
### **重要代码组织规则**
**后端代码组织server/**
- `server/api/v1/app/` - 所有用户端API接口必须放在此目录下
- `server/service/app/` - 所有用户端服务层代码必须放在此目录下
- `server/router/app/` - 所有用户端路由定义必须放在此目录下
- `server/api/v1/system/` - 管理后台API接口放在此目录下
- `server/service/system/` - 管理后台服务层代码放在此目录下
- `server/router/system/` - 管理后台路由定义放在此目录下
**前端代码组织**
- `web/` - 管理后台前端,供管理员使用,用于系统管理和数据统计
- `web-app/` - 用户端前端,供普通用户使用,用于业务操作
**命名约定**
- 用户端相关的所有后端代码文件必须组织在 `app` 子目录中
- 这样可以清晰地区分管理端和用户端的代码,便于维护和扩展
- 避免将用户端和管理端代码混在一起
### **后端目录结构 (server/)**
```
server/
├── api/ # API控制器层
│ └── v1/ # API版本控制
│ ├── enter.go # API组入口文件
│ ├── system/ # 系统模块API管理后台
│ ├── example/ # 示例模块API
│ └── app/ # 用户端API重要所有用户端API放这里
│ └── enter.go # 用户端API入口
├── config/ # 配置结构体定义
├── core/ # 核心启动文件
├── docs/ # Swagger文档
├── global/ # 全局变量和模型
├── initialize/ # 初始化模块
├── middleware/ # 中间件
├── model/ # 数据模型层
│ ├── system/ # 系统模块模型
│ ├── example/ # 示例模块模型
│ └── common/ # 通用模型
├── plugin/ # 插件目录
│ ├── announcement/ # 公告插件
│ └── email/ # 邮件插件
├── router/ # 路由层
│ ├── enter.go # 路由组入口
│ ├── system/ # 系统路由(管理后台)
│ ├── example/ # 示例路由
│ └── app/ # 用户端路由(重要:所有用户端路由放这里)
│ └── enter.go # 用户端路由入口
├── service/ # 服务层
│ ├── enter.go # 服务组入口
│ ├── system/ # 系统服务(管理后台)
│ ├── example/ # 示例服务
│ └── app/ # 用户端服务(重要:所有用户端服务放这里)
│ └── enter.go # 用户端服务入口
├── source/ # 数据初始化
├── utils/ # 工具包
├── config.yaml # 配置文件
└── main.go # 程序入口
```
### **前端目录结构**
#### **管理后台前端 (web/)**
```
web/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义管理后台
│ │ ├── user.js # 用户相关API
│ │ ├── menu.js # 菜单相关API
│ │ └── drama/ # 业务模块API
│ ├── assets/ # 资源文件
│ │ ├── icons/ # 图标
│ │ └── images/ # 图片
│ ├── core/ # 核心配置
│ ├── directive/ # 自定义指令
│ ├── hooks/ # 组合式API钩子
│ ├── pinia/ # 状态管理
│ │ ├── index.js # Pinia入口
│ │ └── modules/ # 状态模块
│ ├── plugin/ # 前端插件
│ │ ├── announcement/ # 公告插件
│ │ └── email/ # 邮件插件
│ ├── router/ # 路由配置
│ ├── style/ # 样式文件
│ ├── utils/ # 工具函数
│ ├── view/ # 页面组件(管理后台)
│ │ ├── dashboard/ # 仪表盘
│ │ ├── layout/ # 布局组件
│ │ ├── login/ # 登录页
│ │ ├── superAdmin/ # 超级管理员
│ │ ├── systemTools/ # 系统工具
│ │ └── drama/ # 业务页面
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
#### **用户端前端 (web-app/)**
```
web-app/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口定义用户端
│ │ └── drama/ # Drama业务API
│ ├── assets/ # 资源文件
│ ├── components/ # 组件
│ ├── views/ # 页面组件(用户端)
│ │ ├── login/ # 登录页
│ │ ├── home/ # 首页
│ │ └── drama/ # Drama业务页面
│ ├── router/ # 路由配置
│ ├── pinia/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.js # 程序入口
├── package.json # 依赖配置
├── vite.config.js # Vite配置
└── uno.config.js # UnoCSS配置
```
---
#### 后端规则
**【重要】代码组织强制规则**
1. **用户端代码组织**
- 所有用户端相关的API代码必须放在 `server/api/v1/app/` 目录下
- 所有用户端相关的Service代码必须放在 `server/service/app/` 目录下
- 所有用户端相关的Router代码必须放在 `server/router/app/` 目录下
- 例如Drama模块用户端API应放在 `server/api/v1/app/drama/`
2. **管理后台代码组织**
- 管理后台相关的API代码放在 `server/api/v1/system/` 或其他业务模块目录
- 管理后台相关的Service代码放在 `server/service/system/` 或其他业务模块目录
- 管理后台相关的Router代码放在 `server/router/system/` 或其他业务模块目录
3. **严格遵守**
- 绝不能将用户端代码和管理端代码混在一起
- 必须按照上述目录结构组织代码
- 这是项目的强制性规范必须100%遵守
在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的分层架构**:
- **职责单一**: 每个层Model, Service, API, Router都有其唯一职责**严禁跨层调用**。例如API层绝不能直接操作数据库必须通过Service层。Service层绝不能直接处理`gin.Context`
- **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`
2. **`enter.go` 组管理模式**:
- 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`
- 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。
3. **详尽的 Swagger 注释 (API层强制要求)**:
- **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。
4. **统一的响应与错误处理**:
- Service 层函数遇到业务错误时,应返回 `error` 对象。
- API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed``response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。
---
### **各层级代码实现规范**
#### **1. 模型层 (`model/`)**
- **数据模型 (`model/xxx.go`)**:
- 用于定义与数据库表映射的 GORM 结构体。
- 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。
- 必须为字段添加清晰的 `json``gorm` 标签。
- **⚠️ 重要提醒:数据类型一致性**
- **必须确保**同一字段在不同模型文件中的数据类型保持严格一致
- 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型
- **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常
- **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致
- **检查要点**特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段
- **⚠️ 指针类型处理**
- 当数据模型中使用指针类型(如 `*string``*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换
- **转换规则**从指针到非指针需要检查nil值从非指针到指针需要取地址
- **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }`
- **请求模型 (`model/request/xxx.go`)**:
- 用于定义接收前端请求参数的结构体DTOs
- **必须**为字段添加 `json``form` 标签,以便 Gin 进行参数绑定。
- 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。
#### **2. 服务层 (`service/`)**
- **职责**: 封装所有核心业务逻辑进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码`gin.Context`**。
- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。
- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx``request.XxxSearch`),并返回处理结果和 `error`
- **⚠️ 数据类型处理注意事项**:
- 在进行数据模型转换时,**必须确保**字段类型的一致性
- 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型
- 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑
#### **3. API层 (`api/`)**
- **职责**: 作为HTTP请求的入口负责参数校验、调用Service层方法、并返回格式化的JSON响应。
- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。
- **Swagger 示例 (必须遵循)**:
Go
```
// CreateXxx 创建XXX
// @Tags XxxModule
// @Summary 创建一个新的XXX
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.CreateXxxRequest true "XXX的名称和描述"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /xxx/createXxx [post]
func (a *XxxApi) CreateXxx(c *gin.Context) {
// ...
}
```
#### **4. 路由层 (`router/`)**
- **职责**: 定义API路由规则并将HTTP请求路径映射到具体的API处理函数上同时配置中间件。
- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。
- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。
- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。
#### **5. 初始化层 (`initialize/`)**
- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。
- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。
- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法注册所有API路由。
- **`menu.go`**: 实现 `InitializeMenu` 函数负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。
- viper.go: 加载插件配置文件
- api.go: 注册API到系统
#### **6. 插件入口 (`plugin.go`)
- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。
- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。
- **插件注册**: **必须**调用 ```
func init() {
interfaces.Register(Plugin)
}
```
方法,让插件自动注册到本体中
- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。
- **`RouterPath`方法**: 实现 `RouterPath` 方法返回该插件所有API的根路径例如 `"/myPlugin"`。
### 模块间引用关系:
- API层引用Service层在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService`
- Router层引用API层在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod`
- Initialize/Router引用Router层通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter`
- 各模块通过enter.go文件组织和暴露功能避免循环引用
### 插件默认注册功能
`plugin/register.go` 文件下用 ` _ "git.echol.cn/loser/st/server/plugin/插件"
` 的方式匿名引用用于激活插件本体的init
### 代码组织示例:
1. Service入口 (service/enter.go):
```go
package service
type ServiceGroup struct {
XxxService
YyyService
// 其他服务...
}
var ServiceGroupApp = new(ServiceGroup)
```
2. API入口 (api/enter.go):
```go
package api
type ApiGroup struct {
XxxApi
YyyApi
// 其他API...
}
var ApiGroupApp = new(ApiGroup)
```
3. Router入口 (router/enter.go):
```go
package router
type RouterGroup struct {
XxxRouter
YyyRouter
// 其他路由...
}
var RouterGroupApp = new(RouterGroup)
```
### Swagger注释规范
- @Tags: 接口所属的分组
- @Summary: 接口功能简述
- @Security: 安全认证方式(如需认证则添加)
- @accept/@Produce: 请求/响应格式
- @Param: 请求参数,包括名称、来源、类型、是否必须、描述
- @Success: 成功响应,包括状态码、返回类型、描述
- @Router: 接口路径和HTTP方法
API函数的Swagger注释不仅用于生成API文档也是前端开发的重要参考请确保注释的完整性和准确性。
---
### **开发工作流**
1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。
2. **【第一步】模型设计 (奠定基础)**:
- 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。
3. **【第二步】自下而上,分层实现**:
- 具体项目结构可以参考server/plugin/announcement 这个插件,非常经典!
- 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。
- 确保每一层的代码都完整、健壮,并严格遵守上述规范。
4. **【第三步】插件初始化与注册**:
- 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。
5. **【第四步】提供完整代码**:
- 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。
---
## **前端开发规范**
### **角色与目标**
你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。
你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。
### **核心开发指令:绝不可违背的原则**
#### 前端规则
在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则:
1. **严格的模块化架构**:
- **职责单一**: 每个模块API、组件、页面、状态都有其唯一职责**严禁跨模块直接调用**
- **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口`
2. **统一的API调用模式**:
- 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装
- **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求
- API函数**必须**包含完整的JSDoc注释描述接口功能、参数和返回值
3. **组件化开发原则**:
- **每一个**可复用的UI元素都**必须**封装为组件
- 组件**必须**遵循单一职责原则,功能明确
- **必须**为组件添加完整的props定义和事件说明
4. **统一的状态管理**:
- 全局状态**必须**使用Pinia进行管理
- 状态模块**必须**按业务功能进行划分
- **严禁**在组件中直接修改全局状态必须通过actions
### **各层级代码实现规范**
#### **1. API层 (`src/api/`)**
- **职责**: 封装所有后端API调用提供统一的接口服务
- **结构**: 按业务模块创建API文件如 `user.js`、`menu.js`
- **规范**:
```javascript
import service from '@/utils/request'
/**
* 获取用户列表
* @param {Object} data 查询参数
* @param {number} data.page 页码
* @param {number} data.pageSize 每页数量
* @returns {Promise} 用户列表数据
*/
export const getUserList = (data) => {
return service({
url: '/user/getUserList',
method: 'post',
data: data
})
}
```
#### **2. 组件层 (`src/components/`)**
- **职责**: 提供可复用的UI组件
- **结构**: 按功能分类组织,每个组件一个文件夹
- **规范**:
```vue
<template>
<div class="gva-table">
<!-- 组件内容 -->
</div>
</template>
<script setup>
/**
* 通用表格组件
* @component GvaTable
* @description 提供统一的表格展示功能
*/
// Props定义
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
// 事件定义
const emit = defineEmits(['refresh', 'edit', 'delete'])
</script>
```
#### **3. 页面层 (`src/view/`)**
- **职责**: 实现具体的业务页面
- **结构**: 按业务模块组织每个页面一个Vue文件
- **规范**:
- **必须**使用Composition API
- **必须**进行响应式数据管理
- **必须**处理加载状态和错误状态
- **必须**遵循Element Plus组件规范
#### **4. 状态管理 (`src/pinia/`)**
- **职责**: 管理全局状态和业务逻辑
- **结构**: 按业务模块创建store文件
- **规范**:
```javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'
export const useUserStore = defineStore('user', () => {
// 状态定义 - 使用 ref() 创建响应式状态
const userInfo = ref({
uuid: '',
nickName: '',
headerImg: '',
authority: {}
})
const token = useStorage('token', '')
// 计算属性 - 使用 computed() 定义
const isLogin = computed(() => !!token.value)
// 方法定义 - 直接定义函数作为 actions
const setUserInfo = (val) => {
userInfo.value = val
}
const setToken = (val) => {
token.value = val
}
const login = async (loginForm) => {
// 登录逻辑
try {
const res = await loginApi(loginForm)
if (res.code === 0) {
setUserInfo(res.data.user)
setToken(res.data.token)
return true
}
return false
} catch (error) {
console.error('Login error:', error)
return false
}
}
const logout = async () => {
// 登出逻辑
token.value = ''
userInfo.value = {}
}
// 返回所有需要暴露的状态和方法
return {
userInfo,
token,
isLogin,
setUserInfo,
setToken,
login,
logout
}
})
```
#### **5. 路由管理 (`src/router/`)**
- **职责**: 管理页面路由和权限控制
- **规范**:
- **必须**配置路由元信息
- **必须**实现权限验证
- **必须**支持动态路由
### **前端代码组织规则**
**【重要】前端代码强制规则**
1. **用户端前端 (web-app/)**
- 用户端所有页面放在 `web-app/src/views/` 目录下
- 用户端所有API调用放在 `web-app/src/api/` 目录下
- 用户端所有组件放在 `web-app/src/components/` 目录下
- 面向普通用户的业务功能开发
2. **管理后台前端 (web/)**
- 管理后台页面放在 `web/src/view/` 目录下
- 管理后台API调用放在 `web/src/api/` 目录下
- 管理后台组件放在 `web/src/components/` 目录下
- 面向管理员的管理功能开发
3. **开发原则**
- 用户端和管理端前端代码完全分离
- 不同的项目目录,不同的依赖管理
- 可以独立部署和开发
### **前端插件开发规范**
#### **插件目录结构**
```
src/plugin/[插件名]/
├── api/ # 插件API接口
│ └── [模块].js
├── components/ # 插件组件(可选)
│ └── [组件名].vue
├── view/ # 插件页面
│ └── [页面名].vue
├── form/ # 插件表单(可选)
│ └── [表单名].vue
└── index.js # 插件入口文件(可选)
```
#### **插件开发原则**
1. **独立性**: 插件应该是自包含的,不依赖其他业务模块
2. **可配置性**: 插件应该支持配置化,便于定制
3. **可扩展性**: 插件应该预留扩展接口
4. **一致性**: 插件UI风格应与主系统保持一致
### **代码质量要求**
1. **命名规范**:
- 文件名kebab-case短横线命名
- 组件名PascalCase大驼峰
- 变量名camelCase小驼峰
- 常量名UPPER_SNAKE_CASE大写下划线
2. **注释规范**:
- **必须**为所有API函数添加JSDoc注释
- **必须**为复杂组件添加功能说明
- **必须**为关键业务逻辑添加行内注释
3. **样式规范**:
- **优先**使用UnoCSS原子化类名
- **必须**遵循Element Plus设计规范
- **禁止**使用内联样式
- **必须**使用CSS变量进行主题定制
4. **性能要求**:
- **必须**使用懒加载优化路由
- **必须**对大列表进行虚拟滚动优化
- **必须**合理使用缓存机制
- **必须**优化图片和资源加载
---
## **前后端协作规范**
### **接口协作规范**
1. **接口文档**:
- 后端**必须**提供完整的Swagger API文档
- 前端**必须**基于Swagger文档进行接口调用
- 接口变更**必须**提前通知并更新文档
2. **数据格式**:
- **统一**使用JSON格式进行数据交换
- **统一**响应格式:`{code, data, msg}`
- **统一**分页格式:`{page, pageSize, total, list}`
- **统一**时间格式ISO 8601标准
- **⚠️ 数据类型一致性**
- 前后端对于同一字段**必须**使用相同的数据类型
- 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致
- 特别注意状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段
- 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型
- **指针类型处理**后端Go中的指针类型在JSON序列化时会自动处理nil值前端接收到的是对应的基础类型或null值
3. **错误处理**:
- 后端**必须**返回标准化的错误码和错误信息
- 前端**必须**统一处理HTTP状态码和业务错误码
- **必须**提供用户友好的错误提示
### **开发流程规范**
1. **需求分析阶段**:
- 确定功能需求和接口设计
- 定义数据模型和业务流程
- 制定前后端开发计划
2. **开发阶段**:
- 后端优先开发API接口
- 前端基于Mock数据进行并行开发
- 定期进行接口联调测试
3. **测试阶段**:
- 单元测试:前后端各自负责
- 集成测试:前后端协作完成
- 用户验收测试:产品团队主导
### **版本管理规范**
1. **分支策略**:
- `main`:生产环境分支
- `develop`:开发环境分支
- `feature/*`:功能开发分支
- `hotfix/*`:紧急修复分支
2. **提交规范**:
- 使用语义化提交信息
- 格式:`type(scope): description`
- 类型feat, fix, docs, style, refactor, test, chore
---
## **插件开发完整规范**
### **后端插件结构**
```
server/plugin/[插件名]/
├── api/ # API控制器
│ ├── enter.go # API组入口
│ └── [模块].go # 具体API实现
├── config/ # 插件配置
│ └── config.go
├── initialize/ # 初始化模块
│ ├── api.go # API注册
│ ├── gorm.go # 数据库初始化
│ ├── menu.go # 菜单初始化
│ ├── router.go # 路由初始化
│ └── viper.go # 配置初始化
├── model/ # 数据模型
│ ├── [模型].go # 数据库模型
│ └── request/ # 请求模型
├── router/ # 路由定义
│ ├── enter.go # 路由组入口
│ └── [模块].go # 具体路由
├── service/ # 业务服务
│ ├── enter.go # 服务组入口
│ └── [模块].go # 具体服务
└── plugin.go # 插件入口
```
### **前端插件结构**
```
web/src/plugin/[插件名]/
├── api/ # API接口
│ └── [模块].js
├── components/ # 插件组件
│ └── [组件].vue
├── view/ # 插件页面
│ └── [页面].vue
├── form/ # 表单组件
│ └── [表单].vue
└── config.js # 插件配置
```
### **插件开发工作流**
1. **【第一步】需求分析**:
- 明确插件功能和业务需求
- 设计数据模型和接口规范
- 规划前端页面和交互流程
2. **【第二步】后端开发**:
- 创建数据模型和请求模型
- 实现服务层业务逻辑
- 开发API控制器和路由
- 编写初始化和配置代码
3. **【第三步】前端开发**:
- 创建API接口封装
- 开发页面组件和表单
- 实现业务逻辑和状态管理
- 集成到主系统菜单
4. **【第四步】测试集成**:
- 单元测试和集成测试
- 前后端联调测试
- 用户体验测试
- 性能和安全测试
### **插件质量标准**
1. **功能完整性**: 插件功能完整,满足业务需求
2. **代码质量**: 代码规范,注释完整,易于维护
3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误
4. **性能表现**: 响应速度快,资源占用合理
5. **用户体验**: 界面友好,操作流畅,错误处理完善
6. **兼容性**: 与主系统兼容,不影响其他功能
7. **安全性**: 数据安全,权限控制,防止安全漏洞
---
### **建议和方案**
基于以上规范建议AI在开发gin-vue-admin项目时
1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织
2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格
3. **注重文档完整性**确保API文档、代码注释和使用说明的完整性
4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理
5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强
6. **重视安全性**:实现完善的权限控制和数据验证机制

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.sql linguist-language=GO
*.html linguist-language=GO

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

11
.idea/go.imports.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/st.iml" filepath="$PROJECT_DIR$/.idea/st.iml" />
</modules>
</component>
</project>

9
.idea/st.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

75
Makefile Normal file
View File

@@ -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 = git.echol.cn/loser/st/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 ..

View File

@@ -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'
# 设置mysqlredis持久化保存
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

18
deploy/docker/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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

View File

@@ -0,0 +1,148 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: config.yaml
annotations:
flipped-aurora/gin-vue-admin: backend
github: "https://git.echol.cn/loser/st.git"
app.kubernetes.io/version: 0.0.1
labels:
app: gva-server
version: gva-vue3
data:
config.yaml: |
# git.echol.cn/loser/st/server Global Configuration
# jwt configuration
jwt:
signing-key: 'qmPlus'
expires-time: 604800
buffer-time: 86400
# zap logger configuration
zap:
level: 'info'
format: 'console'
prefix: '[git.echol.cn/loser/st/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.git.echol.cn/loser/st/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: 'git.echol.cn/loser/st/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" }
]

View File

@@ -0,0 +1,74 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gva-server
annotations:
flipped-aurora/gin-vue-admin: backend
github: "https://git.echol.cn/loser/st.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/git.echol.cn/loser/st/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

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: gva-server
annotations:
flipped-aurora/gin-vue-admin: backend
github: "https://git.echol.cn/loser/st.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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gva-web
annotations:
flipped-aurora/gin-vue-admin: ui
github: "https://git.echol.cn/loser/st.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

View File

@@ -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

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: gva-web
annotations:
flipped-aurora/gin-vue-admin: ui
github: "https://git.echol.cn/loser/st.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

BIN
docs/gin-vue-admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,49 @@
{
"folders": [
{
"path": "server",
"name": "backend"
},
{
"path": "web",
"name": "frontend"
},
{
"path": ".",
"name": "root"
}
],
"settings": {
"go.toolsEnvVars": {
"GOPROXY": "https://goproxy.cn,direct",
"GONOPROXY": "none;"
}
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"type": "go",
"request": "launch",
"name": "Backend",
"cwd": "${workspaceFolder:backend}",
"program": "${workspaceFolder:backend}/"
},
{
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder:frontend}",
"name": "Frontend",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "serve"]
}
],
"compounds": [
{
"name": "Both (Backend & Frontend)",
"configurations": ["Backend", "Frontend"],
"stopAll": true
}
]
}
}

31
server/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM golang:alpine as builder
WORKDIR /go/src/git.echol.cn/loser/st/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/git.echol.cn/loser/st/server
COPY --from=0 /go/src/git.echol.cn/loser/st/server/server ./
COPY --from=0 /go/src/git.echol.cn/loser/st/server/resource ./resource/
COPY --from=0 /go/src/git.echol.cn/loser/st/server/config.docker.yaml ./
# 挂载目录如果使用了sqlite数据库容器命令示例docker run -d -v /宿主机路径/gva.db:/go/src/git.echol.cn/loser/st/server/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0
# VOLUME ["/go/src/git.echol.cn/loser/st/server"]
EXPOSE 8888
ENTRYPOINT ./server -c config.docker.yaml

54
server/README.md Normal file
View File

@@ -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接口封装 |

View File

@@ -0,0 +1,5 @@
package app
type ApiGroup struct {
AuthApi
}

15
server/api/v1/enter.go Normal file
View File

@@ -0,0 +1,15 @@
package v1
import (
"git.echol.cn/loser/st/server/api/v1/app"
"git.echol.cn/loser/st/server/api/v1/example"
"git.echol.cn/loser/st/server/api/v1/system"
)
var ApiGroupApp = new(ApiGroup)
type ApiGroup struct {
SystemApiGroup system.ApiGroup
ExampleApiGroup example.ApiGroup
AppApiGroup app.ApiGroup
}

View File

@@ -0,0 +1,15 @@
package example
import "git.echol.cn/loser/st/server/service"
type ApiGroup struct {
CustomerApi
FileUploadAndDownloadApi
AttachmentCategoryApi
}
var (
customerService = service.ServiceGroupApp.ExampleServiceGroup.CustomerService
fileUploadAndDownloadService = service.ServiceGroupApp.ExampleServiceGroup.FileUploadAndDownloadService
attachmentCategoryService = service.ServiceGroupApp.ExampleServiceGroup.AttachmentCategoryService
)

View File

@@ -0,0 +1,82 @@
package example
import (
"git.echol.cn/loser/st/server/global"
common "git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,156 @@
package example
import (
"fmt"
"io"
"mime/multipart"
"strconv"
"strings"
"git.echol.cn/loser/st/server/model/example"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
exampleRes "git.echol.cn/loser/st/server/model/example/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,176 @@
package example
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/example"
exampleRes "git.echol.cn/loser/st/server/model/example/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,135 @@
package example
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/example"
"git.echol.cn/loser/st/server/model/example/request"
exampleRes "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,115 @@
package system
import (
"git.echol.cn/loser/st/server/global"
common "git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
request "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,144 @@
package system
import (
"fmt"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/mcp/client"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
"github.com/gin-gonic/gin"
"github.com/mark3labs/mcp-go/mcp"
)
// Create
// @Tags mcp
// @Summary 自动McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.AutoMcpTool true "创建自动代码"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/mcp [post]
func (a *AutoCodeTemplateApi) MCP(c *gin.Context) {
var info request.AutoMcpTool
err := c.ShouldBindJSON(&info)
if err != nil {
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)
}
// Create
// @Tags mcp
// @Summary 自动McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.AutoMcpTool true "创建自动代码"
// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
// @Router /autoCode/mcpList [post]
func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) {
baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
defer testClient.Close()
toolsRequest := mcp.ListToolsRequest{}
list, err := testClient.ListTools(c.Request.Context(), toolsRequest)
if err != nil {
response.FailWithMessage("创建失败", c)
global.GVA_LOG.Error(err.Error())
return
}
mcpServerConfig := map[string]interface{}{
"mcpServers": map[string]interface{}{
global.GVA_CONFIG.MCP.Name: map[string]string{
"url": baseUrl,
},
},
}
response.OkWithData(gin.H{
"mcpServerConfig": mcpServerConfig,
"list": list,
}, c)
}
// Create
// @Tags mcp
// @Summary 测试McpTool
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body object true "调用MCP Tool的参数"
// @Success 200 {object} response.Response "{"success":true,"data":{},"msg":"测试成功"}"
// @Router /autoCode/mcpTest [post]
func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) {
// 定义接口请求结构
var testRequest struct {
Name string `json:"name" binding:"required"` // 工具名称
Arguments map[string]interface{} `json:"arguments" binding:"required"` // 工具参数
}
// 绑定JSON请求体
if err := c.ShouldBindJSON(&testRequest); err != nil {
response.FailWithMessage("参数解析失败:"+err.Error(), c)
return
}
// 创建MCP客户端
baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath)
testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name)
if err != nil {
response.FailWithMessage("创建MCP客户端失败:"+err.Error(), c)
return
}
defer testClient.Close()
ctx := c.Request.Context()
// 初始化MCP连接
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "testClient",
Version: "v1.0.0",
}
_, err = testClient.Initialize(ctx, initRequest)
if err != nil {
response.FailWithMessage("初始化MCP连接失败:"+err.Error(), c)
return
}
// 构建工具调用请求
request := mcp.CallToolRequest{}
request.Params.Name = testRequest.Name
request.Params.Arguments = testRequest.Arguments
// 调用工具
result, err := testClient.CallTool(ctx, request)
if err != nil {
response.FailWithMessage("工具调用失败:"+err.Error(), c)
return
}
// 处理响应结果
if len(result.Content) == 0 {
response.FailWithMessage("工具未返回任何内容", c)
return
}
// 返回结果
response.OkWithData(result.Content, c)
}

View File

@@ -0,0 +1,100 @@
package system
import (
"git.echol.cn/loser/st/server/global"
common "git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,218 @@
package system
import (
"fmt"
"os"
"path/filepath"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,121 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
"git.echol.cn/loser/st/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)
}
}

View File

@@ -0,0 +1,57 @@
package system
import "git.echol.cn/loser/st/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
}
var (
apiService = service.ServiceGroupApp.SystemServiceGroup.ApiService
jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService
menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService
userService = service.ServiceGroupApp.SystemServiceGroup.UserService
initDBService = service.ServiceGroupApp.SystemServiceGroup.InitDBService
casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService
baseMenuService = service.ServiceGroupApp.SystemServiceGroup.BaseMenuService
authorityService = service.ServiceGroupApp.SystemServiceGroup.AuthorityService
dictionaryService = service.ServiceGroupApp.SystemServiceGroup.DictionaryService
authorityBtnService = service.ServiceGroupApp.SystemServiceGroup.AuthorityBtnService
systemConfigService = service.ServiceGroupApp.SystemServiceGroup.SystemConfigService
sysParamsService = service.ServiceGroupApp.SystemServiceGroup.SysParamsService
operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService
dictionaryDetailService = service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService
autoCodeService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeService
autoCodePluginService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePlugin
autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService
loginLogService = service.ServiceGroupApp.SystemServiceGroup.LoginLogService
apiTokenService = service.ServiceGroupApp.SystemServiceGroup.ApiTokenService
skillsService = service.ServiceGroupApp.SystemServiceGroup.SkillsService
)

View File

@@ -0,0 +1,323 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,81 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
sysReq "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,202 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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(&copyInfo)
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)
}

View File

@@ -0,0 +1,80 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,117 @@
package system
import (
"git.echol.cn/loser/st/server/model/common"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type AutoCodeApi struct{}
// GetDB
// @Tags AutoCode
// @Summary 获取当前所有数据库
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前所有数据库"
// @Router /autoCode/getDB [get]
func (autoApi *AutoCodeApi) GetDB(c *gin.Context) {
businessDB := c.Query("businessDB")
dbs, err := autoCodeService.Database(businessDB).GetDB(businessDB)
var dbList []map[string]interface{}
for _, db := range global.GVA_CONFIG.DBList {
var item = make(map[string]interface{})
item["aliasName"] = db.AliasName
item["dbName"] = db.Dbname
item["disable"] = db.Disable
item["dbtype"] = db.Type
dbList = append(dbList, item)
}
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败", c)
} else {
response.OkWithDetailed(gin.H{"dbs": dbs, "dbList": dbList}, "获取成功", c)
}
}
// GetTables
// @Tags AutoCode
// @Summary 获取当前数据库所有表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前数据库所有表"
// @Router /autoCode/getTables [get]
func (autoApi *AutoCodeApi) GetTables(c *gin.Context) {
dbName := c.Query("dbName")
businessDB := c.Query("businessDB")
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)
} else {
response.OkWithDetailed(gin.H{"tables": tables}, "获取成功", c)
}
}
// GetColumn
// @Tags AutoCode
// @Summary 获取当前表所有字段
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取当前表所有字段"
// @Router /autoCode/getColumn [get]
func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
businessDB := c.Query("businessDB")
dbName := c.Query("dbName")
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)
} else {
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
}
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)
}

View File

@@ -0,0 +1,70 @@
package system
import (
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
systemRes "git.echol.cn/loser/st/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
}

View File

@@ -0,0 +1,69 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,191 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,267 @@
package system
import (
"strconv"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/model/system/request"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,199 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,456 @@
package system
import (
"fmt"
"net/http"
"net/url"
"sync"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
"git.echol.cn/loser/st/server/service"
"git.echol.cn/loser/st/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)
}
}

View File

@@ -0,0 +1,59 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,33 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,82 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,265 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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(&param)
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(&param)
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)
}
// 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)
}

View File

@@ -0,0 +1,124 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,171 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,219 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type SkillsApi struct{}
func (s *SkillsApi) GetTools(c *gin.Context) {
data, err := skillsService.Tools(c.Request.Context())
if err != nil {
global.GVA_LOG.Error("获取工具列表失败", zap.Error(err))
response.FailWithMessage("获取工具列表失败", c)
return
}
response.OkWithDetailed(gin.H{"tools": data}, "获取成功", c)
}
func (s *SkillsApi) GetSkillList(c *gin.Context) {
var req request.SkillToolRequest
_ = c.ShouldBindJSON(&req)
data, err := skillsService.List(c.Request.Context(), req.Tool)
if err != nil {
global.GVA_LOG.Error("获取技能列表失败", zap.Error(err))
response.FailWithMessage("获取技能列表失败", c)
return
}
response.OkWithDetailed(gin.H{"skills": data}, "获取成功", c)
}
func (s *SkillsApi) GetSkillDetail(c *gin.Context) {
var req request.SkillDetailRequest
_ = c.ShouldBindJSON(&req)
data, err := skillsService.Detail(c.Request.Context(), req.Tool, req.Skill)
if err != nil {
global.GVA_LOG.Error("获取技能详情失败", zap.Error(err))
response.FailWithMessage("获取技能详情失败", c)
return
}
response.OkWithDetailed(gin.H{"detail": data}, "获取成功", c)
}
func (s *SkillsApi) SaveSkill(c *gin.Context) {
var req request.SkillSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.Save(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存技能失败", zap.Error(err))
response.FailWithMessage("保存技能失败", c)
return
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) CreateScript(c *gin.Context) {
var req request.SkillScriptCreateRequest
_ = c.ShouldBindJSON(&req)
fileName, content, err := skillsService.CreateScript(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("创建脚本失败", zap.Error(err))
response.FailWithMessage("创建脚本失败", c)
return
}
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
}
func (s *SkillsApi) GetScript(c *gin.Context) {
var req request.SkillFileRequest
_ = c.ShouldBindJSON(&req)
content, err := skillsService.GetScript(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("读取脚本失败", zap.Error(err))
response.FailWithMessage("读取脚本失败", c)
return
}
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
}
func (s *SkillsApi) SaveScript(c *gin.Context) {
var req request.SkillFileSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.SaveScript(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存脚本失败", zap.Error(err))
response.FailWithMessage("保存脚本失败", c)
return
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) CreateResource(c *gin.Context) {
var req request.SkillResourceCreateRequest
_ = c.ShouldBindJSON(&req)
fileName, content, err := skillsService.CreateResource(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("创建资源失败", zap.Error(err))
response.FailWithMessage("创建资源失败", c)
return
}
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
}
func (s *SkillsApi) GetResource(c *gin.Context) {
var req request.SkillFileRequest
_ = c.ShouldBindJSON(&req)
content, err := skillsService.GetResource(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("读取资源失败", zap.Error(err))
response.FailWithMessage("读取资源失败", c)
return
}
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
}
func (s *SkillsApi) SaveResource(c *gin.Context) {
var req request.SkillFileSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.SaveResource(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存资源失败", zap.Error(err))
response.FailWithMessage("保存资源失败", c)
return
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) CreateReference(c *gin.Context) {
var req request.SkillReferenceCreateRequest
_ = c.ShouldBindJSON(&req)
fileName, content, err := skillsService.CreateReference(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("创建参考失败", zap.Error(err))
response.FailWithMessage("创建参考失败", c)
return
}
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
}
func (s *SkillsApi) GetReference(c *gin.Context) {
var req request.SkillFileRequest
_ = c.ShouldBindJSON(&req)
content, err := skillsService.GetReference(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("读取参考失败", zap.Error(err))
response.FailWithMessage("读取参考失败", c)
return
}
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
}
func (s *SkillsApi) SaveReference(c *gin.Context) {
var req request.SkillFileSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.SaveReference(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存参考失败", zap.Error(err))
response.FailWithMessage("保存参考失败", c)
return
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) CreateTemplate(c *gin.Context) {
var req request.SkillTemplateCreateRequest
_ = c.ShouldBindJSON(&req)
fileName, content, err := skillsService.CreateTemplate(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("创建模板失败", zap.Error(err))
response.FailWithMessage("创建模板失败", c)
return
}
response.OkWithDetailed(gin.H{"fileName": fileName, "content": content}, "创建成功", c)
}
func (s *SkillsApi) GetTemplate(c *gin.Context) {
var req request.SkillFileRequest
_ = c.ShouldBindJSON(&req)
content, err := skillsService.GetTemplate(c.Request.Context(), req)
if err != nil {
global.GVA_LOG.Error("读取模板失败", zap.Error(err))
response.FailWithMessage("读取模板失败", c)
return
}
response.OkWithDetailed(gin.H{"content": content}, "获取成功", c)
}
func (s *SkillsApi) SaveTemplate(c *gin.Context) {
var req request.SkillFileSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.SaveTemplate(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存模板失败", zap.Error(err))
response.FailWithMessage("保存模板失败", c)
return
}
response.OkWithMessage("保存成功", c)
}
func (s *SkillsApi) GetGlobalConstraint(c *gin.Context) {
var req request.SkillToolRequest
_ = c.ShouldBindJSON(&req)
content, exists, err := skillsService.GetGlobalConstraint(c.Request.Context(), req.Tool)
if err != nil {
global.GVA_LOG.Error("读取全局约束失败", zap.Error(err))
response.FailWithMessage("读取全局约束失败", c)
return
}
response.OkWithDetailed(gin.H{"content": content, "exists": exists}, "获取成功", c)
}
func (s *SkillsApi) SaveGlobalConstraint(c *gin.Context) {
var req request.SkillGlobalConstraintSaveRequest
_ = c.ShouldBindJSON(&req)
if err := skillsService.SaveGlobalConstraint(c.Request.Context(), req); err != nil {
global.GVA_LOG.Error("保存全局约束失败", zap.Error(err))
response.FailWithMessage("保存全局约束失败", c)
return
}
response.OkWithMessage("保存成功", c)
}

View File

@@ -0,0 +1,89 @@
package system
import (
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,516 @@
package system
import (
"strconv"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common"
"git.echol.cn/loser/st/server/model/common/request"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

View File

@@ -0,0 +1,486 @@
package system
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/model/system"
systemReq "git.echol.cn/loser/st/server/model/system/request"
systemRes "git.echol.cn/loser/st/server/model/system/response"
"git.echol.cn/loser/st/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)
}

285
server/config.docker.yaml Normal file
View File

@@ -0,0 +1,285 @@
# git.echol.cn/loser/st/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: "[git.echol.cn/loser/st/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: 'git.echol.cn/loser/st/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: git.echol.cn/loser/st/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: git.echol.cn/loser/st/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
sse_path: /sse
message_path: /message
url_prefix: ''
addr: 8889
separate: false

251
server/config.yaml Normal file
View File

@@ -0,0 +1,251 @@
aliyun-oss:
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
bucket-url: yourBucketUrl
base-path: yourBasePath
autocode:
web: web/src
root: /Users/en/GolandProjects/st
server: server
module: git.echol.cn/loser/st/server
ai-path: ""
aws-s3:
bucket: xxxxx-10005608
region: ap-shanghai
endpoint: ""
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: git.echol.cn/loser/st/server
s3-force-path-style: false
disable-ssl: false
captcha:
key-long: 4
img-width: 240
img-height: 80
open-captcha: 0
open-captcha-timeout: 3600
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
cors:
mode: strict-whitelist
whitelist:
- allow-origin: example1.com
allow-methods: POST, GET
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true
- allow-origin: example2.com
allow-methods: GET, POST
allow-headers: content-type
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true
db-list:
- type: ""
alias-name: ""
prefix: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
path: ""
engine: ""
log-mode: ""
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
disable: true
disk-list:
- mount-point: /
email:
to: xxx@qq.com
from: xxx@163.com
host: smtp.163.com
secret: xxx
nickname: test
port: 465
is-ssl: true
is-loginauth: false
excel:
dir: ./resource/excel/
hua-wei-obs:
path: you-path
bucket: you-bucket
endpoint: you-endpoint
access-key: you-access-key
secret-key: you-secret-key
jwt:
signing-key: 53d59b59-dba8-4f83-886e-e5bd1bf3cbda
expires-time: 7d
buffer-time: 1d
issuer: qmPlus
local:
path: uploads/file
store-path: uploads/file
mcp:
name: GVA_MCP
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ""
addr: 8889
separate: false
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
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: ""
mssql:
prefix: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
path: ""
engine: ""
log-mode: ""
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
mysql:
prefix: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
path: ""
engine: ""
log-mode: ""
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
oracle:
prefix: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
path: ""
engine: ""
log-mode: ""
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
pgsql:
prefix: ""
port: "5432"
config: sslmode=disable TimeZone=Asia/Shanghai
db-name: st_dev
username: postgres
password: loser765911.
path: 149.88.74.188
engine: ""
log-mode: error
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
qiniu:
zone: ZoneHuaDong
bucket: ""
img-path: ""
access-key: ""
secret-key: ""
use-https: false
use-cdn-domains: false
redis:
name: "sys-cache"
addr: 219.152.55.29:6379
password: "THBA@6688"
db: 7
useCluster: false
clusterAddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
redis-list:
- name: app-cache
addr: 219.152.55.29:6379
password: "THBA@6688"
db: 6
useCluster: false
clusterAddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
sqlite:
prefix: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
path: ""
engine: ""
log-mode: ""
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: false
system:
db-type: pgsql
oss-type: local
router-prefix: ""
addr: 8888
iplimit-count: 15000
iplimit-time: 3600
use-multipoint: false
use-redis: true
use-mongo: false
use-strict-auth: false
disable-auto-migrate: false
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: git.echol.cn/loser/st/server
zap:
level: info
prefix: '[git.echol.cn/loser/st/server]'
format: console
director: log
encode-level: LowercaseColorLevelEncoder
stacktrace-key: stacktrace
show-line: true
log-in-console: true
retention-day: -1

View File

@@ -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...)
}

9
server/config/captcha.go Normal file
View File

@@ -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(秒)
}

40
server/config/config.go Normal file
View File

@@ -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"`
}

14
server/config/cors.go Normal file
View File

@@ -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"`
}

53
server/config/db_list.go Normal file
View File

@@ -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"`
}

9
server/config/disk.go Normal file
View File

@@ -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"`
}

12
server/config/email.go Normal file
View File

@@ -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、微软邮箱服务器等
}

5
server/config/excel.go Normal file
View File

@@ -0,0 +1,5 @@
package config
type Excel struct {
Dir string `mapstructure:"dir" json:"dir" yaml:"dir"`
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}

8
server/config/jwt.go Normal file
View File

@@ -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"` // 签发者
}

11
server/config/mcp.go Normal file
View File

@@ -0,0 +1,11 @@
package config
type MCP struct {
Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称
Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本
SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径
MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径
UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀
Addr int `mapstructure:"addr" json:"addr" yaml:"addr"` // 独立MCP服务端口
Separate bool `mapstructure:"separate" json:"separate" yaml:"separate"` // 是否独立运行MCP服务
}

41
server/config/mongo.go Normal file
View File

@@ -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)
}

View File

@@ -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"`
}

13
server/config/oss_aws.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"` // 本地文件存储路径
}

View File

@@ -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"`
}

View File

@@ -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上传加速
}

View File

@@ -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"`
}

10
server/config/redis.go Normal file
View File

@@ -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"` // 集群模式下的节点地址列表
}

15
server/config/system.go Normal file
View File

@@ -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手动迁移
}

71
server/config/zap.go Normal file
View File

@@ -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
}
}

View File

@@ -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"
)

View File

@@ -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
})
}

View File

@@ -0,0 +1,133 @@
package internal
import (
"context"
"fmt"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/system"
"git.echol.cn/loser/st/server/service"
astutil "git.echol.cn/loser/st/server/utils/ast"
"git.echol.cn/loser/st/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()
}

44
server/core/server.go Normal file
View File

@@ -0,0 +1,44 @@
package core
import (
"fmt"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/initialize"
"git.echol.cn/loser/st/server/service/system"
"go.uber.org/zap"
)
func RunServer() {
if global.GVA_CONFIG.System.UseRedis {
// 初始化redis服务
initialize.Redis()
if global.GVA_CONFIG.System.UseMultipoint {
initialize.RedisList()
}
}
if global.GVA_CONFIG.System.UseMongo {
err := initialize.Mongo.Initialization()
if err != nil {
zap.L().Error(fmt.Sprintf("%+v", err))
}
}
// 从db加载jwt数据
if global.GVA_DB != nil {
system.LoadAll()
}
Router := initialize.Routers()
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
fmt.Printf(`
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认MCP SSE地址:http://127.0.0.1%s%s
默认MCP Message地址:http://127.0.0.1%s%s
默认前端文件运行地址:http://127.0.0.1:8080
`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
initServer(address, Router, 10*time.Minute, 10*time.Minute)
}

60
server/core/server_run.go Normal file
View File

@@ -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服务已关闭")
}

76
server/core/viper.go Normal file
View File

@@ -0,0 +1,76 @@
package core
import (
"flag"
"fmt"
"os"
"path/filepath"
"git.echol.cn/loser/st/server/core/internal"
"git.echol.cn/loser/st/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
}

36
server/core/zap.go Normal file
View File

@@ -0,0 +1,36 @@
package core
import (
"fmt"
"git.echol.cn/loser/st/server/core/internal"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/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
}

9314
server/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

9286
server/docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

5677
server/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

68
server/global/global.go Normal file
View File

@@ -0,0 +1,68 @@
package global
import (
"fmt"
"github.com/mark3labs/mcp-go/server"
"sync"
"github.com/gin-gonic/gin"
"github.com/qiniu/qmgo"
"git.echol.cn/loser/st/server/utils/timer"
"github.com/songzhibin97/gkit/cache/local_cache"
"golang.org/x/sync/singleflight"
"go.uber.org/zap"
"git.echol.cn/loser/st/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
}

Some files were not shown because too many files have changed in this diff Show More