commit f4e166c5ee04cd94636a9c22519e3e85b5cc9a8c Author: Echo <1711788888@qq.com> Date: Fri Feb 27 21:52:00 2026 +0800 :tada: 初始化项目 Signed-off-by: Echo <1711788888@qq.com> diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d7c5e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +uploads +docs +.claude \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..80e8a98 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# 云酒馆 - SillyTavern Cloud + +现代化的 AI 角色对话平台,采用前后端分离架构。 + +## 项目结构 + +``` +st-ui/ +├── web-app/ # 前端应用 (React + TypeScript + Tailwind) +├── server/ # 后端服务 (Node.js + Express) +├── docs/ # 项目文档 +├── .claude/ # Claude AI 配置 +└── README.md # 项目说明 +``` + +## 快速开始 + +### 前端开发 + +```bash +cd web-app +npm install +npm run dev +``` + +前端将运行在 `http://localhost:5174` + +### 后端开发 + +```bash +cd server +npm install +cp .env.example .env # 配置环境变量 +npm run dev +``` + +后端将运行在 `http://localhost:3000` + +## 功能特性 + +### 前端功能 +- 🎨 现代化 Glassmorphism 设计 +- 🌙 深色主题 (OLED 优化) +- 💬 实时聊天界面 +- 👤 角色卡管理(支持 PNG/JSON 导入导出) +- ⚙️ 预设管理(支持多种格式) +- 🔐 用户认证系统 +- 📱 响应式设计 + +### 后端功能 +- 🔒 JWT 身份认证 +- 📦 角色卡存储和管理 +- 💾 对话历史持久化 +- 🤖 AI API 集成 +- 📤 文件上传处理 + +## 技术栈 + +### 前端 +- React 18 +- TypeScript +- Tailwind CSS +- Vite +- React Router +- Lucide Icons + +### 后端 +- Node.js +- Express +- MongoDB +- JWT +- Multer + +## 开发指南 + +详细的开发文档请查看 [docs](./docs/) 目录。 + +## 设计系统 + +项目采用统一的设计系统,详见 [design-system](./docs/design-system/)。 + +- 主色调: #7C3AED (紫色) +- 次要色: #A78BFA (淡紫色) +- 强调色: #F97316 (橙色) +- 字体: Inter + +## 贡献 + +欢迎提交 Issue 和 Pull Request。 + +## 许可证 + +MIT diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..42c578c --- /dev/null +++ b/server/Dockerfile @@ -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 diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..9a34870 --- /dev/null +++ b/server/README.md @@ -0,0 +1,54 @@ +## server项目结构 + +```shell +├── api +│   └── v1 +├── config +├── core +├── docs +├── global +├── initialize +│   └── internal +├── middleware +├── model +│   ├── request +│   └── response +├── packfile +├── resource +│   ├── excel +│   ├── page +│   └── template +├── router +├── service +├── source +└── utils + ├── timer + └── upload +``` + +| 文件夹 | 说明 | 描述 | +| ------------ | ----------------------- | --------------------------- | +| `api` | api层 | api层 | +| `--v1` | v1版本接口 | v1版本接口 | +| `config` | 配置包 | config.yaml对应的配置结构体 | +| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 | +| `docs` | swagger文档目录 | swagger文档目录 | +| `global` | 全局对象 | 全局对象 | +| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 | +| `--internal` | 初始化内部函数 | gorm 的 longger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 | +| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 | +| `model` | 模型层 | 模型对应数据表 | +| `--request` | 入参结构体 | 接收前端发送到后端的数据。 | +| `--response` | 出参结构体 | 返回给前端的数据结构体 | +| `packfile` | 静态文件打包 | 静态文件打包 | +| `resource` | 静态资源文件夹 | 负责存放静态文件 | +| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 | +| `--page` | 表单生成器 | 表单生成器 打包后的dist | +| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 | +| `router` | 路由层 | 路由层 | +| `service` | service层 | 存放业务逻辑问题 | +| `source` | source层 | 存放初始化数据的函数 | +| `utils` | 工具包 | 工具函数封装 | +| `--timer` | timer | 定时器接口封装 | +| `--upload` | oss | oss接口封装 | + diff --git a/server/api/v1/app/ai_config.go b/server/api/v1/app/ai_config.go new file mode 100644 index 0000000..7362182 --- /dev/null +++ b/server/api/v1/app/ai_config.go @@ -0,0 +1,218 @@ +package app + +import ( + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app/request" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AIConfigApi struct{} + +// CreateAIConfig +// @Tags AppAIConfig +// @Summary 创建AI配置 +// @Produce application/json +// @Param data body request.CreateAIConfigRequest true "AI配置信息" +// @Success 200 {object} commonResponse.Response{data=response.AIConfigResponse} "创建成功" +// @Router /app/ai-config [post] +// @Security ApiKeyAuth +func (a *AIConfigApi) CreateAIConfig(c *gin.Context) { + var req request.CreateAIConfigRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.CreateAIConfig(&req) + if err != nil { + global.GVA_LOG.Error("创建AI配置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetAIConfigList +// @Tags AppAIConfig +// @Summary 获取AI配置列表 +// @Produce application/json +// @Success 200 {object} commonResponse.Response{data=response.AIConfigListResponse} "获取成功" +// @Router /app/ai-config [get] +// @Security ApiKeyAuth +func (a *AIConfigApi) GetAIConfigList(c *gin.Context) { + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetAIConfigList() + if err != nil { + global.GVA_LOG.Error("获取AI配置列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// UpdateAIConfig +// @Tags AppAIConfig +// @Summary 更新AI配置 +// @Produce application/json +// @Param id path int true "配置ID" +// @Param data body request.UpdateAIConfigRequest true "AI配置信息" +// @Success 200 {object} commonResponse.Response{msg=string} "更新成功" +// @Router /app/ai-config/:id [put] +// @Security ApiKeyAuth +func (a *AIConfigApi) UpdateAIConfig(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的配置ID", c) + return + } + + var req request.UpdateAIConfigRequest + err = c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.AIConfigService.UpdateAIConfig(uint(id), &req) + if err != nil { + global.GVA_LOG.Error("更新AI配置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("更新成功", c) +} + +// DeleteAIConfig +// @Tags AppAIConfig +// @Summary 删除AI配置 +// @Produce application/json +// @Param id path int true "配置ID" +// @Success 200 {object} commonResponse.Response{msg=string} "删除成功" +// @Router /app/ai-config/:id [delete] +// @Security ApiKeyAuth +func (a *AIConfigApi) DeleteAIConfig(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的配置ID", c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.AIConfigService.DeleteAIConfig(uint(id)) + if err != nil { + global.GVA_LOG.Error("删除AI配置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("删除成功", c) +} + +// GetModels +// @Tags AppAIConfig +// @Summary 获取可用模型列表 +// @Produce application/json +// @Param data body request.GetModelsRequest true "获取模型请求" +// @Success 200 {object} commonResponse.Response{data=response.GetModelsResponse} "获取成功" +// @Router /app/ai-config/models [post] +// @Security ApiKeyAuth +func (a *AIConfigApi) GetModels(c *gin.Context) { + var req request.GetModelsRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetModels(&req) + if err != nil { + global.GVA_LOG.Error("获取模型列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetModelsByID +// @Tags AppAIConfig +// @Summary 通过配置ID获取可用模型列表 +// @Produce application/json +// @Param id path int true "配置ID" +// @Success 200 {object} commonResponse.Response{data=response.GetModelsResponse} "获取成功" +// @Router /app/ai-config/:id/models [get] +// @Security ApiKeyAuth +func (a *AIConfigApi) GetModelsByID(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的配置ID", c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.GetModelsByID(uint(id)) + if err != nil { + global.GVA_LOG.Error("获取模型列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// TestAIConfig +// @Tags AppAIConfig +// @Summary 测试AI配置 +// @Produce application/json +// @Param data body request.TestAIConfigRequest true "测试配置请求" +// @Success 200 {object} commonResponse.Response{data=response.TestAIConfigResponse} "测试完成" +// @Router /app/ai-config/test [post] +// @Security ApiKeyAuth +func (a *AIConfigApi) TestAIConfig(c *gin.Context) { + var req request.TestAIConfigRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.TestAIConfig(&req) + if err != nil { + global.GVA_LOG.Error("测试AI配置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// TestAIConfigByID +// @Tags AppAIConfig +// @Summary 通过ID测试AI配置 +// @Produce application/json +// @Param id path int true "配置ID" +// @Success 200 {object} commonResponse.Response{data=response.TestAIConfigResponse} "测试完成" +// @Router /app/ai-config/:id/test [post] +// @Security ApiKeyAuth +func (a *AIConfigApi) TestAIConfigByID(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的配置ID", c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AIConfigService.TestAIConfigByID(uint(id)) + if err != nil { + global.GVA_LOG.Error("测试AI配置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} diff --git a/server/api/v1/app/auth.go b/server/api/v1/app/auth.go new file mode 100644 index 0000000..53159d6 --- /dev/null +++ b/server/api/v1/app/auth.go @@ -0,0 +1,189 @@ +package app + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/common" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type AuthApi struct{} + +// Register +// @Tags AppAuth +// @Summary 用户注册 +// @Produce application/json +// @Param data body request.RegisterRequest true "用户注册信息" +// @Success 200 {object} commonResponse.Response{msg=string} "注册成功" +// @Router /app/auth/register [post] +func (a *AuthApi) Register(c *gin.Context) { + var req request.RegisterRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.AuthService.Register(&req) + if err != nil { + global.GVA_LOG.Error("注册失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("注册成功", c) +} + +// Login +// @Tags AppAuth +// @Summary 用户登录 +// @Produce application/json +// @Param data body request.LoginRequest true "用户登录信息" +// @Success 200 {object} commonResponse.Response{data=response.LoginResponse} "登录成功" +// @Router /app/auth/login [post] +func (a *AuthApi) Login(c *gin.Context) { + var req request.LoginRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + ip := c.ClientIP() + resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.Login(&req, ip) + if err != nil { + global.GVA_LOG.Error("登录失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// RefreshToken +// @Tags AppAuth +// @Summary 刷新Token +// @Produce application/json +// @Param data body request.RefreshTokenRequest true "刷新Token" +// @Success 200 {object} commonResponse.Response{data=response.LoginResponse} "刷新成功" +// @Router /app/auth/refresh [post] +func (a *AuthApi) RefreshToken(c *gin.Context) { + var req request.RefreshTokenRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.RefreshToken(&req) + if err != nil { + global.GVA_LOG.Error("刷新Token失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// Logout +// @Tags AppAuth +// @Summary 用户登出 +// @Produce application/json +// @Success 200 {object} commonResponse.Response{msg=string} "登出成功" +// @Router /app/auth/logout [post] +// @Security ApiKeyAuth +func (a *AuthApi) Logout(c *gin.Context) { + userID := common.GetAppUserID(c) + token := c.GetHeader("Authorization") + if len(token) > 7 { + token = token[7:] // 移除 "Bearer " 前缀 + } + + err := service.ServiceGroupApp.AppServiceGroup.AuthService.Logout(userID, token) + if err != nil { + global.GVA_LOG.Error("登出失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("登出成功", c) +} + +// GetUserInfo +// @Tags AppAuth +// @Summary 获取用户信息 +// @Produce application/json +// @Success 200 {object} commonResponse.Response{data=response.AppUserResponse} "获取成功" +// @Router /app/auth/userinfo [get] +// @Security ApiKeyAuth +func (a *AuthApi) GetUserInfo(c *gin.Context) { + userID := common.GetAppUserID(c) + + resp, err := service.ServiceGroupApp.AppServiceGroup.AuthService.GetUserInfo(userID) + if err != nil { + global.GVA_LOG.Error("获取用户信息失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// UpdateProfile +// @Tags AppAuth +// @Summary 更新用户信息 +// @Produce application/json +// @Param data body request.UpdateProfileRequest true "用户信息" +// @Success 200 {object} commonResponse.Response{msg=string} "更新成功" +// @Router /app/user/profile [put] +// @Security ApiKeyAuth +func (a *AuthApi) UpdateProfile(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.UpdateProfileRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.AuthService.UpdateProfile(userID, &req) + if err != nil { + global.GVA_LOG.Error("更新用户信息失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("更新成功", c) +} + +// ChangePassword +// @Tags AppAuth +// @Summary 修改密码 +// @Produce application/json +// @Param data body request.ChangePasswordRequest true "密码信息" +// @Success 200 {object} commonResponse.Response{msg=string} "修改成功" +// @Router /app/user/change-password [post] +// @Security ApiKeyAuth +func (a *AuthApi) ChangePassword(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.ChangePasswordRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.AuthService.ChangePassword(userID, &req) + if err != nil { + global.GVA_LOG.Error("修改密码失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("修改成功", c) +} diff --git a/server/api/v1/app/character.go b/server/api/v1/app/character.go new file mode 100644 index 0000000..ff1ceec --- /dev/null +++ b/server/api/v1/app/character.go @@ -0,0 +1,246 @@ +package app + +import ( + "encoding/json" + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/common" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type CharacterApi struct{} + +// CreateCharacter +// @Tags AppCharacter +// @Summary 创建角色卡 +// @Produce application/json +// @Param data body request.CreateCharacterRequest true "角色卡信息" +// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "创建成功" +// @Router /app/character [post] +// @Security ApiKeyAuth +func (a *CharacterApi) CreateCharacter(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.CreateCharacterRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.CreateCharacter(userID, &req) + if err != nil { + global.GVA_LOG.Error("创建角色卡失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetCharacterList +// @Tags AppCharacter +// @Summary 获取角色卡列表 +// @Produce application/json +// @Param page query int false "页码" +// @Param pageSize query int false "每页数量" +// @Param keyword query string false "关键词" +// @Param tag query string false "标签" +// @Param isPublic query bool false "是否公开" +// @Success 200 {object} commonResponse.Response{data=response.CharacterListResponse} "获取成功" +// @Router /app/character [get] +// @Security ApiKeyAuth +func (a *CharacterApi) GetCharacterList(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.GetCharacterListRequest + req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1")) + req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20")) + req.Keyword = c.Query("keyword") + req.Tag = c.Query("tag") + + if isPublicStr := c.Query("isPublic"); isPublicStr != "" { + isPublic := isPublicStr == "true" + req.IsPublic = &isPublic + } + + // 参数验证 + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.GetCharacterList(userID, &req) + if err != nil { + global.GVA_LOG.Error("获取角色卡列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetCharacterByID +// @Tags AppCharacter +// @Summary 获取角色卡详情 +// @Produce application/json +// @Param id path int true "角色卡ID" +// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "获取成功" +// @Router /app/character/:id [get] +// @Security ApiKeyAuth +func (a *CharacterApi) GetCharacterByID(c *gin.Context) { + userID := common.GetAppUserID(c) + characterID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的角色卡ID", c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.GetCharacterByID(userID, uint(characterID)) + if err != nil { + global.GVA_LOG.Error("获取角色卡详情失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// UpdateCharacter +// @Tags AppCharacter +// @Summary 更新角色卡 +// @Produce application/json +// @Param id path int true "角色卡ID" +// @Param data body request.UpdateCharacterRequest true "角色卡信息" +// @Success 200 {object} commonResponse.Response{msg=string} "更新成功" +// @Router /app/character/:id [put] +// @Security ApiKeyAuth +func (a *CharacterApi) UpdateCharacter(c *gin.Context) { + userID := common.GetAppUserID(c) + characterID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的角色卡ID", c) + return + } + + var req request.UpdateCharacterRequest + err = c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.CharacterService.UpdateCharacter(userID, uint(characterID), &req) + if err != nil { + global.GVA_LOG.Error("更新角色卡失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("更新成功", c) +} + +// DeleteCharacter +// @Tags AppCharacter +// @Summary 删除角色卡 +// @Produce application/json +// @Param id path int true "角色卡ID" +// @Success 200 {object} commonResponse.Response{msg=string} "删除成功" +// @Router /app/character/:id [delete] +// @Security ApiKeyAuth +func (a *CharacterApi) DeleteCharacter(c *gin.Context) { + userID := common.GetAppUserID(c) + characterID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的角色卡ID", c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.CharacterService.DeleteCharacter(userID, uint(characterID)) + if err != nil { + global.GVA_LOG.Error("删除角色卡失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("删除成功", c) +} + +// UploadCharacter +// @Tags AppCharacter +// @Summary 上传角色卡文件(PNG/JSON) +// @Accept multipart/form-data +// @Produce application/json +// @Param file formData file true "角色卡文件" +// @Success 200 {object} commonResponse.Response{data=response.CharacterResponse} "上传成功" +// @Router /app/character/upload [post] +// @Security ApiKeyAuth +func (a *CharacterApi) UploadCharacter(c *gin.Context) { + userID := common.GetAppUserID(c) + + file, err := c.FormFile("file") + if err != nil { + commonResponse.FailWithMessage("请选择文件", c) + return + } + + // 根据文件类型选择导入方式 + contentType := file.Header.Get("Content-Type") + var resp interface{} + + if contentType == "image/png" || contentType == "image/x-png" { + resp, err = service.ServiceGroupApp.AppServiceGroup.CharacterService.ImportCharacterFromPNG(userID, file) + } else if contentType == "application/json" { + resp, err = service.ServiceGroupApp.AppServiceGroup.CharacterService.ImportCharacterFromJSON(userID, file) + } else { + commonResponse.FailWithMessage("不支持的文件类型,仅支持 PNG 和 JSON", c) + return + } + + if err != nil { + global.GVA_LOG.Error("上传角色卡失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// ExportCharacter +// @Tags AppCharacter +// @Summary 导出角色卡为 JSON +// @Produce application/json +// @Param id path int true "角色卡ID" +// @Success 200 {object} utils.CharacterCardV2 "导出成功" +// @Router /app/character/:id/export [get] +// @Security ApiKeyAuth +func (a *CharacterApi) ExportCharacter(c *gin.Context) { + userID := common.GetAppUserID(c) + characterID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的角色卡ID", c) + return + } + + card, err := service.ServiceGroupApp.AppServiceGroup.CharacterService.ExportCharacterToJSON(userID, uint(characterID)) + if err != nil { + global.GVA_LOG.Error("导出角色卡失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 设置下载响应头 + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", "attachment; filename=character_"+c.Param("id")+".json") + + // 直接返回 JSON + jsonData, _ := json.MarshalIndent(card, "", " ") + c.Data(200, "application/json", jsonData) +} diff --git a/server/api/v1/app/conversation.go b/server/api/v1/app/conversation.go new file mode 100644 index 0000000..38444f0 --- /dev/null +++ b/server/api/v1/app/conversation.go @@ -0,0 +1,301 @@ +package app + +import ( + "fmt" + "net/http" + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/common" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type ConversationApi struct{} + +// CreateConversation +// @Tags AppConversation +// @Summary 创建对话 +// @Produce application/json +// @Param data body request.CreateConversationRequest true "对话信息" +// @Success 200 {object} commonResponse.Response{data=response.ConversationResponse} "创建成功" +// @Router /app/conversation [post] +// @Security ApiKeyAuth +func (a *ConversationApi) CreateConversation(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.CreateConversationRequest + err := c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.CreateConversation(userID, &req) + if err != nil { + global.GVA_LOG.Error("创建对话失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetConversationList +// @Tags AppConversation +// @Summary 获取对话列表 +// @Produce application/json +// @Param page query int false "页码" +// @Param pageSize query int false "每页数量" +// @Success 200 {object} commonResponse.Response{data=response.ConversationListResponse} "获取成功" +// @Router /app/conversation [get] +// @Security ApiKeyAuth +func (a *ConversationApi) GetConversationList(c *gin.Context) { + userID := common.GetAppUserID(c) + + var req request.GetConversationListRequest + req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1")) + req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20")) + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetConversationList(userID, &req) + if err != nil { + global.GVA_LOG.Error("获取对话列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// GetConversationByID +// @Tags AppConversation +// @Summary 获取对话详情 +// @Produce application/json +// @Param id path int true "对话ID" +// @Success 200 {object} commonResponse.Response{data=response.ConversationResponse} "获取成功" +// @Router /app/conversation/:id [get] +// @Security ApiKeyAuth +func (a *ConversationApi) GetConversationByID(c *gin.Context) { + userID := common.GetAppUserID(c) + conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的对话ID", c) + return + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetConversationByID(userID, uint(conversationID)) + if err != nil { + global.GVA_LOG.Error("获取对话详情失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// UpdateConversationSettings +// @Tags AppConversation +// @Summary 更新对话设置 +// @Produce application/json +// @Param id path int true "对话ID" +// @Param data body request.UpdateConversationSettingsRequest true "设置信息" +// @Success 200 {object} commonResponse.Response{msg=string} "更新成功" +// @Router /app/conversation/:id/settings [put] +// @Security ApiKeyAuth +func (a *ConversationApi) UpdateConversationSettings(c *gin.Context) { + userID := common.GetAppUserID(c) + conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的对话ID", c) + return + } + + var req request.UpdateConversationSettingsRequest + err = c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.ConversationService.UpdateConversationSettings(userID, uint(conversationID), req.Settings) + if err != nil { + global.GVA_LOG.Error("更新对话设置失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("更新成功", c) +} + +// DeleteConversation +// @Tags AppConversation +// @Summary 删除对话 +// @Produce application/json +// @Param id path int true "对话ID" +// @Success 200 {object} commonResponse.Response{msg=string} "删除成功" +// @Router /app/conversation/:id [delete] +// @Security ApiKeyAuth +func (a *ConversationApi) DeleteConversation(c *gin.Context) { + userID := common.GetAppUserID(c) + conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的对话ID", c) + return + } + + err = service.ServiceGroupApp.AppServiceGroup.ConversationService.DeleteConversation(userID, uint(conversationID)) + if err != nil { + global.GVA_LOG.Error("删除对话失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("删除成功", c) +} + +// GetMessageList +// @Tags AppConversation +// @Summary 获取消息列表 +// @Produce application/json +// @Param id path int true "对话ID" +// @Param page query int false "页码" +// @Param pageSize query int false "每页数量" +// @Success 200 {object} commonResponse.Response{data=response.MessageListResponse} "获取成功" +// @Router /app/conversation/:id/messages [get] +// @Security ApiKeyAuth +func (a *ConversationApi) GetMessageList(c *gin.Context) { + userID := common.GetAppUserID(c) + conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的对话ID", c) + return + } + + var req request.GetMessageListRequest + req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1")) + req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "50")) + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 50 + } + + resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.GetMessageList(userID, uint(conversationID), &req) + if err != nil { + global.GVA_LOG.Error("获取消息列表失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithData(resp, c) +} + +// SendMessage +// @Tags AppConversation +// @Summary 发送消息 +// @Produce application/json +// @Param id path int true "对话ID" +// @Param data body request.SendMessageRequest true "消息内容" +// @Success 200 {object} commonResponse.Response{data=response.MessageResponse} "发送成功" +// @Router /app/conversation/:id/message [post] +// @Security ApiKeyAuth +func (a *ConversationApi) SendMessage(c *gin.Context) { + userID := common.GetAppUserID(c) + conversationID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的对话ID", c) + return + } + + var req request.SendMessageRequest + err = c.ShouldBindJSON(&req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 检查是否启用流式传输 + stream := c.Query("stream") == "true" + + if stream { + // 流式传输 + a.SendMessageStream(c, userID, uint(conversationID), &req) + } else { + // 普通传输 + resp, err := service.ServiceGroupApp.AppServiceGroup.ConversationService.SendMessage(userID, uint(conversationID), &req) + if err != nil { + global.GVA_LOG.Error("发送消息失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + commonResponse.OkWithData(resp, c) + } +} + +// SendMessageStream 流式传输消息 +func (a *ConversationApi) SendMessageStream(c *gin.Context, userID, conversationID uint, req *request.SendMessageRequest) { + // 设置SSE响应头 + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + + // 创建流式传输通道 + streamChan := make(chan string, 100) + errorChan := make(chan error, 1) + doneChan := make(chan bool, 1) + + // 启动流式传输 + go func() { + err := service.ServiceGroupApp.AppServiceGroup.ConversationService.SendMessageStream( + userID, conversationID, req, streamChan, doneChan, + ) + if err != nil { + errorChan <- err + } + }() + + // 发送流式数据 + flusher, ok := c.Writer.(http.Flusher) + if !ok { + commonResponse.FailWithMessage("不支持流式传输", c) + return + } + + for { + select { + case chunk := <-streamChan: + // 手动写入 SSE 格式,避免 Gin 的 SSEvent 进行 JSON 序列化 + c.Writer.Write([]byte("event: message\n")) + c.Writer.Write([]byte(fmt.Sprintf("data: %s\n\n", chunk))) + flusher.Flush() + case err := <-errorChan: + // 发送错误 + c.Writer.Write([]byte("event: error\n")) + c.Writer.Write([]byte(fmt.Sprintf("data: %s\n\n", err.Error()))) + flusher.Flush() + return + case <-doneChan: + // 发送完成信号 + c.Writer.Write([]byte("event: done\n")) + c.Writer.Write([]byte("data: \n\n")) + flusher.Flush() + return + case <-c.Request.Context().Done(): + // 客户端断开连接 + return + } + } +} diff --git a/server/api/v1/app/enter.go b/server/api/v1/app/enter.go new file mode 100644 index 0000000..303d1fe --- /dev/null +++ b/server/api/v1/app/enter.go @@ -0,0 +1,10 @@ +package app + +type ApiGroup struct { + AuthApi + CharacterApi + ConversationApi + AIConfigApi + PresetApi + UploadApi +} diff --git a/server/api/v1/app/preset.go b/server/api/v1/app/preset.go new file mode 100644 index 0000000..b4a04d6 --- /dev/null +++ b/server/api/v1/app/preset.go @@ -0,0 +1,265 @@ +package app + +import ( + "io" + "strconv" + + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/app/response" + "git.echol.cn/loser/st/server/model/common" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" +) + +type PresetApi struct{} + +// CreatePreset 创建预设 +// @Summary 创建预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param data body request.CreatePresetRequest true "预设信息" +// @Success 200 {object} response.PresetResponse +// @Router /app/preset [post] +func (a *PresetApi) CreatePreset(c *gin.Context) { + var req request.CreatePresetRequest + if err := c.ShouldBindJSON(&req); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + userID := common.GetAppUserID(c) + preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.CreatePreset(userID, &req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp := response.ToPresetResponse(preset) + commonResponse.OkWithData(resp, c) +} + +// GetPresetList 获取预设列表 +// @Summary 获取预设列表 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param page query int false "页码" +// @Param pageSize query int false "每页数量" +// @Param keyword query string false "关键词" +// @Param isPublic query bool false "是否公开" +// @Success 200 {object} response.PresetListResponse +// @Router /app/preset [get] +func (a *PresetApi) GetPresetList(c *gin.Context) { + var req request.GetPresetListRequest + if err := c.ShouldBindQuery(&req); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 设置默认值 + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 20 + } + + userID := common.GetAppUserID(c) + presets, total, err := service.ServiceGroupApp.AppServiceGroup.PresetService.GetPresetList(userID, &req) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 转换为响应格式 + list := make([]response.PresetResponse, 0, len(presets)) + for _, preset := range presets { + list = append(list, response.ToPresetResponse(&preset)) + } + + resp := response.PresetListResponse{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + } + + commonResponse.OkWithData(resp, c) +} + +// GetPresetByID 根据ID获取预设 +// @Summary 根据ID获取预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param id path int true "预设ID" +// @Success 200 {object} response.PresetResponse +// @Router /app/preset/:id [get] +func (a *PresetApi) GetPresetByID(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的预设ID", c) + return + } + + userID := common.GetAppUserID(c) + preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.GetPresetByID(userID, uint(id)) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp := response.ToPresetResponse(preset) + commonResponse.OkWithData(resp, c) +} + +// UpdatePreset 更新预设 +// @Summary 更新预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param id path int true "预设ID" +// @Param data body request.UpdatePresetRequest true "预设信息" +// @Success 200 {string} string "更新成功" +// @Router /app/preset/:id [put] +func (a *PresetApi) UpdatePreset(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的预设ID", c) + return + } + + var req request.UpdatePresetRequest + if err := c.ShouldBindJSON(&req); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + userID := common.GetAppUserID(c) + if err := service.ServiceGroupApp.AppServiceGroup.PresetService.UpdatePreset(userID, uint(id), &req); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("更新成功", c) +} + +// DeletePreset 删除预设 +// @Summary 删除预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param id path int true "预设ID" +// @Success 200 {string} string "删除成功" +// @Router /app/preset/:id [delete] +func (a *PresetApi) DeletePreset(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的预设ID", c) + return + } + + userID := common.GetAppUserID(c) + if err := service.ServiceGroupApp.AppServiceGroup.PresetService.DeletePreset(userID, uint(id)); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("删除成功", c) +} + +// SetDefaultPreset 设置默认预设 +// @Summary 设置默认预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param id path int true "预设ID" +// @Success 200 {string} string "设置成功" +// @Router /app/preset/:id/default [post] +func (a *PresetApi) SetDefaultPreset(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的预设ID", c) + return + } + + userID := common.GetAppUserID(c) + if err := service.ServiceGroupApp.AppServiceGroup.PresetService.SetDefaultPreset(userID, uint(id)); err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + commonResponse.OkWithMessage("设置成功", c) +} + +// ImportPreset 导入预设 +// @Summary 导入预设 +// @Tags Preset +// @Accept multipart/form-data +// @Produce JSON +// @Param file formData file true "预设JSON文件" +// @Success 200 {object} response.PresetResponse +// @Router /app/preset/import [post] +func (a *PresetApi) ImportPreset(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + commonResponse.FailWithMessage("请上传文件", c) + return + } + + // 读取文件内容 + f, err := file.Open() + if err != nil { + commonResponse.FailWithMessage("读取文件失败", c) + return + } + defer f.Close() + + jsonData, err := io.ReadAll(f) + if err != nil { + commonResponse.FailWithMessage("读取文件内容失败", c) + return + } + + userID := common.GetAppUserID(c) + preset, err := service.ServiceGroupApp.AppServiceGroup.PresetService.ImportPresetFromJSON(userID, jsonData, file.Filename) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + resp := response.ToPresetResponse(preset) + commonResponse.OkWithData(resp, c) +} + +// ExportPreset 导出预设 +// @Summary 导出预设 +// @Tags Preset +// @Accept JSON +// @Produce JSON +// @Param id path int true "预设ID" +// @Success 200 {file} file "预设JSON文件" +// @Router /app/preset/:id/export [get] +func (a *PresetApi) ExportPreset(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + commonResponse.FailWithMessage("无效的预设ID", c) + return + } + + userID := common.GetAppUserID(c) + jsonData, err := service.ServiceGroupApp.AppServiceGroup.PresetService.ExportPresetToJSON(userID, uint(id)) + if err != nil { + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 设置响应头 + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", "attachment; filename=preset-"+c.Param("id")+".json") + + // 返回JSON数据 + c.Data(200, "application/json", jsonData) +} diff --git a/server/api/v1/app/upload.go b/server/api/v1/app/upload.go new file mode 100644 index 0000000..037adc9 --- /dev/null +++ b/server/api/v1/app/upload.go @@ -0,0 +1,41 @@ +package app + +import ( + "git.echol.cn/loser/st/server/global" + commonResponse "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type UploadApi struct{} + +// UploadImage 上传图片 +// @Summary 上传图片 +// @Tags Upload +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "图片文件" +// @Success 200 {object} map[string]string "返回图片URL" +// @Router /app/upload/image [post] +func (a *UploadApi) UploadImage(c *gin.Context) { + _, header, err := c.Request.FormFile("file") + if err != nil { + global.GVA_LOG.Error("接收文件失败", zap.Error(err)) + commonResponse.FailWithMessage("请上传图片文件", c) + return + } + + // 上传图片到 OSS + imageURL, err := service.ServiceGroupApp.AppServiceGroup.UploadService.UploadImage(header) + if err != nil { + global.GVA_LOG.Error("上传图片失败", zap.Error(err)) + commonResponse.FailWithMessage(err.Error(), c) + return + } + + // 返回图片 URL + commonResponse.OkWithData(gin.H{ + "url": imageURL, + }, c) +} diff --git a/server/api/v1/enter.go b/server/api/v1/enter.go new file mode 100644 index 0000000..7a5b528 --- /dev/null +++ b/server/api/v1/enter.go @@ -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 +} diff --git a/server/api/v1/example/enter.go b/server/api/v1/example/enter.go new file mode 100644 index 0000000..93e2d3f --- /dev/null +++ b/server/api/v1/example/enter.go @@ -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 +) diff --git a/server/api/v1/example/exa_attachment_category.go b/server/api/v1/example/exa_attachment_category.go new file mode 100644 index 0000000..41f255c --- /dev/null +++ b/server/api/v1/example/exa_attachment_category.go @@ -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) +} diff --git a/server/api/v1/example/exa_breakpoint_continue.go b/server/api/v1/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..1d429ca --- /dev/null +++ b/server/api/v1/example/exa_breakpoint_continue.go @@ -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) +} diff --git a/server/api/v1/example/exa_customer.go b/server/api/v1/example/exa_customer.go new file mode 100644 index 0000000..bd75fee --- /dev/null +++ b/server/api/v1/example/exa_customer.go @@ -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) +} diff --git a/server/api/v1/example/exa_file_upload_download.go b/server/api/v1/example/exa_file_upload_download.go new file mode 100644 index 0000000..c4f639a --- /dev/null +++ b/server/api/v1/example/exa_file_upload_download.go @@ -0,0 +1,136 @@ +package example + +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/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" +) + +type FileUploadAndDownloadApi struct{} + +// UploadFile +// @Tags ExaFileUploadAndDownload +// @Summary 上传文件示例 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件示例" +// @Success 200 {object} response.Response{data=exampleRes.ExaFileResponse,msg=string} "上传文件示例,返回包括文件详情" +// @Router /fileUploadAndDownload/upload [post] +func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + noSave := c.DefaultQuery("noSave", "0") + _, header, err := c.Request.FormFile("file") + classId, _ := strconv.Atoi(c.DefaultPostForm("classId", "0")) + if err != nil { + global.GVA_LOG.Error("接收文件失败!", zap.Error(err)) + response.FailWithMessage("接收文件失败", c) + return + } + file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径 + if err != nil { + global.GVA_LOG.Error("上传文件失败!", zap.Error(err)) + response.FailWithMessage("上传文件失败", c) + return + } + response.OkWithDetailed(exampleRes.ExaFileResponse{File: file}, "上传成功", c) +} + +// EditFileName 编辑文件名或者备注 +func (b *FileUploadAndDownloadApi) EditFileName(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = fileUploadAndDownloadService.EditFileName(file) + if err != nil { + global.GVA_LOG.Error("编辑失败!", zap.Error(err)) + response.FailWithMessage("编辑失败", c) + return + } + response.OkWithMessage("编辑成功", c) +} + +// DeleteFile +// @Tags ExaFileUploadAndDownload +// @Summary 删除文件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "传入文件里面id即可" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @Router /fileUploadAndDownload/deleteFile [post] +func (b *FileUploadAndDownloadApi) DeleteFile(c *gin.Context) { + var file example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.DeleteFile(file); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// GetFileList +// @Tags ExaFileUploadAndDownload +// @Summary 分页文件列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.ExaAttachmentCategorySearch true "页码, 每页大小, 分类id" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页文件列表,返回包括列表,总数,页码,每页数量" +// @Router /fileUploadAndDownload/getFileList [post] +func (b *FileUploadAndDownloadApi) GetFileList(c *gin.Context) { + var pageInfo request.ExaAttachmentCategorySearch + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := fileUploadAndDownloadService.GetFileRecordInfoList(pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// ImportURL +// @Tags ExaFileUploadAndDownload +// @Summary 导入URL +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body example.ExaFileUploadAndDownload true "对象" +// @Success 200 {object} response.Response{msg=string} "导入URL" +// @Router /fileUploadAndDownload/importURL [post] +func (b *FileUploadAndDownloadApi) ImportURL(c *gin.Context) { + var file []example.ExaFileUploadAndDownload + err := c.ShouldBindJSON(&file) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err := fileUploadAndDownloadService.ImportURL(&file); err != nil { + global.GVA_LOG.Error("导入URL失败!", zap.Error(err)) + response.FailWithMessage("导入URL失败", c) + return + } + response.OkWithMessage("导入URL成功", c) +} diff --git a/server/api/v1/system/auto_code_history.go b/server/api/v1/system/auto_code_history.go new file mode 100644 index 0000000..abc2fc8 --- /dev/null +++ b/server/api/v1/system/auto_code_history.go @@ -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) +} diff --git a/server/api/v1/system/auto_code_mcp.go b/server/api/v1/system/auto_code_mcp.go new file mode 100644 index 0000000..ca8fdfc --- /dev/null +++ b/server/api/v1/system/auto_code_mcp.go @@ -0,0 +1,145 @@ +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) +} diff --git a/server/api/v1/system/auto_code_package.go b/server/api/v1/system/auto_code_package.go new file mode 100644 index 0000000..b32846a --- /dev/null +++ b/server/api/v1/system/auto_code_package.go @@ -0,0 +1,101 @@ +package system + +import ( + "strings" + + "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" +) + +type AutoCodePackageApi struct{} + +// Create +// @Tags AutoCodePackage +// @Summary 创建package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.SysAutoCodePackageCreate true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/createPackage [post] +func (a *AutoCodePackageApi) Create(c *gin.Context) { + var info request.SysAutoCodePackageCreate + _ = c.ShouldBindJSON(&info) + if err := utils.Verify(info, utils.AutoPackageVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if strings.Contains(info.PackageName, "\\") || strings.Contains(info.PackageName, "/") || strings.Contains(info.PackageName, "..") { + response.FailWithMessage("包名不合法", c) + return + } // PackageName可能导致路径穿越的问题 / 和 \ 都要防止 + err := autoCodePackageService.Create(c.Request.Context(), &info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败", c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete +// @Tags AutoCode +// @Summary 删除package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body common.GetById true "创建package" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "删除package成功" +// @Router /autoCode/delPackage [post] +func (a *AutoCodePackageApi) Delete(c *gin.Context) { + var info common.GetById + _ = c.ShouldBindJSON(&info) + err := autoCodePackageService.Delete(c.Request.Context(), info) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败", c) + return + } + response.OkWithMessage("删除成功", c) +} + +// All +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getPackage [post] +func (a *AutoCodePackageApi) All(c *gin.Context) { + data, err := autoCodePackageService.All(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(gin.H{"pkgs": data}, "获取成功", c) +} + +// Templates +// @Tags AutoCodePackage +// @Summary 获取package +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "创建package成功" +// @Router /autoCode/getTemplates [get] +func (a *AutoCodePackageApi) Templates(c *gin.Context) { + data, err := autoCodePackageService.Templates(c.Request.Context()) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(data, "获取成功", c) +} diff --git a/server/api/v1/system/auto_code_plugin.go b/server/api/v1/system/auto_code_plugin.go new file mode 100644 index 0000000..917aca6 --- /dev/null +++ b/server/api/v1/system/auto_code_plugin.go @@ -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) +} diff --git a/server/api/v1/system/auto_code_template.go b/server/api/v1/system/auto_code_template.go new file mode 100644 index 0000000..b1297bc --- /dev/null +++ b/server/api/v1/system/auto_code_template.go @@ -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) + } +} diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go new file mode 100644 index 0000000..ee7c7bd --- /dev/null +++ b/server/api/v1/system/enter.go @@ -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 +) diff --git a/server/api/v1/system/sys_api.go b/server/api/v1/system/sys_api.go new file mode 100644 index 0000000..bcc0e7d --- /dev/null +++ b/server/api/v1/system/sys_api.go @@ -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) +} diff --git a/server/api/v1/system/sys_api_token.go b/server/api/v1/system/sys_api_token.go new file mode 100644 index 0000000..3f5a29d --- /dev/null +++ b/server/api/v1/system/sys_api_token.go @@ -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) +} diff --git a/server/api/v1/system/sys_authority.go b/server/api/v1/system/sys_authority.go new file mode 100644 index 0000000..635e0dd --- /dev/null +++ b/server/api/v1/system/sys_authority.go @@ -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(©Info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo, utils.OldAuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(copyInfo.Authority, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + authBack, err := authorityService.CopyAuthority(adminAuthorityID, copyInfo) + if err != nil { + global.GVA_LOG.Error("拷贝失败!", zap.Error(err)) + response.FailWithMessage("拷贝失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authBack}, "拷贝成功", c) +} + +// DeleteAuthority +// @Tags Authority +// @Summary 删除角色 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "删除角色" +// @Success 200 {object} response.Response{msg=string} "删除角色" +// @Router /authority/deleteAuthority [post] +func (a *AuthorityApi) DeleteAuthority(c *gin.Context) { + var authority system.SysAuthority + var err error + if err = c.ShouldBindJSON(&authority); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if err = utils.Verify(authority, utils.AuthorityIdVerify); err != nil { + response.FailWithMessage(err.Error(), c) + return + } + // 删除角色之前需要判断是否有用户正在使用此角色 + if err = authorityService.DeleteAuthority(&authority); err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败"+err.Error(), c) + return + } + _ = casbinService.FreshCasbin() + response.OkWithMessage("删除成功", c) +} + +// UpdateAuthority +// @Tags Authority +// @Summary 更新角色信息 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "权限id, 权限名, 父角色id" +// @Success 200 {object} response.Response{data=systemRes.SysAuthorityResponse,msg=string} "更新角色信息,返回包括系统角色详情" +// @Router /authority/updateAuthority [put] +func (a *AuthorityApi) UpdateAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + authority, err := authorityService.UpdateAuthority(auth) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败"+err.Error(), c) + return + } + response.OkWithDetailed(systemRes.SysAuthorityResponse{Authority: authority}, "更新成功", c) +} + +// GetAuthorityList +// @Tags Authority +// @Summary 分页获取角色列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取角色列表,返回包括列表,总数,页码,每页数量" +// @Router /authority/getAuthorityList [post] +func (a *AuthorityApi) GetAuthorityList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + list, err := authorityService.GetAuthorityInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败"+err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} + +// SetDataAuthority +// @Tags Authority +// @Summary 设置角色资源权限 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysAuthority true "设置角色资源权限" +// @Success 200 {object} response.Response{msg=string} "设置角色资源权限" +// @Router /authority/setDataAuthority [post] +func (a *AuthorityApi) SetDataAuthority(c *gin.Context) { + var auth system.SysAuthority + err := c.ShouldBindJSON(&auth) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(auth, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + adminAuthorityID := utils.GetUserAuthorityId(c) + err = authorityService.SetDataAuthority(adminAuthorityID, auth) + if err != nil { + global.GVA_LOG.Error("设置失败!", zap.Error(err)) + response.FailWithMessage("设置失败"+err.Error(), c) + return + } + response.OkWithMessage("设置成功", c) +} diff --git a/server/api/v1/system/sys_authority_btn.go b/server/api/v1/system/sys_authority_btn.go new file mode 100644 index 0000000..2d09c42 --- /dev/null +++ b/server/api/v1/system/sys_authority_btn.go @@ -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) +} diff --git a/server/api/v1/system/sys_auto_code.go b/server/api/v1/system/sys_auto_code.go new file mode 100644 index 0000000..f45cb05 --- /dev/null +++ b/server/api/v1/system/sys_auto_code.go @@ -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) +} diff --git a/server/api/v1/system/sys_captcha.go b/server/api/v1/system/sys_captcha.go new file mode 100644 index 0000000..849c110 --- /dev/null +++ b/server/api/v1/system/sys_captcha.go @@ -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 +} diff --git a/server/api/v1/system/sys_casbin.go b/server/api/v1/system/sys_casbin.go new file mode 100644 index 0000000..9ac19cd --- /dev/null +++ b/server/api/v1/system/sys_casbin.go @@ -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) +} diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go new file mode 100644 index 0000000..ea74696 --- /dev/null +++ b/server/api/v1/system/sys_dictionary.go @@ -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) +} diff --git a/server/api/v1/system/sys_dictionary_detail.go b/server/api/v1/system/sys_dictionary_detail.go new file mode 100644 index 0000000..eca655e --- /dev/null +++ b/server/api/v1/system/sys_dictionary_detail.go @@ -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) +} diff --git a/server/api/v1/system/sys_error.go b/server/api/v1/system/sys_error.go new file mode 100644 index 0000000..54de19c --- /dev/null +++ b/server/api/v1/system/sys_error.go @@ -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) +} diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go new file mode 100644 index 0000000..2270f76 --- /dev/null +++ b/server/api/v1/system/sys_export_template.go @@ -0,0 +1,456 @@ +package system + +import ( + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "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) + } +} diff --git a/server/api/v1/system/sys_initdb.go b/server/api/v1/system/sys_initdb.go new file mode 100644 index 0000000..3b37384 --- /dev/null +++ b/server/api/v1/system/sys_initdb.go @@ -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) +} diff --git a/server/api/v1/system/sys_jwt_blacklist.go b/server/api/v1/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..44b1a16 --- /dev/null +++ b/server/api/v1/system/sys_jwt_blacklist.go @@ -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) +} diff --git a/server/api/v1/system/sys_login_log.go b/server/api/v1/system/sys_login_log.go new file mode 100644 index 0000000..4579ebc --- /dev/null +++ b/server/api/v1/system/sys_login_log.go @@ -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) +} diff --git a/server/api/v1/system/sys_menu.go b/server/api/v1/system/sys_menu.go new file mode 100644 index 0000000..8d2f998 --- /dev/null +++ b/server/api/v1/system/sys_menu.go @@ -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(¶m) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(param, utils.AuthorityIdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menus, err := menuService.GetMenuAuthority(¶m) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithDetailed(systemRes.SysMenusResponse{Menus: menus}, "获取失败", c) + return + } + response.OkWithDetailed(gin.H{"menus": menus}, "获取成功", c) +} + +// AddBaseMenu +// @Tags Menu +// @Summary 新增菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "新增菜单" +// @Router /menu/addBaseMenu [post] +func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = menuService.AddBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("添加失败!", zap.Error(err)) + response.FailWithMessage("添加失败:"+err.Error(), c) + return + } + response.OkWithMessage("添加成功", c) +} + +// DeleteBaseMenu +// @Tags Menu +// @Summary 删除菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{msg=string} "删除菜单" +// @Router /menu/deleteBaseMenu [post] +func (a *AuthorityMenuApi) DeleteBaseMenu(c *gin.Context) { + var menu request.GetById + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.DeleteBaseMenu(menu.ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// UpdateBaseMenu +// @Tags Menu +// @Summary 更新菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body system.SysBaseMenu true "路由path, 父菜单ID, 路由name, 对应前端文件路径, 排序标记" +// @Success 200 {object} response.Response{msg=string} "更新菜单" +// @Router /menu/updateBaseMenu [post] +func (a *AuthorityMenuApi) UpdateBaseMenu(c *gin.Context) { + var menu system.SysBaseMenu + err := c.ShouldBindJSON(&menu) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu, utils.MenuVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(menu.Meta, utils.MenuMetaVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = baseMenuService.UpdateBaseMenu(menu) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败", c) + return + } + response.OkWithMessage("更新成功", c) +} + +// GetBaseMenuById +// @Tags Menu +// @Summary 根据id获取菜单 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.GetById true "菜单id" +// @Success 200 {object} response.Response{data=systemRes.SysBaseMenuResponse,msg=string} "根据id获取菜单,返回包括系统菜单列表" +// @Router /menu/getBaseMenuById [post] +func (a *AuthorityMenuApi) GetBaseMenuById(c *gin.Context) { + var idInfo request.GetById + err := c.ShouldBindJSON(&idInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = utils.Verify(idInfo, utils.IdVerify) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + menu, err := baseMenuService.GetBaseMenuById(idInfo.ID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(systemRes.SysBaseMenuResponse{Menu: menu}, "获取成功", c) +} + +// GetMenuList +// @Tags Menu +// @Summary 分页获取基础menu列表 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.PageInfo true "页码, 每页大小" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取基础menu列表,返回包括列表,总数,页码,每页数量" +// @Router /menu/getMenuList [post] +func (a *AuthorityMenuApi) GetMenuList(c *gin.Context) { + authorityID := utils.GetUserAuthorityId(c) + menuList, err := menuService.GetInfoList(authorityID) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + return + } + response.OkWithDetailed(menuList, "获取成功", c) +} diff --git a/server/api/v1/system/sys_operation_record.go b/server/api/v1/system/sys_operation_record.go new file mode 100644 index 0000000..44058fe --- /dev/null +++ b/server/api/v1/system/sys_operation_record.go @@ -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) +} diff --git a/server/api/v1/system/sys_params.go b/server/api/v1/system/sys_params.go new file mode 100644 index 0000000..b565217 --- /dev/null +++ b/server/api/v1/system/sys_params.go @@ -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) +} diff --git a/server/api/v1/system/sys_skills.go b/server/api/v1/system/sys_skills.go new file mode 100644 index 0000000..167dec8 --- /dev/null +++ b/server/api/v1/system/sys_skills.go @@ -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) +} diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go new file mode 100644 index 0000000..df1add4 --- /dev/null +++ b/server/api/v1/system/sys_system.go @@ -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) +} diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go new file mode 100644 index 0000000..be738b0 --- /dev/null +++ b/server/api/v1/system/sys_user.go @@ -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) +} diff --git a/server/api/v1/system/sys_version.go b/server/api/v1/system/sys_version.go new file mode 100644 index 0000000..1371891 --- /dev/null +++ b/server/api/v1/system/sys_version.go @@ -0,0 +1,486 @@ +package system + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "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) +} diff --git a/server/config.docker.yaml b/server/config.docker.yaml new file mode 100644 index 0000000..8dc0980 --- /dev/null +++ b/server/config.docker.yaml @@ -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 diff --git a/server/config.yaml b/server/config.yaml new file mode 100644 index 0000000..bc3147a --- /dev/null +++ b/server/config.yaml @@ -0,0 +1,267 @@ +aliyun-oss: + endpoint: oss-cn-hangzhou.aliyuncs.com + access-key-id: LTAI5tB3Mn5Y7mVo8h3zkf46 + access-key-secret: FtuHdFy4NFdVItEiNBnTun3Ddi8BMK + bucket-name: lckt + bucket-url: https://lckt.oss-cn-hangzhou.aliyuncs.com + base-path: st +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 + use-ssl: false +jwt: + signing-key: 53d59b59-dba8-4f83-886e-e5bd1bf3cbda + expires-time: 7d + buffer-time: 1d + issuer: qmPlus +local: + path: http://localhost:8888/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 +pgsql: + prefix: "" + port: "5432" + config: sslmode=disable TimeZone=Asia/Shanghai + db-name: st2 + username: postgres + password: e5zse3Adrja7PNfA + path: 219.152.55.29 + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: true +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: aliyun-oss + 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 + data-dir: data +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 diff --git a/server/config/auto_code.go b/server/config/auto_code.go new file mode 100644 index 0000000..ade79a0 --- /dev/null +++ b/server/config/auto_code.go @@ -0,0 +1,22 @@ +package config + +import ( + "path/filepath" + "strings" +) + +type Autocode struct { + Web string `mapstructure:"web" json:"web" yaml:"web"` + Root string `mapstructure:"root" json:"root" yaml:"root"` + Server string `mapstructure:"server" json:"server" yaml:"server"` + Module string `mapstructure:"module" json:"module" yaml:"module"` + AiPath string `mapstructure:"ai-path" json:"ai-path" yaml:"ai-path"` +} + +func (a *Autocode) WebRoot() string { + webs := strings.Split(a.Web, "/") + if len(webs) == 0 { + webs = strings.Split(a.Web, "\\") + } + return filepath.Join(webs...) +} diff --git a/server/config/captcha.go b/server/config/captcha.go new file mode 100644 index 0000000..d678a41 --- /dev/null +++ b/server/config/captcha.go @@ -0,0 +1,9 @@ +package config + +type Captcha struct { + KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 + ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 + ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 + OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码 + OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒) +} diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..3abac5a --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,40 @@ +package config + +type Server struct { + JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"` + Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` + Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` + RedisList []Redis `mapstructure:"redis-list" json:"redis-list" yaml:"redis-list"` + Mongo Mongo `mapstructure:"mongo" json:"mongo" yaml:"mongo"` + Email Email `mapstructure:"email" json:"email" yaml:"email"` + System System `mapstructure:"system" json:"system" yaml:"system"` + Captcha Captcha `mapstructure:"captcha" json:"captcha" yaml:"captcha"` + // auto + AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"` + // gorm + Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"` + Mssql Mssql `mapstructure:"mssql" json:"mssql" yaml:"mssql"` + Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"` + Oracle Oracle `mapstructure:"oracle" json:"oracle" yaml:"oracle"` + Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"` + DBList []SpecializedDB `mapstructure:"db-list" json:"db-list" yaml:"db-list"` + // oss + Local Local `mapstructure:"local" json:"local" yaml:"local"` + Qiniu Qiniu `mapstructure:"qiniu" json:"qiniu" yaml:"qiniu"` + AliyunOSS AliyunOSS `mapstructure:"aliyun-oss" json:"aliyun-oss" yaml:"aliyun-oss"` + HuaWeiObs HuaWeiObs `mapstructure:"hua-wei-obs" json:"hua-wei-obs" yaml:"hua-wei-obs"` + TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"` + AwsS3 AwsS3 `mapstructure:"aws-s3" json:"aws-s3" yaml:"aws-s3"` + CloudflareR2 CloudflareR2 `mapstructure:"cloudflare-r2" json:"cloudflare-r2" yaml:"cloudflare-r2"` + Minio Minio `mapstructure:"minio" json:"minio" yaml:"minio"` + + Excel Excel `mapstructure:"excel" json:"excel" yaml:"excel"` + + DiskList []DiskList `mapstructure:"disk-list" json:"disk-list" yaml:"disk-list"` + + // 跨域配置 + Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` + + // MCP配置 + MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"` +} diff --git a/server/config/cors.go b/server/config/cors.go new file mode 100644 index 0000000..7fba993 --- /dev/null +++ b/server/config/cors.go @@ -0,0 +1,14 @@ +package config + +type CORS struct { + Mode string `mapstructure:"mode" json:"mode" yaml:"mode"` + Whitelist []CORSWhitelist `mapstructure:"whitelist" json:"whitelist" yaml:"whitelist"` +} + +type CORSWhitelist struct { + AllowOrigin string `mapstructure:"allow-origin" json:"allow-origin" yaml:"allow-origin"` + AllowMethods string `mapstructure:"allow-methods" json:"allow-methods" yaml:"allow-methods"` + AllowHeaders string `mapstructure:"allow-headers" json:"allow-headers" yaml:"allow-headers"` + ExposeHeaders string `mapstructure:"expose-headers" json:"expose-headers" yaml:"expose-headers"` + AllowCredentials bool `mapstructure:"allow-credentials" json:"allow-credentials" yaml:"allow-credentials"` +} diff --git a/server/config/db_list.go b/server/config/db_list.go new file mode 100644 index 0000000..17674b7 --- /dev/null +++ b/server/config/db_list.go @@ -0,0 +1,53 @@ +package config + +import ( + "strings" + + "gorm.io/gorm/logger" +) + +type DsnProvider interface { + Dsn() string +} + +// Embeded 结构体可以压平到上一层,从而保持 config 文件的结构和原来一样 +// 见 playground: https://go.dev/play/p/KIcuhqEoxmY + +// GeneralDB 也被 Pgsql 和 Mysql 原样使用 +type GeneralDB struct { + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 数据库前缀 + Port string `mapstructure:"port" json:"port" yaml:"port"` // 数据库端口 + Config string `mapstructure:"config" json:"config" yaml:"config"` // 高级配置 + Dbname string `mapstructure:"db-name" json:"db-name" yaml:"db-name"` // 数据库名 + Username string `mapstructure:"username" json:"username" yaml:"username"` // 数据库账号 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 数据库密码 + Path string `mapstructure:"path" json:"path" yaml:"path"` // 数据库地址 + Engine string `mapstructure:"engine" json:"engine" yaml:"engine" default:"InnoDB"` // 数据库引擎,默认InnoDB + LogMode string `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode"` // 是否开启Gorm全局日志 + MaxIdleConns int `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 + MaxOpenConns int `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 + Singular bool `mapstructure:"singular" json:"singular" yaml:"singular"` // 是否开启全局禁用复数,true表示开启 + LogZap bool `mapstructure:"log-zap" json:"log-zap" yaml:"log-zap"` // 是否通过zap写入日志文件 +} + +func (c GeneralDB) LogLevel() logger.LogLevel { + switch strings.ToLower(c.LogMode) { + case "silent": + return logger.Silent + case "error": + return logger.Error + case "warn": + return logger.Warn + case "info": + return logger.Info + default: + return logger.Info + } +} + +type SpecializedDB struct { + Type string `mapstructure:"type" json:"type" yaml:"type"` + AliasName string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name"` + GeneralDB `yaml:",inline" mapstructure:",squash"` + Disable bool `mapstructure:"disable" json:"disable" yaml:"disable"` +} diff --git a/server/config/disk.go b/server/config/disk.go new file mode 100644 index 0000000..59a6332 --- /dev/null +++ b/server/config/disk.go @@ -0,0 +1,9 @@ +package config + +type Disk struct { + MountPoint string `mapstructure:"mount-point" json:"mount-point" yaml:"mount-point"` +} + +type DiskList struct { + Disk `yaml:",inline" mapstructure:",squash"` +} diff --git a/server/config/email.go b/server/config/email.go new file mode 100644 index 0000000..9fd7642 --- /dev/null +++ b/server/config/email.go @@ -0,0 +1,12 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) +} diff --git a/server/config/excel.go b/server/config/excel.go new file mode 100644 index 0000000..13caab7 --- /dev/null +++ b/server/config/excel.go @@ -0,0 +1,5 @@ +package config + +type Excel struct { + Dir string `mapstructure:"dir" json:"dir" yaml:"dir"` +} diff --git a/server/config/gorm_mssql.go b/server/config/gorm_mssql.go new file mode 100644 index 0000000..d187119 --- /dev/null +++ b/server/config/gorm_mssql.go @@ -0,0 +1,10 @@ +package config + +type Mssql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm" +func (m *Mssql) Dsn() string { + return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable" +} diff --git a/server/config/gorm_mysql.go b/server/config/gorm_mysql.go new file mode 100644 index 0000000..77e0245 --- /dev/null +++ b/server/config/gorm_mysql.go @@ -0,0 +1,9 @@ +package config + +type Mysql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Mysql) Dsn() string { + return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config +} diff --git a/server/config/gorm_oracle.go b/server/config/gorm_oracle.go new file mode 100644 index 0000000..52cf21c --- /dev/null +++ b/server/config/gorm_oracle.go @@ -0,0 +1,18 @@ +package config + +import ( + "fmt" + "net" + "net/url" +) + +type Oracle struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (m *Oracle) Dsn() string { + dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password), + net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config) + return dsn + +} diff --git a/server/config/gorm_pgsql.go b/server/config/gorm_pgsql.go new file mode 100644 index 0000000..29fe03f --- /dev/null +++ b/server/config/gorm_pgsql.go @@ -0,0 +1,17 @@ +package config + +type Pgsql struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +// Dsn 基于配置文件获取 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) Dsn() string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + p.Dbname + " port=" + p.Port + " " + p.Config +} + +// LinkDsn 根据 dbname 生成 dsn +// Author [SliverHorn](https://github.com/SliverHorn) +func (p *Pgsql) LinkDsn(dbname string) string { + return "host=" + p.Path + " user=" + p.Username + " password=" + p.Password + " dbname=" + dbname + " port=" + p.Port + " " + p.Config +} diff --git a/server/config/gorm_sqlite.go b/server/config/gorm_sqlite.go new file mode 100644 index 0000000..46f2e19 --- /dev/null +++ b/server/config/gorm_sqlite.go @@ -0,0 +1,13 @@ +package config + +import ( + "path/filepath" +) + +type Sqlite struct { + GeneralDB `yaml:",inline" mapstructure:",squash"` +} + +func (s *Sqlite) Dsn() string { + return filepath.Join(s.Path, s.Dbname+".db") +} diff --git a/server/config/jwt.go b/server/config/jwt.go new file mode 100644 index 0000000..c95d30d --- /dev/null +++ b/server/config/jwt.go @@ -0,0 +1,8 @@ +package config + +type JWT struct { + SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名 + ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间 + BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间 + Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者 +} diff --git a/server/config/mcp.go b/server/config/mcp.go new file mode 100644 index 0000000..15a7876 --- /dev/null +++ b/server/config/mcp.go @@ -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服务 +} diff --git a/server/config/mongo.go b/server/config/mongo.go new file mode 100644 index 0000000..2034a3f --- /dev/null +++ b/server/config/mongo.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "strings" +) + +type Mongo struct { + Coll string `json:"coll" yaml:"coll" mapstructure:"coll"` // collection name + Options string `json:"options" yaml:"options" mapstructure:"options"` // mongodb options + Database string `json:"database" yaml:"database" mapstructure:"database"` // database name + Username string `json:"username" yaml:"username" mapstructure:"username"` // 用户名 + Password string `json:"password" yaml:"password" mapstructure:"password"` // 密码 + AuthSource string `json:"auth-source" yaml:"auth-source" mapstructure:"auth-source"` // 验证数据库 + MinPoolSize uint64 `json:"min-pool-size" yaml:"min-pool-size" mapstructure:"min-pool-size"` // 最小连接池 + MaxPoolSize uint64 `json:"max-pool-size" yaml:"max-pool-size" mapstructure:"max-pool-size"` // 最大连接池 + SocketTimeoutMs int64 `json:"socket-timeout-ms" yaml:"socket-timeout-ms" mapstructure:"socket-timeout-ms"` // socket超时时间 + ConnectTimeoutMs int64 `json:"connect-timeout-ms" yaml:"connect-timeout-ms" mapstructure:"connect-timeout-ms"` // 连接超时时间 + IsZap bool `json:"is-zap" yaml:"is-zap" mapstructure:"is-zap"` // 是否开启zap日志 + Hosts []*MongoHost `json:"hosts" yaml:"hosts" mapstructure:"hosts"` // 主机列表 +} + +type MongoHost struct { + Host string `json:"host" yaml:"host" mapstructure:"host"` // ip地址 + Port string `json:"port" yaml:"port" mapstructure:"port"` // 端口 +} + +// Uri . +func (x *Mongo) Uri() string { + length := len(x.Hosts) + hosts := make([]string, 0, length) + for i := 0; i < length; i++ { + if x.Hosts[i].Host != "" && x.Hosts[i].Port != "" { + hosts = append(hosts, x.Hosts[i].Host+":"+x.Hosts[i].Port) + } + } + if x.Options != "" { + return fmt.Sprintf("mongodb://%s/%s?%s", strings.Join(hosts, ","), x.Database, x.Options) + } + return fmt.Sprintf("mongodb://%s/%s", strings.Join(hosts, ","), x.Database) +} diff --git a/server/config/oss_aliyun.go b/server/config/oss_aliyun.go new file mode 100644 index 0000000..934bd78 --- /dev/null +++ b/server/config/oss_aliyun.go @@ -0,0 +1,10 @@ +package config + +type AliyunOSS struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` +} diff --git a/server/config/oss_aws.go b/server/config/oss_aws.go new file mode 100644 index 0000000..7ec6acc --- /dev/null +++ b/server/config/oss_aws.go @@ -0,0 +1,13 @@ +package config + +type AwsS3 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` + S3ForcePathStyle bool `mapstructure:"s3-force-path-style" json:"s3-force-path-style" yaml:"s3-force-path-style"` + DisableSSL bool `mapstructure:"disable-ssl" json:"disable-ssl" yaml:"disable-ssl"` +} diff --git a/server/config/oss_cloudflare.go b/server/config/oss_cloudflare.go new file mode 100644 index 0000000..ab7a393 --- /dev/null +++ b/server/config/oss_cloudflare.go @@ -0,0 +1,10 @@ +package config + +type CloudflareR2 struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + Path string `mapstructure:"path" json:"path" yaml:"path"` + AccountID string `mapstructure:"account-id" json:"account-id" yaml:"account-id"` + AccessKeyID string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + SecretAccessKey string `mapstructure:"secret-access-key" json:"secret-access-key" yaml:"secret-access-key"` +} diff --git a/server/config/oss_huawei.go b/server/config/oss_huawei.go new file mode 100644 index 0000000..45dfbcd --- /dev/null +++ b/server/config/oss_huawei.go @@ -0,0 +1,9 @@ +package config + +type HuaWeiObs struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` +} diff --git a/server/config/oss_local.go b/server/config/oss_local.go new file mode 100644 index 0000000..7038d4a --- /dev/null +++ b/server/config/oss_local.go @@ -0,0 +1,6 @@ +package config + +type Local struct { + Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径 + StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径 +} diff --git a/server/config/oss_minio.go b/server/config/oss_minio.go new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/server/config/oss_minio.go @@ -0,0 +1,11 @@ +package config + +type Minio struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` +} diff --git a/server/config/oss_qiniu.go b/server/config/oss_qiniu.go new file mode 100644 index 0000000..298fe2d --- /dev/null +++ b/server/config/oss_qiniu.go @@ -0,0 +1,11 @@ +package config + +type Qiniu struct { + Zone string `mapstructure:"zone" json:"zone" yaml:"zone"` // 存储区域 + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` // 空间名称 + ImgPath string `mapstructure:"img-path" json:"img-path" yaml:"img-path"` // CDN加速域名 + AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` // 秘钥AK + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` // 秘钥SK + UseHTTPS bool `mapstructure:"use-https" json:"use-https" yaml:"use-https"` // 是否使用https + UseCdnDomains bool `mapstructure:"use-cdn-domains" json:"use-cdn-domains" yaml:"use-cdn-domains"` // 上传是否使用CDN上传加速 +} diff --git a/server/config/oss_tencent.go b/server/config/oss_tencent.go new file mode 100644 index 0000000..39a29d1 --- /dev/null +++ b/server/config/oss_tencent.go @@ -0,0 +1,10 @@ +package config + +type TencentCOS struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` +} diff --git a/server/config/redis.go b/server/config/redis.go new file mode 100644 index 0000000..94b5bf6 --- /dev/null +++ b/server/config/redis.go @@ -0,0 +1,10 @@ +package config + +type Redis struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // 代表当前实例的名字 + Addr string `mapstructure:"addr" json:"addr" yaml:"addr"` // 服务器地址:端口 + Password string `mapstructure:"password" json:"password" yaml:"password"` // 密码 + DB int `mapstructure:"db" json:"db" yaml:"db"` // 单实例模式下redis的哪个数据库 + UseCluster bool `mapstructure:"useCluster" json:"useCluster" yaml:"useCluster"` // 是否使用集群模式 + ClusterAddrs []string `mapstructure:"clusterAddrs" json:"clusterAddrs" yaml:"clusterAddrs"` // 集群模式下的节点地址列表 +} diff --git a/server/config/system.go b/server/config/system.go new file mode 100644 index 0000000..4f09773 --- /dev/null +++ b/server/config/system.go @@ -0,0 +1,16 @@ +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,手动迁移 + DataDir string `mapstructure:"data-dir" json:"data-dir" yaml:"data-dir"` // 数据目录 +} diff --git a/server/config/zap.go b/server/config/zap.go new file mode 100644 index 0000000..6beb238 --- /dev/null +++ b/server/config/zap.go @@ -0,0 +1,72 @@ +package config + +import ( + "time" + + "go.uber.org/zap/zapcore" +) + +type Zap struct { + Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别 + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 + Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出 + Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹 + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` // 编码级 + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 + RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数 +} + +// Levels 根据字符串转化为 zapcore.Levels +func (c *Zap) Levels() []zapcore.Level { + levels := make([]zapcore.Level, 0, 7) + level, err := zapcore.ParseLevel(c.Level) + if err != nil { + level = zapcore.DebugLevel + } + for ; level <= zapcore.FatalLevel; level++ { + levels = append(levels, level) + } + return levels +} + +func (c *Zap) Encoder() zapcore.Encoder { + config := zapcore.EncoderConfig{ + TimeKey: "time", + NameKey: "name", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "message", + StacktraceKey: c.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000")) + }, + EncodeLevel: c.LevelEncoder(), + EncodeCaller: zapcore.FullCallerEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + } + if c.Format == "json" { + return zapcore.NewJSONEncoder(config) + } + return zapcore.NewConsoleEncoder(config) + +} + +// LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder +// Author [SliverHorn](https://github.com/SliverHorn) +func (c *Zap) LevelEncoder() zapcore.LevelEncoder { + switch { + case c.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) + return zapcore.LowercaseLevelEncoder + case c.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 + return zapcore.LowercaseColorLevelEncoder + case c.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 + return zapcore.CapitalLevelEncoder + case c.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 + return zapcore.CapitalColorLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} diff --git a/server/core/internal/constant.go b/server/core/internal/constant.go new file mode 100644 index 0000000..b22362c --- /dev/null +++ b/server/core/internal/constant.go @@ -0,0 +1,9 @@ +package internal + +const ( + ConfigEnv = "GVA_CONFIG" + ConfigDefaultFile = "config.yaml" + ConfigTestFile = "config.test.yaml" + ConfigDebugFile = "config.debug.yaml" + ConfigReleaseFile = "config.release.yaml" +) diff --git a/server/core/internal/cutter.go b/server/core/internal/cutter.go new file mode 100644 index 0000000..2873b7c --- /dev/null +++ b/server/core/internal/cutter.go @@ -0,0 +1,125 @@ +package internal + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "time" +) + +// Cutter 实现 io.Writer 接口 +// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator) +type Cutter struct { + level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal) + layout string // 时间格式 2006-01-02 15:04:05 + formats []string // 自定义参数([]string{Director,"2006-01-02", "business"(此参数可不写), level+".log"} + director string // 日志文件夹 + retentionDay int //日志保留天数 + file *os.File // 文件句柄 + mutex *sync.RWMutex // 读写锁 +} + +type CutterOption func(*Cutter) + +// CutterWithLayout 时间格式 +func CutterWithLayout(layout string) CutterOption { + return func(c *Cutter) { + c.layout = layout + } +} + +// CutterWithFormats 格式化参数 +func CutterWithFormats(format ...string) CutterOption { + return func(c *Cutter) { + if len(format) > 0 { + c.formats = format + } + } +} + +func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter { + rotate := &Cutter{ + level: level, + director: director, + retentionDay: retentionDay, + mutex: new(sync.RWMutex), + } + for i := 0; i < len(options); i++ { + options[i](rotate) + } + return rotate +} + +// Write satisfies the io.Writer interface. It writes to the +// appropriate file handle that is currently being used. +// If we have reached rotation time, the target file gets +// automatically rotated, and also purged if necessary. +func (c *Cutter) Write(bytes []byte) (n int, err error) { + c.mutex.Lock() + defer func() { + if c.file != nil { + _ = c.file.Close() + c.file = nil + } + c.mutex.Unlock() + }() + length := len(c.formats) + values := make([]string, 0, 3+length) + values = append(values, c.director) + if c.layout != "" { + values = append(values, time.Now().Format(c.layout)) + } + for i := 0; i < length; i++ { + values = append(values, c.formats[i]) + } + values = append(values, c.level+".log") + filename := filepath.Join(values...) + director := filepath.Dir(filename) + err = os.MkdirAll(director, os.ModePerm) + if err != nil { + return 0, err + } + defer func() { + err := removeNDaysFolders(c.director, c.retentionDay) + if err != nil { + fmt.Println("清理过期日志失败", err) + } + }() + + c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return 0, err + } + return c.file.Write(bytes) +} + +func (c *Cutter) Sync() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.file != nil { + return c.file.Sync() + } + return nil +} + +// 增加日志目录文件清理 小于等于零的值默认忽略不再处理 +func removeNDaysFolders(dir string, days int) error { + if days <= 0 { + return nil + } + cutoff := time.Now().AddDate(0, 0, -days) + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.ModTime().Before(cutoff) && path != dir { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + return nil + }) +} diff --git a/server/core/internal/zap_core.go b/server/core/internal/zap_core.go new file mode 100644 index 0000000..d722a33 --- /dev/null +++ b/server/core/internal/zap_core.go @@ -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() +} diff --git a/server/core/server.go b/server/core/server.go new file mode 100644 index 0000000..213ae09 --- /dev/null +++ b/server/core/server.go @@ -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) +} diff --git a/server/core/server_run.go b/server/core/server_run.go new file mode 100644 index 0000000..067ce6b --- /dev/null +++ b/server/core/server_run.go @@ -0,0 +1,60 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.L().Info("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") +} diff --git a/server/core/viper.go b/server/core/viper.go new file mode 100644 index 0000000..ac310d1 --- /dev/null +++ b/server/core/viper.go @@ -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 +} diff --git a/server/core/zap.go b/server/core/zap.go new file mode 100644 index 0000000..f7a27cb --- /dev/null +++ b/server/core/zap.go @@ -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 +} diff --git a/server/global/global.go b/server/global/global.go new file mode 100644 index 0000000..002bf4c --- /dev/null +++ b/server/global/global.go @@ -0,0 +1,69 @@ +package global + +import ( + "fmt" + "sync" + + "github.com/mark3labs/mcp-go/server" + + "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 +} diff --git a/server/global/model.go b/server/global/model.go new file mode 100644 index 0000000..9772eb3 --- /dev/null +++ b/server/global/model.go @@ -0,0 +1,14 @@ +package global + +import ( + "time" + + "gorm.io/gorm" +) + +type GVA_MODEL struct { + ID uint `gorm:"primarykey" json:"ID"` // 主键ID + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间 +} diff --git a/server/global/version.go b/server/global/version.go new file mode 100644 index 0000000..544d423 --- /dev/null +++ b/server/global/version.go @@ -0,0 +1,12 @@ +package global + +// Version 版本信息 +// 目前只有Version正式使用 其余为预留 +const ( + // Version 当前版本号 + Version = "v2.8.9" + // AppName 应用名称 + AppName = "Gin-Vue-Admin" + // Description 应用描述 + Description = "使用gin+vue进行极速开发的全栈开发基础平台" +) diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..90468ce --- /dev/null +++ b/server/go.mod @@ -0,0 +1,190 @@ +module git.echol.cn/loser/st/server + +go 1.24.0 + +toolchain go1.24.2 + +require ( + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go v1.55.6 + github.com/casbin/casbin/v2 v2.103.0 + github.com/casbin/gorm-adapter/v3 v3.32.0 + github.com/dzwvip/gorm-oracle v0.1.2 + github.com/fsnotify/fsnotify v1.8.0 + github.com/gin-gonic/gin v1.10.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-sql-driver/mysql v1.8.1 + github.com/goccy/go-json v0.10.4 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 + github.com/gookit/color v1.5.4 + github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/lib/pq v1.10.9 + github.com/mark3labs/mcp-go v0.41.1 + github.com/mholt/archives v0.1.1 + github.com/minio/minio-go/v7 v7.0.84 + github.com/mojocn/base64Captcha v1.3.8 + github.com/otiai10/copy v1.14.1 + github.com/pgvector/pgvector-go v0.3.0 + github.com/pkg/errors v0.9.1 + github.com/qiniu/go-sdk/v7 v7.25.2 + github.com/qiniu/qmgo v1.1.9 + github.com/redis/go-redis/v9 v9.7.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/songzhibin97/gkit v1.2.13 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 + github.com/tencentyun/cos-go-sdk-v5 v0.7.60 + github.com/unrolled/secure v1.17.0 + github.com/xuri/excelize/v2 v2.9.0 + go.mongodb.org/mongo-driver v1.17.2 + go.uber.org/automaxprocs v1.6.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.37.0 + golang.org/x/sync v0.13.0 + golang.org/x/text v0.24.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/datatypes v1.2.5 + gorm.io/driver/mysql v1.5.7 + gorm.io/driver/postgres v1.5.11 + gorm.io/driver/sqlserver v1.5.4 + gorm.io/gorm v1.25.12 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/sonic v1.12.7 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/casbin/govaluate v1.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.12.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gammazero/toposort v0.1.1 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/microsoft/go-mssqldb v1.8.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/mozillazg/go-httpheader v0.4.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sijms/go-ora/v2 v2.7.17 // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/thoas/go-funk v0.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect + github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect + golang.org/x/arch v0.13.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect + golang.org/x/image v0.23.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.31.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gorm.io/driver/sqlite v1.5.0 // indirect + gorm.io/plugin/dbresolver v1.5.3 // indirect + modernc.org/fileutil v1.3.0 // indirect + modernc.org/libc v1.61.9 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect + modernc.org/sqlite v1.34.5 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..0361ac4 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,873 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= +entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= +github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= +github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= +github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic= +github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus= +github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= +github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= +github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ4fSNB6mMRuYNvFWou7BZs6SZB925hPrnk= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= +github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY= +github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= +github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= +github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w= +github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc= +github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo= +github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= +github.com/qiniu/qmgo v1.1.9 h1:3G3h9RLyjIUW9YSAQEPP2WqqNnboZ2Z/zO3mugjVb3E= +github.com/qiniu/qmgo v1.1.9/go.mod h1:aba4tNSlMWrwUhe7RdILfwBRIgvBujt1y10X+T1YZSI= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= +github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/songzhibin97/gkit v1.2.13 h1:paY0XJkdRuy9/8k9nTnbdrzo8pC22jIIFldUkOQv5nU= +github.com/songzhibin97/gkit v1.2.13/go.mod h1:38CreNR27eTGaG1UMGihrXqI4xc3nGfYxLVKKVx6Ngg= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= +github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= +github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= +github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= +github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= +github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= +github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= +github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= +golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= +gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= +gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= +mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= +modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= +modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= +modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/server/initialize/db_list.go b/server/initialize/db_list.go new file mode 100644 index 0000000..5d65c0a --- /dev/null +++ b/server/initialize/db_list.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "gorm.io/gorm" +) + +const sys = "system" + +func DBList() { + dbMap := make(map[string]*gorm.DB) + for _, info := range global.GVA_CONFIG.DBList { + if info.Disable { + continue + } + switch info.Type { + case "mysql": + dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB}) + case "mssql": + dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB}) + case "pgsql": + dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB}) + case "oracle": + dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB}) + default: + continue + } + } + // 做特殊判断,是否有迁移 + // 适配低版本迁移多数据库版本 + if sysDB, ok := dbMap[sys]; ok { + global.GVA_DB = sysDB + } + global.GVA_DBList = dbMap +} diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go new file mode 100644 index 0000000..022b917 --- /dev/null +++ b/server/initialize/ensure_tables.go @@ -0,0 +1,112 @@ +package initialize + +import ( + "context" + + "git.echol.cn/loser/st/server/model/example" + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + adapter "github.com/casbin/gorm-adapter/v3" + "gorm.io/gorm" +) + +const initOrderEnsureTables = system.InitOrderExternal - 1 + +type ensureTables struct{} + +// auto run +func init() { + system.RegisterInit(initOrderEnsureTables, &ensureTables{}) +} + +func (e *ensureTables) InitializerName() string { + return "ensure_tables_created" +} +func (e *ensureTables) InitializeData(ctx context.Context) (next context.Context, err error) { + return ctx, nil +} + +func (e *ensureTables) DataInserted(ctx context.Context) bool { + return true +} + +func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + sysModel.SysParams{}, + sysModel.SysVersion{}, + sysModel.SysError{}, + sysModel.SysLoginLog{}, + sysModel.SysApiToken{}, + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + } + for _, t := range tables { + _ = db.AutoMigrate(&t) + // 视图 authority_menu 会被当成表来创建,引发冲突错误(更新版本的gorm似乎不会) + // 由于 AutoMigrate() 基本无需考虑错误,因此显式忽略 + } + return ctx, nil +} + +func (e *ensureTables) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + tables := []interface{}{ + sysModel.SysApi{}, + sysModel.SysUser{}, + sysModel.SysBaseMenu{}, + sysModel.SysAuthority{}, + sysModel.JwtBlacklist{}, + sysModel.SysDictionary{}, + sysModel.SysAutoCodeHistory{}, + sysModel.SysOperationRecord{}, + sysModel.SysDictionaryDetail{}, + sysModel.SysBaseMenuParameter{}, + sysModel.SysBaseMenuBtn{}, + sysModel.SysAuthorityBtn{}, + sysModel.SysAutoCodePackage{}, + sysModel.SysExportTemplate{}, + sysModel.Condition{}, + sysModel.JoinTemplate{}, + + adapter.CasbinRule{}, + + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + } + yes := true + for _, t := range tables { + yes = yes && db.Migrator().HasTable(t) + } + return yes +} diff --git a/server/initialize/fix_world_info_table.sql b/server/initialize/fix_world_info_table.sql new file mode 100644 index 0000000..5fd8bf3 --- /dev/null +++ b/server/initialize/fix_world_info_table.sql @@ -0,0 +1,9 @@ +-- 修复 ai_world_info 表结构 +-- 如果表存在旧的 name 字段,需要删除并重新创建 + +-- 删除旧表(如果存在) +DROP TABLE IF EXISTS ai_character_world_info CASCADE; +DROP TABLE IF EXISTS ai_world_info CASCADE; + +-- 表将由 Gorm AutoMigrate 自动创建 +-- 重启服务器即可 diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go new file mode 100644 index 0000000..af5c610 --- /dev/null +++ b/server/initialize/gorm.go @@ -0,0 +1,105 @@ +package initialize + +import ( + "os" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/example" + "git.echol.cn/loser/st/server/model/system" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +func Gorm() *gorm.DB { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + case "pgsql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Pgsql.Dbname + return GormPgSql() + case "oracle": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Oracle.Dbname + return GormOracle() + case "mssql": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mssql.Dbname + return GormMssql() + case "sqlite": + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Sqlite.Dbname + return GormSqlite() + default: + global.GVA_ACTIVE_DBNAME = &global.GVA_CONFIG.Mysql.Dbname + return GormMysql() + } +} + +func RegisterTables() { + if global.GVA_CONFIG.System.DisableAutoMigrate { + global.GVA_LOG.Info("auto-migrate is disabled, skipping table registration") + return + } + + // 初始化 PostgreSQL 扩展(仅创建 pgvector 扩展) + InitPgSQLExtension() + + db := global.GVA_DB + err := db.AutoMigrate( + + // System tables (管理后台表 - 不修改) + system.SysApi{}, + system.SysIgnoreApi{}, + system.SysUser{}, + system.SysBaseMenu{}, + system.JwtBlacklist{}, + system.SysAuthority{}, + system.SysDictionary{}, + system.SysOperationRecord{}, + system.SysAutoCodeHistory{}, + system.SysDictionaryDetail{}, + system.SysBaseMenuParameter{}, + system.SysBaseMenuBtn{}, + system.SysAuthorityBtn{}, + system.SysAutoCodePackage{}, + system.SysExportTemplate{}, + system.Condition{}, + system.JoinTemplate{}, + system.SysParams{}, + system.SysVersion{}, + system.SysError{}, + system.SysApiToken{}, + system.SysLoginLog{}, + + // Example tables + example.ExaFile{}, + example.ExaCustomer{}, + example.ExaFileChunk{}, + example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + + // App tables (前台应用表 - 新增) + app.AppUser{}, + app.AppUserSession{}, + app.AICharacter{}, + app.Conversation{}, + app.Message{}, + app.AIConfig{}, + app.AIPreset{}, + ) + if err != nil { + global.GVA_LOG.Error("register table failed", zap.Error(err)) + os.Exit(0) + } + + // 创建向量索引(必须在 AutoMigrate 之后) + CreateVectorIndexes() + + err = bizModel() + + if err != nil { + global.GVA_LOG.Error("register biz_table failed", zap.Error(err)) + os.Exit(0) + } + global.GVA_LOG.Info("register table success") +} diff --git a/server/initialize/gorm_biz.go b/server/initialize/gorm_biz.go new file mode 100644 index 0000000..1df3094 --- /dev/null +++ b/server/initialize/gorm_biz.go @@ -0,0 +1,14 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/global" +) + +func bizModel() error { + db := global.GVA_DB + err := db.AutoMigrate() + if err != nil { + return err + } + return nil +} diff --git a/server/initialize/gorm_mssql.go b/server/initialize/gorm_mssql.go new file mode 100644 index 0000000..6a6e4c4 --- /dev/null +++ b/server/initialize/gorm_mssql.go @@ -0,0 +1,64 @@ +package initialize + +/* + * @Author: 逆光飞翔 191180776@qq.com + * @Date: 2022-12-08 17:25:49 + * @LastEditors: 逆光飞翔 191180776@qq.com + * @LastEditTime: 2022-12-08 18:00:00 + * @FilePath: \server\initialize\gorm_mssql.go + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" +) + +// GormMssql 初始化Mssql数据库 +// Author [LouisZhang](191180776@qq.com) +func GormMssql() *gorm.DB { + m := global.GVA_CONFIG.Mssql + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { + return nil + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} + +// GormMssqlByConfig 初始化Mysql数据库用过传入配置 +func GormMssqlByConfig(m config.Mssql) *gorm.DB { + if m.Dbname == "" { + return nil + } + mssqlConfig := sqlserver.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_mysql.go b/server/initialize/gorm_mysql.go new file mode 100644 index 0000000..e3222b2 --- /dev/null +++ b/server/initialize/gorm_mysql.go @@ -0,0 +1,48 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + _ "github.com/go-sql-driver/mysql" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// GormMysql 初始化Mysql数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ByteZhou-2018](https://github.com/ByteZhou-2018) +func GormMysql() *gorm.DB { + m := global.GVA_CONFIG.Mysql + return initMysqlDatabase(m) +} + +// GormMysqlByConfig 通过传入配置初始化Mysql数据库 +func GormMysqlByConfig(m config.Mysql) *gorm.DB { + return initMysqlDatabase(m) +} + +// initMysqlDatabase 初始化Mysql数据库的辅助函数 +func initMysqlDatabase(m config.Mysql) *gorm.DB { + if m.Dbname == "" { + return nil + } + + mysqlConfig := mysql.Config{ + DSN: m.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: false, // 根据版本自动配置 + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_oracle.go b/server/initialize/gorm_oracle.go new file mode 100644 index 0000000..ab4f476 --- /dev/null +++ b/server/initialize/gorm_oracle.go @@ -0,0 +1,37 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + oracle "github.com/dzwvip/gorm-oracle" + "gorm.io/gorm" +) + +// GormOracle 初始化oracle数据库 +func GormOracle() *gorm.DB { + m := global.GVA_CONFIG.Oracle + return initOracleDatabase(m) +} + +// GormOracleByConfig 初始化Oracle数据库用过传入配置 +func GormOracleByConfig(m config.Oracle) *gorm.DB { + return initOracleDatabase(m) +} + +// initOracleDatabase 初始化Oracle数据库的辅助函数 +func initOracleDatabase(m config.Oracle) *gorm.DB { + if m.Dbname == "" { + return nil + } + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(m.MaxIdleConns) + sqlDB.SetMaxOpenConns(m.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_pgsql.go b/server/initialize/gorm_pgsql.go new file mode 100644 index 0000000..47ea3f5 --- /dev/null +++ b/server/initialize/gorm_pgsql.go @@ -0,0 +1,43 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// GormPgSql 初始化 Postgresql 数据库 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func GormPgSql() *gorm.DB { + p := global.GVA_CONFIG.Pgsql + return initPgSqlDatabase(p) +} + +// GormPgSqlByConfig 初始化 Postgresql 数据库 通过指定参数 +func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { + return initPgSqlDatabase(p) +} + +// initPgSqlDatabase 初始化 Postgresql 数据库的辅助函数 +func initPgSqlDatabase(p config.Pgsql) *gorm.DB { + if p.Dbname == "" { + return nil + } + pgsqlConfig := postgres.Config{ + DSN: p.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + } + // 数据库配置 + general := p.GeneralDB + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(p.MaxIdleConns) + sqlDB.SetMaxOpenConns(p.MaxOpenConns) + return db + } +} diff --git a/server/initialize/gorm_pgsql_extension.go b/server/initialize/gorm_pgsql_extension.go new file mode 100644 index 0000000..ccf4a2b --- /dev/null +++ b/server/initialize/gorm_pgsql_extension.go @@ -0,0 +1,48 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/global" + "go.uber.org/zap" +) + +// InitPgSQLExtension 初始化 PostgreSQL 扩展(仅创建 pgvector 扩展) +// 必须在 AutoMigrate 之前调用 +func InitPgSQLExtension() { + if global.GVA_CONFIG.System.DbType != "pgsql" { + return + } + + db := global.GVA_DB + + // 安装 pgvector 扩展(用于向量存储) + if err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector").Error; err != nil { + global.GVA_LOG.Error("failed to create pgvector extension", zap.Error(err)) + global.GVA_LOG.Warn("请确保 PostgreSQL 已安装 pgvector 扩展") + } else { + global.GVA_LOG.Info("pgvector extension is ready") + } +} + +// CreateVectorIndexes 创建向量索引 +// 必须在 AutoMigrate 之后调用(确保表已存在) +func CreateVectorIndexes() { + if global.GVA_CONFIG.System.DbType != "pgsql" { + return + } + + db := global.GVA_DB + + // 为 ai_memory_vectors 表创建 HNSW 索引(余弦相似度) + sql := ` + CREATE INDEX IF NOT EXISTS idx_memory_vectors_embedding + ON ai_memory_vectors + USING hnsw (embedding vector_cosine_ops) + ` + + if err := db.Exec(sql).Error; err != nil { + global.GVA_LOG.Error("failed to create vector indexes", zap.Error(err)) + return + } + + global.GVA_LOG.Info("vector indexes created successfully") +} diff --git a/server/initialize/gorm_sqlite.go b/server/initialize/gorm_sqlite.go new file mode 100644 index 0000000..0423d56 --- /dev/null +++ b/server/initialize/gorm_sqlite.go @@ -0,0 +1,38 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +// GormSqlite 初始化Sqlite数据库 +func GormSqlite() *gorm.DB { + s := global.GVA_CONFIG.Sqlite + return initSqliteDatabase(s) +} + +// GormSqliteByConfig 初始化Sqlite数据库用过传入配置 +func GormSqliteByConfig(s config.Sqlite) *gorm.DB { + return initSqliteDatabase(s) +} + +// initSqliteDatabase 初始化Sqlite数据库辅助函数 +func initSqliteDatabase(s config.Sqlite) *gorm.DB { + if s.Dbname == "" { + return nil + } + + // 数据库配置 + general := s.GeneralDB + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil { + panic(err) + } else { + sqlDB, _ := db.DB() + sqlDB.SetMaxIdleConns(s.MaxIdleConns) + sqlDB.SetMaxOpenConns(s.MaxOpenConns) + return db + } +} diff --git a/server/initialize/init.go b/server/initialize/init.go new file mode 100644 index 0000000..097c152 --- /dev/null +++ b/server/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "git.echol.cn/loser/st/server/utils" +) + +// 初始化全局函数 +func SetupHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/server/initialize/internal/gorm.go b/server/initialize/internal/gorm.go new file mode 100644 index 0000000..e398cfa --- /dev/null +++ b/server/initialize/internal/gorm.go @@ -0,0 +1,31 @@ +package internal + +import ( + "time" + + "git.echol.cn/loser/st/server/config" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +var Gorm = new(_gorm) + +type _gorm struct{} + +// Config gorm 自定义配置 +// Author [SliverHorn](https://github.com/SliverHorn) +func (g *_gorm) Config(general config.GeneralDB) *gorm.Config { + return &gorm.Config{ + Logger: logger.New(NewWriter(general), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: general.LogLevel(), + Colorful: true, + }), + NamingStrategy: schema.NamingStrategy{ + TablePrefix: general.Prefix, + SingularTable: general.Singular, + }, + DisableForeignKeyConstraintWhenMigrating: true, + } +} diff --git a/server/initialize/internal/gorm_logger_writer.go b/server/initialize/internal/gorm_logger_writer.go new file mode 100644 index 0000000..3d8562a --- /dev/null +++ b/server/initialize/internal/gorm_logger_writer.go @@ -0,0 +1,42 @@ +package internal + +import ( + "fmt" + + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "gorm.io/gorm/logger" +) + +type Writer struct { + config config.GeneralDB + writer logger.Writer +} + +func NewWriter(config config.GeneralDB) *Writer { + return &Writer{config: config} +} + +// Printf 格式化打印日志 +func (c *Writer) Printf(message string, data ...any) { + + // 当有日志时候均需要输出到控制台 + fmt.Printf(message, data...) + + // 当开启了zap的情况,会打印到日志记录 + if c.config.LogZap { + switch c.config.LogLevel() { + case logger.Silent: + global.GVA_LOG.Debug(fmt.Sprintf(message, data...)) + case logger.Error: + global.GVA_LOG.Error(fmt.Sprintf(message, data...)) + case logger.Warn: + global.GVA_LOG.Warn(fmt.Sprintf(message, data...)) + case logger.Info: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + default: + global.GVA_LOG.Info(fmt.Sprintf(message, data...)) + } + return + } +} diff --git a/server/initialize/internal/mongo.go b/server/initialize/internal/mongo.go new file mode 100644 index 0000000..56ea61a --- /dev/null +++ b/server/initialize/internal/mongo.go @@ -0,0 +1,30 @@ +package internal + +import ( + "context" + "fmt" + + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/event" + opt "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap" +) + +var Mongo = new(mongo) + +type mongo struct{} + +func (m *mongo) GetClientOptions() []options.ClientOptions { + cmdMonitor := &event.CommandMonitor{ + Started: func(ctx context.Context, event *event.CommandStartedEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d][database:%s] %s\n", event.RequestID, event.DatabaseName, event.Command), zap.String("business", "mongo")) + }, + Succeeded: func(ctx context.Context, event *event.CommandSucceededEvent) { + zap.L().Info(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Reply), zap.String("business", "mongo")) + }, + Failed: func(ctx context.Context, event *event.CommandFailedEvent) { + zap.L().Error(fmt.Sprintf("[MongoDB][RequestID:%d] [%s] %s\n", event.RequestID, event.Duration.String(), event.Failure), zap.String("business", "mongo")) + }, + } + return []options.ClientOptions{{ClientOptions: &opt.ClientOptions{Monitor: cmdMonitor}}} +} diff --git a/server/initialize/mcp.go b/server/initialize/mcp.go new file mode 100644 index 0000000..282a516 --- /dev/null +++ b/server/initialize/mcp.go @@ -0,0 +1,25 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/global" + mcpTool "git.echol.cn/loser/st/server/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func McpRun() *server.SSEServer { + config := global.GVA_CONFIG.MCP + + s := server.NewMCPServer( + config.Name, + config.Version, + ) + + global.GVA_MCP_SERVER = s + + mcpTool.RegisterAllTools(s) + + return server.NewSSEServer(s, + server.WithSSEEndpoint(config.SSEPath), + server.WithMessageEndpoint(config.MessagePath), + server.WithBaseURL(config.UrlPrefix)) +} diff --git a/server/initialize/mongo.go b/server/initialize/mongo.go new file mode 100644 index 0000000..1512a8f --- /dev/null +++ b/server/initialize/mongo.go @@ -0,0 +1,156 @@ +package initialize + +import ( + "context" + "fmt" + "sort" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize/internal" + "git.echol.cn/loser/st/server/utils" + "github.com/pkg/errors" + "github.com/qiniu/qmgo" + "github.com/qiniu/qmgo/options" + "go.mongodb.org/mongo-driver/bson" + option "go.mongodb.org/mongo-driver/mongo/options" +) + +var Mongo = new(mongo) + +type ( + mongo struct{} + Index struct { + V any `bson:"v"` + Ns any `bson:"ns"` + Key []bson.E `bson:"key"` + Name string `bson:"name"` + } +) + +func (m *mongo) Indexes(ctx context.Context) error { + // 表名:索引列表 列: "表名": [][]string{{"index1", "index2"}} + indexMap := map[string][][]string{} + for collection, indexes := range indexMap { + err := m.CreateIndexes(ctx, collection, indexes) + if err != nil { + return err + } + } + return nil +} + +func (m *mongo) Initialization() error { + var opts []options.ClientOptions + if global.GVA_CONFIG.Mongo.IsZap { + opts = internal.Mongo.GetClientOptions() + } + ctx := context.Background() + config := &qmgo.Config{ + Uri: global.GVA_CONFIG.Mongo.Uri(), + Coll: global.GVA_CONFIG.Mongo.Coll, + Database: global.GVA_CONFIG.Mongo.Database, + MinPoolSize: &global.GVA_CONFIG.Mongo.MinPoolSize, + MaxPoolSize: &global.GVA_CONFIG.Mongo.MaxPoolSize, + SocketTimeoutMS: &global.GVA_CONFIG.Mongo.SocketTimeoutMs, + ConnectTimeoutMS: &global.GVA_CONFIG.Mongo.ConnectTimeoutMs, + } + if global.GVA_CONFIG.Mongo.Username != "" && global.GVA_CONFIG.Mongo.Password != "" { + config.Auth = &qmgo.Credential{ + Username: global.GVA_CONFIG.Mongo.Username, + Password: global.GVA_CONFIG.Mongo.Password, + AuthSource: global.GVA_CONFIG.Mongo.AuthSource, + } + } + client, err := qmgo.Open(ctx, config, opts...) + + if err != nil { + return errors.Wrap(err, "链接mongodb数据库失败!") + } + global.GVA_MONGO = client + err = m.Indexes(ctx) + if err != nil { + return err + } + return nil +} + +func (m *mongo) CreateIndexes(ctx context.Context, name string, indexes [][]string) error { + collection, err := global.GVA_MONGO.Database.Collection(name).CloneCollection() + if err != nil { + return errors.Wrapf(err, "获取[%s]的表对象失败!", name) + } + list, err := collection.Indexes().List(ctx) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引对象失败!", name) + } + var entities []Index + err = list.All(ctx, &entities) + if err != nil { + return errors.Wrapf(err, "获取[%s]的索引列表失败!", name) + } + length := len(indexes) + indexMap1 := make(map[string][]string, length) + for i := 0; i < length; i++ { + sort.Strings(indexes[i]) // 对索引key进行排序, 在使用bson.M搜索时, bson会自动按照key的字母顺序进行排序 + length1 := len(indexes[i]) + keys := make([]string, 0, length1) + for j := 0; j < length1; j++ { + if indexes[i][i][0] == '-' { + keys = append(keys, indexes[i][j], "-1") + continue + } + keys = append(keys, indexes[i][j], "1") + } + key := strings.Join(keys, "_") + _, o1 := indexMap1[key] + if o1 { + return errors.Errorf("索引[%s]重复!", key) + } + indexMap1[key] = indexes[i] + } + length = len(entities) + indexMap2 := make(map[string]map[string]string, length) + for i := 0; i < length; i++ { + v1, o1 := indexMap2[entities[i].Name] + if !o1 { + keyLength := len(entities[i].Key) + v1 = make(map[string]string, keyLength) + for j := 0; j < keyLength; j++ { + v2, o2 := v1[entities[i].Key[j].Key] + if !o2 { + v1 = make(map[string]string) + } + v2 = entities[i].Key[j].Key + v1[entities[i].Key[j].Key] = v2 + indexMap2[entities[i].Name] = v1 + } + } + } + for k1, v1 := range indexMap1 { + _, o2 := indexMap2[k1] + if o2 { + continue + } // 索引存在 + if len(fmt.Sprintf("%s.%s.$%s", collection.Name(), name, v1)) > 127 { + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间, 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + return nil + } + err = global.GVA_MONGO.Database.Collection(name).CreateOneIndex(ctx, options.IndexModel{ + Key: v1, + IndexOptions: option.Index().SetExpireAfterSeconds(86400), + // IndexOptions: option.Index().SetName(utils.MD5V([]byte(k1))).SetExpireAfterSeconds(86400), // SetExpireAfterSeconds(86400) 设置索引过期时间(秒), 86400 = 1天 + }) + if err != nil { + return errors.Wrapf(err, "创建索引[%s]失败!", k1) + } + } + return nil +} diff --git a/server/initialize/other.go b/server/initialize/other.go new file mode 100644 index 0000000..21394cc --- /dev/null +++ b/server/initialize/other.go @@ -0,0 +1,33 @@ +package initialize + +import ( + "bufio" + "os" + "strings" + + "github.com/songzhibin97/gkit/cache/local_cache" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/utils" +) + +func OtherInit() { + dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + panic(err) + } + _, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + if err != nil { + panic(err) + } + + global.BlackCache = local_cache.NewCache( + local_cache.SetDefaultExpire(dr), + ) + file, err := os.Open("go.mod") + if err == nil && global.GVA_CONFIG.AutoCode.Module == "" { + scanner := bufio.NewScanner(file) + scanner.Scan() + global.GVA_CONFIG.AutoCode.Module = strings.TrimPrefix(scanner.Text(), "module ") + } +} diff --git a/server/initialize/plugin.go b/server/initialize/plugin.go new file mode 100644 index 0000000..470e057 --- /dev/null +++ b/server/initialize/plugin.go @@ -0,0 +1,15 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/global" + "github.com/gin-gonic/gin" +) + +func InstallPlugin(PrivateGroup *gin.RouterGroup, PublicRouter *gin.RouterGroup, engine *gin.Engine) { + if global.GVA_DB == nil { + global.GVA_LOG.Info("项目暂未初始化,无法安装插件,初始化后重启项目即可完成插件安装") + return + } + bizPluginV1(PrivateGroup, PublicRouter) + bizPluginV2(engine) +} diff --git a/server/initialize/plugin_biz_v1.go b/server/initialize/plugin_biz_v1.go new file mode 100644 index 0000000..97de009 --- /dev/null +++ b/server/initialize/plugin_biz_v1.go @@ -0,0 +1,36 @@ +package initialize + +import ( + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/plugin/email" + "git.echol.cn/loser/st/server/utils/plugin" + "github.com/gin-gonic/gin" +) + +func PluginInit(group *gin.RouterGroup, Plugin ...plugin.Plugin) { + for i := range Plugin { + fmt.Println(Plugin[i].RouterPath(), "注册开始!") + PluginGroup := group.Group(Plugin[i].RouterPath()) + Plugin[i].Register(PluginGroup) + fmt.Println(Plugin[i].RouterPath(), "注册成功!") + } +} + +func bizPluginV1(group ...*gin.RouterGroup) { + private := group[0] + public := group[1] + // 添加跟角色挂钩权限的插件 示例 本地示例模式于在线仓库模式注意上方的import 可以自行切换 效果相同 + PluginInit(private, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, + )) + holder(public, private) +} diff --git a/server/initialize/plugin_biz_v2.go b/server/initialize/plugin_biz_v2.go new file mode 100644 index 0000000..57e6474 --- /dev/null +++ b/server/initialize/plugin_biz_v2.go @@ -0,0 +1,16 @@ +package initialize + +import ( + _ "git.echol.cn/loser/st/server/plugin" + "git.echol.cn/loser/st/server/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) { + for i := 0; i < len(plugins); i++ { + plugins[i].Register(group) + } +} +func bizPluginV2(engine *gin.Engine) { + PluginInitV2(engine, plugin.Registered()...) +} diff --git a/server/initialize/redis.go b/server/initialize/redis.go new file mode 100644 index 0000000..5e09d59 --- /dev/null +++ b/server/initialize/redis.go @@ -0,0 +1,59 @@ +package initialize + +import ( + "context" + + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +func initRedisClient(redisCfg config.Redis) (redis.UniversalClient, error) { + var client redis.UniversalClient + // 使用集群模式 + if redisCfg.UseCluster { + client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: redisCfg.ClusterAddrs, + Password: redisCfg.Password, + }) + } else { + // 使用单例模式 + client = redis.NewClient(&redis.Options{ + Addr: redisCfg.Addr, + Password: redisCfg.Password, + DB: redisCfg.DB, + }) + } + pong, err := client.Ping(context.Background()).Result() + if err != nil { + global.GVA_LOG.Error("redis connect ping failed, err:", zap.String("name", redisCfg.Name), zap.Error(err)) + return nil, err + } + + global.GVA_LOG.Info("redis connect ping response:", zap.String("name", redisCfg.Name), zap.String("pong", pong)) + return client, nil +} + +func Redis() { + redisClient, err := initRedisClient(global.GVA_CONFIG.Redis) + if err != nil { + panic(err) + } + global.GVA_REDIS = redisClient +} + +func RedisList() { + redisMap := make(map[string]redis.UniversalClient) + + for _, redisCfg := range global.GVA_CONFIG.RedisList { + client, err := initRedisClient(redisCfg) + if err != nil { + panic(err) + } + redisMap[redisCfg.Name] = client + } + + global.GVA_REDISList = redisMap +} diff --git a/server/initialize/register_init.go b/server/initialize/register_init.go new file mode 100644 index 0000000..0a945f8 --- /dev/null +++ b/server/initialize/register_init.go @@ -0,0 +1,10 @@ +package initialize + +import ( + _ "git.echol.cn/loser/st/server/source/example" + _ "git.echol.cn/loser/st/server/source/system" +) + +func init() { + // do nothing,only import source package so that inits can be registered +} diff --git a/server/initialize/reload.go b/server/initialize/reload.go new file mode 100644 index 0000000..4a461d3 --- /dev/null +++ b/server/initialize/reload.go @@ -0,0 +1,45 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/global" + "go.uber.org/zap" +) + +// Reload 优雅地重新加载系统配置 +func Reload() error { + global.GVA_LOG.Info("正在重新加载系统配置...") + + // 重新加载配置文件 + if err := global.GVA_VP.ReadInConfig(); err != nil { + global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err)) + return err + } + + // 重新初始化数据库连接 + if global.GVA_DB != nil { + db, _ := global.GVA_DB.DB() + err := db.Close() + if err != nil { + global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err)) + return err + } + } + + // 重新建立数据库连接 + global.GVA_DB = Gorm() + + // 重新初始化其他配置 + OtherInit() + DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + RegisterTables() + } + + // 重新初始化定时任务 + Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil +} diff --git a/server/initialize/router.go b/server/initialize/router.go new file mode 100644 index 0000000..8a6d4d0 --- /dev/null +++ b/server/initialize/router.go @@ -0,0 +1,168 @@ +package initialize + +import ( + "net/http" + "os" + + "git.echol.cn/loser/st/server/docs" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/middleware" + "git.echol.cn/loser/st/server/router" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +type justFilesFilesystem struct { + fs http.FileSystem +} + +func (fs justFilesFilesystem) Open(name string) (http.File, error) { + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + + stat, err := f.Stat() + if stat.IsDir() { + return nil, os.ErrPermission + } + + return f, nil +} + +// 初始化总路由 + +func Routers() *gin.Engine { + Router := gin.New() + + // 设置文件上传大小限制(10MB) + Router.MaxMultipartMemory = 10 << 20 // 10 MB + + // 使用自定义的 Recovery 中间件,记录 panic 并入库 + Router.Use(middleware.GinRecovery(true)) + if gin.Mode() == gin.DebugMode { + Router.Use(gin.Logger()) + } + + // 跨域配置(前台应用需要) + // 必须在静态文件路由之前注册,否则静态文件跨域会失败 + Router.Use(middleware.Cors()) + global.GVA_LOG.Info("use middleware cors") + + if !global.GVA_CONFIG.MCP.Separate { + + sseServer := McpRun() + + // 注册mcp服务 + Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { + sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) + }) + + Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { + sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) + }) + } + + systemRouter := router.RouterGroupApp.System + exampleRouter := router.RouterGroupApp.Example + appRouter := router.RouterGroupApp.App // 前台应用路由 + + // SillyTavern 核心脚本静态文件服务 + // 所有核心文件存储在 data/st-core-scripts/ 下,完全独立于 web-app/ 目录 + stCorePath := "data/st-core-scripts" + if _, err := os.Stat(stCorePath); err == nil { + Router.Static("/scripts", stCorePath+"/scripts") + Router.Static("/css", stCorePath+"/css") + Router.Static("/img", stCorePath+"/img") + Router.Static("/webfonts", stCorePath+"/webfonts") + Router.Static("/lib", stCorePath+"/lib") // SillyTavern 依赖的第三方库 + Router.Static("/locales", stCorePath+"/locales") // 国际化文件 + Router.StaticFile("/script.js", stCorePath+"/script.js") // SillyTavern 主入口 + Router.StaticFile("/lib.js", stCorePath+"/lib.js") // Webpack 编译后的 lib.js + global.GVA_LOG.Info("SillyTavern 核心脚本服务已启动: " + stCorePath) + } else { + global.GVA_LOG.Warn("SillyTavern 核心脚本目录不存在: " + stCorePath) + } + + // 管理后台前端静态文件(web) + // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 + // VUE_APP_BASE_API = / + // VUE_APP_BASE_PATH = http://localhost + // 然后执行打包命令 npm run build。在打开下面3行注释 + // Router.StaticFile("/favicon.ico", "./dist/favicon.ico") + // Router.Static("/assets", "./dist/assets") // dist里面的静态资源 + // Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面 + + Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件") + + docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix + Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + global.GVA_LOG.Info("register swagger handler") + // 方便统一添加路由组前缀 多服务器上线使用 + + PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + + PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + + { + // 健康监测 + PublicGroup.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, "ok") + }) + } + { + systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权 + systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关 + } + + { + systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 + systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 + systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 + systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 + systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由 + systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 + systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 + systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 + systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理 + systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史 + systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录 + systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理 + systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 + systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板 + systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志 + systemRouter.InitLoginLogRouter(PrivateGroup) // 登录日志 + systemRouter.InitApiTokenRouter(PrivateGroup) // apiToken签发 + systemRouter.InitSkillsRouter(PrivateGroup) // Skills 定义器 + exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 + exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 + exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 + + } + + // 前台应用路由(新增) + { + appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀 + appRouter.InitAuthRouter(appGroup) // 认证路由:/app/auth/* 和 /app/user/* + appRouter.InitCharacterRouter(appGroup) // 角色卡路由:/app/character/* + appRouter.InitConversationRouter(appGroup) // 对话路由:/app/conversation/* + appRouter.InitAIConfigRouter(appGroup) // AI配置路由:/app/ai-config/* + appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/* + appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/* + } + + //插件路由安装 + InstallPlugin(PrivateGroup, PublicGroup, Router) + + // 注册业务路由 + initBizRouter(PrivateGroup, PublicGroup) + + global.GVA_ROUTERS = Router.Routes() + + global.GVA_LOG.Info("router register success") + return Router +} diff --git a/server/initialize/router_biz.go b/server/initialize/router_biz.go new file mode 100644 index 0000000..2d83f48 --- /dev/null +++ b/server/initialize/router_biz.go @@ -0,0 +1,19 @@ +package initialize + +import ( + "git.echol.cn/loser/st/server/router" + "github.com/gin-gonic/gin" +) + +// 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。 +func holder(routers ...*gin.RouterGroup) { + _ = routers + _ = router.RouterGroupApp +} + +func initBizRouter(routers ...*gin.RouterGroup) { + privateGroup := routers[0] + publicGroup := routers[1] + + holder(publicGroup, privateGroup) +} diff --git a/server/initialize/timer.go b/server/initialize/timer.go new file mode 100644 index 0000000..e0f68f3 --- /dev/null +++ b/server/initialize/timer.go @@ -0,0 +1,38 @@ +package initialize + +import ( + "fmt" + + "git.echol.cn/loser/st/server/task" + + "github.com/robfig/cron/v3" + + "git.echol.cn/loser/st/server/global" +) + +func Timer() { + go func() { + var option []cron.Option + option = append(option, cron.WithSeconds()) + // 清理DB定时任务 + _, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() { + err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中 + if err != nil { + fmt.Println("timer error:", err) + } + }, "定时清理数据库【日志,黑名单】内容", option...) + if err != nil { + fmt.Println("add timer error:", err) + } + + // 其他定时任务定在这里 参考上方使用方法 + + //_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() { + // 具体执行内容... + // ...... + //}, option...) + //if err != nil { + // fmt.Println("add timer error:", err) + //} + }() +} diff --git a/server/initialize/validator.go b/server/initialize/validator.go new file mode 100644 index 0000000..7b927d2 --- /dev/null +++ b/server/initialize/validator.go @@ -0,0 +1,22 @@ +package initialize + +import "git.echol.cn/loser/st/server/utils" + +func init() { + _ = utils.RegisterRule("PageVerify", + utils.Rules{ + "Page": {utils.NotEmpty()}, + "PageSize": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("IdVerify", + utils.Rules{ + "Id": {utils.NotEmpty()}, + }, + ) + _ = utils.RegisterRule("AuthorityIdVerify", + utils.Rules{ + "AuthorityId": {utils.NotEmpty()}, + }, + ) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..dead586 --- /dev/null +++ b/server/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "git.echol.cn/loser/st/server/core" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/initialize" + _ "go.uber.org/automaxprocs" + "go.uber.org/zap" +) + +//go:generate go env -w GO111MODULE=on +//go:generate go env -w GOPROXY=https://goproxy.cn,direct +//go:generate go mod tidy +//go:generate go mod download + +// 这部分 @Tag 设置用于排序, 需要排序的接口请按照下面的格式添加 +// swag init 对 @Tag 只会从入口文件解析, 默认 main.go +// 也可通过 --generalInfo flag 指定其他文件 +// @Tag.Name Base +// @Tag.Name SysUser +// @Tag.Description 用户 + +// @title Gin-Vue-Admin Swagger API接口文档 +// @version v2.8.9 +// @description 使用gin+vue进行极速开发的全栈开发基础平台 +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name x-token +// @BasePath / +func main() { + // 初始化系统 + initializeSystem() + // 运行服务器 + core.RunServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { + global.GVA_VP = core.Viper() // 初始化Viper + initialize.OtherInit() + global.GVA_LOG = core.Zap() // 初始化zap日志库 + zap.ReplaceGlobals(global.GVA_LOG) + global.GVA_DB = initialize.Gorm() // gorm连接数据库 + initialize.Timer() + initialize.DBList() + initialize.SetupHandlers() // 注册全局函数 + if global.GVA_DB != nil { + initialize.RegisterTables() // 初始化表 + } +} diff --git a/server/mcp/api_creator.go b/server/mcp/api_creator.go new file mode 100644 index 0000000..1459092 --- /dev/null +++ b/server/mcp/api_creator.go @@ -0,0 +1,191 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&ApiCreator{}) +} + +// ApiCreateRequest API创建请求结构 +type ApiCreateRequest struct { + Path string `json:"path"` // API路径 + Description string `json:"description"` // API中文描述 + ApiGroup string `json:"apiGroup"` // API组 + Method string `json:"method"` // HTTP方法 +} + +// ApiCreateResponse API创建响应结构 +type ApiCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + ApiID uint `json:"apiId"` + Path string `json:"path"` + Method string `json:"method"` +} + +// ApiCreator API创建工具 +type ApiCreator struct{} + +// New 创建API创建工具 +func (a *ApiCreator) New() mcp.Tool { + return mcp.NewTool("create_api", + mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具 +- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`), + mcp.WithString("path", + mcp.Required(), + mcp.Description("API路径,如:/user/create"), + ), + mcp.WithString("description", + mcp.Required(), + mcp.Description("API中文描述,如:创建用户"), + ), + mcp.WithString("apiGroup", + mcp.Required(), + mcp.Description("API组名称,用于分类管理,如:用户管理"), + ), + mcp.WithString("method", + mcp.Description("HTTP方法"), + mcp.DefaultString("POST"), + ), + mcp.WithString("apis", + mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"), + ), + ) +} + +// Handle 处理API创建请求 +func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + var apis []ApiCreateRequest + + // 检查是否是批量创建 + if apisStr, ok := args["apis"].(string); ok && apisStr != "" { + if err := json.Unmarshal([]byte(apisStr), &apis); err != nil { + return nil, fmt.Errorf("apis 参数格式错误: %v", err) + } + } else { + // 单个API创建 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + description, ok := args["description"].(string) + if !ok || description == "" { + return nil, errors.New("description 参数是必需的") + } + + apiGroup, ok := args["apiGroup"].(string) + if !ok || apiGroup == "" { + return nil, errors.New("apiGroup 参数是必需的") + } + + method := "POST" + if val, ok := args["method"].(string); ok && val != "" { + method = val + } + + apis = append(apis, ApiCreateRequest{ + Path: path, + Description: description, + ApiGroup: apiGroup, + Method: method, + }) + } + + if len(apis) == 0 { + return nil, errors.New("没有要创建的API") + } + + // 创建API记录 + apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService + var responses []ApiCreateResponse + successCount := 0 + + for _, apiReq := range apis { + api := system.SysApi{ + Path: apiReq.Path, + Description: apiReq.Description, + ApiGroup: apiReq.ApiGroup, + Method: apiReq.Method, + } + + err := apiService.CreateApi(api) + if err != nil { + global.GVA_LOG.Warn("创建API失败", + zap.String("path", apiReq.Path), + zap.String("method", apiReq.Method), + zap.Error(err)) + + responses = append(responses, ApiCreateResponse{ + Success: false, + Message: fmt.Sprintf("创建API失败: %v", err), + Path: apiReq.Path, + Method: apiReq.Method, + }) + } else { + // 获取创建的API ID + var createdApi system.SysApi + err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err)) + } + + responses = append(responses, ApiCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path), + ApiID: createdApi.ID, + Path: apiReq.Path, + Method: apiReq.Method, + }) + successCount++ + } + } + + // 构建总体响应 + var resultMessage string + if len(apis) == 1 { + resultMessage = responses[0].Message + } else { + resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount) + } + + result := map[string]interface{}{ + "success": successCount > 0, + "message": resultMessage, + "totalCount": len(apis), + "successCount": successCount, + "failedCount": len(apis) - successCount, + "details": responses, + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("API创建结果:\n\n%s", string(resultJSON)), + }, + }, + }, nil +} diff --git a/server/mcp/api_lister.go b/server/mcp/api_lister.go new file mode 100644 index 0000000..c9ef696 --- /dev/null +++ b/server/mcp/api_lister.go @@ -0,0 +1,168 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&ApiLister{}) +} + +// ApiInfo API信息结构 +type ApiInfo struct { + ID uint `json:"id,omitempty"` // 数据库ID(仅数据库API有) + Path string `json:"path"` // API路径 + Description string `json:"description,omitempty"` // API描述 + ApiGroup string `json:"apiGroup,omitempty"` // API组 + Method string `json:"method"` // HTTP方法 + Source string `json:"source"` // 来源:database 或 gin +} + +// ApiListResponse API列表响应结构 +type ApiListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API + GinApis []ApiInfo `json:"ginApis"` // gin框架中的API + TotalCount int `json:"totalCount"` // 总数量 +} + +// ApiLister API列表工具 +type ApiLister struct{} + +// New 创建API列表工具 +func (a *ApiLister) New() mcp.Tool { + return mcp.NewTool("list_all_apis", + mcp.WithDescription(`获取系统中所有的API接口,分为两组: + +**功能说明:** +- 返回数据库中已注册的API列表 +- 返回gin框架中实际注册的路由API列表 +- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用 + +**返回数据结构:** +- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息) +- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`), + mcp.WithString("_placeholder", + mcp.Description("占位符,防止json schema校验失败"), + ), + ) +} + +// Handle 处理API列表请求 +func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + + // 获取数据库中的API + databaseApis, err := a.getDatabaseApis() + if err != nil { + global.GVA_LOG.Error("获取数据库API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取数据库API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 获取gin路由中的API + ginApis, err := a.getGinApis() + if err != nil { + global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取gin路由API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 构建响应 + response := ApiListResponse{ + Success: true, + Message: "获取API列表成功", + DatabaseApis: databaseApis, + GinApis: ginApis, + TotalCount: len(databaseApis) + len(ginApis), + } + + global.GVA_LOG.Info("API列表获取成功", + zap.Int("数据库API数量", len(databaseApis)), + zap.Int("gin路由API数量", len(ginApis)), + zap.Int("总数量", response.TotalCount)) + + resultJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil +} + +// getDatabaseApis 获取数据库中的所有API +func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) { + var apis []system.SysApi + err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error + if err != nil { + return nil, err + } + + // 转换为ApiInfo格式 + var result []ApiInfo + for _, api := range apis { + result = append(result, ApiInfo{ + ID: api.ID, + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + Source: "database", + }) + } + + return result, nil +} + +// getGinApis 获取gin路由中的所有API(包含被忽略的API) +func (a *ApiLister) getGinApis() ([]ApiInfo, error) { + // 从gin路由信息中获取所有API + var result []ApiInfo + for _, route := range global.GVA_ROUTERS { + result = append(result, ApiInfo{ + Path: route.Path, + Method: route.Method, + Source: "gin", + }) + } + + return result, nil +} diff --git a/server/mcp/client/client.go b/server/mcp/client/client.go new file mode 100644 index 0000000..3f1a385 --- /dev/null +++ b/server/mcp/client/client.go @@ -0,0 +1,40 @@ +package client + +import ( + "context" + "errors" + + mcpClient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) { + client, err := mcpClient.NewSSEMCPClient(baseUrl) + if err != nil { + return nil, err + } + + ctx := context.Background() + + // 启动client + if err := client.Start(ctx); err != nil { + return nil, err + } + + // 初始化 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: name, + Version: version, + } + + result, err := client.Initialize(ctx, initRequest) + if err != nil { + return nil, err + } + if result.ServerInfo.Name != serverName { + return nil, errors.New("server name mismatch") + } + return client, nil +} diff --git a/server/mcp/client/client_test.go b/server/mcp/client/client_test.go new file mode 100644 index 0000000..a0b2122 --- /dev/null +++ b/server/mcp/client/client_test.go @@ -0,0 +1,133 @@ +package client + +import ( + "context" + "fmt" + "testing" + + "github.com/mark3labs/mcp-go/mcp" +) + +// 测试 MCP 客户端连接 +func TestMcpClientConnection(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestTools(t *testing.T) { + t.Run("currentTime", func(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + request := mcp.CallToolRequest{} + request.Params.Name = "currentTime" + request.Params.Arguments = map[string]interface{}{ + "timezone": "UTC+8", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) + + t.Run("getNickname", func(t *testing.T) { + + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Initialize + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "test-client", + Version: "1.0.0", + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + t.Fatalf("初始化失败: %v", err) + } + + request := mcp.CallToolRequest{} + request.Params.Name = "getNickname" + request.Params.Arguments = map[string]interface{}{ + "username": "admin", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) +} + +func TestGetTools(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + toolsRequest := mcp.ListToolsRequest{} + + toolListResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + t.Fatalf("获取工具列表失败: %v", err) + } + for i := range toolListResult.Tools { + tool := toolListResult.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } +} diff --git a/server/mcp/dictionary_generator.go b/server/mcp/dictionary_generator.go new file mode 100644 index 0000000..821adc6 --- /dev/null +++ b/server/mcp/dictionary_generator.go @@ -0,0 +1,229 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func init() { + RegisterTool(&DictionaryOptionsGenerator{}) +} + +// DictionaryOptionsGenerator 字典选项生成器 +type DictionaryOptionsGenerator struct{} + +// DictionaryOption 字典选项结构 +type DictionaryOption struct { + Label string `json:"label"` + Value string `json:"value"` + Sort int `json:"sort"` +} + +// DictionaryGenerateRequest 字典生成请求 +type DictionaryGenerateRequest struct { + DictType string `json:"dictType"` // 字典类型 + FieldDesc string `json:"fieldDesc"` // 字段描述 + Options []DictionaryOption `json:"options"` // AI生成的字典选项 + DictName string `json:"dictName"` // 字典名称(可选) + Description string `json:"description"` // 字典描述(可选) +} + +// DictionaryGenerateResponse 字典生成响应 +type DictionaryGenerateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DictType string `json:"dictType"` + OptionsCount int `json:"optionsCount"` +} + +// New 返回工具注册信息 +func (d *DictionaryOptionsGenerator) New() mcp.Tool { + return mcp.NewTool("generate_dictionary_options", + mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"), + mcp.WithString("dictType", + mcp.Required(), + mcp.Description("字典类型,用于标识字典的唯一性"), + ), + mcp.WithString("fieldDesc", + mcp.Required(), + mcp.Description("字段描述,用于AI理解字段含义"), + ), + mcp.WithString("options", + mcp.Required(), + mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"), + ), + mcp.WithString("dictName", + mcp.Description("字典名称,如果不提供将自动生成"), + ), + mcp.WithString("description", + mcp.Description("字典描述"), + ), + ) +} + +// Handle 处理工具调用 +func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + args := request.GetArguments() + + dictType, ok := args["dictType"].(string) + if !ok || dictType == "" { + return nil, errors.New("dictType 参数是必需的") + } + + fieldDesc, ok := args["fieldDesc"].(string) + if !ok || fieldDesc == "" { + return nil, errors.New("fieldDesc 参数是必需的") + } + + optionsStr, ok := args["options"].(string) + if !ok || optionsStr == "" { + return nil, errors.New("options 参数是必需的") + } + + // 解析options JSON字符串 + var options []DictionaryOption + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return nil, fmt.Errorf("options 参数格式错误: %v", err) + } + + if len(options) == 0 { + return nil, errors.New("options 不能为空") + } + + dictName, _ := args["dictName"].(string) + description, _ := args["description"].(string) + + // 构建请求对象 + req := &DictionaryGenerateRequest{ + DictType: dictType, + FieldDesc: fieldDesc, + Options: options, + DictName: dictName, + Description: description, + } + + // 创建字典 + response, err := d.createDictionaryWithOptions(ctx, req) + if err != nil { + return nil, err + } + + // 构建响应 + resultJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("字典选项生成结果:\n\n%s", string(resultJSON)), + }, + }, + }, nil +} + +// createDictionaryWithOptions 创建字典和字典选项 +func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) { + // 检查字典是否已存在 + exists, err := d.checkDictionaryExists(req.DictType) + if err != nil { + return nil, fmt.Errorf("检查字典是否存在失败: %v", err) + } + + if exists { + return &DictionaryGenerateResponse{ + Success: false, + Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType), + DictType: req.DictType, + OptionsCount: 0, + }, nil + } + + // 生成字典名称 + dictName := req.DictName + if dictName == "" { + dictName = d.generateDictionaryName(req.DictType, req.FieldDesc) + } + + // 创建字典 + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + dictionary := system.SysDictionary{ + Name: dictName, + Type: req.DictType, + Status: &[]bool{true}[0], // 默认启用 + Desc: req.Description, + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + return nil, fmt.Errorf("创建字典失败: %v", err) + } + + // 获取刚创建的字典ID + var createdDict system.SysDictionary + err = global.GVA_DB.Where("type = ?", req.DictType).First(&createdDict).Error + if err != nil { + return nil, fmt.Errorf("获取创建的字典失败: %v", err) + } + + // 创建字典详情项 + dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + successCount := 0 + + for _, option := range req.Options { + dictionaryDetail := system.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: &[]bool{true}[0], // 默认启用 + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + } + + err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail) + if err != nil { + global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err)) + } else { + successCount++ + } + } + + return &DictionaryGenerateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount), + DictType: req.DictType, + OptionsCount: successCount, + }, nil +} + +// checkDictionaryExists 检查字典是否存在 +func (d *DictionaryOptionsGenerator) checkDictionaryExists(dictType string) (bool, error) { + var dictionary system.SysDictionary + err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil // 字典不存在 + } + return false, err // 其他错误 + } + return true, nil // 字典存在 +} + +// generateDictionaryName 生成字典名称 +func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string { + if fieldDesc != "" { + return fmt.Sprintf("%s字典", fieldDesc) + } + return fmt.Sprintf("%s字典", dictType) +} diff --git a/server/mcp/dictionary_query.go b/server/mcp/dictionary_query.go new file mode 100644 index 0000000..d5eb745 --- /dev/null +++ b/server/mcp/dictionary_query.go @@ -0,0 +1,239 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// 注册工具 +func init() { + RegisterTool(&DictionaryQuery{}) +} + +type DictionaryPre struct { + Type string `json:"type"` // 字典名(英) + Desc string `json:"desc"` // 描述 +} + +// DictionaryInfo 字典信息结构 +type DictionaryInfo struct { + ID uint `json:"id"` + Name string `json:"name"` // 字典名(中) + Type string `json:"type"` // 字典名(英) + Status *bool `json:"status"` // 状态 + Desc string `json:"desc"` // 描述 + Details []DictionaryDetailInfo `json:"details"` // 字典详情 +} + +// DictionaryDetailInfo 字典详情信息结构 +type DictionaryDetailInfo struct { + ID uint `json:"id"` + Label string `json:"label"` // 展示值 + Value string `json:"value"` // 字典值 + Extend string `json:"extend"` // 扩展值 + Status *bool `json:"status"` // 启用状态 + Sort int `json:"sort"` // 排序标记 +} + +// DictionaryQueryResponse 字典查询响应结构 +type DictionaryQueryResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Total int `json:"total"` + Dictionaries []DictionaryInfo `json:"dictionaries"` +} + +// DictionaryQuery 字典查询工具 +type DictionaryQuery struct{} + +// New 创建字典查询工具 +func (d *DictionaryQuery) New() mcp.Tool { + return mcp.NewTool("query_dictionaries", + mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"), + mcp.WithString("dictType", + mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"), + ), + mcp.WithBoolean("includeDisabled", + mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"), + ), + mcp.WithBoolean("detailsOnly", + mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"), + ), + ) +} + +// Handle 处理字典查询请求 +func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + // 获取参数 + dictType := "" + if val, ok := args["dictType"].(string); ok { + dictType = val + } + + includeDisabled := false + if val, ok := args["includeDisabled"].(bool); ok { + includeDisabled = val + } + + detailsOnly := false + if val, ok := args["detailsOnly"].(bool); ok { + detailsOnly = val + } + + // 获取字典服务 + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + + var dictionaries []DictionaryInfo + var err error + + if dictType != "" { + // 查询指定类型的字典 + var status *bool + if !includeDisabled { + status = &[]bool{true}[0] + } + + sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status) + if err != nil { + global.GVA_LOG.Error("查询字典失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + // 转换为响应格式 + dictInfo := DictionaryInfo{ + ID: sysDictionary.ID, + Name: sysDictionary.Name, + Type: sysDictionary.Type, + Status: sysDictionary.Status, + Desc: sysDictionary.Desc, + } + + // 获取字典详情 + for _, detail := range sysDictionary.SysDictionaryDetails { + if includeDisabled || (detail.Status != nil && *detail.Status) { + dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{ + ID: detail.ID, + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + }) + } + } + + dictionaries = append(dictionaries, dictInfo) + } else { + // 查询所有字典 + var sysDictionaries []system.SysDictionary + db := global.GVA_DB.Model(&system.SysDictionary{}) + + if !includeDisabled { + db = db.Where("status = ?", true) + } + + err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + if includeDisabled { + return db.Order("sort") + } else { + return db.Where("status = ?", true).Order("sort") + } + }).Find(&sysDictionaries).Error + + if err != nil { + global.GVA_LOG.Error("查询字典列表失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典列表失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + // 转换为响应格式 + for _, dict := range sysDictionaries { + dictInfo := DictionaryInfo{ + ID: dict.ID, + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + } + + // 获取字典详情 + for _, detail := range dict.SysDictionaryDetails { + if includeDisabled || (detail.Status != nil && *detail.Status) { + dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{ + ID: detail.ID, + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + }) + } + } + + dictionaries = append(dictionaries, dictInfo) + } + } + + // 如果只需要详情信息,则提取所有详情 + if detailsOnly { + var allDetails []DictionaryDetailInfo + for _, dict := range dictionaries { + allDetails = append(allDetails, dict.Details...) + } + + response := map[string]interface{}{ + "success": true, + "message": "查询字典详情成功", + "total": len(allDetails), + "details": allDetails, + } + + responseJSON, _ := json.Marshal(response) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil + } + + // 构建响应 + response := DictionaryQueryResponse{ + Success: true, + Message: "查询字典成功", + Total: len(dictionaries), + Dictionaries: dictionaries, + } + + responseJSON, err := json.Marshal(response) + if err != nil { + global.GVA_LOG.Error("序列化响应失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "序列化响应失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil +} diff --git a/server/mcp/enter.go b/server/mcp/enter.go new file mode 100644 index 0000000..7445525 --- /dev/null +++ b/server/mcp/enter.go @@ -0,0 +1,32 @@ +package mcpTool + +import ( + "context" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // New 返回工具注册信息 + New() mcp.Tool +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + mcpTool := tool.New() + toolRegister[mcpTool.Name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.MCPServer) { + for _, tool := range toolRegister { + mcpServer.AddTool(tool.New(), tool.Handle) + } +} diff --git a/server/mcp/gva_analyze.go b/server/mcp/gva_analyze.go new file mode 100644 index 0000000..e983a2f --- /dev/null +++ b/server/mcp/gva_analyze.go @@ -0,0 +1,503 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + model "git.echol.cn/loser/st/server/model/system" + + "git.echol.cn/loser/st/server/global" + "github.com/mark3labs/mcp-go/mcp" +) + +// 注册工具 +func init() { + RegisterTool(&GVAAnalyzer{}) +} + +// GVAAnalyzer GVA分析器 - 用于分析当前功能是否需要创建独立的package和module +type GVAAnalyzer struct{} + +// AnalyzeRequest 分析请求结构体 +type AnalyzeRequest struct { + Requirement string `json:"requirement" binding:"required"` // 用户需求描述 +} + +// AnalyzeResponse 分析响应结构体 +type AnalyzeResponse struct { + ExistingPackages []PackageInfo `json:"existingPackages"` // 现有包信息 + PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` // 预设计模块信息 + Dictionaries []DictionaryPre `json:"dictionaries"` // 字典信息 + CleanupInfo *CleanupInfo `json:"cleanupInfo"` // 清理信息(如果有) +} + +// ModuleInfo 模块信息 +type ModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + StructName string `json:"structName"` // 结构体名称 + TableName string `json:"tableName"` // 表名 + Description string `json:"description"` // 描述 + FilePaths []string `json:"filePaths"` // 相关文件路径 +} + +// PackageInfo 包信息 +type PackageInfo struct { + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + Label string `json:"label"` // 标签 + Desc string `json:"desc"` // 描述 + Module string `json:"module"` // 模块 + IsEmpty bool `json:"isEmpty"` // 是否为空包 +} + +// PredesignedModuleInfo 预设计模块信息 +type PredesignedModuleInfo struct { + ModuleName string `json:"moduleName"` // 模块名称 + PackageName string `json:"packageName"` // 包名 + Template string `json:"template"` // 模板类型 + FilePaths []string `json:"filePaths"` // 文件路径列表 + Description string `json:"description"` // 描述 +} + +// CleanupInfo 清理信息 +type CleanupInfo struct { + DeletedPackages []string `json:"deletedPackages"` // 已删除的包 + DeletedModules []string `json:"deletedModules"` // 已删除的模块 + CleanupMessage string `json:"cleanupMessage"` // 清理消息 +} + +// New 创建GVA分析器工具 +func (g *GVAAnalyzer) New() mcp.Tool { + return mcp.NewTool("gva_analyze", + mcp.WithDescription("返回当前系统中有效的包和模块信息,并分析用户需求是否需要创建新的包、模块和字典。同时检查并清理空包,确保系统整洁。"), + mcp.WithString("requirement", + mcp.Description("用户需求描述,用于分析是否需要创建新的包和模块"), + mcp.Required(), + ), + ) +} + +// Handle 处理分析请求 +func (g *GVAAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + requirementStr, ok := request.GetArguments()["requirement"].(string) + if !ok || requirementStr == "" { + return nil, errors.New("参数错误:requirement 必须是非空字符串") + } + + // 创建分析请求 + analyzeReq := AnalyzeRequest{ + Requirement: requirementStr, + } + + // 执行分析逻辑 + response, err := g.performAnalysis(ctx, analyzeReq) + if err != nil { + return nil, fmt.Errorf("分析失败: %v", err) + } + + // 序列化响应 + responseJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil +} + +// performAnalysis 执行分析逻辑 +func (g *GVAAnalyzer) performAnalysis(ctx context.Context, req AnalyzeRequest) (*AnalyzeResponse, error) { + // 1. 获取数据库中的包信息 + var packages []model.SysAutoCodePackage + if err := global.GVA_DB.Find(&packages).Error; err != nil { + return nil, fmt.Errorf("获取包信息失败: %v", err) + } + + // 2. 获取历史记录 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Find(&histories).Error; err != nil { + return nil, fmt.Errorf("获取历史记录失败: %v", err) + } + + // 3. 检查空包并进行清理 + cleanupInfo := &CleanupInfo{ + DeletedPackages: []string{}, + DeletedModules: []string{}, + } + + var validPackages []model.SysAutoCodePackage + var emptyPackageHistoryIDs []uint + + for _, pkg := range packages { + isEmpty, err := g.isPackageFolderEmpty(pkg.PackageName, pkg.Template) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 是否为空时出错: %v", pkg.PackageName, err)) + continue + } + + if isEmpty { + // 删除空包文件夹 + if err := g.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) + } else { + cleanupInfo.DeletedPackages = append(cleanupInfo.DeletedPackages, pkg.PackageName) + } + + // 删除数据库记录 + if err := global.GVA_DB.Delete(&pkg).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除包数据库记录 %s 失败: %v", pkg.PackageName, err)) + } + + // 收集相关的历史记录ID + for _, history := range histories { + if history.Package == pkg.PackageName { + emptyPackageHistoryIDs = append(emptyPackageHistoryIDs, history.ID) + cleanupInfo.DeletedModules = append(cleanupInfo.DeletedModules, history.StructName) + } + } + } else { + validPackages = append(validPackages, pkg) + } + } + + // 5. 清理空包相关的历史记录和脏历史记录 + var dirtyHistoryIDs []uint + for _, history := range histories { + // 检查是否为空包相关的历史记录 + for _, emptyID := range emptyPackageHistoryIDs { + if history.ID == emptyID { + dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) + break + } + } + } + + // 删除脏历史记录 + if len(dirtyHistoryIDs) > 0 { + if err := global.GVA_DB.Delete(&model.SysAutoCodeHistory{}, "id IN ?", dirtyHistoryIDs).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 条脏历史记录", len(dirtyHistoryIDs))) + } + + // 清理相关的API和菜单记录 + if err := g.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理相关API和菜单记录失败: %v", err)) + } + } + + // 6. 扫描预设计模块 + predesignedModules, err := g.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描预设计模块失败: %v", err)) + predesignedModules = []PredesignedModuleInfo{} // 设置为空列表,不影响主流程 + } + + // 7. 过滤掉与已删除包相关的模块 + filteredModules := []PredesignedModuleInfo{} + for _, module := range predesignedModules { + isDeleted := false + for _, deletedPkg := range cleanupInfo.DeletedPackages { + if module.PackageName == deletedPkg { + isDeleted = true + break + } + } + if !isDeleted { + filteredModules = append(filteredModules, module) + } + } + + // 8. 构建分析结果消息 + var analysisMessage strings.Builder + if len(cleanupInfo.DeletedPackages) > 0 || len(cleanupInfo.DeletedModules) > 0 { + analysisMessage.WriteString("**系统清理完成**\n\n") + if len(cleanupInfo.DeletedPackages) > 0 { + analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个空包: %s\n", len(cleanupInfo.DeletedPackages), strings.Join(cleanupInfo.DeletedPackages, ", "))) + } + if len(cleanupInfo.DeletedModules) > 0 { + analysisMessage.WriteString(fmt.Sprintf("- 删除了 %d 个相关模块: %s\n", len(cleanupInfo.DeletedModules), strings.Join(cleanupInfo.DeletedModules, ", "))) + } + analysisMessage.WriteString("\n") + cleanupInfo.CleanupMessage = analysisMessage.String() + } + + analysisMessage.WriteString(" **分析结果**\n\n") + analysisMessage.WriteString(fmt.Sprintf("- **现有包数量**: %d\n", len(validPackages))) + analysisMessage.WriteString(fmt.Sprintf("- **预设计模块数量**: %d\n\n", len(filteredModules))) + + // 9. 转换包信息 + existingPackages := make([]PackageInfo, len(validPackages)) + for i, pkg := range validPackages { + existingPackages[i] = PackageInfo{ + PackageName: pkg.PackageName, + Template: pkg.Template, + Label: pkg.Label, + Desc: pkg.Desc, + Module: pkg.Module, + IsEmpty: false, // 已经过滤掉空包 + } + } + + dictionaries := []DictionaryPre{} // 这里可以根据需要填充字典信息 + err = global.GVA_DB.Table("sys_dictionaries").Find(&dictionaries, "deleted_at is null").Error + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("获取字典信息失败: %v", err)) + dictionaries = []DictionaryPre{} // 设置为空列表,不影响主流程 + } + + // 10. 构建响应 + response := &AnalyzeResponse{ + ExistingPackages: existingPackages, + PredesignedModules: filteredModules, + Dictionaries: dictionaries, + } + + return response, nil +} + +// isPackageFolderEmpty 检查包文件夹是否为空 +func (g *GVAAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { + // 根据模板类型确定基础路径 + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName) + } + + // 检查文件夹是否存在 + if _, err := os.Stat(basePath); os.IsNotExist(err) { + return true, nil // 文件夹不存在,视为空 + } else if err != nil { + return false, err // 其他错误 + } + // 递归检查是否有.go文件 + return g.hasGoFilesRecursive(basePath) +} + +// hasGoFilesRecursive 递归检查目录及其子目录中是否有.go文件 +func (g *GVAAnalyzer) hasGoFilesRecursive(dirPath string) (bool, error) { + entries, err := os.ReadDir(dirPath) + if err != nil { + return true, err // 读取失败,返回空 + } + + // 检查当前目录下的.go文件 + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + return false, nil // 找到.go文件,不为空 + } + } + + // 递归检查子目录 + for _, entry := range entries { + if entry.IsDir() { + subDirPath := filepath.Join(dirPath, entry.Name()) + isEmpty, err := g.hasGoFilesRecursive(subDirPath) + if err != nil { + continue // 忽略子目录的错误,继续检查其他目录 + } + if !isEmpty { + return false, nil // 子目录中找到.go文件,不为空 + } + } + } + + return true, nil // 没有找到.go文件,为空 +} + +// removeEmptyPackageFolder 删除空包文件夹 +func (g *GVAAnalyzer) removeEmptyPackageFolder(packageName, template string) error { + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + // 对于package类型,需要删除多个目录 + paths := []string{ + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), + } + for _, path := range paths { + if err := g.removeDirectoryIfExists(path); err != nil { + return err + } + } + return nil + } + + return g.removeDirectoryIfExists(basePath) +} + +// removeDirectoryIfExists 删除目录(如果存在) +func (g *GVAAnalyzer) removeDirectoryIfExists(dirPath string) error { + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + return nil // 目录不存在,无需删除 + } else if err != nil { + return err // 其他错误 + } + + // 检查目录中是否包含go文件 + noGoFiles, err := g.hasGoFilesRecursive(dirPath) + if err != nil { + return err + } + // hasGoFilesRecursive 返回 false 表示发现了 go 文件 + if noGoFiles { + return os.RemoveAll(dirPath) + } + return nil +} + +// cleanupRelatedApiAndMenus 清理相关的API和菜单记录 +func (g *GVAAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { + if len(historyIDs) == 0 { + return nil + } + + // 这里可以根据需要实现具体的API和菜单清理逻辑 + // 由于涉及到具体的业务逻辑,这里只做日志记录 + global.GVA_LOG.Info(fmt.Sprintf("清理历史记录ID %v 相关的API和菜单记录", historyIDs)) + + // 可以调用service层的相关方法进行清理 + // 例如:service.ServiceGroupApp.SystemApiService.DeleteApisByIds(historyIDs) + // 例如:service.ServiceGroupApp.MenuService.DeleteMenusByIds(historyIDs) + + return nil +} + +// scanPredesignedModules 扫描预设计模块 +func (g *GVAAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { + // 获取autocode配置路径 + autocodeRoot := global.GVA_CONFIG.AutoCode.Root + if autocodeRoot == "" { + return nil, errors.New("autocode根路径未配置") + } + + var modules []PredesignedModuleInfo + + // 扫描plugin目录 + pluginModules, err := g.scanPluginModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "plugin")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描plugin模块失败: %v", err)) + } else { + modules = append(modules, pluginModules...) + } + + // 扫描model目录 + modelModules, err := g.scanModelModules(filepath.Join(autocodeRoot, global.GVA_CONFIG.AutoCode.Server, "model")) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描model模块失败: %v", err)) + } else { + modules = append(modules, modelModules...) + } + + return modules, nil +} + +// scanPluginModules 扫描插件模块 +func (g *GVAAnalyzer) scanPluginModules(pluginDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(pluginDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(pluginDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + pluginName := entry.Name() + pluginPath := filepath.Join(pluginDir, pluginName) + + // 查找model目录 + modelDir := filepath.Join(pluginPath, "model") + if _, err := os.Stat(modelDir); err == nil { + // 扫描model目录下的模块 + pluginModules, err := g.scanModulesInDirectory(modelDir, pluginName, "plugin") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描插件 %s 的模块失败: %v", pluginName, err)) + continue + } + modules = append(modules, pluginModules...) + } + } + } + + return modules, nil +} + +// scanModelModules 扫描模型模块 +func (g *GVAAnalyzer) scanModelModules(modelDir string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(modelDir); os.IsNotExist(err) { + return modules, nil // 目录不存在,返回空列表 + } + + entries, err := os.ReadDir(modelDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + packageName := entry.Name() + packagePath := filepath.Join(modelDir, packageName) + + // 扫描包目录下的模块 + packageModules, err := g.scanModulesInDirectory(packagePath, packageName, "package") + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("扫描包 %s 的模块失败: %v", packageName, err)) + continue + } + modules = append(modules, packageModules...) + } + } + + return modules, nil +} + +// scanModulesInDirectory 扫描目录中的模块 +func (g *GVAAnalyzer) scanModulesInDirectory(dir, packageName, template string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + moduleName := strings.TrimSuffix(entry.Name(), ".go") + filePath := filepath.Join(dir, entry.Name()) + + module := PredesignedModuleInfo{ + ModuleName: moduleName, + PackageName: packageName, + Template: template, + FilePaths: []string{filePath}, + Description: fmt.Sprintf("%s模块中的%s", packageName, moduleName), + } + modules = append(modules, module) + } + } + + return modules, nil +} diff --git a/server/mcp/gva_execute.go b/server/mcp/gva_execute.go new file mode 100644 index 0000000..fbbeecf --- /dev/null +++ b/server/mcp/gva_execute.go @@ -0,0 +1,793 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + model "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/utils" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + + "git.echol.cn/loser/st/server/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&GVAExecutor{}) +} + +// GVAExecutor GVA代码生成器 +type GVAExecutor struct{} + +// ExecuteRequest 执行请求结构 +type ExecuteRequest struct { + ExecutionPlan ExecutionPlan `json:"executionPlan"` // 执行计划 + Requirement string `json:"requirement"` // 原始需求(可选,用于日志记录) +} + +// ExecuteResponse 执行响应结构 +type ExecuteResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + PackageID uint `json:"packageId,omitempty"` + HistoryID uint `json:"historyId,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + GeneratedPaths []string `json:"generatedPaths,omitempty"` + NextActions []string `json:"nextActions,omitempty"` +} + +// ExecutionPlan 执行计划结构 +type ExecutionPlan struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + NeedCreatedDictionaries bool `json:"needCreatedDictionaries"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + DictionariesInfo []*DictionaryGenerateRequest `json:"dictionariesInfo,omitempty"` +} + +// New 创建GVA代码生成执行器工具 +func (g *GVAExecutor) New() mcp.Tool { + return mcp.NewTool("gva_execute", + mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤** + +**核心功能:** +根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。 + +**使用场景:** +在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。 + +**重要提示:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具 +- 字段使用字典类型时,系统会自动检查并创建字典 +- 字典创建会在模块创建之前执行 +- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`), + mcp.WithObject("executionPlan", + mcp.Description("执行计划,包含包信息、模块与字典信息"), + mcp.Required(), + mcp.Properties(map[string]interface{}{ + "packageName": map[string]interface{}{ + "type": "string", + "description": "包名(小写开头)", + }, + "packageType": map[string]interface{}{ + "type": "string", + "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", + "enum": []string{"package", "plugin"}, + }, + "needCreatedPackage": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建包,为true时packageInfo必需", + }, + "needCreatedModules": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建模块,为true时modulesInfo必需", + }, + "needCreatedDictionaries": map[string]interface{}{ + "type": "boolean", + "description": "是否需要创建字典,为true时dictionariesInfo必需", + }, + "packageInfo": map[string]interface{}{ + "type": "object", + "description": "包创建信息,当needCreatedPackage=true时必需", + "properties": map[string]interface{}{ + "desc": map[string]interface{}{"type": "string", "description": "包描述"}, + "label": map[string]interface{}{"type": "string", "description": "展示名"}, + "template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}}, + "packageName": map[string]interface{}{"type": "string", "description": "包名"}, + }, + }, + "modulesInfo": map[string]interface{}{ + "type": "array", + "description": "模块配置列表,支持批量创建多个模块", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "package": map[string]interface{}{"type": "string", "description": "包名(小写开头,示例: userInfo)"}, + "tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法,示例:user_info)"}, + "businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"}, + "structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰示例:UserInfo)"}, + "packageName": map[string]interface{}{"type": "string", "description": "文件名称"}, + "description": map[string]interface{}{"type": "string", "description": "中文描述"}, + "abbreviation": map[string]interface{}{"type": "string", "description": "简称"}, + "humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰示例:userInfo"}, + "gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"}, + "autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"}, + "autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"}, + "autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"}, + "autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"}, + "autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"}, + "onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"}, + "isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"}, + "treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"}, + "isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"}, + "generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"}, + "generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"}, + "fields": map[string]interface{}{ + "type": "array", + "description": "字段列表", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头示例:UserName)"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"}, + "fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"}, + "fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签,示例: userName"}, + "dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"}, + "comment": map[string]interface{}{"type": "string", "description": "注释"}, + "columnName": map[string]interface{}{"type": "string", "description": "数据库列名,示例: user_name"}, + "fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"}, + "fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"}, + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"}, + "form": map[string]interface{}{"type": "boolean", "description": "表单显示"}, + "table": map[string]interface{}{"type": "boolean", "description": "表格显示"}, + "desc": map[string]interface{}{"type": "boolean", "description": "详情显示"}, + "excel": map[string]interface{}{"type": "boolean", "description": "导入导出"}, + "require": map[string]interface{}{"type": "boolean", "description": "是否必填"}, + "defaultValue": map[string]interface{}{"type": "string", "description": "默认值"}, + "errorText": map[string]interface{}{"type": "string", "description": "错误提示"}, + "clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"}, + "sort": map[string]interface{}{"type": "boolean", "description": "是否排序"}, + "primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"}, + "dataSource": map[string]interface{}{ + "type": "object", + "description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空", + "properties": map[string]interface{}{ + "dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"}, + "table": map[string]interface{}{"type": "string", "description": "关联的表名"}, + "label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"}, + "value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"}, + "association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"}, + "hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"}, + }, + }, + "checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"}, + "fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"}, + }, + }, + }, + }, + }, + }, + "paths": map[string]interface{}{ + "type": "object", + "description": "生成的文件路径映射", + "additionalProperties": map[string]interface{}{"type": "string"}, + }, + "dictionariesInfo": map[string]interface{}{ + "type": "array", + "description": "字典创建信息,字典创建会在模块创建之前执行", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"}, + "dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"}, + "description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"}, + "status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"}, + "options": map[string]interface{}{ + "type": "array", + "description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"}, + "value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"}, + "sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"}, + }, + }, + }, + }, + }, + }, + }), + mcp.AdditionalProperties(false), + ), + mcp.WithString("requirement", + mcp.Description("原始需求描述(可选,用于日志记录)"), + ), + ) +} + +// Handle 处理执行请求(移除确认步骤) +func (g *GVAExecutor) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := g.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 获取原始需求(可选) + var originalRequirement string + if reqData, ok := request.GetArguments()["requirement"]; ok { + if reqStr, ok := reqData.(string); ok { + originalRequirement = reqStr + } + } + + // 直接执行创建操作(无确认步骤) + result := g.executeCreation(ctx, &plan) + + // 如果执行成功且有原始需求,提供代码复检建议 + var reviewMessage string + if result.Success && originalRequirement != "" { + global.GVA_LOG.Info("执行完成,返回生成的文件路径供AI进行代码复检...") + + // 构建文件路径信息供AI使用 + var pathsInfo []string + for _, path := range result.GeneratedPaths { + pathsInfo = append(pathsInfo, fmt.Sprintf("- %s", path)) + } + + reviewMessage = fmt.Sprintf("\n\n📁 已生成以下文件:\n%s\n\n💡 提示:可以检查生成的代码是否满足原始需求。", strings.Join(pathsInfo, "\n")) + } else if originalRequirement == "" { + reviewMessage = "\n\n💡 提示:如需代码复检,请提供原始需求描述。" + } + + // 序列化响应 + response := ExecuteResponse{ + Success: result.Success, + Message: result.Message, + PackageID: result.PackageID, + HistoryID: result.HistoryID, + Paths: result.Paths, + GeneratedPaths: result.GeneratedPaths, + NextActions: result.NextActions, + } + + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("执行结果:\n\n%s%s", string(responseJSON), reviewMessage)), + }, + }, nil +} + +// validateExecutionPlan 验证执行计划的完整性 +func (g *GVAExecutor) validateExecutionPlan(plan *ExecutionPlan) error { + // 验证基本字段 + if plan.PackageName == "" { + return errors.New("packageName 不能为空") + } + if plan.PackageType != "package" && plan.PackageType != "plugin" { + return errors.New("packageType 必须是 'package' 或 'plugin'") + } + + // 验证packageType和template字段的一致性 + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + if plan.PackageType != plan.PackageInfo.Template { + return errors.New("packageType 和 packageInfo.template 必须保持一致") + } + } + + // 验证包信息 + if plan.NeedCreatedPackage { + if plan.PackageInfo == nil { + return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") + } + if plan.PackageInfo.PackageName == "" { + return errors.New("packageInfo.packageName 不能为空") + } + if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { + return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") + } + if plan.PackageInfo.Label == "" { + return errors.New("packageInfo.label 不能为空") + } + if plan.PackageInfo.Desc == "" { + return errors.New("packageInfo.desc 不能为空") + } + } + + // 验证模块信息(批量验证) + if plan.NeedCreatedModules { + if len(plan.ModulesInfo) == 0 { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + + // 遍历验证每个模块 + for moduleIndex, moduleInfo := range plan.ModulesInfo { + if moduleInfo.Package == "" { + return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) + } + if moduleInfo.StructName == "" { + return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) + } + if moduleInfo.TableName == "" { + return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) + } + if moduleInfo.Description == "" { + return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) + } + if moduleInfo.Abbreviation == "" { + return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) + } + if moduleInfo.PackageName == "" { + return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) + } + if moduleInfo.HumpPackageName == "" { + return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) + } + + // 验证字段信息 + if len(moduleInfo.Fields) == 0 { + return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) + } + + for i, field := range moduleInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) + } + + // 确保字段名首字母大写 + if len(field.FieldName) > 0 { + firstChar := string(field.FieldName[0]) + if firstChar >= "a" && firstChar <= "z" { + moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] + } + } + if field.FieldDesc == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) + } + if field.FieldType == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) + } + + // 验证字段类型 + validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} + validType := false + for _, validFieldType := range validFieldTypes { + if field.FieldType == validFieldType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes) + } + + // 验证搜索类型(如果设置了) + if field.FieldSearchType != "" { + validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} + validSearchType := false + for _, validType := range validSearchTypes { + if field.FieldSearchType == validType { + validSearchType = true + break + } + } + if !validSearchType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes) + } + } + + // 验证 dataSource 字段配置 + if field.DataSource != nil { + associationValue := field.DataSource.Association + // 当 association 为 2(一对多关联)时,强制修改 fieldType 为 array + if associationValue == 2 { + if field.FieldType != "array" { + global.GVA_LOG.Info(fmt.Sprintf("模块 %d 字段 %d:检测到一对多关联(association=2),自动将 fieldType 从 '%s' 修改为 'array'", moduleIndex+1, i+1, field.FieldType)) + moduleInfo.Fields[i].FieldType = "array" + } + } + + // 验证 association 值的有效性 + if associationValue != 1 && associationValue != 2 { + return fmt.Errorf("模块 %d 字段 %d 的 dataSource.association 必须是 1(一对一)或 2(一对多)", moduleIndex+1, i+1) + } + } + } + + // 验证主键设置 + if !moduleInfo.GvaModel { + // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 + primaryKeyCount := 0 + for _, field := range moduleInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) + } + if primaryKeyCount > 1 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) + } + } else { + // 当使用GVA模型时,所有字段的primaryKey都应该为false + for i, field := range moduleInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1) + } + } + } + } + } + + return nil +} + +// executeCreation 执行创建操作 +func (g *GVAExecutor) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecuteResponse { + result := &ExecuteResponse{ + Success: false, + Paths: make(map[string]string), + GeneratedPaths: []string{}, // 初始化生成文件路径列表 + } + + // 无论如何都先构建目录结构信息,确保paths始终返回 + result.Paths = g.buildDirectoryStructure(plan) + + // 记录预期生成的文件路径 + result.GeneratedPaths = g.collectExpectedFilePaths(plan) + + if !plan.NeedCreatedModules { + result.Success = true + result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " + return result + } + + // 创建包(如果需要) + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + err := packageService.Create(ctx, plan.PackageInfo) + if err != nil { + result.Message = fmt.Sprintf("创建包失败: %v", err) + // 即使创建包失败,也要返回paths信息 + return result + } + result.Message += "包创建成功; " + } + + // 创建指定字典(如果需要) + if plan.NeedCreatedDictionaries && len(plan.DictionariesInfo) > 0 { + dictResult := g.createDictionariesFromInfo(ctx, plan.DictionariesInfo) + result.Message += dictResult + } + + // 批量创建字典和模块(如果需要) + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + + // 遍历所有模块进行创建 + for _, moduleInfo := range plan.ModulesInfo { + + // 创建模块 + err := moduleInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + + err = templateService.Create(ctx, *moduleInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) + } + + result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) + + // 添加重要提醒:不要使用其他MCP工具 + result.Message += "\n\n⚠️ 重要提醒:\n" + result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" + result.Message += "- api_creator:API权限已在模块创建时自动生成\n" + result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" + result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// buildDirectoryStructure 构建目录结构信息 +func (g *GVAExecutor) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { + paths := make(map[string]string) + + // 获取配置信息 + autoCodeConfig := global.GVA_CONFIG.AutoCode + + // 构建基础路径 + rootPath := autoCodeConfig.Root + serverPath := autoCodeConfig.Server + webPath := autoCodeConfig.Web + moduleName := autoCodeConfig.Module + + // 如果计划中有包名,使用计划中的包名,否则使用默认 + packageName := "example" + if plan.PackageName != "" { + packageName = plan.PackageName + } + + // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 + structName := "ExampleStruct" + if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { + structName = plan.ModulesInfo[0].StructName + } + + // 根据包类型构建不同的路径结构 + packageType := plan.PackageType + if packageType == "" { + packageType = "package" // 默认为package模式 + } + + // 构建服务端路径 + if serverPath != "" { + serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) + + if packageType == "plugin" { + // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下 + plugingBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) + + // API 路径 + paths["api"] = fmt.Sprintf("%s/api", plugingBasePath) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service", plugingBasePath) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model", plugingBasePath) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router", plugingBasePath) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/request", plugingBasePath) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/response", plugingBasePath) + + // Plugin 特有文件 + paths["plugin_main"] = fmt.Sprintf("%s/main.go", plugingBasePath) + paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", plugingBasePath) + paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", plugingBasePath) + } else { + // Package 模式:传统的目录结构 + // API 路径 + paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) + } + } + + // 构建前端路径(两种模式相同) + if webPath != "" { + webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) + + if packageType == "plugin" { + // Plugin 模式:前端文件也在 /plugin/packageName/ 目录下 + pluginWebBasePath := fmt.Sprintf("%s/plugin/%s", webBasePath, packageName) + + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view", pluginWebBasePath) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api", pluginWebBasePath) + } else { + // Package 模式:传统的目录结构 + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) + } + } + + // 添加模块信息 + paths["module"] = moduleName + paths["package_name"] = packageName + paths["package_type"] = packageType + paths["struct_name"] = structName + paths["root_path"] = rootPath + paths["server_path"] = serverPath + paths["web_path"] = webPath + + return paths +} + +// collectExpectedFilePaths 收集预期生成的文件路径 +func (g *GVAExecutor) collectExpectedFilePaths(plan *ExecutionPlan) []string { + var paths []string + + // 获取目录结构 + dirPaths := g.buildDirectoryStructure(plan) + + // 如果需要创建模块,添加预期的文件路径 + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + for _, moduleInfo := range plan.ModulesInfo { + structName := moduleInfo.StructName + + // 后端文件 + if apiPath, ok := dirPaths["api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", apiPath, strings.ToLower(structName))) + } + if servicePath, ok := dirPaths["service"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", servicePath, strings.ToLower(structName))) + } + if modelPath, ok := dirPaths["model"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", modelPath, strings.ToLower(structName))) + } + if routerPath, ok := dirPaths["router"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", routerPath, strings.ToLower(structName))) + } + if requestPath, ok := dirPaths["request"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", requestPath, strings.ToLower(structName))) + } + if responsePath, ok := dirPaths["response"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.go", responsePath, strings.ToLower(structName))) + } + + // 前端文件 + if vuePage, ok := dirPaths["vue_page"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.vue", vuePage, strings.ToLower(structName))) + } + if vueApi, ok := dirPaths["vue_api"]; ok { + paths = append(paths, fmt.Sprintf("%s/%s.js", vueApi, strings.ToLower(structName))) + } + } + } + + return paths +} + +// checkDictionaryExists 检查字典是否存在 +func (g *GVAExecutor) checkDictionaryExists(dictType string) (bool, error) { + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + _, err := dictionaryService.GetSysDictionary(dictType, 0, nil) + if err != nil { + // 如果是记录不存在的错误,返回false + if strings.Contains(err.Error(), "record not found") { + return false, nil + } + // 其他错误返回错误信息 + return false, err + } + return true, nil +} + +// createDictionariesFromInfo 根据 DictionariesInfo 创建字典 +func (g *GVAExecutor) createDictionariesFromInfo(ctx context.Context, dictionariesInfo []*DictionaryGenerateRequest) string { + var messages []string + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + + messages = append(messages, fmt.Sprintf("开始创建 %d 个指定字典: ", len(dictionariesInfo))) + + for _, dictInfo := range dictionariesInfo { + // 检查字典是否存在 + exists, err := g.checkDictionaryExists(dictInfo.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", dictInfo.DictType, err)) + continue + } + + if !exists { + // 字典不存在,创建字典 + dictionary := model.SysDictionary{ + Name: dictInfo.DictName, + Type: dictInfo.DictType, + Status: utils.Pointer(true), + Desc: dictInfo.Description, + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", dictInfo.DictType, err)) + continue + } + + messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", dictInfo.DictType, dictInfo.DictName)) + + // 获取刚创建的字典ID + var createdDict model.SysDictionary + err = global.GVA_DB.Where("type = ?", dictInfo.DictType).First(&createdDict).Error + if err != nil { + messages = append(messages, fmt.Sprintf("获取创建的字典失败: %v; ", err)) + continue + } + + // 创建字典选项 + if len(dictInfo.Options) > 0 { + successCount := 0 + for _, option := range dictInfo.Options { + dictionaryDetail := model.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: &[]bool{true}[0], // 默认启用 + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + } + + err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail) + if err != nil { + global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err)) + } else { + successCount++ + } + } + messages = append(messages, fmt.Sprintf("创建了 %d 个字典选项; ", successCount)) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", dictInfo.DictType)) + } + } + + return strings.Join(messages, "") +} diff --git a/server/mcp/gva_review.go b/server/mcp/gva_review.go new file mode 100644 index 0000000..a32a544 --- /dev/null +++ b/server/mcp/gva_review.go @@ -0,0 +1,170 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/mark3labs/mcp-go/mcp" +) + +// GVAReviewer GVA代码审查工具 +type GVAReviewer struct{} + +// init 注册工具 +func init() { + RegisterTool(&GVAReviewer{}) +} + +// ReviewRequest 审查请求结构 +type ReviewRequest struct { + UserRequirement string `json:"userRequirement"` // 经过requirement_analyze后的用户需求 + GeneratedFiles []string `json:"generatedFiles"` // gva_execute创建的文件列表 +} + +// ReviewResponse 审查响应结构 +type ReviewResponse struct { + Success bool `json:"success"` // 是否审查成功 + Message string `json:"message"` // 审查结果消息 + AdjustmentPrompt string `json:"adjustmentPrompt"` // 调整代码的提示 + ReviewDetails string `json:"reviewDetails"` // 详细的审查结果 +} + +// New 创建GVA代码审查工具 +func (g *GVAReviewer) New() mcp.Tool { + return mcp.NewTool("gva_review", + mcp.WithDescription(`**GVA代码审查工具 - 在gva_execute调用后使用** + +**核心功能:** +- 接收经过requirement_analyze处理的用户需求和gva_execute生成的文件列表 +- 分析生成的代码是否满足用户的原始需求 +- 检查是否涉及到关联、交互等复杂功能 +- 如果代码不满足需求,提供调整建议和新的prompt + +**使用场景:** +- 在gva_execute成功执行后调用 +- 用于验证生成的代码是否完整满足用户需求 +- 检查模块间的关联关系是否正确实现 +- 发现缺失的交互功能或业务逻辑 + +**工作流程:** +1. 接收用户原始需求和生成的文件列表 +2. 分析需求中的关键功能点 +3. 检查生成的文件是否覆盖所有功能 +4. 识别缺失的关联关系、交互功能等 +5. 生成调整建议和新的开发prompt + +**输出内容:** +- 审查结果和是否需要调整 +- 详细的缺失功能分析 +- 针对性的代码调整建议 +- 可直接使用的开发prompt + +**重要提示:** +- 本工具专门用于代码质量审查,不执行实际的代码修改 +- 重点关注模块间关联、用户交互、业务流程完整性 +- 提供的调整建议应该具体可执行`), + mcp.WithString("userRequirement", + mcp.Description("经过requirement_analyze处理后的用户需求描述,包含详细的功能要求和字段信息"), + mcp.Required(), + ), + mcp.WithString("generatedFiles", + mcp.Description("gva_execute创建的文件列表,JSON字符串格式,包含所有生成的后端和前端文件路径"), + mcp.Required(), + ), + ) +} + +// Handle 处理审查请求 +func (g *GVAReviewer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取用户需求 + userRequirementData, ok := request.GetArguments()["userRequirement"] + if !ok { + return nil, errors.New("参数错误:userRequirement 必须提供") + } + + userRequirement, ok := userRequirementData.(string) + if !ok { + return nil, errors.New("参数错误:userRequirement 必须是字符串类型") + } + + // 获取生成的文件列表 + generatedFilesData, ok := request.GetArguments()["generatedFiles"] + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须提供") + } + + generatedFilesStr, ok := generatedFilesData.(string) + if !ok { + return nil, errors.New("参数错误:generatedFiles 必须是JSON字符串") + } + + // 解析JSON字符串为字符串数组 + var generatedFiles []string + err := json.Unmarshal([]byte(generatedFilesStr), &generatedFiles) + if err != nil { + return nil, fmt.Errorf("解析generatedFiles失败: %v", err) + } + + if len(generatedFiles) == 0 { + return nil, errors.New("参数错误:generatedFiles 不能为空") + } + + // 直接生成调整提示,不进行复杂分析 + adjustmentPrompt := g.generateAdjustmentPrompt(userRequirement, generatedFiles) + + // 构建简化的审查详情 + reviewDetails := fmt.Sprintf("📋 **代码审查报告**\n\n **用户原始需求:**\n%s\n\n **已生成文件数量:** %d\n\n **建议进行代码优化和完善**", userRequirement, len(generatedFiles)) + + // 构建审查结果 + reviewResult := &ReviewResponse{ + Success: true, + Message: "代码审查完成", + AdjustmentPrompt: adjustmentPrompt, + ReviewDetails: reviewDetails, + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(reviewResult, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化审查结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("代码审查结果:\n\n%s", string(responseJSON))), + }, + }, nil +} + +// generateAdjustmentPrompt 生成调整代码的提示 +func (g *GVAReviewer) generateAdjustmentPrompt(userRequirement string, generatedFiles []string) string { + var prompt strings.Builder + + prompt.WriteString("🔧 **代码调整指导 Prompt:**\n\n") + prompt.WriteString(fmt.Sprintf("**用户的原始需求为:** %s\n\n", userRequirement)) + prompt.WriteString("**经过GVA生成后的文件有如下内容:**\n") + for _, file := range generatedFiles { + prompt.WriteString(fmt.Sprintf("- %s\n", file)) + } + prompt.WriteString("\n") + + prompt.WriteString("**请帮我优化和完善代码,确保:**\n") + prompt.WriteString("1. 代码完全满足用户的原始需求\n") + prompt.WriteString("2. 完善模块间的关联关系,确保数据一致性\n") + prompt.WriteString("3. 实现所有必要的用户交互功能\n") + prompt.WriteString("4. 保持代码的完整性和可维护性\n") + prompt.WriteString("5. 遵循GVA框架的开发规范和最佳实践\n") + prompt.WriteString("6. 确保前后端功能完整对接\n") + prompt.WriteString("7. 添加必要的错误处理和数据验证\n\n") + prompt.WriteString("8. 如果需要vue路由跳转,请使用 menu_lister获取完整路由表,并且路由跳转使用 router.push({\"name\":从menu_lister中获取的name})\n\n") + prompt.WriteString("9. 如果当前所有的vue页面内容无法满足需求,则自行书写vue文件,并且调用 menu_creator创建菜单记录\n\n") + prompt.WriteString("10. 如果需要API调用,请使用 api_lister获取api表,根据需求调用对应接口\n\n") + prompt.WriteString("11. 如果当前所有API无法满足则自行书写接口,补全前后端代码,并使用 api_creator创建api记录\n\n") + prompt.WriteString("12. 无论前后端都不要随意删除import的内容\n\n") + prompt.WriteString("**请基于用户需求和现有文件,提供完整的代码优化方案。**") + + return prompt.String() +} diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go new file mode 100644 index 0000000..3905606 --- /dev/null +++ b/server/mcp/menu_creator.go @@ -0,0 +1,277 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&MenuCreator{}) +} + +// MenuCreateRequest 菜单创建请求结构 +type MenuCreateRequest struct { + ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单 + Path string `json:"path"` // 路由path + Name string `json:"name"` // 路由name + Hidden bool `json:"hidden"` // 是否在列表隐藏 + Component string `json:"component"` // 对应前端文件路径 + Sort int `json:"sort"` // 排序标记 + Title string `json:"title"` // 菜单名 + Icon string `json:"icon"` // 菜单图标 + KeepAlive bool `json:"keepAlive"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由 + CloseTab bool `json:"closeTab"` // 自动关闭tab + ActiveName string `json:"activeName"` // 高亮菜单 + Parameters []MenuParameterRequest `json:"parameters"` // 路由参数 + MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮 +} + +// MenuParameterRequest 菜单参数请求结构 +type MenuParameterRequest struct { + Type string `json:"type"` // 参数类型:params或query + Key string `json:"key"` // 参数key + Value string `json:"value"` // 参数值 +} + +// MenuButtonRequest 菜单按钮请求结构 +type MenuButtonRequest struct { + Name string `json:"name"` // 按钮名称 + Desc string `json:"desc"` // 按钮描述 +} + +// MenuCreateResponse 菜单创建响应结构 +type MenuCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + MenuID uint `json:"menuId"` + Name string `json:"name"` + Path string `json:"path"` +} + +// MenuCreator 菜单创建工具 +type MenuCreator struct{} + +// New 创建菜单创建工具 +func (m *MenuCreator) New() mcp.Tool { + return mcp.NewTool("create_menu", + mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具 +- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`), + mcp.WithNumber("parentId", + mcp.Description("父菜单ID,0表示根菜单"), + mcp.DefaultNumber(0), + ), + mcp.WithString("path", + mcp.Required(), + mcp.Description("路由path,如:userList"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("路由name,用于Vue Router,如:userList"), + ), + mcp.WithBoolean("hidden", + mcp.Description("是否在菜单列表中隐藏"), + ), + mcp.WithString("component", + mcp.Required(), + mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"), + ), + mcp.WithNumber("sort", + mcp.Description("菜单排序号,数字越小越靠前"), + mcp.DefaultNumber(1), + ), + mcp.WithString("title", + mcp.Required(), + mcp.Description("菜单显示标题"), + ), + mcp.WithString("icon", + mcp.Description("菜单图标名称"), + mcp.DefaultString("menu"), + ), + mcp.WithBoolean("keepAlive", + mcp.Description("是否缓存页面"), + ), + mcp.WithBoolean("defaultMenu", + mcp.Description("是否是基础路由"), + ), + mcp.WithBoolean("closeTab", + mcp.Description("是否自动关闭tab"), + ), + mcp.WithString("activeName", + mcp.Description("高亮菜单名称"), + ), + mcp.WithString("parameters", + mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"), + ), + mcp.WithString("menuBtn", + mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"), + ), + ) +} + +// Handle 处理菜单创建请求 +func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + args := request.GetArguments() + + // 必需参数 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return nil, errors.New("name 参数是必需的") + } + + component, ok := args["component"].(string) + if !ok || component == "" { + return nil, errors.New("component 参数是必需的") + } + + title, ok := args["title"].(string) + if !ok || title == "" { + return nil, errors.New("title 参数是必需的") + } + + // 可选参数 + parentId := uint(0) + if val, ok := args["parentId"].(float64); ok { + parentId = uint(val) + } + + hidden := false + if val, ok := args["hidden"].(bool); ok { + hidden = val + } + + sort := 1 + if val, ok := args["sort"].(float64); ok { + sort = int(val) + } + + icon := "menu" + if val, ok := args["icon"].(string); ok && val != "" { + icon = val + } + + keepAlive := false + if val, ok := args["keepAlive"].(bool); ok { + keepAlive = val + } + + defaultMenu := false + if val, ok := args["defaultMenu"].(bool); ok { + defaultMenu = val + } + + closeTab := false + if val, ok := args["closeTab"].(bool); ok { + closeTab = val + } + + activeName := "" + if val, ok := args["activeName"].(string); ok { + activeName = val + } + + // 解析参数和按钮 + var parameters []system.SysBaseMenuParameter + if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" { + var paramReqs []MenuParameterRequest + if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil { + return nil, fmt.Errorf("parameters 参数格式错误: %v", err) + } + for _, param := range paramReqs { + parameters = append(parameters, system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + }) + } + } + + var menuBtn []system.SysBaseMenuBtn + if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" { + var btnReqs []MenuButtonRequest + if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil { + return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err) + } + for _, btn := range btnReqs { + menuBtn = append(menuBtn, system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + }) + } + } + + // 构建菜单对象 + menu := system.SysBaseMenu{ + ParentId: parentId, + Path: path, + Name: name, + Hidden: hidden, + Component: component, + Sort: sort, + Meta: system.Meta{ + Title: title, + Icon: icon, + KeepAlive: keepAlive, + DefaultMenu: defaultMenu, + CloseTab: closeTab, + ActiveName: activeName, + }, + Parameters: parameters, + MenuBtn: menuBtn, + } + + // 创建菜单 + menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService + err := menuService.AddBaseMenu(menu) + if err != nil { + return nil, fmt.Errorf("创建菜单失败: %v", err) + } + + // 获取创建的菜单ID + var createdMenu system.SysBaseMenu + err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err)) + } + + // 构建响应 + response := &MenuCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建菜单 %s", title), + MenuID: createdMenu.ID, + Name: name, + Path: path, + } + + resultJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("菜单创建结果:\n\n%s", string(resultJSON)), + }, + }, + }, nil +} diff --git a/server/mcp/menu_lister.go b/server/mcp/menu_lister.go new file mode 100644 index 0000000..21b6d60 --- /dev/null +++ b/server/mcp/menu_lister.go @@ -0,0 +1,114 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&MenuLister{}) +} + +// MenuListResponse 菜单列表响应结构 +type MenuListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Menus []system.SysBaseMenu `json:"menus"` + TotalCount int `json:"totalCount"` + Description string `json:"description"` +} + +// MenuLister 菜单列表工具 +type MenuLister struct{} + +// New 创建菜单列表工具 +func (m *MenuLister) New() mcp.Tool { + return mcp.NewTool("list_all_menus", + mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转 + +**功能说明:** +- 返回完整的菜单树形结构 +- 包含路由配置信息(path、name、component) +- 包含菜单元数据(title、icon、keepAlive等) +- 包含菜单参数和按钮配置 +- 支持父子菜单关系展示 + +**使用场景:** +- 前端路由配置:获取所有菜单信息用于配置vue-router +- 菜单权限管理:了解系统中所有可用的菜单项 +- 导航组件开发:构建动态导航菜单 +- 系统架构分析:了解系统的菜单结构和页面组织`), + mcp.WithString("_placeholder", + mcp.Description("占位符,防止json schema校验失败"), + ), + ) +} + +// Handle 处理菜单列表请求 +func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取所有基础菜单 + allMenus, err := m.getAllMenus() + if err != nil { + global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("获取菜单列表失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + // 构建返回结果 + response := MenuListResponse{ + Success: true, + Message: "获取菜单列表成功", + Menus: allMenus, + TotalCount: len(allMenus), + Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息", + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("序列化响应失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(responseJSON), + }, + }, + }, nil +} + +// getAllMenus 获取所有基础菜单 +func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) { + var menus []system.SysBaseMenu + err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + if err != nil { + return nil, err + } + return menus, nil +} diff --git a/server/mcp/requirement_analyzer.go b/server/mcp/requirement_analyzer.go new file mode 100644 index 0000000..765b750 --- /dev/null +++ b/server/mcp/requirement_analyzer.go @@ -0,0 +1,199 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&RequirementAnalyzer{}) +} + +type RequirementAnalyzer struct{} + +// RequirementAnalysisRequest 需求分析请求 +type RequirementAnalysisRequest struct { + UserRequirement string `json:"userRequirement"` +} + +// RequirementAnalysisResponse 需求分析响应 +type RequirementAnalysisResponse struct { + AIPrompt string `json:"aiPrompt"` // 给AI的提示词 +} + +// New 返回工具注册信息 +func (t *RequirementAnalyzer) New() mcp.Tool { + return mcp.NewTool("requirement_analyzer", + mcp.WithDescription(`** 智能需求分析与模块设计工具 - 首选入口工具(最高优先级)** + +** 重要提示:这是所有MCP工具的首选入口,请优先使用!** + +** 核心能力:** +作为资深系统架构师,智能分析用户需求并自动设计完整的模块架构 + +** 核心功能:** +1. **智能需求解构**:深度分析用户需求,识别核心业务实体、业务流程、数据关系 +2. **自动模块设计**:基于需求分析,智能确定需要多少个模块及各模块功能 +3. **字段智能推导**:为每个模块自动设计详细字段,包含数据类型、关联关系、字典需求 +4. **架构优化建议**:提供模块拆分、关联设计、扩展性等专业建议 + +** 输出内容:** +- 模块数量和架构设计 +- 每个模块的详细字段清单 +- 数据类型和关联关系设计 +- 字典需求和类型定义 +- 模块间关系图和扩展建议 + +** 适用场景:** +- 用户需求描述不完整,需要智能补全 +- 复杂业务系统的模块架构设计 +- 需要专业的数据库设计建议 +- 想要快速搭建生产级业务系统 + +** 推荐工作流:** + requirement_analyzer → gva_analyze → gva_execute → 其他辅助工具 + + `), + mcp.WithString("userRequirement", + mcp.Required(), + mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"), + ), + ) +} + +// Handle 处理工具调用 +func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userRequirement, ok := request.GetArguments()["userRequirement"].(string) + if !ok || userRequirement == "" { + return nil, errors.New("参数错误:userRequirement 必须是非空字符串") + } + + // 分析用户需求 + analysisResponse, err := t.analyzeRequirement(userRequirement) + if err != nil { + return nil, fmt.Errorf("需求分析失败: %v", err) + } + + // 序列化响应 + responseData, err := json.Marshal(analysisResponse) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseData)), + }, + }, nil +} + +// analyzeRequirement 分析用户需求 - 专注于AI需求传递 +func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) { + // 生成AI提示词 - 这是唯一功能 + aiPrompt := t.generateAIPrompt(userRequirement) + + return &RequirementAnalysisResponse{ + AIPrompt: aiPrompt, + }, nil +} + +// generateAIPrompt 生成AI提示词 - 智能分析需求并确定模块结构 +func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string { + prompt := fmt.Sprintf(`# 智能需求分析与模块设计任务 + +## 用户原始需求 +%s + +## 核心任务 +你需要作为一个资深的系统架构师,深度分析用户需求,智能设计出完整的模块架构。 + +## 分析步骤 + +### 第一步:需求解构分析 +请仔细分析用户需求,识别出: +1. **核心业务实体**(如:用户、商品、订单、疫苗、宠物等) +2. **业务流程**(如:注册、购买、记录、管理等) +3. **数据关系**(实体间的关联关系) +4. **功能模块**(需要哪些独立的管理模块) + +### 第二步:模块架构设计 +基于需求分析,设计出模块架构,格式如下: + +**模块1:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系,明确一对一/一对多] +- 字典需求:[需要哪些字典类型] + +**模块2:[模块名称]** +- 功能描述:[该模块的核心功能] +- 主要字段:[列出关键字段,注明数据类型] +- 关联关系:[与其他模块的关系] +- 字典需求:[需要哪些字典类型] + +**...** + +### 第三步:字段详细设计 +为每个模块详细设计字段: + +#### 模块1字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- 字段名2 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +#### 模块2字段清单: +- 字段名1 (数据类型) - 字段描述 [是否必填] [关联信息/字典类型] +- ... + +## 智能分析指导原则 + +### 模块拆分原则 +1. **单一职责**:每个模块只负责一个核心业务实体 +2. **数据完整性**:相关数据应该在同一模块中 +3. **业务独立性**:模块应该能够独立完成特定业务功能 +4. **扩展性考虑**:为未来功能扩展预留空间 + +### 字段设计原则 +1. **必要性**:只包含业务必需的字段 +2. **规范性**:遵循数据库设计规范 +3. **关联性**:正确识别实体间关系 +4. **字典化**:状态、类型等枚举值使用字典 + +### 关联关系识别 +- **一对一**:一个实体只能关联另一个实体的一个记录 +- **一对多**:一个实体可以关联另一个实体的多个记录 +- **多对多**:通过中间表实现复杂关联 + +## 特殊场景处理 + +### 复杂实体识别 +当用户提到某个概念时,要判断它是否需要独立模块: +- **字典处理**:简单的常见的状态、类型(如:开关、性别、完成状态等) +- **独立模块**:复杂实体(如:疫苗管理、宠物档案、注射记录) + +## 输出要求 + +### 必须包含的信息 +1. **模块数量**:明确需要几个模块 +2. **模块关系图**:用文字描述模块间关系 +3. **核心字段**:每个模块的关键字段(至少5-10个) +4. **数据类型**:string、int、bool、time.Time、float64等 +5. **关联设计**:明确哪些字段是关联字段 +6. **字典需求**:列出需要创建的字典类型 + +### 严格遵循用户输入 +- 如果用户提供了具体字段,**必须使用**用户提供的字段 +- 如果用户提供了SQL文件,**严格按照**SQL结构设计 +- **不要**随意发散,**不要**添加用户未提及的功能 +--- + +**现在请开始深度分析用户需求:"%s"** + +请按照上述框架进行系统性分析,确保输出的模块设计既满足当前需求,又具备良好的扩展性。`, userRequirement, userRequirement) + + return prompt +} diff --git a/server/middleware/app_jwt.go b/server/middleware/app_jwt.go new file mode 100644 index 0000000..cbb4508 --- /dev/null +++ b/server/middleware/app_jwt.go @@ -0,0 +1,147 @@ +package middleware + +import ( + "errors" + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/utils" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +// AppJWTAuth 前台用户 JWT 认证中间件 +func AppJWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + token := GetToken(c) + if token == "" { + response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c) + c.Abort() + return + } + + // 解析 JWT + claims, err := utils.ParseAppToken(token) + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + response.FailWithDetailed(gin.H{"reload": true}, "Token 已过期", c) + } else { + response.FailWithDetailed(gin.H{"reload": true}, "Token 无效", c) + } + c.Abort() + return + } + + // 验证用户类型(确保是前台用户) + if claims.UserType != utils.UserTypeApp { + response.FailWithMessage("无效的用户类型", c) + c.Abort() + return + } + + // 查询用户是否存在 + var user app.AppUser + err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error + if err != nil { + response.FailWithMessage("用户不存在", c) + c.Abort() + return + } + + // 检查用户状态 + if !user.Enable { + response.FailWithMessage("用户已被禁用", c) + c.Abort() + return + } + + if user.Status != "active" { + response.FailWithMessage("账户状态异常", c) + c.Abort() + return + } + + // 将用户信息存入上下文 + c.Set("appUserId", user.ID) + c.Set("appUser", &user) + c.Set("appUsername", user.Username) + + c.Next() + } +} + +// GetAppUserID 从上下文获取前台用户 ID(需要鉴权的接口使用) +func GetAppUserID(c *gin.Context) uint { + if userID, exists := c.Get("appUserId"); exists { + return userID.(uint) + } + return 0 +} + +// GetOptionalAppUserID 从上下文获取可选的前台用户 ID(公开接口使用) +// 如果用户已登录,返回用户 ID;否则返回 nil +func GetOptionalAppUserID(c *gin.Context) *uint { + // 先尝试从上下文获取(通过鉴权中间件设置) + if userID, exists := c.Get("appUserId"); exists { + if id, ok := userID.(uint); ok { + return &id + } + } + + // 如果上下文中没有,尝试手动解析 Token(用于公开接口) + token := GetToken(c) + if token == "" { + return nil + } + + claims, err := utils.ParseAppToken(token) + if err != nil { + return nil + } + + if claims.UserType != utils.UserTypeApp { + return nil + } + + return &claims.UserID +} + +// GetAppUser 从上下文获取前台用户信息 +func GetAppUser(c *gin.Context) *app.AppUser { + if user, exists := c.Get("appUser"); exists { + return user.(*app.AppUser) + } + return nil +} + +// GetAppUsername 从上下文获取前台用户名 +func GetAppUsername(c *gin.Context) string { + if username, exists := c.Get("appUsername"); exists { + return username.(string) + } + return "" +} + +// GetToken 从请求中获取 Token +// 优先从 Header 获取,其次从 Query 参数获取 +func GetToken(c *gin.Context) string { + token := c.Request.Header.Get("x-token") + if token == "" { + token = c.Request.Header.Get("Authorization") + if token != "" && len(token) > 7 && token[:7] == "Bearer " { + token = token[7:] + } + } + if token == "" { + token = c.Query("token") + } + return token +} + +// SetAppUserID 设置用户 ID 到上下文(用于某些特殊场景) +func SetAppUserID(c *gin.Context, userID uint) { + c.Set("appUserId", userID) + c.Set("appUserIdStr", strconv.Itoa(int(userID))) +} diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go new file mode 100644 index 0000000..3b21e51 --- /dev/null +++ b/server/middleware/casbin_rbac.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "strconv" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/utils" + "github.com/gin-gonic/gin" +) + +// CasbinHandler 拦截器 +func CasbinHandler() gin.HandlerFunc { + return func(c *gin.Context) { + waitUse, _ := utils.GetClaims(c) + //获取请求的PATH + path := c.Request.URL.Path + obj := strings.TrimPrefix(path, global.GVA_CONFIG.System.RouterPrefix) + // 获取请求方法 + act := c.Request.Method + // 获取用户的角色 + sub := strconv.Itoa(int(waitUse.AuthorityId)) + e := utils.GetCasbin() // 判断策略中是否存在 + success, _ := e.Enforce(sub, obj, act) + if !success { + response.FailWithDetailed(gin.H{}, "权限不足", c) + c.Abort() + return + } + c.Next() + } +} diff --git a/server/middleware/cors.go b/server/middleware/cors.go new file mode 100644 index 0000000..4c2097d --- /dev/null +++ b/server/middleware/cors.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "net/http" + + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "github.com/gin-gonic/gin" +) + +// Cors 直接放行所有跨域请求并放行所有 OPTIONS 方法 +func Cors() gin.HandlerFunc { + return func(c *gin.Context) { + method := c.Request.Method + origin := c.Request.Header.Get("Origin") + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id") + c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT") + c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At") + c.Header("Access-Control-Allow-Credentials", "true") + + // 放行所有OPTIONS方法 + if method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + } + // 处理请求 + c.Next() + } +} + +// CorsByRules 按照配置处理跨域请求 +func CorsByRules() gin.HandlerFunc { + // 放行全部 + if global.GVA_CONFIG.Cors.Mode == "allow-all" { + return Cors() + } + return func(c *gin.Context) { + whitelist := checkCors(c.GetHeader("origin")) + + // 通过检查, 添加请求头 + if whitelist != nil { + c.Header("Access-Control-Allow-Origin", whitelist.AllowOrigin) + c.Header("Access-Control-Allow-Headers", whitelist.AllowHeaders) + c.Header("Access-Control-Allow-Methods", whitelist.AllowMethods) + c.Header("Access-Control-Expose-Headers", whitelist.ExposeHeaders) + if whitelist.AllowCredentials { + c.Header("Access-Control-Allow-Credentials", "true") + } + } + + // 严格白名单模式且未通过检查,直接拒绝处理请求 + if whitelist == nil && global.GVA_CONFIG.Cors.Mode == "strict-whitelist" && !(c.Request.Method == "GET" && c.Request.URL.Path == "/health") { + c.AbortWithStatus(http.StatusForbidden) + } else { + // 非严格白名单模式,无论是否通过检查均放行所有 OPTIONS 方法 + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + } + } + + // 处理请求 + c.Next() + } +} + +func checkCors(currentOrigin string) *config.CORSWhitelist { + for _, whitelist := range global.GVA_CONFIG.Cors.Whitelist { + // 遍历配置中的跨域头,寻找匹配项 + if currentOrigin == whitelist.AllowOrigin { + return &whitelist + } + } + return nil +} diff --git a/server/middleware/email.go b/server/middleware/email.go new file mode 100644 index 0000000..968294e --- /dev/null +++ b/server/middleware/email.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "bytes" + "io" + "strconv" + "time" + + "git.echol.cn/loser/st/server/plugin/email/utils" + utils2 "git.echol.cn/loser/st/server/utils" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func ErrorToEmail() gin.HandlerFunc { + return func(c *gin.Context) { + var username string + claims, _ := utils2.GetClaims(c) + if claims.Username != "" { + username = claims.Username + } else { + id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) + var u system.SysUser + err := global.GVA_DB.Where("id = ?", id).First(&u).Error + if err != nil { + username = "Unknown" + } + username = u.Username + } + body, _ := io.ReadAll(c.Request.Body) + // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: string(body), + } + now := time.Now() + + c.Next() + + latency := time.Since(now) + status := c.Writer.Status() + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + str := "接收到的请求为" + record.Body + "\n" + "请求方式为" + record.Method + "\n" + "报错信息如下" + record.ErrorMessage + "\n" + "耗时" + latency.String() + "\n" + if status != 200 { + subject := username + "" + record.Ip + "调用了" + record.Path + "报错了" + if err := utils.ErrorToEmail(subject, str); err != nil { + global.GVA_LOG.Error("ErrorToEmail Failed, err:", zap.Error(err)) + } + } + } +} diff --git a/server/middleware/error.go b/server/middleware/error.go new file mode 100644 index 0000000..9708705 --- /dev/null +++ b/server/middleware/error.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 +func GinRecovery(stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + global.GVA_LOG.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + // If the connection is dead, we can't write a status to it. + _ = c.Error(err.(error)) // nolint: errcheck + c.Abort() + return + } + + if stack { + form := "后端" + info := fmt.Sprintf("Panic: %v\nRequest: %s\nStack: %s", err, string(httpRequest), string(debug.Stack())) + level := "error" + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) + global.GVA_LOG.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } else { + form := "后端" + info := fmt.Sprintf("Panic: %v\nRequest: %s", err, string(httpRequest)) + level := "error" + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) + global.GVA_LOG.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + c.Next() + } +} diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go new file mode 100644 index 0000000..f373828 --- /dev/null +++ b/server/middleware/jwt.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "errors" + "strconv" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/utils" + "github.com/golang-jwt/jwt/v5" + + "git.echol.cn/loser/st/server/model/common/response" + "github.com/gin-gonic/gin" +) + +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 + token := utils.GetToken(c) + if token == "" { + response.NoAuth("未登录或非法访问,请登录", c) + c.Abort() + return + } + if isBlacklist(token) { + response.NoAuth("您的帐户异地登陆或令牌失效", c) + utils.ClearToken(c) + c.Abort() + return + } + j := utils.NewJWT() + // parseToken 解析token包含的信息 + claims, err := j.ParseToken(token) + if err != nil { + if errors.Is(err, utils.TokenExpired) { + response.NoAuth("登录已过期,请重新登录", c) + utils.ClearToken(c) + c.Abort() + return + } + response.NoAuth(err.Error(), c) + utils.ClearToken(c) + c.Abort() + return + } + + // 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开 + // 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开 + + //if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 { + // _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token}) + // response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + // c.Abort() + //} + c.Set("claims", claims) + if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime { + dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr)) + newToken, _ := j.CreateTokenByOldToken(token, *claims) + newClaims, _ := j.ParseToken(newToken) + c.Header("new-token", newToken) + c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10)) + utils.SetToken(c, newToken, int(dr.Seconds()/60)) + if global.GVA_CONFIG.System.UseMultipoint { + // 记录新的活跃jwt + _ = utils.SetRedisJWT(newToken, newClaims.Username) + } + } + c.Next() + + if newToken, exists := c.Get("new-token"); exists { + c.Header("new-token", newToken.(string)) + } + if newExpiresAt, exists := c.Get("new-expires-at"); exists { + c.Header("new-expires-at", newExpiresAt.(string)) + } + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func isBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok +} diff --git a/server/middleware/limit_ip.go b/server/middleware/limit_ip.go new file mode 100644 index 0000000..2988a00 --- /dev/null +++ b/server/middleware/limit_ip.go @@ -0,0 +1,92 @@ +package middleware + +import ( + "context" + "errors" + "net/http" + "time" + + "go.uber.org/zap" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/response" + "github.com/gin-gonic/gin" +) + +type LimitConfig struct { + // GenerationKey 根据业务生成key 下面CheckOrMark查询生成 + GenerationKey func(c *gin.Context) string + // 检查函数,用户可修改具体逻辑,更加灵活 + CheckOrMark func(key string, expire int, limit int) error + // Expire key 过期时间 + Expire int + // Limit 周期时间 + Limit int +} + +func (l LimitConfig) LimitWithTime() gin.HandlerFunc { + return func(c *gin.Context) { + if err := l.CheckOrMark(l.GenerationKey(c), l.Expire, l.Limit); err != nil { + c.JSON(http.StatusOK, gin.H{"code": response.ERROR, "msg": err.Error()}) + c.Abort() + return + } else { + c.Next() + } + } +} + +// DefaultGenerationKey 默认生成key +func DefaultGenerationKey(c *gin.Context) string { + return "GVA_Limit" + c.ClientIP() +} + +func DefaultCheckOrMark(key string, expire int, limit int) (err error) { + // 判断是否开启redis + if global.GVA_REDIS == nil { + return err + } + if err = SetLimitWithTime(key, limit, time.Duration(expire)*time.Second); err != nil { + global.GVA_LOG.Error("limit", zap.Error(err)) + } + return err +} + +func DefaultLimit() gin.HandlerFunc { + return LimitConfig{ + GenerationKey: DefaultGenerationKey, + CheckOrMark: DefaultCheckOrMark, + Expire: global.GVA_CONFIG.System.LimitTimeIP, + Limit: global.GVA_CONFIG.System.LimitCountIP, + }.LimitWithTime() +} + +// SetLimitWithTime 设置访问次数 +func SetLimitWithTime(key string, limit int, expiration time.Duration) error { + count, err := global.GVA_REDIS.Exists(context.Background(), key).Result() + if err != nil { + return err + } + if count == 0 { + pipe := global.GVA_REDIS.TxPipeline() + pipe.Incr(context.Background(), key) + pipe.Expire(context.Background(), key, expiration) + _, err = pipe.Exec(context.Background()) + return err + } else { + // 次数 + if times, err := global.GVA_REDIS.Get(context.Background(), key).Int(); err != nil { + return err + } else { + if times >= limit { + if t, err := global.GVA_REDIS.PTTL(context.Background(), key).Result(); err != nil { + return errors.New("请求太过频繁,请稍后再试") + } else { + return errors.New("请求太过频繁, 请 " + t.String() + " 秒后尝试") + } + } else { + return global.GVA_REDIS.Incr(context.Background(), key).Err() + } + } + } +} diff --git a/server/middleware/loadtls.go b/server/middleware/loadtls.go new file mode 100644 index 0000000..a17cf65 --- /dev/null +++ b/server/middleware/loadtls.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/unrolled/secure" +) + +// 用https把这个中间件在router里面use一下就好 + +func LoadTls() gin.HandlerFunc { + return func(c *gin.Context) { + middleware := secure.New(secure.Options{ + SSLRedirect: true, + SSLHost: "localhost:443", + }) + err := middleware.Process(c.Writer, c.Request) + if err != nil { + // 如果出现错误,请不要继续 + fmt.Println(err) + return + } + // 继续往下处理 + c.Next() + } +} diff --git a/server/middleware/logger.go b/server/middleware/logger.go new file mode 100644 index 0000000..fabc334 --- /dev/null +++ b/server/middleware/logger.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +// LogLayout 日志layout +type LogLayout struct { + Time time.Time + Metadata map[string]interface{} // 存储自定义原数据 + Path string // 访问路径 + Query string // 携带query + Body string // 携带body数据 + IP string // ip地址 + UserAgent string // 代理 + Error string // 错误 + Cost time.Duration // 花费时间 + Source string // 来源 +} + +type Logger struct { + // Filter 用户自定义过滤 + Filter func(c *gin.Context) bool + // FilterKeyword 关键字过滤(key) + FilterKeyword func(layout *LogLayout) bool + // AuthProcess 鉴权处理 + AuthProcess func(c *gin.Context, layout *LogLayout) + // 日志处理 + Print func(LogLayout) + // Source 服务唯一标识 + Source string +} + +func (l Logger) SetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + var body []byte + if l.Filter != nil && !l.Filter(c) { + body, _ = c.GetRawData() + // 将原body塞回去 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + c.Next() + cost := time.Since(start) + layout := LogLayout{ + Time: time.Now(), + Path: path, + Query: query, + IP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Error: strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n"), + Cost: cost, + Source: l.Source, + } + if l.Filter != nil && !l.Filter(c) { + layout.Body = string(body) + } + if l.AuthProcess != nil { + // 处理鉴权需要的信息 + l.AuthProcess(c, &layout) + } + if l.FilterKeyword != nil { + // 自行判断key/value 脱敏等 + l.FilterKeyword(&layout) + } + // 自行处理日志 + l.Print(layout) + } +} + +func DefaultLogger() gin.HandlerFunc { + return Logger{ + Print: func(layout LogLayout) { + // 标准输出,k8s做收集 + v, _ := json.Marshal(layout) + fmt.Println(string(v)) + }, + Source: "GVA", + }.SetLoggerMiddleware() +} diff --git a/server/middleware/operation.go b/server/middleware/operation.go new file mode 100644 index 0000000..3b4ff8c --- /dev/null +++ b/server/middleware/operation.go @@ -0,0 +1,129 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "git.echol.cn/loser/st/server/utils" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var respPool sync.Pool +var bufferSize = 1024 + +func init() { + respPool.New = func() interface{} { + return make([]byte, bufferSize) + } +} + +func OperationRecord() gin.HandlerFunc { + return func(c *gin.Context) { + var body []byte + var userId int + if c.Request.Method != http.MethodGet { + var err error + body, err = io.ReadAll(c.Request.Body) + if err != nil { + global.GVA_LOG.Error("read body from request error:", zap.Error(err)) + } else { + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } + } else { + query := c.Request.URL.RawQuery + query, _ = url.QueryUnescape(query) + split := strings.Split(query, "&") + m := make(map[string]string) + for _, v := range split { + kv := strings.Split(v, "=") + if len(kv) == 2 { + m[kv[0]] = kv[1] + } + } + body, _ = json.Marshal(&m) + } + claims, _ := utils.GetClaims(c) + if claims != nil && claims.BaseClaims.ID != 0 { + userId = int(claims.BaseClaims.ID) + } else { + id, err := strconv.Atoi(c.Request.Header.Get("x-user-id")) + if err != nil { + userId = 0 + } + userId = id + } + record := system.SysOperationRecord{ + Ip: c.ClientIP(), + Method: c.Request.Method, + Path: c.Request.URL.Path, + Agent: c.Request.UserAgent(), + Body: "", + UserID: userId, + } + + // 上传文件时候 中间件日志进行裁断操作 + if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") { + record.Body = "[文件]" + } else { + if len(body) > bufferSize { + record.Body = "[超出记录长度]" + } else { + record.Body = string(body) + } + } + + writer := responseBodyWriter{ + ResponseWriter: c.Writer, + body: &bytes.Buffer{}, + } + c.Writer = writer + now := time.Now() + + c.Next() + + latency := time.Since(now) + record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + record.Status = c.Writer.Status() + record.Latency = latency + record.Resp = writer.body.String() + + if strings.Contains(c.Writer.Header().Get("Pragma"), "public") || + strings.Contains(c.Writer.Header().Get("Expires"), "0") || + strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/vnd.ms-excel") || + strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") || + strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") || + strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") { + if len(record.Resp) > bufferSize { + // 截断 + record.Body = "超出记录长度" + } + } + if err := global.GVA_DB.Create(&record).Error; err != nil { + global.GVA_LOG.Error("create operation record error:", zap.Error(err)) + } + } +} + +type responseBodyWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (r responseBodyWriter) Write(b []byte) (int, error) { + r.body.Write(b) + return r.ResponseWriter.Write(b) +} diff --git a/server/middleware/timeout.go b/server/middleware/timeout.go new file mode 100644 index 0000000..2a46ebf --- /dev/null +++ b/server/middleware/timeout.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "context" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// TimeoutMiddleware 创建超时中间件 +// 入参 timeout 设置超时时间(例如:time.Second * 5) +// 使用示例 xxx.Get("path",middleware.TimeoutMiddleware(30*time.Second),HandleFunc) +func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + c.Request = c.Request.WithContext(ctx) + + // 使用 buffered channel 避免 goroutine 泄漏 + done := make(chan struct{}, 1) + panicChan := make(chan interface{}, 1) + + go func() { + defer func() { + if p := recover(); p != nil { + select { + case panicChan <- p: + default: + } + } + select { + case done <- struct{}{}: + default: + } + }() + c.Next() + }() + + select { + case p := <-panicChan: + panic(p) + case <-done: + return + case <-ctx.Done(): + // 确保服务器超时设置足够长 + c.Header("Connection", "close") + c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{ + "code": 504, + "msg": "请求超时", + }) + return + } + } +} diff --git a/server/model/app/README.md b/server/model/app/README.md new file mode 100644 index 0000000..a387e55 --- /dev/null +++ b/server/model/app/README.md @@ -0,0 +1,213 @@ +# App 前台应用数据模型 + +## 📋 模型列表 + +本目录包含所有前台用户应用相关的数据模型,与管理后台的 `system` 模块完全独立。 + +### 1. 用户相关模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `app_user.go` | `AppUser` | `app_users` | 前台用户表 | +| `app_user_session.go` | `AppUserSession` | `app_user_sessions` | 用户会话表 | + +### 2. AI 角色相关模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_character.go` | `AICharacter` | `ai_characters` | AI 角色表 | +| `ai_character.go` | `AppUserFavoriteCharacter` | `app_user_favorite_characters` | 用户收藏角色表 | + +### 3. 对话相关模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_chat.go` | `AIChat` | `ai_chats` | 对话表 | +| `ai_chat.go` | `AIChatMember` | `ai_chat_members` | 群聊成员表 | +| `ai_message.go` | `AIMessage` | `ai_messages` | 消息表 | +| `ai_message.go` | `AIMessageSwipe` | `ai_message_swipes` | 消息变体表 | + +### 4. 向量记忆模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_memory.go` | `AIMemoryVector` | `ai_memory_vectors` | 向量记忆表(使用 pgvector) | + +### 5. AI 服务配置模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_provider.go` | `AIProvider` | `ai_providers` | AI 提供商配置表 | +| `ai_provider.go` | `AIModel` | `ai_models` | AI 模型配置表 | + +### 6. 文件管理模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_file.go` | `AIFile` | `ai_files` | 文件表 | + +### 7. 其他模型 + +| 文件 | 模型 | 表名 | 说明 | +|------|------|------|------| +| `ai_preset.go` | `AIPreset` | `ai_presets` | 对话预设表 | +| `ai_world_info.go` | `AIWorldInfo` | `ai_world_info` | 世界书表 | +| `ai_usage_stat.go` | `AIUsageStat` | `ai_usage_stats` | 使用统计表 | + +## 🔧 使用说明 + +### 1. 数据库自动迁移 + +所有模型已在 `initialize/gorm.go` 中注册,启动服务时会自动创建表: + +```go +// 在 RegisterTables() 函数中已注册 +app.AppUser{}, +app.AppUserSession{}, +app.AICharacter{}, +app.AppUserFavoriteCharacter{}, +app.AIChat{}, +app.AIChatMember{}, +app.AIMessage{}, +app.AIMessageSwipe{}, +app.AIMemoryVector{}, +app.AIProvider{}, +app.AIModel{}, +app.AIFile{}, +app.AIPreset{}, +app.AIWorldInfo{}, +app.AIUsageStat{}, +``` + +### 2. PostgreSQL 向量扩展 + +向量记忆功能依赖 `pgvector` 扩展,已在 `initialize/gorm_pgsql_extension.go` 中自动安装: + +```sql +CREATE EXTENSION IF NOT EXISTS vector; +CREATE INDEX idx_memory_vectors_embedding ON ai_memory_vectors +USING hnsw (embedding vector_cosine_ops); +``` + +### 3. 外键关系 + +模型之间的关系已通过 GORM 标签定义: + +- `AppUser` ← `AppUserSession`(一对多) +- `AppUser` ← `AICharacter`(一对多,创建者) +- `AppUser` ← `AIChat`(一对多) +- `AppUser` ← `AppUserFavoriteCharacter`(多对多,通过中间表) +- `AICharacter` ← `AppUserFavoriteCharacter`(多对多,通过中间表) +- `AICharacter` ← `AIChat`(一对多) +- `AIChat` ← `AIMessage`(一对多) +- `AIChat` ← `AIChatMember`(多对多,通过中间表) +- `AICharacter` ← `AIChatMember`(多对多,通过中间表) +- `AIMessage` ← `AIMessageSwipe`(一对多) +- `AIProvider` ← `AIModel`(一对多) + +### 4. JSONB 字段 + +以下字段使用 PostgreSQL 的 JSONB 类型: + +- `AppUser.AISettings` - AI 相关配置 +- `AppUser.Preferences` - 用户偏好设置 +- `AICharacter.CardData` - 角色卡片数据 +- `AICharacter.Tags` - 角色标签 +- `AICharacter.ExampleMessages` - 消息示例 +- `AIChat.Settings` - 对话设置 +- `AIMessage.GenerationParams` - AI 生成参数 +- `AIMessage.Metadata` - 消息元数据 +- `AIMemoryVector.Metadata` - 记忆元数据 +- `AIProvider.APIConfig` - API 配置 +- `AIModel.Config` - 模型配置 +- `AIFile.RelatedTo` - 文件关联对象 +- `AIFile.Metadata` - 文件元数据 +- `AIPreset.Config` - 预设配置 +- `AIWorldInfo.TriggerConfig` - 触发条件配置 + +### 5. 向量字段 + +`AIMemoryVector.Embedding` 使用 `pgvector.Vector` 类型,维度为 1536(OpenAI text-embedding-ada-002)。 + +## ⚠️ 注意事项 + +1. **不要修改 system 包**:所有管理后台相关的模型在 `model/system/` 包中,**不要修改** +2. **表名前缀**: + - 前台用户相关:`app_*` + - AI 功能相关:`ai_*` + - 系统管理相关:`sys_*`(不修改) +3. **UUID 生成**:`AppUser.UUID` 使用数据库自动生成(PostgreSQL 的 `gen_random_uuid()`) +4. **软删除**:所有模型继承 `global.GVA_MODEL`,自动支持软删除 +5. **时间字段**:`CreatedAt`、`UpdatedAt`、`DeletedAt` 由 GORM 自动管理 + +## 📊 ER 图关系 + +``` +AppUser (前台用户) + ├── AppUserSession (会话) + ├── AICharacter (创建的角色) + ├── AIChat (对话) + ├── AppUserFavoriteCharacter (收藏的角色) + ├── AIMemoryVector (记忆) + ├── AIProvider (AI 提供商配置) + ├── AIFile (文件) + ├── AIPreset (预设) + ├── AIWorldInfo (世界书) + └── AIUsageStat (使用统计) + +AICharacter (AI 角色) + ├── AIChat (对话) + ├── AIChatMember (群聊成员) + ├── AppUserFavoriteCharacter (被收藏) + └── AIMemoryVector (记忆) + +AIChat (对话) + ├── AIMessage (消息) + ├── AIChatMember (群聊成员) + └── AIMemoryVector (记忆) + +AIMessage (消息) + └── AIMessageSwipe (消息变体) + +AIProvider (AI 提供商) + └── AIModel (AI 模型) +``` + +## 🚀 快速开始 + +1. 确保 PostgreSQL 已安装 pgvector 扩展 +2. 配置 `config.yaml` 中的数据库连接 +3. 启动服务,AutoMigrate 会自动创建所有表 +4. 检查日志确认表创建成功 + +```bash +# 启动服务 +go run main.go + +# 查看日志 +# [GVA] pgvector extension is ready +# [GVA] vector indexes created successfully +# [GVA] register table success +``` + +## 📝 开发建议 + +1. 查询时使用预加载避免 N+1 问题: + ```go + db.Preload("User").Preload("Character").Find(&chats) + ``` + +2. 向量搜索示例: + ```go + db.Order("embedding <=> ?", queryVector).Limit(10).Find(&memories) + ``` + +3. JSONB 查询示例: + ```go + db.Where("ai_settings->>'model' = ?", "gpt-4").Find(&users) + ``` + +--- + +**创建日期**: 2026-02-10 +**维护者**: 开发团队 diff --git a/server/model/app/ai_character.go b/server/model/app/ai_character.go new file mode 100644 index 0000000..3164fcd --- /dev/null +++ b/server/model/app/ai_character.go @@ -0,0 +1,49 @@ +package app + +import ( + "time" + + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// AICharacter 角色卡模型 +type AICharacter struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 基本信息 + UserID uint `gorm:"index;not null" json:"userId"` // 所属用户ID + Name string `gorm:"type:varchar(100);not null" json:"name"` // 角色名称 + Avatar string `gorm:"type:text" json:"avatar"` // 头像URL或Base64 + Creator string `gorm:"type:varchar(100)" json:"creator"` // 创建者 + Version string `gorm:"type:varchar(50)" json:"version"` // 角色版本 + IsPublic bool `gorm:"default:false" json:"isPublic"` // 是否公开 + + // SillyTavern V2 格式字段 + Description string `gorm:"type:text" json:"description"` // 角色描述 + Personality string `gorm:"type:text" json:"personality"` // 性格特征 + Scenario string `gorm:"type:text" json:"scenario"` // 场景设定 + FirstMes string `gorm:"type:text" json:"firstMes"` // 第一条消息 + MesExample string `gorm:"type:text" json:"mesExample"` // 消息示例 + CreatorNotes string `gorm:"type:text" json:"creatorNotes"` // 创建者备注 + SystemPrompt string `gorm:"type:text" json:"systemPrompt"` // 系统提示词 + PostHistoryInstructions string `gorm:"type:text" json:"postHistoryInstructions"` // 历史后指令 + Tags datatypes.JSON `gorm:"type:jsonb" json:"tags"` // 标签数组 + AlternateGreetings datatypes.JSON `gorm:"type:jsonb" json:"alternateGreetings"` // 备用问候语 + CharacterBook datatypes.JSON `gorm:"type:jsonb" json:"characterBook"` // 角色书 + Extensions datatypes.JSON `gorm:"type:jsonb" json:"extensions"` // 扩展数据 + Spec string `gorm:"type:varchar(50);default:'chara_card_v2'" json:"spec"` // 规范名称 + SpecVersion string `gorm:"type:varchar(50);default:'2.0'" json:"specVersion"` // 规范版本 + + // 统计信息 + UseCount int `gorm:"default:0" json:"useCount"` // 使用次数 + FavoriteCount int `gorm:"default:0" json:"favoriteCount"` // 收藏次数 +} + +// TableName 指定表名 +func (AICharacter) TableName() string { + return "ai_characters" +} diff --git a/server/model/app/ai_config.go b/server/model/app/ai_config.go new file mode 100644 index 0000000..295656d --- /dev/null +++ b/server/model/app/ai_config.go @@ -0,0 +1,31 @@ +package app + +import ( + "time" + + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// AIConfig AI配置模型 +type AIConfig struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Name string `gorm:"type:varchar(100);not null" json:"name"` // 配置名称 + Provider string `gorm:"type:varchar(50);not null" json:"provider"` // 提供商 (openai/anthropic/custom) + BaseURL string `gorm:"type:varchar(500)" json:"baseUrl"` // API Base URL + APIKey string `gorm:"type:varchar(500)" json:"apiKey"` // API Key (加密存储) + Models datatypes.JSON `gorm:"type:jsonb" json:"models"` // 可用模型列表 + DefaultModel string `gorm:"type:varchar(100)" json:"defaultModel"` // 默认模型 + Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 其他设置 (temperature等) + IsActive bool `gorm:"default:false" json:"isActive"` // 是否激活 + IsDefault bool `gorm:"default:false" json:"isDefault"` // 是否为默认配置 +} + +// TableName 指定表名 +func (AIConfig) TableName() string { + return "ai_configs" +} diff --git a/server/model/app/app_user.go b/server/model/app/app_user.go new file mode 100644 index 0000000..2fdb5c3 --- /dev/null +++ b/server/model/app/app_user.go @@ -0,0 +1,33 @@ +package app + +import ( + "time" + + "git.echol.cn/loser/st/server/global" + "gorm.io/datatypes" +) + +// AppUser 前台用户模型(与 sys_users 独立) +type AppUser struct { + global.GVA_MODEL + UUID string `json:"uuid" gorm:"type:uuid;uniqueIndex;comment:用户UUID"` + Username string `json:"username" gorm:"uniqueIndex;comment:用户登录名"` + Password string `json:"-" gorm:"comment:用户登录密码"` + NickName string `json:"nickName" gorm:"comment:用户昵称"` + Email string `json:"email" gorm:"index;comment:用户邮箱"` + Phone string `json:"phone" gorm:"comment:用户手机号"` + Avatar string `json:"avatar" gorm:"type:varchar(1024);comment:用户头像"` + Status string `json:"status" gorm:"type:varchar(50);default:active;comment:账户状态"` + Enable bool `json:"enable" gorm:"default:true;comment:用户是否启用"` + IsAdmin bool `json:"isAdmin" gorm:"default:false;comment:是否为管理员"` + LastLoginAt *time.Time `json:"lastLoginAt" gorm:"comment:最后登录时间"` + LastLoginIP string `json:"lastLoginIp" gorm:"type:varchar(100);comment:最后登录IP"` + AISettings datatypes.JSON `json:"aiSettings" gorm:"type:jsonb;comment:AI配置"` + Preferences datatypes.JSON `json:"preferences" gorm:"type:jsonb;comment:用户偏好"` + ChatCount int `json:"chatCount" gorm:"default:0;comment:对话数量"` + MessageCount int `json:"messageCount" gorm:"default:0;comment:消息数量"` +} + +func (AppUser) TableName() string { + return "app_users" +} diff --git a/server/model/app/app_user_session.go b/server/model/app/app_user_session.go new file mode 100644 index 0000000..17ac6cb --- /dev/null +++ b/server/model/app/app_user_session.go @@ -0,0 +1,25 @@ +package app + +import ( + "time" + + "git.echol.cn/loser/st/server/global" + "gorm.io/datatypes" +) + +// AppUserSession 前台用户会话 +type AppUserSession struct { + global.GVA_MODEL + UserID uint `json:"userId" gorm:"index;comment:用户ID"` + SessionToken string `json:"sessionToken" gorm:"type:varchar(500);uniqueIndex;comment:会话Token"` + RefreshToken string `json:"refreshToken" gorm:"type:varchar(500);comment:刷新Token"` + ExpiresAt time.Time `json:"expiresAt" gorm:"index;comment:过期时间"` + RefreshExpiresAt *time.Time `json:"refreshExpiresAt" gorm:"comment:刷新Token过期时间"` + IPAddress string `json:"ipAddress" gorm:"type:varchar(100);comment:IP地址"` + UserAgent string `json:"userAgent" gorm:"type:text;comment:用户代理"` + DeviceInfo datatypes.JSON `json:"deviceInfo" gorm:"type:jsonb;comment:设备信息"` +} + +func (AppUserSession) TableName() string { + return "app_user_sessions" +} diff --git a/server/model/app/conversation.go b/server/model/app/conversation.go new file mode 100644 index 0000000..1e3128f --- /dev/null +++ b/server/model/app/conversation.go @@ -0,0 +1,56 @@ +package app + +import ( + "time" + + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// Conversation 对话会话模型 +type Conversation struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + UserID uint `gorm:"index;not null" json:"userId"` // 所属用户ID + CharacterID uint `gorm:"index;not null" json:"characterId"` // 角色卡ID + Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题 + + // 对话配置 + PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID + AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic) + Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型 + Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等) + + // 统计信息 + MessageCount int `gorm:"default:0" json:"messageCount"` // 消息数量 + TokenCount int `gorm:"default:0" json:"tokenCount"` // Token使用量 +} + +// TableName 指定表名 +func (Conversation) TableName() string { + return "conversations" +} + +// Message 消息模型 +type Message struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + ConversationID uint `gorm:"index;not null" json:"conversationId"` // 所属对话ID + Role string `gorm:"type:varchar(20);not null" json:"role"` // 角色 (user/assistant/system) + Content string `gorm:"type:text;not null" json:"content"` // 消息内容 + + // 元数据 + TokenCount int `gorm:"default:0" json:"tokenCount"` // Token数量 + Metadata datatypes.JSON `gorm:"type:jsonb" json:"metadata"` // 额外元数据 +} + +// TableName 指定表名 +func (Message) TableName() string { + return "messages" +} diff --git a/server/model/app/preset.go b/server/model/app/preset.go new file mode 100644 index 0000000..f1bb414 --- /dev/null +++ b/server/model/app/preset.go @@ -0,0 +1,44 @@ +package app + +import ( + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// AIPreset AI预设配置模型 +type AIPreset struct { + gorm.Model + UserID uint `json:"userId" gorm:"not null;index:idx_preset_user"` + Name string `json:"name" gorm:"not null;size:100"` + Description string `json:"description" gorm:"size:500"` + IsPublic bool `json:"isPublic" gorm:"default:false;index:idx_preset_public"` + IsDefault bool `json:"isDefault" gorm:"default:false"` + + // Sampling Parameters (stored as individual fields for query efficiency) + Temperature float64 `json:"temperature" gorm:"default:1.0"` + TopP float64 `json:"topP" gorm:"default:1.0"` + TopK int `json:"topK" gorm:"default:0"` + FrequencyPenalty float64 `json:"frequencyPenalty" gorm:"default:0"` + PresencePenalty float64 `json:"presencePenalty" gorm:"default:0"` + MaxTokens int `json:"maxTokens" gorm:"default:2000"` + RepetitionPenalty float64 `json:"repetitionPenalty" gorm:"default:1.0"` + MinP float64 `json:"minP" gorm:"default:0"` + TopA float64 `json:"topA" gorm:"default:0"` + + // Prompt Configuration (stored as JSON for flexibility) + SystemPrompt string `json:"systemPrompt" gorm:"type:text"` + StopSequences datatypes.JSON `json:"stopSequences" gorm:"type:jsonb"` + + // SillyTavern Extensions (for full compatibility) + Extensions datatypes.JSON `json:"extensions" gorm:"type:jsonb"` + + // Usage Statistics + UseCount int `json:"useCount" gorm:"default:0"` + + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +// TableName 指定表名 +func (AIPreset) TableName() string { + return "ai_presets" +} diff --git a/server/model/app/request/ai_config.go b/server/model/app/request/ai_config.go new file mode 100644 index 0000000..7f05ac6 --- /dev/null +++ b/server/model/app/request/ai_config.go @@ -0,0 +1,37 @@ +package request + +// CreateAIConfigRequest 创建AI配置请求 +type CreateAIConfigRequest struct { + Name string `json:"name" binding:"required,max=100"` + Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"` + BaseURL string `json:"baseUrl" binding:"required,url"` + APIKey string `json:"apiKey" binding:"required"` + DefaultModel string `json:"defaultModel"` + Settings map[string]interface{} `json:"settings"` +} + +// UpdateAIConfigRequest 更新AI配置请求 +type UpdateAIConfigRequest struct { + Name string `json:"name" binding:"max=100"` + BaseURL string `json:"baseUrl" binding:"omitempty,url"` + APIKey string `json:"apiKey"` + DefaultModel string `json:"defaultModel"` + Settings map[string]interface{} `json:"settings"` + IsActive *bool `json:"isActive"` + IsDefault *bool `json:"isDefault"` +} + +// TestAIConfigRequest 测试AI配置请求 +type TestAIConfigRequest struct { + Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"` + BaseURL string `json:"baseUrl" binding:"required,url"` + APIKey string `json:"apiKey" binding:"required"` + Model string `json:"model"` +} + +// GetModelsRequest 获取模型列表请求 +type GetModelsRequest struct { + Provider string `json:"provider" binding:"required,oneof=openai anthropic custom"` + BaseURL string `json:"baseUrl" binding:"required,url"` + APIKey string `json:"apiKey" binding:"required"` +} diff --git a/server/model/app/request/auth.go b/server/model/app/request/auth.go new file mode 100644 index 0000000..d718f77 --- /dev/null +++ b/server/model/app/request/auth.go @@ -0,0 +1,37 @@ +package request + +// RegisterRequest 用户注册请求 +type RegisterRequest struct { + Username string `json:"username" binding:"required,min=3,max=32"` + Password string `json:"password" binding:"required,min=6,max=32"` + NickName string `json:"nickName" binding:"max=50"` + Email string `json:"email" binding:"omitempty,email"` + Phone string `json:"phone" binding:"omitempty"` +} + +// LoginRequest 用户登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// RefreshTokenRequest 刷新 Token 请求 +type RefreshTokenRequest struct { + RefreshToken string `json:"refreshToken" binding:"required"` +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword" binding:"required"` + NewPassword string `json:"newPassword" binding:"required,min=6,max=32"` +} + +// UpdateProfileRequest 更新用户信息请求 +type UpdateProfileRequest struct { + NickName string `json:"nickName" binding:"max=50"` + Email string `json:"email" binding:"omitempty,email"` + Phone string `json:"phone"` + Avatar string `json:"avatar"` + Preferences string `json:"preferences"` // JSON 字符串 + AISettings string `json:"aiSettings"` // JSON 字符串 +} diff --git a/server/model/app/request/character.go b/server/model/app/request/character.go new file mode 100644 index 0000000..3d70ba0 --- /dev/null +++ b/server/model/app/request/character.go @@ -0,0 +1,52 @@ +package request + +// CreateCharacterRequest 创建角色卡请求 +type CreateCharacterRequest struct { + Name string `json:"name" binding:"required,max=100"` + Avatar string `json:"avatar"` + Creator string `json:"creator" binding:"max=100"` + Version string `json:"version" binding:"max=50"` + Description string `json:"description"` + Personality string `json:"personality"` + Scenario string `json:"scenario"` + FirstMes string `json:"firstMes"` + MesExample string `json:"mesExample"` + CreatorNotes string `json:"creatorNotes"` + SystemPrompt string `json:"systemPrompt"` + PostHistoryInstructions string `json:"postHistoryInstructions"` + Tags []string `json:"tags"` + AlternateGreetings []string `json:"alternateGreetings"` + CharacterBook map[string]interface{} `json:"characterBook"` + Extensions map[string]interface{} `json:"extensions"` + IsPublic bool `json:"isPublic"` +} + +// UpdateCharacterRequest 更新角色卡请求 +type UpdateCharacterRequest struct { + Name string `json:"name" binding:"max=100"` + Avatar string `json:"avatar"` + Creator string `json:"creator" binding:"max=100"` + Version string `json:"version" binding:"max=50"` + Description string `json:"description"` + Personality string `json:"personality"` + Scenario string `json:"scenario"` + FirstMes string `json:"firstMes"` + MesExample string `json:"mesExample"` + CreatorNotes string `json:"creatorNotes"` + SystemPrompt string `json:"systemPrompt"` + PostHistoryInstructions string `json:"postHistoryInstructions"` + Tags []string `json:"tags"` + AlternateGreetings []string `json:"alternateGreetings"` + CharacterBook map[string]interface{} `json:"characterBook"` + Extensions map[string]interface{} `json:"extensions"` + IsPublic bool `json:"isPublic"` +} + +// GetCharacterListRequest 获取角色卡列表请求 +type GetCharacterListRequest struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"pageSize" binding:"min=1,max=100"` + Keyword string `form:"keyword"` + Tag string `form:"tag"` + IsPublic *bool `form:"isPublic"` +} diff --git a/server/model/app/request/conversation.go b/server/model/app/request/conversation.go new file mode 100644 index 0000000..a179690 --- /dev/null +++ b/server/model/app/request/conversation.go @@ -0,0 +1,32 @@ +package request + +// CreateConversationRequest 创建对话请求 +type CreateConversationRequest struct { + CharacterID uint `json:"characterId" binding:"required"` + Title string `json:"title" binding:"max=200"` + PresetID *uint `json:"presetId"` + AIProvider string `json:"aiProvider" binding:"omitempty,oneof=openai anthropic"` + Model string `json:"model"` +} + +// SendMessageRequest 发送消息请求 +type SendMessageRequest struct { + Content string `json:"content" binding:"required"` +} + +// GetConversationListRequest 获取对话列表请求 +type GetConversationListRequest struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"pageSize" binding:"min=1,max=100"` +} + +// GetMessageListRequest 获取消息列表请求 +type GetMessageListRequest struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"pageSize" binding:"min=1,max=100"` +} + +// UpdateConversationSettingsRequest 更新对话设置请求 +type UpdateConversationSettingsRequest struct { + Settings map[string]interface{} `json:"settings" binding:"required"` +} diff --git a/server/model/app/request/preset.go b/server/model/app/request/preset.go new file mode 100644 index 0000000..bf574b4 --- /dev/null +++ b/server/model/app/request/preset.go @@ -0,0 +1,47 @@ +package request + +// CreatePresetRequest 创建预设请求 +type CreatePresetRequest struct { + Name string `json:"name" binding:"required,min=1,max=100"` + Description string `json:"description" binding:"max=500"` + IsPublic bool `json:"isPublic"` + Temperature float64 `json:"temperature" binding:"min=0,max=2"` + TopP float64 `json:"topP" binding:"min=0,max=1"` + TopK int `json:"topK" binding:"min=0"` + FrequencyPenalty float64 `json:"frequencyPenalty" binding:"min=-2,max=2"` + PresencePenalty float64 `json:"presencePenalty" binding:"min=-2,max=2"` + MaxTokens int `json:"maxTokens" binding:"min=1,max=32000"` + RepetitionPenalty float64 `json:"repetitionPenalty"` + MinP float64 `json:"minP"` + TopA float64 `json:"topA"` + SystemPrompt string `json:"systemPrompt"` + StopSequences []string `json:"stopSequences"` + Extensions map[string]interface{} `json:"extensions"` +} + +// UpdatePresetRequest 更新预设请求 +type UpdatePresetRequest struct { + Name string `json:"name" binding:"min=1,max=100"` + Description string `json:"description" binding:"max=500"` + IsPublic *bool `json:"isPublic"` + Temperature *float64 `json:"temperature" binding:"omitempty,min=0,max=2"` + TopP *float64 `json:"topP" binding:"omitempty,min=0,max=1"` + TopK *int `json:"topK" binding:"omitempty,min=0"` + FrequencyPenalty *float64 `json:"frequencyPenalty" binding:"omitempty,min=-2,max=2"` + PresencePenalty *float64 `json:"presencePenalty" binding:"omitempty,min=-2,max=2"` + MaxTokens *int `json:"maxTokens" binding:"omitempty,min=1,max=32000"` + RepetitionPenalty *float64 `json:"repetitionPenalty"` + MinP *float64 `json:"minP"` + TopA *float64 `json:"topA"` + SystemPrompt *string `json:"systemPrompt"` + StopSequences []string `json:"stopSequences"` + Extensions map[string]interface{} `json:"extensions"` +} + +// GetPresetListRequest 获取预设列表请求 +type GetPresetListRequest struct { + Page int `form:"page" binding:"min=1"` + PageSize int `form:"pageSize" binding:"min=1,max=100"` + Keyword string `form:"keyword"` + IsPublic *bool `form:"isPublic"` +} diff --git a/server/model/app/response/ai_config.go b/server/model/app/response/ai_config.go new file mode 100644 index 0000000..7c52e09 --- /dev/null +++ b/server/model/app/response/ai_config.go @@ -0,0 +1,88 @@ +package response + +import ( + "encoding/json" + "time" + + "git.echol.cn/loser/st/server/model/app" +) + +// AIConfigResponse AI配置响应 +type AIConfigResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + BaseURL string `json:"baseUrl"` + APIKey string `json:"apiKey"` // 前端显示时应该脱敏 + Models []string `json:"models"` + DefaultModel string `json:"defaultModel"` + Settings map[string]interface{} `json:"settings"` + IsActive bool `json:"isActive"` + IsDefault bool `json:"isDefault"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// AIConfigListResponse AI配置列表响应 +type AIConfigListResponse struct { + List []AIConfigResponse `json:"list"` + Total int64 `json:"total"` +} + +// TestAIConfigResponse 测试AI配置响应 +type TestAIConfigResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Latency int64 `json:"latency"` // 响应延迟(ms) +} + +// GetModelsResponse 获取模型列表响应 +type GetModelsResponse struct { + Models []ModelInfo `json:"models"` +} + +// ModelInfo 模型信息 +type ModelInfo struct { + ID string `json:"id"` + Name string `json:"name"` + OwnedBy string `json:"ownedBy"` +} + +// ToAIConfigResponse 转换为AI配置响应结构 +func ToAIConfigResponse(config *app.AIConfig) AIConfigResponse { + resp := AIConfigResponse{ + ID: config.ID, + Name: config.Name, + Provider: config.Provider, + BaseURL: config.BaseURL, + APIKey: maskAPIKey(config.APIKey), + DefaultModel: config.DefaultModel, + IsActive: config.IsActive, + IsDefault: config.IsDefault, + CreatedAt: config.CreatedAt, + UpdatedAt: config.UpdatedAt, + } + + // 解析 JSON 字段 + if len(config.Models) > 0 { + var models []string + json.Unmarshal(config.Models, &models) + resp.Models = models + } + + if len(config.Settings) > 0 { + var settings map[string]interface{} + json.Unmarshal(config.Settings, &settings) + resp.Settings = settings + } + + return resp +} + +// maskAPIKey 脱敏API Key +func maskAPIKey(apiKey string) string { + if len(apiKey) <= 8 { + return "****" + } + return apiKey[:4] + "****" + apiKey[len(apiKey)-4:] +} diff --git a/server/model/app/response/auth.go b/server/model/app/response/auth.go new file mode 100644 index 0000000..f86f177 --- /dev/null +++ b/server/model/app/response/auth.go @@ -0,0 +1,55 @@ +package response + +import ( + "time" + + "git.echol.cn/loser/st/server/model/app" +) + +// AppUserResponse 用户信息响应 +type AppUserResponse struct { + ID uint `json:"id"` + UUID string `json:"uuid"` + Username string `json:"username"` + NickName string `json:"nickName"` + Email string `json:"email"` + Phone string `json:"phone"` + Avatar string `json:"avatar"` + Status string `json:"status"` + Enable bool `json:"enable"` + IsAdmin bool `json:"isAdmin"` + LastLoginAt *time.Time `json:"lastLoginAt"` + LastLoginIP string `json:"lastLoginIp"` + ChatCount int `json:"chatCount"` + MessageCount int `json:"messageCount"` + CreatedAt time.Time `json:"createdAt"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + User AppUserResponse `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refreshToken"` + ExpiresAt int64 `json:"expiresAt"` +} + +// ToAppUserResponse 转换为用户响应结构 +func ToAppUserResponse(user *app.AppUser) AppUserResponse { + return AppUserResponse{ + ID: user.ID, + UUID: user.UUID, + Username: user.Username, + NickName: user.NickName, + Email: user.Email, + Phone: user.Phone, + Avatar: user.Avatar, + Status: user.Status, + Enable: user.Enable, + IsAdmin: user.IsAdmin, + LastLoginAt: user.LastLoginAt, + LastLoginIP: user.LastLoginIP, + ChatCount: user.ChatCount, + MessageCount: user.MessageCount, + CreatedAt: user.CreatedAt, + } +} diff --git a/server/model/app/response/character.go b/server/model/app/response/character.go new file mode 100644 index 0000000..e58ffa0 --- /dev/null +++ b/server/model/app/response/character.go @@ -0,0 +1,108 @@ +package response + +import ( + "encoding/json" + "time" + + "git.echol.cn/loser/st/server/model/app" +) + +// CharacterResponse 角色卡响应 +type CharacterResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Creator string `json:"creator"` + Version string `json:"version"` + Description string `json:"description"` + Personality string `json:"personality"` + Scenario string `json:"scenario"` + FirstMes string `json:"firstMes"` + MesExample string `json:"mesExample"` + CreatorNotes string `json:"creatorNotes"` + SystemPrompt string `json:"systemPrompt"` + PostHistoryInstructions string `json:"postHistoryInstructions"` + Tags []string `json:"tags"` + AlternateGreetings []string `json:"alternateGreetings"` + CharacterBook map[string]interface{} `json:"characterBook"` + Extensions map[string]interface{} `json:"extensions"` + Spec string `json:"spec"` + SpecVersion string `json:"specVersion"` + IsPublic bool `json:"isPublic"` + UseCount int `json:"useCount"` + FavoriteCount int `json:"favoriteCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// CharacterSimpleResponse 角色卡简化响应(用于列表) +type CharacterSimpleResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// CharacterListResponse 角色卡列表响应 +type CharacterListResponse struct { + List []CharacterResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// ToCharacterResponse 转换为角色卡响应结构 +func ToCharacterResponse(character *app.AICharacter) CharacterResponse { + resp := CharacterResponse{ + ID: character.ID, + Name: character.Name, + Avatar: character.Avatar, + Creator: character.Creator, + Version: character.Version, + Description: character.Description, + Personality: character.Personality, + Scenario: character.Scenario, + FirstMes: character.FirstMes, + MesExample: character.MesExample, + CreatorNotes: character.CreatorNotes, + SystemPrompt: character.SystemPrompt, + PostHistoryInstructions: character.PostHistoryInstructions, + Spec: character.Spec, + SpecVersion: character.SpecVersion, + IsPublic: character.IsPublic, + UseCount: character.UseCount, + FavoriteCount: character.FavoriteCount, + CreatedAt: character.CreatedAt, + UpdatedAt: character.UpdatedAt, + } + + // 解析 JSON 字段 + if len(character.Tags) > 0 { + json.Unmarshal(character.Tags, &resp.Tags) + } + if len(character.AlternateGreetings) > 0 { + json.Unmarshal(character.AlternateGreetings, &resp.AlternateGreetings) + } + if len(character.CharacterBook) > 0 { + json.Unmarshal(character.CharacterBook, &resp.CharacterBook) + } + if len(character.Extensions) > 0 { + json.Unmarshal(character.Extensions, &resp.Extensions) + } + + return resp +} + +// ToCharacterSimpleResponse 转换为角色卡简化响应结构(用于列表) +func ToCharacterSimpleResponse(character *app.AICharacter) CharacterSimpleResponse { + return CharacterSimpleResponse{ + ID: character.ID, + Name: character.Name, + Avatar: character.Avatar, + Description: character.Description, + CreatedAt: character.CreatedAt, + UpdatedAt: character.UpdatedAt, + } +} diff --git a/server/model/app/response/conversation.go b/server/model/app/response/conversation.go new file mode 100644 index 0000000..858f5d9 --- /dev/null +++ b/server/model/app/response/conversation.go @@ -0,0 +1,122 @@ +package response + +import ( + "encoding/json" + "time" + + "git.echol.cn/loser/st/server/model/app" +) + +// ConversationResponse 对话响应 +type ConversationResponse struct { + ID uint `json:"id"` + CharacterID uint `json:"characterId"` + Title string `json:"title"` + PresetID *uint `json:"presetId"` + AIProvider string `json:"aiProvider"` + Model string `json:"model"` + Settings map[string]interface{} `json:"settings,omitempty"` + MessageCount int `json:"messageCount"` + TokenCount int `json:"tokenCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + + // 关联数据 + Character *CharacterResponse `json:"character,omitempty"` +} + +// ConversationListItemResponse 对话列表项响应(轻量级) +type ConversationListItemResponse struct { + ID uint `json:"id"` + CharacterID uint `json:"characterId"` + Title string `json:"title"` + MessageCount int `json:"messageCount"` + TokenCount int `json:"tokenCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Character *CharacterSimpleResponse `json:"character,omitempty"` +} + +// MessageResponse 消息响应 +type MessageResponse struct { + ID uint `json:"id"` + ConversationID uint `json:"conversationId"` + Role string `json:"role"` + Content string `json:"content"` + TokenCount int `json:"tokenCount"` + CreatedAt time.Time `json:"createdAt"` +} + +// ConversationListResponse 对话列表响应 +type ConversationListResponse struct { + List []ConversationListItemResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// MessageListResponse 消息列表响应 +type MessageListResponse struct { + List []MessageResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// ToConversationResponse 转换为对话响应结构 +func ToConversationResponse(conv *app.Conversation) ConversationResponse { + // 解析 settings JSON + var settings map[string]interface{} + if len(conv.Settings) > 0 { + // 尝试解析 JSON,如果失败则返回 nil + if err := json.Unmarshal(conv.Settings, &settings); err != nil { + settings = nil + } + } + + return ConversationResponse{ + ID: conv.ID, + CharacterID: conv.CharacterID, + Title: conv.Title, + PresetID: conv.PresetID, + AIProvider: conv.AIProvider, + Model: conv.Model, + Settings: settings, + MessageCount: conv.MessageCount, + TokenCount: conv.TokenCount, + CreatedAt: conv.CreatedAt, + UpdatedAt: conv.UpdatedAt, + } +} + +// ToMessageResponse 转换为消息响应结构 +func ToMessageResponse(msg *app.Message) MessageResponse { + return MessageResponse{ + ID: msg.ID, + ConversationID: msg.ConversationID, + Role: msg.Role, + Content: msg.Content, + TokenCount: msg.TokenCount, + CreatedAt: msg.CreatedAt, + } +} + +// ToConversationListItemResponse 转换为对话列表项响应结构(轻量级) +func ToConversationListItemResponse(conv *app.Conversation, character *app.AICharacter) ConversationListItemResponse { + resp := ConversationListItemResponse{ + ID: conv.ID, + CharacterID: conv.CharacterID, + Title: conv.Title, + MessageCount: conv.MessageCount, + TokenCount: conv.TokenCount, + CreatedAt: conv.CreatedAt, + UpdatedAt: conv.UpdatedAt, + } + + if character != nil { + simpleChar := ToCharacterSimpleResponse(character) + resp.Character = &simpleChar + } + + return resp +} diff --git a/server/model/app/response/preset.go b/server/model/app/response/preset.go new file mode 100644 index 0000000..99700db --- /dev/null +++ b/server/model/app/response/preset.go @@ -0,0 +1,78 @@ +package response + +import ( + "encoding/json" + "time" + + "git.echol.cn/loser/st/server/model/app" +) + +// PresetResponse 预设响应 +type PresetResponse struct { + ID uint `json:"id"` + UserID uint `json:"userId"` + Name string `json:"name"` + Description string `json:"description"` + IsPublic bool `json:"isPublic"` + IsDefault bool `json:"isDefault"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` + TopK int `json:"topK"` + FrequencyPenalty float64 `json:"frequencyPenalty"` + PresencePenalty float64 `json:"presencePenalty"` + MaxTokens int `json:"maxTokens"` + RepetitionPenalty float64 `json:"repetitionPenalty"` + MinP float64 `json:"minP"` + TopA float64 `json:"topA"` + SystemPrompt string `json:"systemPrompt"` + StopSequences []string `json:"stopSequences"` + Extensions map[string]interface{} `json:"extensions"` + UseCount int `json:"useCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// PresetListResponse 预设列表响应 +type PresetListResponse struct { + List []PresetResponse `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +// ToPresetResponse 转换为预设响应结构 +func ToPresetResponse(preset *app.AIPreset) PresetResponse { + var stopSequences []string + if len(preset.StopSequences) > 0 { + json.Unmarshal(preset.StopSequences, &stopSequences) + } + + var extensions map[string]interface{} + if len(preset.Extensions) > 0 { + json.Unmarshal(preset.Extensions, &extensions) + } + + return PresetResponse{ + ID: preset.ID, + UserID: preset.UserID, + Name: preset.Name, + Description: preset.Description, + IsPublic: preset.IsPublic, + IsDefault: preset.IsDefault, + Temperature: preset.Temperature, + TopP: preset.TopP, + TopK: preset.TopK, + FrequencyPenalty: preset.FrequencyPenalty, + PresencePenalty: preset.PresencePenalty, + MaxTokens: preset.MaxTokens, + RepetitionPenalty: preset.RepetitionPenalty, + MinP: preset.MinP, + TopA: preset.TopA, + SystemPrompt: preset.SystemPrompt, + StopSequences: stopSequences, + Extensions: extensions, + UseCount: preset.UseCount, + CreatedAt: preset.CreatedAt, + UpdatedAt: preset.UpdatedAt, + } +} diff --git a/server/model/common/basetypes.go b/server/model/common/basetypes.go new file mode 100644 index 0000000..1a133d5 --- /dev/null +++ b/server/model/common/basetypes.go @@ -0,0 +1,43 @@ +package common + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +type JSONMap map[string]interface{} + +func (m JSONMap) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + return json.Marshal(m) +} + +func (m *JSONMap) Scan(value interface{}) error { + if value == nil { + *m = make(map[string]interface{}) + return nil + } + var err error + switch value.(type) { + case []byte: + err = json.Unmarshal(value.([]byte), m) + case string: + err = json.Unmarshal([]byte(value.(string)), m) + default: + err = errors.New("basetypes.JSONMap.Scan: invalid value type") + } + if err != nil { + return err + } + return nil +} + +type TreeNode[T any] interface { + GetChildren() []T + SetChildren(children T) + GetID() int + GetParentID() int +} diff --git a/server/model/common/clearDB.go b/server/model/common/clearDB.go new file mode 100644 index 0000000..e7fc757 --- /dev/null +++ b/server/model/common/clearDB.go @@ -0,0 +1,7 @@ +package common + +type ClearDB struct { + TableName string + CompareField string + Interval string +} diff --git a/server/model/common/common.go b/server/model/common/common.go new file mode 100644 index 0000000..d9c6595 --- /dev/null +++ b/server/model/common/common.go @@ -0,0 +1,21 @@ +package common + +import ( + "github.com/gin-gonic/gin" +) + +// GetAppUserID 从上下文获取前台用户 ID +func GetAppUserID(c *gin.Context) uint { + if userID, exists := c.Get("appUserId"); exists { + return userID.(uint) + } + return 0 +} + +// GetAppUsername 从上下文获取前台用户名 +func GetAppUsername(c *gin.Context) string { + if username, exists := c.Get("appUsername"); exists { + return username.(string) + } + return "" +} diff --git a/server/model/common/request/common.go b/server/model/common/request/common.go new file mode 100644 index 0000000..b07611d --- /dev/null +++ b/server/model/common/request/common.go @@ -0,0 +1,48 @@ +package request + +import ( + "gorm.io/gorm" +) + +// PageInfo Paging common input parameter structure +type PageInfo struct { + Page int `json:"page" form:"page,default=1"` // 页码 + PageSize int `json:"pageSize" form:"pageSize,default=20"` // 每页大小 + Keyword string `json:"keyword" form:"keyword"` // 关键字 +} + +func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if r.Page <= 0 { + r.Page = 1 + } + switch { + case r.PageSize > 100: + r.PageSize = 100 + case r.PageSize <= 0: + r.PageSize = 10 + } + offset := (r.Page - 1) * r.PageSize + return db.Offset(offset).Limit(r.PageSize) + } +} + +// GetById Find by id structure +type GetById struct { + ID int `json:"id" form:"id"` // 主键ID +} + +func (r *GetById) Uint() uint { + return uint(r.ID) +} + +type IdsReq struct { + Ids []int `json:"ids" form:"ids"` +} + +// GetAuthorityId Get role by id structure +type GetAuthorityId struct { + AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID +} + +type Empty struct{} diff --git a/server/model/common/response/common.go b/server/model/common/response/common.go new file mode 100644 index 0000000..7461096 --- /dev/null +++ b/server/model/common/response/common.go @@ -0,0 +1,8 @@ +package response + +type PageResult struct { + List interface{} `json:"list"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} diff --git a/server/model/common/response/response.go b/server/model/common/response/response.go new file mode 100644 index 0000000..f0e0e53 --- /dev/null +++ b/server/model/common/response/response.go @@ -0,0 +1,62 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type Response struct { + Code int `json:"code"` + Data interface{} `json:"data"` + Msg string `json:"msg"` +} + +const ( + ERROR = 7 + SUCCESS = 0 +) + +func Result(code int, data interface{}, msg string, c *gin.Context) { + c.JSON(http.StatusOK, Response{ + code, + data, + msg, + }) +} + +func Ok(c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, "操作成功", c) +} + +func OkWithMessage(message string, c *gin.Context) { + Result(SUCCESS, map[string]interface{}{}, message, c) +} + +func OkWithData(data interface{}, c *gin.Context) { + Result(SUCCESS, data, "成功", c) +} + +func OkWithDetailed(data interface{}, message string, c *gin.Context) { + Result(SUCCESS, data, message, c) +} + +func Fail(c *gin.Context) { + Result(ERROR, map[string]interface{}{}, "操作失败", c) +} + +func FailWithMessage(message string, c *gin.Context) { + Result(ERROR, map[string]interface{}{}, message, c) +} + +func NoAuth(message string, c *gin.Context) { + c.JSON(http.StatusUnauthorized, Response{ + 7, + nil, + message, + }) +} + +func FailWithDetailed(data interface{}, message string, c *gin.Context) { + Result(ERROR, data, message, c) +} diff --git a/server/model/example/exa_attachment_category.go b/server/model/example/exa_attachment_category.go new file mode 100644 index 0000000..0af352e --- /dev/null +++ b/server/model/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "git.echol.cn/loser/st/server/global" +) + +type ExaAttachmentCategory struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"default:null;type:varchar(255);column:name;comment:分类名称;"` + Pid uint `json:"pid" form:"pid" gorm:"default:0;type:int;column:pid;comment:父节点ID;"` + Children []*ExaAttachmentCategory `json:"children" gorm:"-"` +} + +func (ExaAttachmentCategory) TableName() string { + return "exa_attachment_category" +} diff --git a/server/model/example/exa_breakpoint_continue.go b/server/model/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..e541ee7 --- /dev/null +++ b/server/model/example/exa_breakpoint_continue.go @@ -0,0 +1,24 @@ +package example + +import ( + "git.echol.cn/loser/st/server/global" +) + +// file struct, 文件结构体 +type ExaFile struct { + global.GVA_MODEL + FileName string + FileMd5 string + FilePath string + ExaFileChunk []ExaFileChunk + ChunkTotal int + IsFinish bool +} + +// file chunk struct, 切片结构体 +type ExaFileChunk struct { + global.GVA_MODEL + ExaFileID uint + FileChunkNumber int + FileChunkPath string +} diff --git a/server/model/example/exa_customer.go b/server/model/example/exa_customer.go new file mode 100644 index 0000000..eff8904 --- /dev/null +++ b/server/model/example/exa_customer.go @@ -0,0 +1,15 @@ +package example + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" +) + +type ExaCustomer struct { + global.GVA_MODEL + CustomerName string `json:"customerName" form:"customerName" gorm:"comment:客户名"` // 客户名 + CustomerPhoneData string `json:"customerPhoneData" form:"customerPhoneData" gorm:"comment:客户手机号"` // 客户手机号 + SysUserID uint `json:"sysUserId" form:"sysUserId" gorm:"comment:管理ID"` // 管理ID + SysUserAuthorityID uint `json:"sysUserAuthorityID" form:"sysUserAuthorityID" gorm:"comment:管理角色ID"` // 管理角色ID + SysUser system.SysUser `json:"sysUser" form:"sysUser" gorm:"comment:管理详情"` // 管理详情 +} diff --git a/server/model/example/exa_file_upload_download.go b/server/model/example/exa_file_upload_download.go new file mode 100644 index 0000000..9f09fd7 --- /dev/null +++ b/server/model/example/exa_file_upload_download.go @@ -0,0 +1,18 @@ +package example + +import ( + "git.echol.cn/loser/st/server/global" +) + +type ExaFileUploadAndDownload struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:文件名"` // 文件名 + ClassId int `json:"classId" form:"classId" gorm:"default:0;type:int;column:class_id;comment:分类id;"` // 分类id + Url string `json:"url" form:"url" gorm:"column:url;comment:文件地址"` // 文件地址 + Tag string `json:"tag" form:"tag" gorm:"column:tag;comment:文件标签"` // 文件标签 + Key string `json:"key" form:"key" gorm:"column:key;comment:编号"` // 编号 +} + +func (ExaFileUploadAndDownload) TableName() string { + return "exa_file_upload_and_downloads" +} diff --git a/server/model/example/request/exa_file_upload_and_downloads.go b/server/model/example/request/exa_file_upload_and_downloads.go new file mode 100644 index 0000000..20dd62b --- /dev/null +++ b/server/model/example/request/exa_file_upload_and_downloads.go @@ -0,0 +1,10 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" +) + +type ExaAttachmentCategorySearch struct { + ClassId int `json:"classId" form:"classId"` + request.PageInfo +} diff --git a/server/model/example/response/exa_breakpoint_continue.go b/server/model/example/response/exa_breakpoint_continue.go new file mode 100644 index 0000000..0a17ddd --- /dev/null +++ b/server/model/example/response/exa_breakpoint_continue.go @@ -0,0 +1,11 @@ +package response + +import "git.echol.cn/loser/st/server/model/example" + +type FilePathResponse struct { + FilePath string `json:"filePath"` +} + +type FileResponse struct { + File example.ExaFile `json:"file"` +} diff --git a/server/model/example/response/exa_customer.go b/server/model/example/response/exa_customer.go new file mode 100644 index 0000000..040bbf4 --- /dev/null +++ b/server/model/example/response/exa_customer.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/st/server/model/example" + +type ExaCustomerResponse struct { + Customer example.ExaCustomer `json:"customer"` +} diff --git a/server/model/example/response/exa_file_upload_download.go b/server/model/example/response/exa_file_upload_download.go new file mode 100644 index 0000000..9153521 --- /dev/null +++ b/server/model/example/response/exa_file_upload_download.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/st/server/model/example" + +type ExaFileResponse struct { + File example.ExaFileUploadAndDownload `json:"file"` +} diff --git a/server/model/system/request/jwt.go b/server/model/system/request/jwt.go new file mode 100644 index 0000000..1e1615d --- /dev/null +++ b/server/model/system/request/jwt.go @@ -0,0 +1,21 @@ +package request + +import ( + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +// CustomClaims structure +type CustomClaims struct { + BaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +type BaseClaims struct { + UUID uuid.UUID + ID uint + Username string + NickName string + AuthorityId uint +} diff --git a/server/model/system/request/sys_api.go b/server/model/system/request/sys_api.go new file mode 100644 index 0000000..9f87cd0 --- /dev/null +++ b/server/model/system/request/sys_api.go @@ -0,0 +1,14 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +// api分页条件查询及排序结构体 +type SearchApiParams struct { + system.SysApi + request.PageInfo + OrderKey string `json:"orderKey"` // 排序 + Desc bool `json:"desc"` // 排序方式:升序false(默认)|降序true +} diff --git a/server/model/system/request/sys_api_token.go b/server/model/system/request/sys_api_token.go new file mode 100644 index 0000000..16d955c --- /dev/null +++ b/server/model/system/request/sys_api_token.go @@ -0,0 +1,12 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysApiTokenSearch struct { + system.SysApiToken + request.PageInfo + Status *bool `json:"status" form:"status"` +} diff --git a/server/model/system/request/sys_authority_btn.go b/server/model/system/request/sys_authority_btn.go new file mode 100644 index 0000000..98493ff --- /dev/null +++ b/server/model/system/request/sys_authority_btn.go @@ -0,0 +1,7 @@ +package request + +type SysAuthorityBtnReq struct { + MenuID uint `json:"menuID"` + AuthorityId uint `json:"authorityId"` + Selected []uint `json:"selected"` +} diff --git a/server/model/system/request/sys_auto_code.go b/server/model/system/request/sys_auto_code.go new file mode 100644 index 0000000..fe5d2fe --- /dev/null +++ b/server/model/system/request/sys_auto_code.go @@ -0,0 +1,292 @@ +package request + +import ( + "encoding/json" + "fmt" + "go/token" + "strings" + + "git.echol.cn/loser/st/server/global" + model "git.echol.cn/loser/st/server/model/system" + "github.com/pkg/errors" +) + +type AutoCode struct { + Package string `json:"package"` + PackageT string `json:"-"` + TableName string `json:"tableName" example:"表名"` // 表名 + BusinessDB string `json:"businessDB" example:"业务数据库"` // 业务数据库 + StructName string `json:"structName" example:"Struct名称"` // Struct名称 + PackageName string `json:"packageName" example:"文件名称"` // 文件名称 + Description string `json:"description" example:"Struct中文名称"` // Struct中文名称 + Abbreviation string `json:"abbreviation" example:"Struct简称"` // Struct简称 + HumpPackageName string `json:"humpPackageName" example:"go文件名称"` // go文件名称 + GvaModel bool `json:"gvaModel" example:"false"` // 是否使用gva默认Model + AutoMigrate bool `json:"autoMigrate" example:"false"` // 是否自动迁移表结构 + AutoCreateResource bool `json:"autoCreateResource" example:"false"` // 是否自动创建资源标识 + AutoCreateApiToSql bool `json:"autoCreateApiToSql" example:"false"` // 是否自动创建api + AutoCreateMenuToSql bool `json:"autoCreateMenuToSql" example:"false"` // 是否自动创建menu + AutoCreateBtnAuth bool `json:"autoCreateBtnAuth" example:"false"` // 是否自动创建按钮权限 + OnlyTemplate bool `json:"onlyTemplate" example:"false"` // 是否只生成模板 + IsTree bool `json:"isTree" example:"false"` // 是否树形结构 + TreeJson string `json:"treeJson" example:"展示的树json字段"` // 展示的树json字段 + IsAdd bool `json:"isAdd" example:"false"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` + GenerateWeb bool `json:"generateWeb" example:"true"` // 是否生成web + GenerateServer bool `json:"generateServer" example:"true"` // 是否生成server + Module string `json:"-"` + DictTypes []string `json:"-"` + PrimaryField *AutoCodeField `json:"primaryField"` + DataSourceMap map[string]*DataSource `json:"-"` + HasPic bool `json:"-"` + HasFile bool `json:"-"` + HasTimer bool `json:"-"` + NeedSort bool `json:"-"` + NeedJSON bool `json:"-"` + HasRichText bool `json:"-"` + HasDataSource bool `json:"-"` + HasSearchTimer bool `json:"-"` + HasArray bool `json:"-"` + HasExcel bool `json:"-"` +} + +type DataSource struct { + DBName string `json:"dbName"` + Table string `json:"table"` + Label string `json:"label"` + Value string `json:"value"` + Association int `json:"association"` // 关联关系 1 一对一 2 一对多 + HasDeletedAt bool `json:"hasDeletedAt"` +} + +func (r *AutoCode) Apis() []model.SysApi { + return []model.SysApi{ + { + Path: "/" + r.Abbreviation + "/" + "create" + r.StructName, + Description: "新增" + r.Description, + ApiGroup: r.Description, + Method: "POST", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName, + Description: "删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "delete" + r.StructName + "ByIds", + Description: "批量删除" + r.Description, + ApiGroup: r.Description, + Method: "DELETE", + }, + { + Path: "/" + r.Abbreviation + "/" + "update" + r.StructName, + Description: "更新" + r.Description, + ApiGroup: r.Description, + Method: "PUT", + }, + { + Path: "/" + r.Abbreviation + "/" + "find" + r.StructName, + Description: "根据ID获取" + r.Description, + ApiGroup: r.Description, + Method: "GET", + }, + { + Path: "/" + r.Abbreviation + "/" + "get" + r.StructName + "List", + Description: "获取" + r.Description + "列表", + ApiGroup: r.Description, + Method: "GET", + }, + } +} + +func (r *AutoCode) Menu(template string) model.SysBaseMenu { + component := fmt.Sprintf("view/%s/%s/%s.vue", r.Package, r.PackageName, r.PackageName) + if template != "package" { + component = fmt.Sprintf("plugin/%s/view/%s.vue", r.Package, r.PackageName) + } + return model.SysBaseMenu{ + ParentId: 0, + Path: r.Abbreviation, + Name: r.Abbreviation, + Component: component, + Meta: model.Meta{ + Title: r.Description, + }, + } +} + +// Pretreatment 预处理 +// Author [SliverHorn](https://github.com/SliverHorn) +func (r *AutoCode) Pretreatment() error { + r.Module = global.GVA_CONFIG.AutoCode.Module + if token.IsKeyword(r.Abbreviation) { + r.Abbreviation = r.Abbreviation + "_" + } // go 关键字处理 + if strings.HasSuffix(r.HumpPackageName, "test") { + r.HumpPackageName = r.HumpPackageName + "_" + } // test + length := len(r.Fields) + dict := make(map[string]string, length) + r.DataSourceMap = make(map[string]*DataSource, length) + for i := 0; i < length; i++ { + if r.Fields[i].Excel { + r.HasExcel = true + } + if r.Fields[i].DictType != "" { + dict[r.Fields[i].DictType] = "" + } + if r.Fields[i].Sort { + r.NeedSort = true + } + switch r.Fields[i].FieldType { + case "file": + r.HasFile = true + r.NeedJSON = true + case "json": + r.NeedJSON = true + case "array": + r.NeedJSON = true + r.HasArray = true + case "video": + r.HasPic = true + case "richtext": + r.HasRichText = true + case "picture": + r.HasPic = true + case "pictures": + r.HasPic = true + r.NeedJSON = true + case "time.Time": + r.HasTimer = true + if r.Fields[i].FieldSearchType != "" && r.Fields[i].FieldSearchType != "BETWEEN" && r.Fields[i].FieldSearchType != "NOT BETWEEN" { + r.HasSearchTimer = true + } + } + if r.Fields[i].DataSource != nil { + if r.Fields[i].DataSource.Table != "" && r.Fields[i].DataSource.Label != "" && r.Fields[i].DataSource.Value != "" { + r.HasDataSource = true + r.Fields[i].CheckDataSource = true + r.DataSourceMap[r.Fields[i].FieldJson] = r.Fields[i].DataSource + } + } + if !r.GvaModel && r.PrimaryField == nil && r.Fields[i].PrimaryKey { + r.PrimaryField = r.Fields[i] + } // 自定义主键 + } + { + for key := range dict { + r.DictTypes = append(r.DictTypes, key) + } + } // DictTypes => 字典 + { + if r.GvaModel { + r.PrimaryField = &AutoCodeField{ + FieldName: "ID", + FieldType: "uint", + FieldDesc: "ID", + FieldJson: "ID", + DataTypeLong: "20", + Comment: "主键ID", + ColumnName: "id", + } + } + } // GvaModel + { + if r.IsAdd && r.PrimaryField == nil { + r.PrimaryField = new(AutoCodeField) + } + } // 新增字段模式下不关注主键 + if r.Package == "" { + return errors.New("Package为空!") + } // 增加判断:Package不为空 + packages := []rune(r.Package) + if len(packages) > 0 { + if packages[0] >= 97 && packages[0] <= 122 { + packages[0] = packages[0] - 32 + } + r.PackageT = string(packages) + } // PackageT 是 Package 的首字母大写 + return nil +} + +func (r *AutoCode) History() SysAutoHistoryCreate { + bytes, _ := json.Marshal(r) + return SysAutoHistoryCreate{ + Table: r.TableName, + Package: r.Package, + Request: string(bytes), + StructName: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + } +} + +type AutoCodeField struct { + FieldName string `json:"fieldName"` // Field名 + FieldDesc string `json:"fieldDesc"` // 中文名 + FieldType string `json:"fieldType"` // Field数据类型 + FieldJson string `json:"fieldJson"` // FieldJson + DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 + Comment string `json:"comment"` // 数据库字段描述 + ColumnName string `json:"columnName"` // 数据库字段 + FieldSearchType string `json:"fieldSearchType"` // 搜索条件 + FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 + DictType string `json:"dictType"` // 字典 + //Front bool `json:"front"` // 是否前端可见 + Form bool `json:"form"` // 是否前端新建/编辑 + Table bool `json:"table"` // 是否前端表格列 + Desc bool `json:"desc"` // 是否前端详情 + Excel bool `json:"excel"` // 是否导入/导出 + Require bool `json:"require"` // 是否必填 + DefaultValue string `json:"defaultValue"` // 是否必填 + ErrorText string `json:"errorText"` // 校验失败文字 + Clearable bool `json:"clearable"` // 是否可清空 + Sort bool `json:"sort"` // 是否增加排序 + PrimaryKey bool `json:"primaryKey"` // 是否主键 + DataSource *DataSource `json:"dataSource"` // 数据源 + CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 + FieldIndexType string `json:"fieldIndexType"` // 索引类型 +} + +type AutoFunc struct { + Package string `json:"package"` + FuncName string `json:"funcName"` // 方法名称 + Router string `json:"router"` // 路由名称 + FuncDesc string `json:"funcDesc"` // 方法介绍 + BusinessDB string `json:"businessDB"` // 业务库 + StructName string `json:"structName"` // Struct名称 + PackageName string `json:"packageName"` // 文件名称 + Description string `json:"description"` // Struct中文名称 + Abbreviation string `json:"abbreviation"` // Struct简称 + HumpPackageName string `json:"humpPackageName"` // go文件名称 + Method string `json:"method"` // 方法 + IsPlugin bool `json:"isPlugin"` // 是否插件 + IsAuth bool `json:"isAuth"` // 是否鉴权 + IsPreview bool `json:"isPreview"` // 是否预览 + IsAi bool `json:"isAi"` // 是否AI + ApiFunc string `json:"apiFunc"` // API方法 + ServerFunc string `json:"serverFunc"` // 服务方法 + JsFunc string `json:"jsFunc"` // JS方法 +} + +type InitMenu struct { + PlugName string `json:"plugName"` + ParentMenu string `json:"parentMenu"` + Menus []uint `json:"menus"` +} + +type InitApi struct { + PlugName string `json:"plugName"` + APIs []uint `json:"apis"` +} + +type InitDictionary struct { + PlugName string `json:"plugName"` + Dictionaries []uint `json:"dictionaries"` +} + +type LLMAutoCode struct { + Prompt string `json:"prompt" form:"prompt" gorm:"column:prompt;comment:提示语;type:text;"` //提示语 + Mode string `json:"mode" form:"mode" gorm:"column:mode;comment:模式;type:text;"` //模式 +} diff --git a/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 0000000..a52ec7c --- /dev/null +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,16 @@ +package request + +type AutoMcpTool struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Params []struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array + Required bool `json:"required" form:"required"` + Default string `json:"default" form:"default"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/server/model/system/request/sys_auto_code_package.go b/server/model/system/request/sys_auto_code_package.go new file mode 100644 index 0000000..e5fb49e --- /dev/null +++ b/server/model/system/request/sys_auto_code_package.go @@ -0,0 +1,31 @@ +package request + +import ( + "git.echol.cn/loser/st/server/global" + model "git.echol.cn/loser/st/server/model/system" +) + +type SysAutoCodePackageCreate struct { + Desc string `json:"desc" example:"描述"` + Label string `json:"label" example:"展示名"` + Template string `json:"template" example:"模版"` + PackageName string `json:"packageName" example:"包名"` + Module string `json:"-" example:"模块"` +} + +func (r *SysAutoCodePackageCreate) AutoCode() AutoCode { + return AutoCode{ + Package: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} + +func (r *SysAutoCodePackageCreate) Create() model.SysAutoCodePackage { + return model.SysAutoCodePackage{ + Desc: r.Desc, + Label: r.Label, + Template: r.Template, + PackageName: r.PackageName, + Module: global.GVA_CONFIG.AutoCode.Module, + } +} diff --git a/server/model/system/request/sys_auto_history.go b/server/model/system/request/sys_auto_history.go new file mode 100644 index 0000000..7e358df --- /dev/null +++ b/server/model/system/request/sys_auto_history.go @@ -0,0 +1,57 @@ +package request + +import ( + common "git.echol.cn/loser/st/server/model/common/request" + model "git.echol.cn/loser/st/server/model/system" +) + +type SysAutoHistoryCreate struct { + Table string // 表名 + Package string // 模块名/插件名 + Request string // 前端传入的结构化信息 + StructName string // 结构体名称 + BusinessDB string // 业务库 + Description string // Struct中文名称 + Injections map[string]string // 注入路径 + Templates map[string]string // 模板信息 + ApiIDs []uint // api表注册内容 + MenuID uint // 菜单ID + ExportTemplateID uint // 导出模板ID +} + +func (r *SysAutoHistoryCreate) Create() model.SysAutoCodeHistory { + entity := model.SysAutoCodeHistory{ + Package: r.Package, + Request: r.Request, + Table: r.Table, + StructName: r.StructName, + Abbreviation: r.StructName, + BusinessDB: r.BusinessDB, + Description: r.Description, + Injections: r.Injections, + Templates: r.Templates, + ApiIDs: r.ApiIDs, + MenuID: r.MenuID, + ExportTemplateID: r.ExportTemplateID, + } + if entity.Table == "" { + entity.Table = r.StructName + } + return entity +} + +type SysAutoHistoryRollBack struct { + common.GetById + DeleteApi bool `json:"deleteApi" form:"deleteApi"` // 是否删除接口 + DeleteMenu bool `json:"deleteMenu" form:"deleteMenu"` // 是否删除菜单 + DeleteTable bool `json:"deleteTable" form:"deleteTable"` // 是否删除表 +} + +func (r *SysAutoHistoryRollBack) ApiIds(entity model.SysAutoCodeHistory) common.IdsReq { + length := len(entity.ApiIDs) + ids := make([]int, 0) + for i := 0; i < length; i++ { + ids = append(ids, int(entity.ApiIDs[i])) + } + return common.IdsReq{Ids: ids} +} diff --git a/server/model/system/request/sys_casbin.go b/server/model/system/request/sys_casbin.go new file mode 100644 index 0000000..3ca4212 --- /dev/null +++ b/server/model/system/request/sys_casbin.go @@ -0,0 +1,27 @@ +package request + +// CasbinInfo Casbin info structure +type CasbinInfo struct { + Path string `json:"path"` // 路径 + Method string `json:"method"` // 方法 +} + +// CasbinInReceive Casbin structure for input parameters +type CasbinInReceive struct { + AuthorityId uint `json:"authorityId"` // 权限id + CasbinInfos []CasbinInfo `json:"casbinInfos"` +} + +func DefaultCasbin() []CasbinInfo { + return []CasbinInfo{ + {Path: "/menu/getMenu", Method: "POST"}, + {Path: "/jwt/jsonInBlacklist", Method: "POST"}, + {Path: "/base/login", Method: "POST"}, + {Path: "/user/changePassword", Method: "POST"}, + {Path: "/user/setUserAuthority", Method: "POST"}, + {Path: "/user/getUserInfo", Method: "GET"}, + {Path: "/user/setSelfInfo", Method: "PUT"}, + {Path: "/fileUploadAndDownload/upload", Method: "POST"}, + {Path: "/sysDictionary/findSysDictionary", Method: "GET"}, + } +} diff --git a/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go new file mode 100644 index 0000000..5a84796 --- /dev/null +++ b/server/model/system/request/sys_dictionary.go @@ -0,0 +1,9 @@ +package request + +type SysDictionarySearch struct { + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) +} + +type ImportSysDictionaryRequest struct { + Json string `json:"json" binding:"required"` // JSON字符串 +} diff --git a/server/model/system/request/sys_dictionary_detail.go b/server/model/system/request/sys_dictionary_detail.go new file mode 100644 index 0000000..cc8c166 --- /dev/null +++ b/server/model/system/request/sys_dictionary_detail.go @@ -0,0 +1,43 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysDictionaryDetailSearch struct { + system.SysDictionaryDetail + request.PageInfo + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,用于查询指定父级下的子项 + Level *int `json:"level" form:"level"` // 层级深度,用于查询指定层级的数据 +} + +// CreateSysDictionaryDetailRequest 创建字典详情请求 +type CreateSysDictionaryDetailRequest struct { + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// UpdateSysDictionaryDetailRequest 更新字典详情请求 +type UpdateSysDictionaryDetailRequest struct { + ID uint `json:"ID" form:"ID" binding:"required"` // 主键ID + Label string `json:"label" form:"label" binding:"required"` // 展示值 + Value string `json:"value" form:"value" binding:"required"` // 字典值 + Extend string `json:"extend" form:"extend"` // 扩展值 + Status *bool `json:"status" form:"status"` // 启用状态 + Sort int `json:"sort" form:"sort"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID +} + +// GetDictionaryDetailsByParentRequest 根据父级ID获取字典详情请求 +type GetDictionaryDetailsByParentRequest struct { + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" binding:"required"` // 字典ID + ParentID *uint `json:"parentID" form:"parentID"` // 父级字典详情ID,为空时获取顶级 + IncludeChildren bool `json:"includeChildren" form:"includeChildren"` // 是否包含子级数据 +} diff --git a/server/model/system/request/sys_error.go b/server/model/system/request/sys_error.go new file mode 100644 index 0000000..7e9dc8a --- /dev/null +++ b/server/model/system/request/sys_error.go @@ -0,0 +1,14 @@ +package request + +import ( + "time" + + "git.echol.cn/loser/st/server/model/common/request" +) + +type SysErrorSearch struct { + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + Form *string `json:"form" form:"form"` + Info *string `json:"info" form:"info"` + request.PageInfo +} diff --git a/server/model/system/request/sys_export_template.go b/server/model/system/request/sys_export_template.go new file mode 100644 index 0000000..e2ff01a --- /dev/null +++ b/server/model/system/request/sys_export_template.go @@ -0,0 +1,15 @@ +package request + +import ( + "time" + + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysExportTemplateSearch struct { + system.SysExportTemplate + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + request.PageInfo +} diff --git a/server/model/system/request/sys_init.go b/server/model/system/request/sys_init.go new file mode 100644 index 0000000..7194128 --- /dev/null +++ b/server/model/system/request/sys_init.go @@ -0,0 +1,125 @@ +package request + +import ( + "fmt" + "os" + + "git.echol.cn/loser/st/server/config" +) + +type InitDB struct { + AdminPassword string `json:"adminPassword" binding:"required"` + DBType string `json:"dbType"` // 数据库类型 + Host string `json:"host"` // 服务器地址 + Port string `json:"port"` // 数据库连接端口 + UserName string `json:"userName"` // 数据库用户名 + Password string `json:"password"` // 数据库密码 + DBName string `json:"dbName" binding:"required"` // 数据库名 + DBPath string `json:"dbPath"` // sqlite数据库文件路径 + Template string `json:"template"` // postgresql指定template +} + +// MysqlEmptyDsn msyql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) MysqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "3306" + } + return fmt.Sprintf("%s:%s@tcp(%s:%s)/", i.UserName, i.Password, i.Host, i.Port) +} + +// PgsqlEmptyDsn pgsql 空数据库 建库链接 +// Author SliverHorn +func (i *InitDB) PgsqlEmptyDsn() string { + if i.Host == "" { + i.Host = "127.0.0.1" + } + if i.Port == "" { + i.Port = "5432" + } + return "host=" + i.Host + " user=" + i.UserName + " password=" + i.Password + " port=" + i.Port + " dbname=" + "postgres" + " " + "sslmode=disable TimeZone=Asia/Shanghai" +} + +// SqliteEmptyDsn sqlite 空数据库 建库链接 +// Author Kafumio +func (i *InitDB) SqliteEmptyDsn() string { + separator := string(os.PathSeparator) + return i.DBPath + separator + i.DBName + ".db" +} + +func (i *InitDB) MssqlEmptyDsn() string { + return "sqlserver://" + i.UserName + ":" + i.Password + "@" + i.Host + ":" + i.Port + "?database=" + i.DBName + "&encrypt=disable" +} + +// ToMysqlConfig 转换 config.Mysql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToMysqlConfig() config.Mysql { + return config.Mysql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "charset=utf8mb4&parseTime=True&loc=Local", + }, + } +} + +// ToPgsqlConfig 转换 config.Pgsql +// Author [SliverHorn](https://github.com/SliverHorn) +func (i *InitDB) ToPgsqlConfig() config.Pgsql { + return config.Pgsql{ + GeneralDB: config.GeneralDB{ + Path: i.Host, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "sslmode=disable TimeZone=Asia/Shanghai", + }, + } +} + +// ToSqliteConfig 转换 config.Sqlite +// Author [Kafumio](https://github.com/Kafumio) +func (i *InitDB) ToSqliteConfig() config.Sqlite { + return config.Sqlite{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} + +func (i *InitDB) ToMssqlConfig() config.Mssql { + return config.Mssql{ + GeneralDB: config.GeneralDB{ + Path: i.DBPath, + Port: i.Port, + Dbname: i.DBName, + Username: i.UserName, + Password: i.Password, + MaxIdleConns: 10, + MaxOpenConns: 100, + LogMode: "error", + Config: "", + }, + } +} diff --git a/server/model/system/request/sys_login_log.go b/server/model/system/request/sys_login_log.go new file mode 100644 index 0000000..3f5d5cd --- /dev/null +++ b/server/model/system/request/sys_login_log.go @@ -0,0 +1,11 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysLoginLogSearch struct { + system.SysLoginLog + request.PageInfo +} diff --git a/server/model/system/request/sys_menu.go b/server/model/system/request/sys_menu.go new file mode 100644 index 0000000..2b6fa1e --- /dev/null +++ b/server/model/system/request/sys_menu.go @@ -0,0 +1,27 @@ +package request + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" +) + +// AddMenuAuthorityInfo Add menu authority info structure +type AddMenuAuthorityInfo struct { + Menus []system.SysBaseMenu `json:"menus"` + AuthorityId uint `json:"authorityId"` // 角色ID +} + +func DefaultMenu() []system.SysBaseMenu { + return []system.SysBaseMenu{{ + GVA_MODEL: global.GVA_MODEL{ID: 1}, + ParentId: 0, + Path: "dashboard", + Name: "dashboard", + Component: "view/dashboard/index.vue", + Sort: 1, + Meta: system.Meta{ + Title: "仪表盘", + Icon: "setting", + }, + }} +} diff --git a/server/model/system/request/sys_operation_record.go b/server/model/system/request/sys_operation_record.go new file mode 100644 index 0000000..fa9d611 --- /dev/null +++ b/server/model/system/request/sys_operation_record.go @@ -0,0 +1,11 @@ +package request + +import ( + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysOperationRecordSearch struct { + system.SysOperationRecord + request.PageInfo +} diff --git a/server/model/system/request/sys_params.go b/server/model/system/request/sys_params.go new file mode 100644 index 0000000..8cb2e64 --- /dev/null +++ b/server/model/system/request/sys_params.go @@ -0,0 +1,15 @@ +package request + +import ( + "time" + + "git.echol.cn/loser/st/server/model/common/request" +) + +type SysParamsSearch struct { + StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` + EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + Name string `json:"name" form:"name" ` + Key string `json:"key" form:"key" ` + request.PageInfo +} diff --git a/server/model/system/request/sys_skills.go b/server/model/system/request/sys_skills.go new file mode 100644 index 0000000..953fbd6 --- /dev/null +++ b/server/model/system/request/sys_skills.go @@ -0,0 +1,64 @@ +package request + +import "git.echol.cn/loser/st/server/model/system" + +type SkillToolRequest struct { + Tool string `json:"tool"` +} + +type SkillDetailRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` +} + +type SkillSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + Meta system.SkillMeta `json:"meta"` + Markdown string `json:"markdown"` + SyncTools []string `json:"syncTools"` +} + +type SkillScriptCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + ScriptType string `json:"scriptType"` +} + +type SkillResourceCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillReferenceCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillTemplateCreateRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` +} + +type SkillFileSaveRequest struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + FileName string `json:"fileName"` + Content string `json:"content"` +} + +type SkillGlobalConstraintSaveRequest struct { + Tool string `json:"tool"` + Content string `json:"content"` + SyncTools []string `json:"syncTools"` +} diff --git a/server/model/system/request/sys_user.go b/server/model/system/request/sys_user.go new file mode 100644 index 0000000..a7e6544 --- /dev/null +++ b/server/model/system/request/sys_user.go @@ -0,0 +1,69 @@ +package request + +import ( + common "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +// Register User register structure +type Register struct { + Username string `json:"userName" example:"用户名"` + Password string `json:"passWord" example:"密码"` + NickName string `json:"nickName" example:"昵称"` + HeaderImg string `json:"headerImg" example:"头像链接"` + AuthorityId uint `json:"authorityId" swaggertype:"string" example:"int 角色id"` + Enable int `json:"enable" swaggertype:"string" example:"int 是否启用"` + AuthorityIds []uint `json:"authorityIds" swaggertype:"string" example:"[]uint 角色id"` + Phone string `json:"phone" example:"电话号码"` + Email string `json:"email" example:"电子邮箱"` +} + +// Login User login structure +type Login struct { + Username string `json:"username"` // 用户名 + Password string `json:"password"` // 密码 + Captcha string `json:"captcha"` // 验证码 + CaptchaId string `json:"captchaId"` // 验证码ID +} + +// ChangePasswordReq Modify password structure +type ChangePasswordReq struct { + ID uint `json:"-"` // 从 JWT 中提取 user id,避免越权 + Password string `json:"password"` // 密码 + NewPassword string `json:"newPassword"` // 新密码 +} + +type ResetPassword struct { + ID uint `json:"ID" form:"ID"` + Password string `json:"password" form:"password" gorm:"comment:用户登录密码"` // 用户登录密码 +} + +// SetUserAuth Modify user's auth structure +type SetUserAuth struct { + AuthorityId uint `json:"authorityId"` // 角色ID +} + +// SetUserAuthorities Modify user's auth structure +type SetUserAuthorities struct { + ID uint + AuthorityIds []uint `json:"authorityIds"` // 角色ID +} + +type ChangeUserInfo struct { + ID uint `gorm:"primarykey"` // 主键ID + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户 + Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"` +} + +type GetUserList struct { + common.PageInfo + Username string `json:"username" form:"username"` + NickName string `json:"nickName" form:"nickName"` + Phone string `json:"phone" form:"phone"` + Email string `json:"email" form:"email"` +} diff --git a/server/model/system/request/sys_version.go b/server/model/system/request/sys_version.go new file mode 100644 index 0000000..052a904 --- /dev/null +++ b/server/model/system/request/sys_version.go @@ -0,0 +1,41 @@ +package request + +import ( + "time" + + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" +) + +type SysVersionSearch struct { + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + VersionName *string `json:"versionName" form:"versionName"` + VersionCode *string `json:"versionCode" form:"versionCode"` + request.PageInfo +} + +// ExportVersionRequest 导出版本请求结构体 +type ExportVersionRequest struct { + VersionName string `json:"versionName" binding:"required"` // 版本名称 + VersionCode string `json:"versionCode" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表 + ApiIds []uint `json:"apiIds"` // 选中的API ID列表 + DictIds []uint `json:"dictIds"` // 选中的字典ID列表 +} + +// ImportVersionRequest 导入版本请求结构体 +type ImportVersionRequest struct { + VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 + ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi + ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} + +// VersionInfo 版本信息结构体 +type VersionInfo struct { + Name string `json:"name" binding:"required"` // 版本名称 + Code string `json:"code" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + ExportTime string `json:"exportTime"` // 导出时间 +} diff --git a/server/model/system/response/sys_api.go b/server/model/system/response/sys_api.go new file mode 100644 index 0000000..ec1ed63 --- /dev/null +++ b/server/model/system/response/sys_api.go @@ -0,0 +1,18 @@ +package response + +import ( + "git.echol.cn/loser/st/server/model/system" +) + +type SysAPIResponse struct { + Api system.SysApi `json:"api"` +} + +type SysAPIListResponse struct { + Apis []system.SysApi `json:"apis"` +} + +type SysSyncApis struct { + NewApis []system.SysApi `json:"newApis"` + DeleteApis []system.SysApi `json:"deleteApis"` +} diff --git a/server/model/system/response/sys_authority.go b/server/model/system/response/sys_authority.go new file mode 100644 index 0000000..d5704c9 --- /dev/null +++ b/server/model/system/response/sys_authority.go @@ -0,0 +1,12 @@ +package response + +import "git.echol.cn/loser/st/server/model/system" + +type SysAuthorityResponse struct { + Authority system.SysAuthority `json:"authority"` +} + +type SysAuthorityCopyResponse struct { + Authority system.SysAuthority `json:"authority"` + OldAuthorityId uint `json:"oldAuthorityId"` // 旧角色ID +} diff --git a/server/model/system/response/sys_authority_btn.go b/server/model/system/response/sys_authority_btn.go new file mode 100644 index 0000000..2f772cf --- /dev/null +++ b/server/model/system/response/sys_authority_btn.go @@ -0,0 +1,5 @@ +package response + +type SysAuthorityBtnRes struct { + Selected []uint `json:"selected"` +} diff --git a/server/model/system/response/sys_auto_code.go b/server/model/system/response/sys_auto_code.go new file mode 100644 index 0000000..ec1b552 --- /dev/null +++ b/server/model/system/response/sys_auto_code.go @@ -0,0 +1,27 @@ +package response + +import "git.echol.cn/loser/st/server/model/system" + +type Db struct { + Database string `json:"database" gorm:"column:database"` +} + +type Table struct { + TableName string `json:"tableName" gorm:"column:table_name"` +} + +type Column struct { + DataType string `json:"dataType" gorm:"column:data_type"` + ColumnName string `json:"columnName" gorm:"column:column_name"` + DataTypeLong string `json:"dataTypeLong" gorm:"column:data_type_long"` + ColumnComment string `json:"columnComment" gorm:"column:column_comment"` + PrimaryKey bool `json:"primaryKey" gorm:"column:primary_key"` +} + +type PluginInfo struct { + PluginName string `json:"pluginName"` + PluginType string `json:"pluginType"` // web, server, full + Apis []system.SysApi `json:"apis"` + Menus []system.SysBaseMenu `json:"menus"` + Dictionaries []system.SysDictionary `json:"dictionaries"` +} diff --git a/server/model/system/response/sys_captcha.go b/server/model/system/response/sys_captcha.go new file mode 100644 index 0000000..0c3995a --- /dev/null +++ b/server/model/system/response/sys_captcha.go @@ -0,0 +1,8 @@ +package response + +type SysCaptchaResponse struct { + CaptchaId string `json:"captchaId"` + PicPath string `json:"picPath"` + CaptchaLength int `json:"captchaLength"` + OpenCaptcha bool `json:"openCaptcha"` +} diff --git a/server/model/system/response/sys_casbin.go b/server/model/system/response/sys_casbin.go new file mode 100644 index 0000000..34e5d71 --- /dev/null +++ b/server/model/system/response/sys_casbin.go @@ -0,0 +1,9 @@ +package response + +import ( + "git.echol.cn/loser/st/server/model/system/request" +) + +type PolicyPathResponse struct { + Paths []request.CasbinInfo `json:"paths"` +} diff --git a/server/model/system/response/sys_menu.go b/server/model/system/response/sys_menu.go new file mode 100644 index 0000000..9ca3fdd --- /dev/null +++ b/server/model/system/response/sys_menu.go @@ -0,0 +1,15 @@ +package response + +import "git.echol.cn/loser/st/server/model/system" + +type SysMenusResponse struct { + Menus []system.SysMenu `json:"menus"` +} + +type SysBaseMenusResponse struct { + Menus []system.SysBaseMenu `json:"menus"` +} + +type SysBaseMenuResponse struct { + Menu system.SysBaseMenu `json:"menu"` +} diff --git a/server/model/system/response/sys_system.go b/server/model/system/response/sys_system.go new file mode 100644 index 0000000..a5abe44 --- /dev/null +++ b/server/model/system/response/sys_system.go @@ -0,0 +1,7 @@ +package response + +import "git.echol.cn/loser/st/server/config" + +type SysConfigResponse struct { + Config config.Server `json:"config"` +} diff --git a/server/model/system/response/sys_user.go b/server/model/system/response/sys_user.go new file mode 100644 index 0000000..9b7b4d9 --- /dev/null +++ b/server/model/system/response/sys_user.go @@ -0,0 +1,15 @@ +package response + +import ( + "git.echol.cn/loser/st/server/model/system" +) + +type SysUserResponse struct { + User system.SysUser `json:"user"` +} + +type LoginResponse struct { + User system.SysUser `json:"user"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` +} diff --git a/server/model/system/response/sys_version.go b/server/model/system/response/sys_version.go new file mode 100644 index 0000000..cdaa058 --- /dev/null +++ b/server/model/system/response/sys_version.go @@ -0,0 +1,14 @@ +package response + +import ( + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" +) + +// ExportVersionResponse 导出版本响应结构体 +type ExportVersionResponse struct { + Version request.VersionInfo `json:"version"` // 版本信息 + Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi + Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} diff --git a/server/model/system/sys_api.go b/server/model/system/sys_api.go new file mode 100644 index 0000000..f960951 --- /dev/null +++ b/server/model/system/sys_api.go @@ -0,0 +1,28 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +type SysApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Description string `json:"description" gorm:"comment:api中文描述"` // api中文描述 + ApiGroup string `json:"apiGroup" gorm:"comment:api组"` // api组 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE +} + +func (SysApi) TableName() string { + return "sys_apis" +} + +type SysIgnoreApi struct { + global.GVA_MODEL + Path string `json:"path" gorm:"comment:api路径"` // api路径 + Method string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE + Flag bool `json:"flag" gorm:"-"` // 是否忽略 +} + +func (SysIgnoreApi) TableName() string { + return "sys_ignore_apis" +} diff --git a/server/model/system/sys_api_token.go b/server/model/system/sys_api_token.go new file mode 100644 index 0000000..c062c3d --- /dev/null +++ b/server/model/system/sys_api_token.go @@ -0,0 +1,18 @@ +package system + +import ( + "time" + + "git.echol.cn/loser/st/server/global" +) + +type SysApiToken struct { + global.GVA_MODEL + UserID uint `json:"userId" gorm:"comment:用户ID"` + User SysUser `json:"user" gorm:"foreignKey:UserID;"` + AuthorityID uint `json:"authorityId" gorm:"comment:角色ID"` + Token string `json:"token" gorm:"type:text;comment:Token"` + Status bool `json:"status" gorm:"default:true;comment:状态"` // true有效 false无效 + ExpiresAt time.Time `json:"expiresAt" gorm:"comment:过期时间"` + Remark string `json:"remark" gorm:"comment:备注"` +} diff --git a/server/model/system/sys_authority.go b/server/model/system/sys_authority.go new file mode 100644 index 0000000..01c5efa --- /dev/null +++ b/server/model/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "time" +) + +type SysAuthority struct { + CreatedAt time.Time // 创建时间 + UpdatedAt time.Time // 更新时间 + DeletedAt *time.Time `sql:"index"` + AuthorityId uint `json:"authorityId" gorm:"not null;unique;primary_key;comment:角色ID;size:90"` // 角色ID + AuthorityName string `json:"authorityName" gorm:"comment:角色名"` // 角色名 + ParentId *uint `json:"parentId" gorm:"comment:父角色ID"` // 父角色ID + DataAuthorityId []*SysAuthority `json:"dataAuthorityId" gorm:"many2many:sys_data_authority_id;"` + Children []SysAuthority `json:"children" gorm:"-"` + SysBaseMenus []SysBaseMenu `json:"menus" gorm:"many2many:sys_authority_menus;"` + Users []SysUser `json:"-" gorm:"many2many:sys_user_authority;"` + DefaultRouter string `json:"defaultRouter" gorm:"comment:默认菜单;default:dashboard"` // 默认菜单(默认dashboard) +} + +func (SysAuthority) TableName() string { + return "sys_authorities" +} diff --git a/server/model/system/sys_authority_btn.go b/server/model/system/sys_authority_btn.go new file mode 100644 index 0000000..e005984 --- /dev/null +++ b/server/model/system/sys_authority_btn.go @@ -0,0 +1,8 @@ +package system + +type SysAuthorityBtn struct { + AuthorityId uint `gorm:"comment:角色ID"` + SysMenuID uint `gorm:"comment:菜单ID"` + SysBaseMenuBtnID uint `gorm:"comment:菜单按钮ID"` + SysBaseMenuBtn SysBaseMenuBtn ` gorm:"comment:按钮详情"` +} diff --git a/server/model/system/sys_authority_menu.go b/server/model/system/sys_authority_menu.go new file mode 100644 index 0000000..4467a7e --- /dev/null +++ b/server/model/system/sys_authority_menu.go @@ -0,0 +1,19 @@ +package system + +type SysMenu struct { + SysBaseMenu + MenuId uint `json:"menuId" gorm:"comment:菜单ID"` + AuthorityId uint `json:"-" gorm:"comment:角色ID"` + Children []SysMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters" gorm:"foreignKey:SysBaseMenuID;references:MenuId"` + Btns map[string]uint `json:"btns" gorm:"-"` +} + +type SysAuthorityMenu struct { + MenuId string `json:"menuId" gorm:"comment:菜单ID;column:sys_base_menu_id"` + AuthorityId string `json:"-" gorm:"comment:角色ID;column:sys_authority_authority_id"` +} + +func (s SysAuthorityMenu) TableName() string { + return "sys_authority_menus" +} diff --git a/server/model/system/sys_auto_code_history.go b/server/model/system/sys_auto_code_history.go new file mode 100644 index 0000000..7eaefa5 --- /dev/null +++ b/server/model/system/sys_auto_code_history.go @@ -0,0 +1,69 @@ +package system + +import ( + "os" + "path" + "path/filepath" + "strings" + + "git.echol.cn/loser/st/server/global" + "gorm.io/gorm" +) + +// SysAutoCodeHistory 自动迁移代码记录,用于回滚,重放使用 +type SysAutoCodeHistory struct { + global.GVA_MODEL + Table string `json:"tableName" gorm:"column:table_name;comment:表名"` + Package string `json:"package" gorm:"column:package;comment:模块名/插件名"` + Request string `json:"request" gorm:"type:text;column:request;comment:前端传入的结构化信息"` + StructName string `json:"structName" gorm:"column:struct_name;comment:结构体名称"` + Abbreviation string `json:"abbreviation" gorm:"column:abbreviation;comment:结构体名称缩写"` + BusinessDB string `json:"businessDb" gorm:"column:business_db;comment:业务库"` + Description string `json:"description" gorm:"column:description;comment:Struct中文名称"` + Templates map[string]string `json:"template" gorm:"serializer:json;type:text;column:templates;comment:模板信息"` + Injections map[string]string `json:"injections" gorm:"serializer:json;type:text;column:Injections;comment:注入路径"` + Flag int `json:"flag" gorm:"column:flag;comment:[0:创建,1:回滚]"` + ApiIDs []uint `json:"apiIDs" gorm:"serializer:json;column:api_ids;comment:api表注册内容"` + MenuID uint `json:"menuId" gorm:"column:menu_id;comment:菜单ID"` + ExportTemplateID uint `json:"exportTemplateID" gorm:"column:export_template_id;comment:导出模板ID"` + AutoCodePackage SysAutoCodePackage `json:"autoCodePackage" gorm:"foreignKey:ID;references:PackageID"` + PackageID uint `json:"packageID" gorm:"column:package_id;comment:包ID"` +} + +func (s *SysAutoCodeHistory) BeforeCreate(db *gorm.DB) error { + templates := make(map[string]string, len(s.Templates)) + for key, value := range s.Templates { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + { + hasServer := strings.Index(key, server) + if hasServer != -1 { + key = strings.TrimPrefix(key, server) + keys := strings.Split(key, string(os.PathSeparator)) + key = path.Join(keys...) + } + } // key + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + hasWeb := strings.Index(value, web) + if hasWeb != -1 { + value = strings.TrimPrefix(value, web) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + hasServer := strings.Index(value, server) + if hasServer != -1 { + value = strings.TrimPrefix(value, server) + values := strings.Split(value, string(os.PathSeparator)) + value = path.Join(values...) + templates[key] = value + continue + } + } + s.Templates = templates + return nil +} + +func (s *SysAutoCodeHistory) TableName() string { + return "sys_auto_code_histories" +} diff --git a/server/model/system/sys_auto_code_package.go b/server/model/system/sys_auto_code_package.go new file mode 100644 index 0000000..734f9a2 --- /dev/null +++ b/server/model/system/sys_auto_code_package.go @@ -0,0 +1,18 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +type SysAutoCodePackage struct { + global.GVA_MODEL + Desc string `json:"desc" gorm:"comment:描述"` + Label string `json:"label" gorm:"comment:展示名"` + Template string `json:"template" gorm:"comment:模版"` + PackageName string `json:"packageName" gorm:"comment:包名"` + Module string `json:"-" example:"模块"` +} + +func (s *SysAutoCodePackage) TableName() string { + return "sys_auto_code_packages" +} diff --git a/server/model/system/sys_base_menu.go b/server/model/system/sys_base_menu.go new file mode 100644 index 0000000..620cb90 --- /dev/null +++ b/server/model/system/sys_base_menu.go @@ -0,0 +1,43 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +type SysBaseMenu struct { + global.GVA_MODEL + MenuLevel uint `json:"-"` + ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID + Path string `json:"path" gorm:"comment:路由path"` // 路由path + Name string `json:"name" gorm:"comment:路由name"` // 路由name + Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 + Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 + Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 + Meta `json:"meta" gorm:"embedded"` // 附加属性 + SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"` + Children []SysBaseMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters"` + MenuBtn []SysBaseMenuBtn `json:"menuBtn"` +} + +type Meta struct { + ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` + KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) + Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 + Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 + CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + TransitionType string `json:"transitionType" gorm:"comment:路由切换动画"` // 路由切换动画 +} + +type SysBaseMenuParameter struct { + global.GVA_MODEL + SysBaseMenuID uint + Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"` // 地址栏携带参数为params还是query + Key string `json:"key" gorm:"comment:地址栏携带参数的key"` // 地址栏携带参数的key + Value string `json:"value" gorm:"comment:地址栏携带参数的值"` // 地址栏携带参数的值 +} + +func (SysBaseMenu) TableName() string { + return "sys_base_menus" +} diff --git a/server/model/system/sys_dictionary.go b/server/model/system/sys_dictionary.go new file mode 100644 index 0000000..e19e0de --- /dev/null +++ b/server/model/system/sys_dictionary.go @@ -0,0 +1,22 @@ +// 自动生成模板SysDictionary +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionary struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) + Type string `json:"type" form:"type" gorm:"column:type;comment:字典名(英)"` // 字典名(英) + Status *bool `json:"status" form:"status" gorm:"column:status;comment:状态"` // 状态 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典ID"` // 父级字典ID + Children []SysDictionary `json:"children" gorm:"foreignKey:ParentID"` // 子字典 + SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"` +} + +func (SysDictionary) TableName() string { + return "sys_dictionaries" +} diff --git a/server/model/system/sys_dictionary_detail.go b/server/model/system/sys_dictionary_detail.go new file mode 100644 index 0000000..8935d57 --- /dev/null +++ b/server/model/system/sys_dictionary_detail.go @@ -0,0 +1,26 @@ +// 自动生成模板SysDictionaryDetail +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysDictionaryDetail struct { + global.GVA_MODEL + Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 + Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 + Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 + Status *bool `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 + Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 + SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 + ParentID *uint `json:"parentID" form:"parentID" gorm:"column:parent_id;comment:父级字典详情ID"` // 父级字典详情ID + Children []SysDictionaryDetail `json:"children" gorm:"foreignKey:ParentID"` // 子字典详情 + Level int `json:"level" form:"level" gorm:"column:level;comment:层级深度"` // 层级深度,从0开始 + Path string `json:"path" form:"path" gorm:"column:path;comment:层级路径"` // 层级路径,如 "1,2,3" + Disabled bool `json:"disabled" gorm:"-"` // 禁用状态,根据status字段动态计算 +} + +func (SysDictionaryDetail) TableName() string { + return "sys_dictionary_details" +} diff --git a/server/model/system/sys_error.go b/server/model/system/sys_error.go new file mode 100644 index 0000000..cd8cebd --- /dev/null +++ b/server/model/system/sys_error.go @@ -0,0 +1,21 @@ +// 自动生成模板SysError +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 错误日志 结构体 SysError +type SysError struct { + global.GVA_MODEL + Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源 + Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容 + Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"` + Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案 + Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成 +} + +// TableName 错误日志 SysError自定义表名 sys_error +func (SysError) TableName() string { + return "sys_error" +} diff --git a/server/model/system/sys_export_template.go b/server/model/system/sys_export_template.go new file mode 100644 index 0000000..9484f95 --- /dev/null +++ b/server/model/system/sys_export_template.go @@ -0,0 +1,46 @@ +// 自动生成模板SysExportTemplate +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 导出模板 结构体 SysExportTemplate +type SysExportTemplate struct { + global.GVA_MODEL + DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称 + Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称 + TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称 + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识 + TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息 + SQL string `json:"sql" form:"sql" gorm:"column:sql;type:text;comment:自定义导出SQL;"` //自定义导出SQL + ImportSQL string `json:"importSql" form:"importSql" gorm:"column:import_sql;type:text;comment:自定义导入SQL;"` //自定义导入SQL + Limit *int `json:"limit" form:"limit" gorm:"column:limit;comment:导出限制"` + Order string `json:"order" form:"order" gorm:"column:order;comment:排序"` + Conditions []Condition `json:"conditions" form:"conditions" gorm:"foreignKey:TemplateID;references:TemplateID;comment:条件"` + JoinTemplate []JoinTemplate `json:"joinTemplate" form:"joinTemplate" gorm:"foreignKey:TemplateID;references:TemplateID;comment:关联"` +} + +type JoinTemplate struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + JOINS string `json:"joins" form:"joins" gorm:"column:joins;comment:关联"` + Table string `json:"table" form:"table" gorm:"column:table;comment:关联表"` + ON string `json:"on" form:"on" gorm:"column:on;comment:关联条件"` +} + +func (JoinTemplate) TableName() string { + return "sys_export_template_join" +} + +type Condition struct { + global.GVA_MODEL + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识"` + From string `json:"from" form:"from" gorm:"column:from;comment:条件取的key"` + Column string `json:"column" form:"column" gorm:"column:column;comment:作为查询条件的字段"` + Operator string `json:"operator" form:"operator" gorm:"column:operator;comment:操作符"` +} + +func (Condition) TableName() string { + return "sys_export_template_condition" +} diff --git a/server/model/system/sys_jwt_blacklist.go b/server/model/system/sys_jwt_blacklist.go new file mode 100644 index 0000000..c1c4ff8 --- /dev/null +++ b/server/model/system/sys_jwt_blacklist.go @@ -0,0 +1,10 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +type JwtBlacklist struct { + global.GVA_MODEL + Jwt string `gorm:"type:text;comment:jwt"` +} diff --git a/server/model/system/sys_login_log.go b/server/model/system/sys_login_log.go new file mode 100644 index 0000000..c40cb7c --- /dev/null +++ b/server/model/system/sys_login_log.go @@ -0,0 +1,16 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +type SysLoginLog struct { + global.GVA_MODEL + Username string `json:"username" gorm:"column:username;comment:用户名"` + Ip string `json:"ip" gorm:"column:ip;comment:请求ip"` + Status bool `json:"status" gorm:"column:status;comment:登录状态"` + ErrorMessage string `json:"errorMessage" gorm:"column:error_message;comment:错误信息"` + Agent string `json:"agent" gorm:"column:agent;comment:代理"` + UserID uint `json:"userId" gorm:"column:user_id;comment:用户id"` + User SysUser `json:"user" gorm:"foreignKey:UserID"` +} diff --git a/server/model/system/sys_menu_btn.go b/server/model/system/sys_menu_btn.go new file mode 100644 index 0000000..5f9ecbc --- /dev/null +++ b/server/model/system/sys_menu_btn.go @@ -0,0 +1,10 @@ +package system + +import "git.echol.cn/loser/st/server/global" + +type SysBaseMenuBtn struct { + global.GVA_MODEL + Name string `json:"name" gorm:"comment:按钮关键key"` + Desc string `json:"desc" gorm:"按钮备注"` + SysBaseMenuID uint `json:"sysBaseMenuID" gorm:"comment:菜单ID"` +} diff --git a/server/model/system/sys_operation_record.go b/server/model/system/sys_operation_record.go new file mode 100644 index 0000000..e3b50ca --- /dev/null +++ b/server/model/system/sys_operation_record.go @@ -0,0 +1,24 @@ +// 自动生成模板SysOperationRecord +package system + +import ( + "time" + + "git.echol.cn/loser/st/server/global" +) + +// 如果含有time.Time 请自行import time包 +type SysOperationRecord struct { + global.GVA_MODEL + Ip string `json:"ip" form:"ip" gorm:"column:ip;comment:请求ip"` // 请求ip + Method string `json:"method" form:"method" gorm:"column:method;comment:请求方法"` // 请求方法 + Path string `json:"path" form:"path" gorm:"column:path;comment:请求路径"` // 请求路径 + Status int `json:"status" form:"status" gorm:"column:status;comment:请求状态"` // 请求状态 + Latency time.Duration `json:"latency" form:"latency" gorm:"column:latency;comment:延迟" swaggertype:"string"` // 延迟 + Agent string `json:"agent" form:"agent" gorm:"type:text;column:agent;comment:代理"` // 代理 + ErrorMessage string `json:"error_message" form:"error_message" gorm:"column:error_message;comment:错误信息"` // 错误信息 + Body string `json:"body" form:"body" gorm:"type:text;column:body;comment:请求Body"` // 请求Body + Resp string `json:"resp" form:"resp" gorm:"type:text;column:resp;comment:响应Body"` // 响应Body + UserID int `json:"user_id" form:"user_id" gorm:"column:user_id;comment:用户id"` // 用户id + User SysUser `json:"user"` +} diff --git a/server/model/system/sys_params.go b/server/model/system/sys_params.go new file mode 100644 index 0000000..7e9511d --- /dev/null +++ b/server/model/system/sys_params.go @@ -0,0 +1,20 @@ +// 自动生成模板SysParams +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 参数 结构体 SysParams +type SysParams struct { + global.GVA_MODEL + Name string `json:"name" form:"name" gorm:"column:name;comment:参数名称;" binding:"required"` //参数名称 + Key string `json:"key" form:"key" gorm:"column:key;comment:参数键;" binding:"required"` //参数键 + Value string `json:"value" form:"value" gorm:"column:value;comment:参数值;" binding:"required"` //参数值 + Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:参数说明;"` //参数说明 +} + +// TableName 参数 SysParams自定义表名 sys_params +func (SysParams) TableName() string { + return "sys_params" +} diff --git a/server/model/system/sys_skills.go b/server/model/system/sys_skills.go new file mode 100644 index 0000000..e7013f6 --- /dev/null +++ b/server/model/system/sys_skills.go @@ -0,0 +1,25 @@ +package system + +type SkillMeta struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + AllowedTools string `json:"allowedTools" yaml:"allowed-tools,omitempty"` + Context string `json:"context" yaml:"context,omitempty"` + Agent string `json:"agent" yaml:"agent,omitempty"` +} + +type SkillDetail struct { + Tool string `json:"tool"` + Skill string `json:"skill"` + Meta SkillMeta `json:"meta"` + Markdown string `json:"markdown"` + Scripts []string `json:"scripts"` + Resources []string `json:"resources"` + References []string `json:"references"` + Templates []string `json:"templates"` +} + +type SkillTool struct { + Key string `json:"key"` + Label string `json:"label"` +} diff --git a/server/model/system/sys_system.go b/server/model/system/sys_system.go new file mode 100644 index 0000000..22daad6 --- /dev/null +++ b/server/model/system/sys_system.go @@ -0,0 +1,10 @@ +package system + +import ( + "git.echol.cn/loser/st/server/config" +) + +// 配置文件结构体 +type System struct { + Config config.Server `json:"config"` +} diff --git a/server/model/system/sys_user.go b/server/model/system/sys_user.go new file mode 100644 index 0000000..cffdf65 --- /dev/null +++ b/server/model/system/sys_user.go @@ -0,0 +1,62 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common" + "github.com/google/uuid" +) + +type Login interface { + GetUsername() string + GetNickname() string + GetUUID() uuid.UUID + GetUserId() uint + GetAuthorityId() uint + GetUserInfo() any +} + +var _ Login = new(SysUser) + +type SysUser struct { + global.GVA_MODEL + UUID uuid.UUID `json:"uuid" gorm:"index;comment:用户UUID"` // 用户UUID + Username string `json:"userName" gorm:"index;comment:用户登录名"` // 用户登录名 + Password string `json:"-" gorm:"comment:用户登录密码"` // 用户登录密码 + NickName string `json:"nickName" gorm:"default:系统用户;comment:用户昵称"` // 用户昵称 + HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 + AuthorityId uint `json:"authorityId" gorm:"default:888;comment:用户角色ID"` // 用户角色ID + Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"` // 用户角色 + Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"` // 多用户角色 + Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号 + Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 + Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结 + OriginSetting common.JSONMap `json:"originSetting" form:"originSetting" gorm:"type:text;default:null;column:origin_setting;comment:配置;"` //配置 +} + +func (SysUser) TableName() string { + return "sys_users" +} + +func (s *SysUser) GetUsername() string { + return s.Username +} + +func (s *SysUser) GetNickname() string { + return s.NickName +} + +func (s *SysUser) GetUUID() uuid.UUID { + return s.UUID +} + +func (s *SysUser) GetUserId() uint { + return s.ID +} + +func (s *SysUser) GetAuthorityId() uint { + return s.AuthorityId +} + +func (s *SysUser) GetUserInfo() any { + return *s +} diff --git a/server/model/system/sys_user_authority.go b/server/model/system/sys_user_authority.go new file mode 100644 index 0000000..1aa83cb --- /dev/null +++ b/server/model/system/sys_user_authority.go @@ -0,0 +1,11 @@ +package system + +// SysUserAuthority 是 sysUser 和 sysAuthority 的连接表 +type SysUserAuthority struct { + SysUserId uint `gorm:"column:sys_user_id"` + SysAuthorityAuthorityId uint `gorm:"column:sys_authority_authority_id"` +} + +func (s *SysUserAuthority) TableName() string { + return "sys_user_authority" +} diff --git a/server/model/system/sys_version.go b/server/model/system/sys_version.go new file mode 100644 index 0000000..ab6d398 --- /dev/null +++ b/server/model/system/sys_version.go @@ -0,0 +1,20 @@ +// 自动生成模板SysVersion +package system + +import ( + "git.echol.cn/loser/st/server/global" +) + +// 版本管理 结构体 SysVersion +type SysVersion struct { + global.GVA_MODEL + VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称 + VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号 + Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述 + VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据 +} + +// TableName 版本管理 SysVersion自定义表名 sys_versions +func (SysVersion) TableName() string { + return "sys_versions" +} diff --git a/server/plugin/email/README.MD b/server/plugin/email/README.MD new file mode 100644 index 0000000..685cdd6 --- /dev/null +++ b/server/plugin/email/README.MD @@ -0,0 +1,78 @@ +## GVA 邮件发送功能插件 +#### 开发者:GIN-VUE-ADMIN 官方 + +### 使用步骤 + +#### 1. 前往GVA主程序下的initialize/router.go 在Routers 方法最末尾按照你需要的及安全模式添加本插件 + 例: + 本插件可以采用gva的配置文件 也可以直接写死内容作为配置 建议为gva添加配置文件结构 然后将配置传入 + PluginInit(PrivateGroup, email.CreateEmailPlug( + global.GVA_CONFIG.Email.To, + global.GVA_CONFIG.Email.From, + global.GVA_CONFIG.Email.Host, + global.GVA_CONFIG.Email.Secret, + global.GVA_CONFIG.Email.Nickname, + global.GVA_CONFIG.Email.Port, + global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, + )) + + 同样也可以再传入时写死 + + PluginInit(PrivateGroup, email.CreateEmailPlug( + "a@qq.com", + "b@qq.com", + "smtp.qq.com", + "global.GVA_CONFIG.Email.Secret", + "登录密钥", + 465, + true, + true, + )) + +### 2. 配置说明 + +#### 2-1 全局配置结构体说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 + From string // 发件人 你自己要发邮件的邮箱 + Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string // 昵称 发件人昵称 自定义即可 可以不填 + Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool // 是否SSL 是否开启SSL + IsLoginAuth bool // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) + } +#### 2-2 入参结构说明 + //其中 Form 和 Secret 通常来说就是用户名和密码 + + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + + +### 3. 方法API + + utils.EmailTest(邮件标题,邮件主体) 发送测试邮件 + 例:utils.EmailTest("测试邮件","测试邮件") + utils.ErrorToEmail(邮件标题,邮件主体) 错误监控 + 例:utils.ErrorToEmail("测试邮件","测试邮件") + utils.Email(目标邮箱多个的话用逗号分隔,邮件标题,邮件主体) 发送测试邮件 + 例:utils.Email(”a.qq.com,b.qq.com“,"测试邮件","测试邮件") + +### 4. 可直接调用的接口 + + 测试接口: /email/emailTest [post] 已配置swagger + + 发送邮件接口接口: /email/emailSend [post] 已配置swagger + 入参: + type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 + } + diff --git a/server/plugin/email/api/enter.go b/server/plugin/email/api/enter.go new file mode 100644 index 0000000..353404d --- /dev/null +++ b/server/plugin/email/api/enter.go @@ -0,0 +1,7 @@ +package api + +type ApiGroup struct { + EmailApi +} + +var ApiGroupApp = new(ApiGroup) diff --git a/server/plugin/email/api/sys_email.go b/server/plugin/email/api/sys_email.go new file mode 100644 index 0000000..2befee6 --- /dev/null +++ b/server/plugin/email/api/sys_email.go @@ -0,0 +1,53 @@ +package api + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/response" + email_response "git.echol.cn/loser/st/server/plugin/email/model/response" + "git.echol.cn/loser/st/server/plugin/email/service" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type EmailApi struct{} + +// EmailTest +// @Tags System +// @Summary 发送测试邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/emailTest [post] +func (s *EmailApi) EmailTest(c *gin.Context) { + err := service.ServiceGroupApp.EmailTest() + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} + +// SendEmail +// @Tags System +// @Summary 发送邮件 +// @Security ApiKeyAuth +// @Produce application/json +// @Param data body email_response.Email true "发送邮件必须的参数" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"发送成功"}" +// @Router /email/sendEmail [post] +func (s *EmailApi) SendEmail(c *gin.Context) { + var email email_response.Email + err := c.ShouldBindJSON(&email) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = service.ServiceGroupApp.SendEmail(email.To, email.Subject, email.Body) + if err != nil { + global.GVA_LOG.Error("发送失败!", zap.Error(err)) + response.FailWithMessage("发送失败", c) + return + } + response.OkWithMessage("发送成功", c) +} diff --git a/server/plugin/email/config/email.go b/server/plugin/email/config/email.go new file mode 100644 index 0000000..412b5a8 --- /dev/null +++ b/server/plugin/email/config/email.go @@ -0,0 +1,12 @@ +package config + +type Email struct { + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证 +} diff --git a/server/plugin/email/global/gloabl.go b/server/plugin/email/global/gloabl.go new file mode 100644 index 0000000..45ec5ec --- /dev/null +++ b/server/plugin/email/global/gloabl.go @@ -0,0 +1,5 @@ +package global + +import "git.echol.cn/loser/st/server/plugin/email/config" + +var GlobalConfig = new(config.Email) diff --git a/server/plugin/email/main.go b/server/plugin/email/main.go new file mode 100644 index 0000000..4e925e1 --- /dev/null +++ b/server/plugin/email/main.go @@ -0,0 +1,29 @@ +package email + +import ( + "git.echol.cn/loser/st/server/plugin/email/global" + "git.echol.cn/loser/st/server/plugin/email/router" + "github.com/gin-gonic/gin" +) + +type emailPlugin struct{} + +func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, IsLoginAuth bool) *emailPlugin { + global.GlobalConfig.To = To + global.GlobalConfig.From = From + global.GlobalConfig.Host = Host + global.GlobalConfig.Secret = Secret + global.GlobalConfig.Nickname = Nickname + global.GlobalConfig.Port = Port + global.GlobalConfig.IsSSL = IsSSL + global.GlobalConfig.IsLoginAuth = IsLoginAuth + return &emailPlugin{} +} + +func (*emailPlugin) Register(group *gin.RouterGroup) { + router.RouterGroupApp.InitEmailRouter(group) +} + +func (*emailPlugin) RouterPath() string { + return "email" +} diff --git a/server/plugin/email/model/response/email.go b/server/plugin/email/model/response/email.go new file mode 100644 index 0000000..ed25475 --- /dev/null +++ b/server/plugin/email/model/response/email.go @@ -0,0 +1,7 @@ +package response + +type Email struct { + To string `json:"to"` // 邮件发送给谁 + Subject string `json:"subject"` // 邮件标题 + Body string `json:"body"` // 邮件内容 +} diff --git a/server/plugin/email/router/enter.go b/server/plugin/email/router/enter.go new file mode 100644 index 0000000..e081a54 --- /dev/null +++ b/server/plugin/email/router/enter.go @@ -0,0 +1,7 @@ +package router + +type RouterGroup struct { + EmailRouter +} + +var RouterGroupApp = new(RouterGroup) diff --git a/server/plugin/email/router/sys_email.go b/server/plugin/email/router/sys_email.go new file mode 100644 index 0000000..3d1abeb --- /dev/null +++ b/server/plugin/email/router/sys_email.go @@ -0,0 +1,19 @@ +package router + +import ( + "git.echol.cn/loser/st/server/middleware" + "git.echol.cn/loser/st/server/plugin/email/api" + "github.com/gin-gonic/gin" +) + +type EmailRouter struct{} + +func (s *EmailRouter) InitEmailRouter(Router *gin.RouterGroup) { + emailRouter := Router.Use(middleware.OperationRecord()) + EmailApi := api.ApiGroupApp.EmailApi.EmailTest + SendEmail := api.ApiGroupApp.EmailApi.SendEmail + { + emailRouter.POST("emailTest", EmailApi) // 发送测试邮件 + emailRouter.POST("sendEmail", SendEmail) // 发送邮件 + } +} diff --git a/server/plugin/email/service/enter.go b/server/plugin/email/service/enter.go new file mode 100644 index 0000000..e96e267 --- /dev/null +++ b/server/plugin/email/service/enter.go @@ -0,0 +1,7 @@ +package service + +type ServiceGroup struct { + EmailService +} + +var ServiceGroupApp = new(ServiceGroup) diff --git a/server/plugin/email/service/sys_email.go b/server/plugin/email/service/sys_email.go new file mode 100644 index 0000000..059db38 --- /dev/null +++ b/server/plugin/email/service/sys_email.go @@ -0,0 +1,32 @@ +package service + +import ( + "git.echol.cn/loser/st/server/plugin/email/utils" +) + +type EmailService struct{} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error + +func (e *EmailService) EmailTest() (err error) { + subject := "test" + body := "test" + err = utils.EmailTest(subject, body) + return err +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: 发送邮件测试 +//@return: err error +//@params to string 收件人 +//@params subject string 标题(主题) +//@params body string 邮件内容 + +func (e *EmailService) SendEmail(to, subject, body string) (err error) { + err = utils.Email(to, subject, body) + return err +} diff --git a/server/plugin/email/utils/email.go b/server/plugin/email/utils/email.go new file mode 100644 index 0000000..288e56c --- /dev/null +++ b/server/plugin/email/utils/email.go @@ -0,0 +1,122 @@ +package utils + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strings" + + "git.echol.cn/loser/st/server/plugin/email/global" + + "github.com/jordan-wright/email" +) + +//@author: [maplepie](https://github.com/maplepie) +//@function: Email +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func Email(To, subject string, body string) error { + to := strings.Split(To, ",") + return send(to, subject, body) +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: ErrorToEmail +//@description: 给email中间件错误发送邮件到指定邮箱 +//@param: subject string, body string +//@return: error + +func ErrorToEmail(subject string, body string) error { + to := strings.Split(global.GlobalConfig.To, ",") + if to[len(to)-1] == "" { // 判断切片的最后一个元素是否为空,为空则移除 + to = to[:len(to)-1] + } + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: EmailTest +//@description: Email测试方法 +//@param: subject string, body string +//@return: error + +func EmailTest(subject string, body string) error { + to := []string{global.GlobalConfig.To} + return send(to, subject, body) +} + +//@author: [maplepie](https://github.com/maplepie) +//@function: send +//@description: Email发送方法 +//@param: subject string, body string +//@return: error + +func send(to []string, subject string, body string) error { + from := global.GlobalConfig.From + nickname := global.GlobalConfig.Nickname + secret := global.GlobalConfig.Secret + host := global.GlobalConfig.Host + port := global.GlobalConfig.Port + isSSL := global.GlobalConfig.IsSSL + isLoginAuth := global.GlobalConfig.IsLoginAuth + + var auth smtp.Auth + if isLoginAuth { + auth = LoginAuth(from, secret) + } else { + auth = smtp.PlainAuth("", from, secret, host) + } + e := email.NewEmail() + if nickname != "" { + e.From = fmt.Sprintf("%s <%s>", nickname, from) + } else { + e.From = from + } + e.To = to + e.Subject = subject + e.HTML = []byte(body) + var err error + hostAddr := fmt.Sprintf("%s:%d", host, port) + if isSSL { + err = e.SendWithTLS(hostAddr, auth, &tls.Config{ServerName: host}) + } else { + err = e.Send(hostAddr, auth) + } + return err +} + +// LoginAuth 用于IBM、微软邮箱服务器的LOGIN认证方式 +type loginAuth struct { + username, password string +} + +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + // 邮箱服务器可能发送的其他提示信息 + prompt := strings.ToLower(string(fromServer)) + if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") { + return []byte(a.username), nil + } + if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") { + return []byte(a.password), nil + } + } + } + return nil, nil +} diff --git a/server/plugin/plugin-tool/utils/check.go b/server/plugin/plugin-tool/utils/check.go new file mode 100644 index 0000000..36ce274 --- /dev/null +++ b/server/plugin/plugin-tool/utils/check.go @@ -0,0 +1,138 @@ +package utils + +import ( + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/pkg/errors" + "go.uber.org/zap" + "gorm.io/gorm" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" +) + +var ( + ApiMap = make(map[string][]system.SysApi) + MenuMap = make(map[string][]system.SysBaseMenu) + DictMap = make(map[string][]system.SysDictionary) + rw sync.Mutex +) + +func getPluginName() string { + _, file, _, ok := runtime.Caller(2) + pluginName := "" + if ok { + file = filepath.ToSlash(file) + const key = "server/plugin/" + if idx := strings.Index(file, key); idx != -1 { + remain := file[idx+len(key):] + parts := strings.Split(remain, "/") + if len(parts) > 0 { + pluginName = parts[0] + } + } + } + return pluginName +} + +func RegisterApis(apis ...system.SysApi) { + name := getPluginName() + if name != "" { + rw.Lock() + ApiMap[name] = apis + rw.Unlock() + } + + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error + if err != nil { + zap.L().Error("注册API失败", zap.Error(err), zap.String("api", api.Path), zap.String("method", api.Method), zap.String("apiGroup", api.ApiGroup)) + return err + } + } + return nil + }) + if err != nil { + zap.L().Error("注册API失败", zap.Error(err)) + } +} + +func RegisterMenus(menus ...system.SysBaseMenu) { + name := getPluginName() + if name != "" { + rw.Lock() + MenuMap[name] = menus + rw.Unlock() + } + + parentMenu := menus[0] + otherMenus := menus[1:] + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + err := tx.Model(system.SysBaseMenu{}).Where("name = ? ", parentMenu.Name).FirstOrCreate(&parentMenu).Error + if err != nil { + zap.L().Error("注册菜单失败", zap.Error(err)) + return errors.Wrap(err, "注册菜单失败") + } + pid := parentMenu.ID + for i := range otherMenus { + otherMenus[i].ParentId = pid + err = tx.Model(system.SysBaseMenu{}).Where("name = ? ", otherMenus[i].Name).FirstOrCreate(&otherMenus[i]).Error + if err != nil { + zap.L().Error("注册菜单失败", zap.Error(err)) + return errors.Wrap(err, "注册菜单失败") + } + } + return nil + }) + if err != nil { + zap.L().Error("注册菜单失败", zap.Error(err)) + } + +} + +func RegisterDictionaries(dictionaries ...system.SysDictionary) { + name := getPluginName() + if name != "" { + rw.Lock() + DictMap[name] = dictionaries + rw.Unlock() + } + + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + details := dict.SysDictionaryDetails + dict.SysDictionaryDetails = nil + err := tx.Model(system.SysDictionary{}).Where("type = ?", dict.Type).FirstOrCreate(&dict).Error + if err != nil { + zap.L().Error("注册字典失败", zap.Error(err), zap.String("type", dict.Type)) + return err + } + for _, detail := range details { + detail.SysDictionaryID = int(dict.ID) + err = tx.Model(system.SysDictionaryDetail{}).Where("sys_dictionary_id = ? AND value = ?", dict.ID, detail.Value).FirstOrCreate(&detail).Error + if err != nil { + zap.L().Error("注册字典详情失败", zap.Error(err), zap.String("value", detail.Value)) + return err + } + } + } + return nil + }) + if err != nil { + zap.L().Error("注册字典失败", zap.Error(err)) + } +} + +func Pointer[T any](in T) *T { + return &in +} + +func GetPluginData(pluginName string) ([]system.SysApi, []system.SysBaseMenu, []system.SysDictionary) { + rw.Lock() + defer rw.Unlock() + return ApiMap[pluginName], MenuMap[pluginName], DictMap[pluginName] +} diff --git a/server/plugin/register.go b/server/plugin/register.go new file mode 100644 index 0000000..b0736c3 --- /dev/null +++ b/server/plugin/register.go @@ -0,0 +1 @@ +package plugin diff --git a/server/resource/function/api.go.tpl b/server/resource/function/api.go.tpl new file mode 100644 index 0000000..35a1cd5 --- /dev/null +++ b/server/resource/function/api.go.tpl @@ -0,0 +1,44 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func (a *{{.Abbreviation}}) {{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := service{{ .StructName }}.{{.FuncName}}(ctx) + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "成功" +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +func ({{.Abbreviation}}Api *{{.StructName}}Api){{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 请添加自己的业务逻辑 + err := {{.Abbreviation}}Service.{{.FuncName}}(ctx) + if err != nil { + global.GVA_LOG.Error("失败!", zap.Error(err)) + response.FailWithMessage("失败", c) + return + } + response.OkWithData("返回数据",c) +} +{{end}} diff --git a/server/resource/function/api.js.tpl b/server/resource/function/api.js.tpl new file mode 100644 index 0000000..a07b102 --- /dev/null +++ b/server/resource/function/api.js.tpl @@ -0,0 +1,32 @@ +{{if .IsPlugin}} +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// @Tags {{.StructName}} +// @Summary {{.FuncDesc}} +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "成功" +// @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] +export const {{.Router}} = () => { + return service({ + url: '/{{.Abbreviation}}/{{.Router}}', + method: '{{.Method}}' + }) +} + +{{- end -}} diff --git a/server/resource/function/server.go.tpl b/server/resource/function/server.go.tpl new file mode 100644 index 0000000..7327604 --- /dev/null +++ b/server/resource/function/server.go.tpl @@ -0,0 +1,25 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{if .IsPlugin}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) {{.FuncName}}(ctx context.Context) (err error) { + db := {{$db}}.Model(&model.{{.StructName}}{}) + return db.Error +} + +{{- else -}} + +// {{.FuncName}} {{.FuncDesc}} +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service){{.FuncName}}(ctx context.Context) (err error) { + // 请在这里实现自己的业务逻辑 + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + return db.Error +} +{{end}} diff --git a/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl new file mode 100644 index 0000000..49bfa20 --- /dev/null +++ b/server/resource/mcp/tools.tpl @@ -0,0 +1,56 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&{{.Name | title}}{}) +} + +type {{.Name | title}} struct { +} + +// {{.Description}} +func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // TODO: 实现工具逻辑 + // 参数示例: + // {{- range .Params}} + // {{.Name}} := request.GetArguments()["{{.Name}}"] + // {{- end}} + return &mcp.CallToolResult{ + Content: []mcp.Content{ + {{- range .Response}} + mcp.{{.Type | title}}Content{ + Type: "{{.Type}}", + // TODO: 填充{{.Type}}内容 + }, + {{- end}} + }, + }, nil +} + +func (t *{{.Name | title}}) New() mcp.Tool { + return mcp.NewTool("{{.Name}}", + mcp.WithDescription("{{.Description}}"), + {{- range .Params}} + mcp.With{{.Type | title}}("{{.Name}}", + {{- if .Required}}mcp.Required(),{{end}} + mcp.Description("{{.Description}}"), + {{- if .Default}} + {{- if eq .Type "string"}} + mcp.DefaultString("{{.Default}}"), + {{- else if eq .Type "number"}} + mcp.DefaultNumber({{.Default}}), + {{- else if eq .Type "boolean"}} + mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}), + {{- else if eq .Type "array"}} + // 注意:数组默认值需要在后端代码中预处理为正确的格式 + // mcp.DefaultArray({{.Default}}), + {{- end}} + {{- end}} + ), + {{- end}} + ) +} diff --git a/server/resource/package/readme.txt.tpl b/server/resource/package/readme.txt.tpl new file mode 100644 index 0000000..a737810 --- /dev/null +++ b/server/resource/package/readme.txt.tpl @@ -0,0 +1,7 @@ +代码解压后把fe的api文件内容粘贴进前端api文件夹下并修改为自己想要的名字即可 + +后端代码解压后同理,放到自己想要的 mvc对应路径 并且到 initRouter中注册自动生成的路由 到registerTable中注册自动生成的model + +项目github:"https://github.com/piexlmax/git.echol.cn/loser/st/server" + +希望大家给个star多多鼓励 diff --git a/server/resource/package/server/api/api.go.tpl b/server/resource/package/server/api/api.go.tpl new file mode 100644 index 0000000..528487f --- /dev/null +++ b/server/resource/package/server/api/api.go.tpl @@ -0,0 +1,260 @@ +package {{.Package}} + +import ( + {{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- end }} + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} + {{- else}} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" + {{- end}} +) + +type {{.StructName}}Api struct {} + +{{if not .OnlyTemplate}} + +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.CreatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Create{{.StructName}}(ctx,&{{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") + {{- if .AutoCreateResource }} + userID := utils.GetUserID(c) + {{- end }} + err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body {{.Package}}.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + + var {{.Abbreviation}} {{.Package}}.{{.StructName}} + err := c.ShouldBindJSON(&{{.Abbreviation}}) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + {{.Abbreviation}}.UpdatedBy = utils.GetUserID(c) + {{- end }} + err = {{.Abbreviation}}Service.Update{{.StructName}}(ctx,{{.Abbreviation}}) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data={{.Package}}.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表,Tree模式下不接受参数 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo {{.Package}}Req.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx,pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} +{{- end }} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} + +{{- end }} + +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + {{.Abbreviation}}Service.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的{{.Description}}接口信息", + }, "获取成功", c) +} diff --git a/server/resource/package/server/api/enter.go.tpl b/server/resource/package/server/api/enter.go.tpl new file mode 100644 index 0000000..778b314 --- /dev/null +++ b/server/resource/package/server/api/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ApiGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/model/model.go.tpl b/server/resource/package/server/model/model.go.tpl new file mode 100644 index 0000000..e1603ed --- /dev/null +++ b/server/resource/package/server/model/model.go.tpl @@ -0,0 +1,75 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + +{{ else }} +// 自动生成模板{{.StructName}} +package {{.Package}} + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.Description}} 结构体 {{.StructName}} +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} +{{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + +{{ end }} diff --git a/server/resource/package/server/model/request/request.go.tpl b/server/resource/package/server/model/request/request.go.tpl new file mode 100644 index 0000000..f8749f3 --- /dev/null +++ b/server/resource/package/server/model/request/request.go.tpl @@ -0,0 +1,39 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request + +import ( +{{- if not .OnlyTemplate }} + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} +{{- end }} +) + +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end}} +} +{{- end }} diff --git a/server/resource/package/server/router/enter.go.tpl b/server/resource/package/server/router/enter.go.tpl new file mode 100644 index 0000000..178aecf --- /dev/null +++ b/server/resource/package/server/router/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type RouterGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/router/router.go.tpl b/server/resource/package/server/router/router.go.tpl new file mode 100644 index 0000000..cac47ab --- /dev/null +++ b/server/resource/package/server/router/router.go.tpl @@ -0,0 +1,42 @@ +package {{.Package}} + +import ( + {{if .OnlyTemplate}}// {{ end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +type {{.StructName}}Router struct {} + +// Init{{.StructName}}Router 初始化 {{.Description}} 路由信息 +func (s *{{.StructName}}Router) Init{{.StructName}}Router(Router *gin.RouterGroup,PublicRouter *gin.RouterGroup) { + {{- if not .OnlyTemplate}} + {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- else }} + // {{.Abbreviation}}Router := Router.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // {{.Abbreviation}}RouterWithoutRecord := Router.Group("{{.Abbreviation}}") + {{- end}} + {{.Abbreviation}}RouterWithoutAuth := PublicRouter.Group("{{.Abbreviation}}") + {{- if not .OnlyTemplate}} + { + {{.Abbreviation}}Router.POST("create{{.StructName}}", {{.Abbreviation}}Api.Create{{.StructName}}) // 新建{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}", {{.Abbreviation}}Api.Delete{{.StructName}}) // 删除{{.Description}} + {{.Abbreviation}}Router.DELETE("delete{{.StructName}}ByIds", {{.Abbreviation}}Api.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + {{.Abbreviation}}Router.PUT("update{{.StructName}}", {{.Abbreviation}}Api.Update{{.StructName}}) // 更新{{.Description}} + } + { + {{.Abbreviation}}RouterWithoutRecord.GET("find{{.StructName}}", {{.Abbreviation}}Api.Find{{.StructName}}) // 根据ID获取{{.Description}} + {{.Abbreviation}}RouterWithoutRecord.GET("get{{.StructName}}List", {{.Abbreviation}}Api.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + {{- if .HasDataSource}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}DataSource", {{.Abbreviation}}Api.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{- else}} + { + {{.Abbreviation}}RouterWithoutAuth.GET("get{{.StructName}}Public", {{.Abbreviation}}Api.Get{{.StructName}}Public) // {{.Description}}开放接口 + } + {{ end }} +} diff --git a/server/resource/package/server/service/enter.go.tpl b/server/resource/package/server/service/enter.go.tpl new file mode 100644 index 0000000..adf1db0 --- /dev/null +++ b/server/resource/package/server/service/enter.go.tpl @@ -0,0 +1,4 @@ +package {{ .Package }} + +type ServiceGroup struct { +} \ No newline at end of file diff --git a/server/resource/package/server/service/service.go.tpl b/server/resource/package/server/service/service.go.tpl new file mode 100644 index 0000000..acd57a3 --- /dev/null +++ b/server/resource/package/server/service/service.go.tpl @@ -0,0 +1,213 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + {{ GenerateSearchConditions .Fields }} +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{ $dataDB := "" }} +{{- if eq $value.DBName "" }} +{{ $dataDB = $db }} +{{- else}} +{{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} +{{- end}} +{{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package {{.Package}} + +import ( +{{- if not .OnlyTemplate }} + "context" + "{{.Module}}/global" + "{{.Module}}/model/{{.Package}}" + {{- if not .IsTree}} + {{.Package}}Req "{{.Module}}/model/{{.Package}}/request" + {{- else }} + "{{.Module}}/utils" + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- end }} +) + +type {{.StructName}}Service struct {} + +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&{{.Package}}.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&{{.Package}}.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]{{.Package}}.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} {{.Package}}.{{.StructName}}) (err error) { + err = {{$db}}.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context) (list []*{{.Package}}.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []*{{.Package}}.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context, info {{.Package}}Req.{{.StructName}}Search) (list []{{.Package}}.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) + var {{.Abbreviation}}s []{{.Package}}.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } +{{- end }} + {{ GenerateSearchConditions .Fields }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} + +{{- end }} + +{{- if .HasDataSource }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{ $dataDB := "" }} + {{- if eq $value.DBName "" }} + {{ $dataDB = "global.GVA_DB" }} + {{- else}} + {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} + {{- end}} + {{$dataDB}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}Public(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} +{{- end }} diff --git a/server/resource/package/web/api/api.js.tpl b/server/resource/package/web/api/api.js.tpl new file mode 100644 index 0000000..a41ef6f --- /dev/null +++ b/server/resource/package/web/api/api.js.tpl @@ -0,0 +1,130 @@ +import service from '@/utils/request' + +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} + +{{- end}} + +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Param data query {{.Package}}Req.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/server/resource/package/web/view/form.vue.tpl b/server/resource/package/web/view/form.vue.tpl new file mode 100644 index 0000000..28c1f02 --- /dev/null +++ b/server/resource/package/web/view/form.vue.tpl @@ -0,0 +1,274 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/server/resource/package/web/view/table.vue.tpl b/server/resource/package/web/view/table.vue.tpl new file mode 100644 index 0000000..b2662da --- /dev/null +++ b/server/resource/package/web/view/table.vue.tpl @@ -0,0 +1,694 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} + +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/server/resource/plugin/server/api/api.go.tpl b/server/resource/plugin/server/api/api.go.tpl new file mode 100644 index 0000000..e69ae82 --- /dev/null +++ b/server/resource/plugin/server/api/api.go.tpl @@ -0,0 +1,255 @@ +package api + +import ( +{{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree}} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- end }} + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} +{{- else }} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} +{{if not .OnlyTemplate}} +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func (a *{{.Abbreviation}}) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + info.CreatedBy = utils.GetUserID(c) + {{- end }} + err = service{{ .StructName }}.Create{{.StructName}}(ctx,&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func (a *{{.Abbreviation}}) Update{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } +{{- if .AutoCreateResource }} + info.UpdatedBy = utils.GetUserID(c) +{{- end }} + err = service{{ .StructName }}.Update{{.StructName}}(ctx,info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data=model.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func (a *{{.Abbreviation}}) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := service{{ .StructName }}.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := service{{ .StructName }}.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo request.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service{{ .StructName }}.Get{{.StructName}}InfoList(ctx,pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} +{{- end }} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := service{{ .StructName }}.Get{{.StructName}}DataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} +{{- end }} +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + service{{ .StructName }}.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{"info": "不需要鉴权的{{.Description}}接口信息"}, "获取成功", c) +} diff --git a/server/resource/plugin/server/api/enter.go.tpl b/server/resource/plugin/server/api/enter.go.tpl new file mode 100644 index 0000000..989fb35 --- /dev/null +++ b/server/resource/plugin/server/api/enter.go.tpl @@ -0,0 +1,6 @@ +package api + +var Api = new(api) + +type api struct { +} diff --git a/server/resource/plugin/server/config/config.go.tpl b/server/resource/plugin/server/config/config.go.tpl new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/server/resource/plugin/server/config/config.go.tpl @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/server/resource/plugin/server/gen/gen.go.tpl b/server/resource/plugin/server/gen/gen.go.tpl new file mode 100644 index 0000000..5639d4a --- /dev/null +++ b/server/resource/plugin/server/gen/gen.go.tpl @@ -0,0 +1,18 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" +) + +//go:generate go mod tidy +//go:generate go mod download +//go:generate go run gen.go +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: filepath.Join("..", "..", "..", "{{ .Package }}", "blender", "model", "dao"), + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, + }) + g.ApplyBasic() + g.Execute() +} diff --git a/server/resource/plugin/server/initialize/api.go.tpl b/server/resource/plugin/server/initialize/api.go.tpl new file mode 100644 index 0000000..dfbea23 --- /dev/null +++ b/server/resource/plugin/server/initialize/api.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Api(ctx context.Context) { + entities := []model.SysApi{} + utils.RegisterApis(entities...) +} diff --git a/server/resource/plugin/server/initialize/dictionary.go.tpl b/server/resource/plugin/server/initialize/dictionary.go.tpl new file mode 100644 index 0000000..e61b42c --- /dev/null +++ b/server/resource/plugin/server/initialize/dictionary.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Dictionary(ctx context.Context) { + entities := []model.SysDictionary{} + utils.RegisterDictionaries(entities...) +} diff --git a/server/resource/plugin/server/initialize/gorm.go.tpl b/server/resource/plugin/server/initialize/gorm.go.tpl new file mode 100644 index 0000000..52c8183 --- /dev/null +++ b/server/resource/plugin/server/initialize/gorm.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "context" + "fmt" + "{{.Module}}/global" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate() + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/resource/plugin/server/initialize/menu.go.tpl b/server/resource/plugin/server/initialize/menu.go.tpl new file mode 100644 index 0000000..8774f35 --- /dev/null +++ b/server/resource/plugin/server/initialize/menu.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Menu(ctx context.Context) { + entities := []model.SysBaseMenu{} + utils.RegisterMenus(entities...) +} diff --git a/server/resource/plugin/server/initialize/router.go.tpl b/server/resource/plugin/server/initialize/router.go.tpl new file mode 100644 index 0000000..fbf03a3 --- /dev/null +++ b/server/resource/plugin/server/initialize/router.go.tpl @@ -0,0 +1,14 @@ +package initialize + +import ( + "{{.Module}}/global" + "{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + public.Use() + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) +} diff --git a/server/resource/plugin/server/initialize/viper.go.tpl b/server/resource/plugin/server/initialize/viper.go.tpl new file mode 100644 index 0000000..e759ad6 --- /dev/null +++ b/server/resource/plugin/server/initialize/viper.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "{{.Module}}/global" + "{{.Module}}/plugin/{{ .Package }}/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("{{ .Package }}", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/server/resource/plugin/server/model/model.go.tpl b/server/resource/plugin/server/model/model.go.tpl new file mode 100644 index 0000000..283841c --- /dev/null +++ b/server/resource/plugin/server/model/model.go.tpl @@ -0,0 +1,76 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + +{{ else }} +package model + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.StructName}} {{.Description}} 结构体 +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} + {{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + + +{{ end }} diff --git a/server/resource/plugin/server/model/request/request.go.tpl b/server/resource/plugin/server/model/request/request.go.tpl new file mode 100644 index 0000000..60cf677 --- /dev/null +++ b/server/resource/plugin/server/model/request/request.go.tpl @@ -0,0 +1,38 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request +{{- if not .OnlyTemplate}} +import ( + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} +) +{{- end}} +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} + +{{- if .GvaModel }} + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end }} +} +{{- end }} diff --git a/server/resource/plugin/server/plugin.go.tpl b/server/resource/plugin/server/plugin.go.tpl new file mode 100644 index 0000000..43ed056 --- /dev/null +++ b/server/resource/plugin/server/plugin.go.tpl @@ -0,0 +1,33 @@ +package {{ .Package }} + +import ( + "context" + "{{.Module}}/plugin/{{ .Package }}/initialize" + interfaces "{{.Module}}/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +func init() { + interfaces.Register(Plugin) +} + + +// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法 +// initialize.Viper() +// 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法 +// initialize.Api(ctx) +// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法 +// initialize.Menu(ctx) +// 安装插件时候自动注册的api数据请到下方法.Dictionary方法中实现并添加如下方法 +// initialize.Dictionary(ctx) +func (p *plugin) Register(group *gin.Engine) { + ctx := context.Background() + initialize.Gorm(ctx) + initialize.Router(group) +} diff --git a/server/resource/plugin/server/plugin/plugin.go.tpl b/server/resource/plugin/server/plugin/plugin.go.tpl new file mode 100644 index 0000000..7e25e07 --- /dev/null +++ b/server/resource/plugin/server/plugin/plugin.go.tpl @@ -0,0 +1,5 @@ +package plugin + +import "{{.Module}}/plugin/{{ .Package }}/config" + +var Config config.Config diff --git a/server/resource/plugin/server/router/enter.go.tpl b/server/resource/plugin/server/router/enter.go.tpl new file mode 100644 index 0000000..78517b3 --- /dev/null +++ b/server/resource/plugin/server/router/enter.go.tpl @@ -0,0 +1,6 @@ +package router + +var Router = new(router) + +type router struct { +} diff --git a/server/resource/plugin/server/router/router.go.tpl b/server/resource/plugin/server/router/router.go.tpl new file mode 100644 index 0000000..34bf4d8 --- /dev/null +++ b/server/resource/plugin/server/router/router.go.tpl @@ -0,0 +1,46 @@ +package router + +import ( + {{if .OnlyTemplate }} // {{end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +// Init 初始化 {{.Description}} 路由信息 +func (r *{{.Abbreviation}}) Init(public *gin.RouterGroup, private *gin.RouterGroup) { +{{- if not .OnlyTemplate }} + { + group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + group.POST("create{{.StructName}}", api{{.StructName}}.Create{{.StructName}}) // 新建{{.Description}} + group.DELETE("delete{{.StructName}}", api{{.StructName}}.Delete{{.StructName}}) // 删除{{.Description}} + group.DELETE("delete{{.StructName}}ByIds", api{{.StructName}}.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + group.PUT("update{{.StructName}}", api{{.StructName}}.Update{{.StructName}}) // 更新{{.Description}} + } + { + group := private.Group("{{.Abbreviation}}") + group.GET("find{{.StructName}}", api{{.StructName}}.Find{{.StructName}}) // 根据ID获取{{.Description}} + group.GET("get{{.StructName}}List", api{{.StructName}}.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + group := public.Group("{{.Abbreviation}}") + {{- if .HasDataSource}} + group.GET("get{{.StructName}}DataSource", api{{.StructName}}.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- else}} + // { + // group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // } + // { + // group := private.Group("{{.Abbreviation}}") + // } + { + group := public.Group("{{.Abbreviation}}") + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- end}} +} diff --git a/server/resource/plugin/server/service/enter.go.tpl b/server/resource/plugin/server/service/enter.go.tpl new file mode 100644 index 0000000..034facb --- /dev/null +++ b/server/resource/plugin/server/service/enter.go.tpl @@ -0,0 +1,7 @@ +package service + +var Service = new(service) + +type service struct { +} + diff --git a/server/resource/plugin/server/service/service.go.tpl b/server/resource/plugin/server/service/service.go.tpl new file mode 100644 index 0000000..9743602 --- /dev/null +++ b/server/resource/plugin/server/service/service.go.tpl @@ -0,0 +1,211 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + + {{ GenerateSearchConditions .Fields }} + +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package service + +import ( +{{- if not .OnlyTemplate }} + "context" + "{{.Module}}/global" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree }} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- else }} + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- if .IsTree }} + "{{.Module}}/utils" +{{- end }} +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *model.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&model.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&model.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} model.{{.StructName}}) (err error) { + err = {{$db}}.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} model.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context) (list []*model.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []*model.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context, info request.{{.StructName}}Search) (list []model.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []model.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } +{{- end }} + {{ GenerateSearchConditions .Fields }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} +{{- end }} +{{- if .HasDataSource }} +func (s *{{.Abbreviation}})Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} + +func (s *{{.Abbreviation}})Get{{.StructName}}Public(ctx context.Context) { + +} +{{- end }} diff --git a/server/resource/plugin/web/api/api.js.tpl b/server/resource/plugin/web/api/api.js.tpl new file mode 100644 index 0000000..0462fde --- /dev/null +++ b/server/resource/plugin/web/api/api.js.tpl @@ -0,0 +1,127 @@ +import service from '@/utils/request' +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} +{{- end}} +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/server/resource/plugin/web/form/form.vue.tpl b/server/resource/plugin/web/form/form.vue.tpl new file mode 100644 index 0000000..7d3406a --- /dev/null +++ b/server/resource/plugin/web/form/form.vue.tpl @@ -0,0 +1,464 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/server/resource/plugin/web/view/view.vue.tpl b/server/resource/plugin/web/view/view.vue.tpl new file mode 100644 index 0000000..98b557a --- /dev/null +++ b/server/resource/plugin/web/view/view.vue.tpl @@ -0,0 +1,689 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data || [] + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/server/router/app/ai_config.go b/server/router/app/ai_config.go new file mode 100644 index 0000000..7fd5f8d --- /dev/null +++ b/server/router/app/ai_config.go @@ -0,0 +1,26 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type AIConfigRouter struct{} + +// InitAIConfigRouter 初始化AI配置路由 +func (r *AIConfigRouter) InitAIConfigRouter(Router *gin.RouterGroup) { + aiConfigRouter := Router.Group("ai-config").Use(middleware.AppJWTAuth()) + aiConfigApi := v1.ApiGroupApp.AppApiGroup.AIConfigApi + + { + aiConfigRouter.POST("", aiConfigApi.CreateAIConfig) // 创建AI配置 + aiConfigRouter.GET("", aiConfigApi.GetAIConfigList) // 获取AI配置列表 + aiConfigRouter.PUT(":id", aiConfigApi.UpdateAIConfig) // 更新AI配置 + aiConfigRouter.DELETE(":id", aiConfigApi.DeleteAIConfig) // 删除AI配置 + aiConfigRouter.POST(":id/test", aiConfigApi.TestAIConfigByID) // 通过ID测试AI配置 + aiConfigRouter.GET(":id/models", aiConfigApi.GetModelsByID) // 通过ID获取模型列表 + aiConfigRouter.POST("models", aiConfigApi.GetModels) // 获取模型列表 + aiConfigRouter.POST("test", aiConfigApi.TestAIConfig) // 测试AI配置(用于新建时) + } +} diff --git a/server/router/app/auth.go b/server/router/app/auth.go new file mode 100644 index 0000000..3bb8b09 --- /dev/null +++ b/server/router/app/auth.go @@ -0,0 +1,36 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type AuthRouter struct{} + +// InitAuthRouter 初始化前台用户认证路由 +func (r *AuthRouter) InitAuthRouter(Router *gin.RouterGroup) { + authRouter := Router.Group("auth") + authApi := v1.ApiGroupApp.AppApiGroup.AuthApi + + { + // 公开路由(无需认证) + authRouter.POST("register", authApi.Register) // 注册 + authRouter.POST("login", authApi.Login) // 登录 + authRouter.POST("refresh", authApi.RefreshToken) // 刷新Token + } + + // 需要认证的路由 + authRouterAuth := Router.Group("auth").Use(middleware.AppJWTAuth()) + { + authRouterAuth.POST("logout", authApi.Logout) // 登出 + authRouterAuth.GET("userinfo", authApi.GetUserInfo) // 获取用户信息 + } + + // 用户相关路由 + userRouter := Router.Group("user").Use(middleware.AppJWTAuth()) + { + userRouter.PUT("profile", authApi.UpdateProfile) // 更新用户信息 + userRouter.POST("change-password", authApi.ChangePassword) // 修改密码 + } +} diff --git a/server/router/app/character.go b/server/router/app/character.go new file mode 100644 index 0000000..1589a00 --- /dev/null +++ b/server/router/app/character.go @@ -0,0 +1,25 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type CharacterRouter struct{} + +// InitCharacterRouter 初始化角色卡路由 +func (r *CharacterRouter) InitCharacterRouter(Router *gin.RouterGroup) { + characterRouter := Router.Group("character").Use(middleware.AppJWTAuth()) + characterApi := v1.ApiGroupApp.AppApiGroup.CharacterApi + + { + characterRouter.POST("", characterApi.CreateCharacter) // 创建角色卡 + characterRouter.GET("", characterApi.GetCharacterList) // 获取角色卡列表 + characterRouter.GET(":id", characterApi.GetCharacterByID) // 获取角色卡详情 + characterRouter.PUT(":id", characterApi.UpdateCharacter) // 更新角色卡 + characterRouter.DELETE(":id", characterApi.DeleteCharacter) // 删除角色卡 + characterRouter.POST("upload", characterApi.UploadCharacter) // 上传角色卡文件 + characterRouter.GET(":id/export", characterApi.ExportCharacter) // 导出角色卡 + } +} diff --git a/server/router/app/conversation.go b/server/router/app/conversation.go new file mode 100644 index 0000000..1d02520 --- /dev/null +++ b/server/router/app/conversation.go @@ -0,0 +1,25 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type ConversationRouter struct{} + +// InitConversationRouter 初始化对话路由 +func (r *ConversationRouter) InitConversationRouter(Router *gin.RouterGroup) { + conversationRouter := Router.Group("conversation").Use(middleware.AppJWTAuth()) + conversationApi := v1.ApiGroupApp.AppApiGroup.ConversationApi + + { + conversationRouter.POST("", conversationApi.CreateConversation) // 创建对话 + conversationRouter.GET("", conversationApi.GetConversationList) // 获取对话列表 + conversationRouter.GET(":id", conversationApi.GetConversationByID) // 获取对话详情 + conversationRouter.PUT(":id/settings", conversationApi.UpdateConversationSettings) // 更新对话设置 + conversationRouter.DELETE(":id", conversationApi.DeleteConversation) // 删除对话 + conversationRouter.GET(":id/messages", conversationApi.GetMessageList) // 获取消息列表 + conversationRouter.POST(":id/message", conversationApi.SendMessage) // 发送消息 + } +} diff --git a/server/router/app/enter.go b/server/router/app/enter.go new file mode 100644 index 0000000..fa158fc --- /dev/null +++ b/server/router/app/enter.go @@ -0,0 +1,10 @@ +package app + +type RouterGroup struct { + AuthRouter + CharacterRouter + ConversationRouter + AIConfigRouter + PresetRouter + UploadRouter +} diff --git a/server/router/app/preset.go b/server/router/app/preset.go new file mode 100644 index 0000000..15fb378 --- /dev/null +++ b/server/router/app/preset.go @@ -0,0 +1,24 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "github.com/gin-gonic/gin" +) + +type PresetRouter struct{} + +// InitPresetRouter 初始化预设路由 +func (r *PresetRouter) InitPresetRouter(Router *gin.RouterGroup) { + presetRouter := Router.Group("preset") + presetApi := v1.ApiGroupApp.AppApiGroup.PresetApi + { + presetRouter.POST("", presetApi.CreatePreset) // 创建预设 + presetRouter.GET("", presetApi.GetPresetList) // 获取预设列表 + presetRouter.GET("/:id", presetApi.GetPresetByID) // 获取预设详情 + presetRouter.PUT("/:id", presetApi.UpdatePreset) // 更新预设 + presetRouter.DELETE("/:id", presetApi.DeletePreset) // 删除预设 + presetRouter.POST("/:id/default", presetApi.SetDefaultPreset) // 设置默认预设 + presetRouter.POST("/import", presetApi.ImportPreset) // 导入预设 + presetRouter.GET("/:id/export", presetApi.ExportPreset) // 导出预设 + } +} diff --git a/server/router/app/upload.go b/server/router/app/upload.go new file mode 100644 index 0000000..1e57b8d --- /dev/null +++ b/server/router/app/upload.go @@ -0,0 +1,17 @@ +package app + +import ( + v1 "git.echol.cn/loser/st/server/api/v1" + "github.com/gin-gonic/gin" +) + +type UploadRouter struct{} + +// InitUploadRouter 初始化上传路由 +func (r *UploadRouter) InitUploadRouter(Router *gin.RouterGroup) { + uploadRouter := Router.Group("upload") + uploadApi := v1.ApiGroupApp.AppApiGroup.UploadApi + { + uploadRouter.POST("/image", uploadApi.UploadImage) // 上传图片 + } +} diff --git a/server/router/enter.go b/server/router/enter.go new file mode 100644 index 0000000..0ffad75 --- /dev/null +++ b/server/router/enter.go @@ -0,0 +1,15 @@ +package router + +import ( + "git.echol.cn/loser/st/server/router/app" + "git.echol.cn/loser/st/server/router/example" + "git.echol.cn/loser/st/server/router/system" +) + +var RouterGroupApp = new(RouterGroup) + +type RouterGroup struct { + System system.RouterGroup + Example example.RouterGroup + App app.RouterGroup +} diff --git a/server/router/example/enter.go b/server/router/example/enter.go new file mode 100644 index 0000000..9308d66 --- /dev/null +++ b/server/router/example/enter.go @@ -0,0 +1,17 @@ +package example + +import ( + api "git.echol.cn/loser/st/server/api/v1" +) + +type RouterGroup struct { + CustomerRouter + FileUploadAndDownloadRouter + AttachmentCategoryRouter +} + +var ( + exaCustomerApi = api.ApiGroupApp.ExampleApiGroup.CustomerApi + exaFileUploadAndDownloadApi = api.ApiGroupApp.ExampleApiGroup.FileUploadAndDownloadApi + attachmentCategoryApi = api.ApiGroupApp.ExampleApiGroup.AttachmentCategoryApi +) diff --git a/server/router/example/exa_attachment_category.go b/server/router/example/exa_attachment_category.go new file mode 100644 index 0000000..4900292 --- /dev/null +++ b/server/router/example/exa_attachment_category.go @@ -0,0 +1,16 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type AttachmentCategoryRouter struct{} + +func (r *AttachmentCategoryRouter) InitAttachmentCategoryRouterRouter(Router *gin.RouterGroup) { + router := Router.Group("attachmentCategory") + { + router.GET("getCategoryList", attachmentCategoryApi.GetCategoryList) // 分类列表 + router.POST("addCategory", attachmentCategoryApi.AddCategory) // 添加/编辑分类 + router.POST("deleteCategory", attachmentCategoryApi.DeleteCategory) // 删除分类 + } +} diff --git a/server/router/example/exa_customer.go b/server/router/example/exa_customer.go new file mode 100644 index 0000000..b0160b7 --- /dev/null +++ b/server/router/example/exa_customer.go @@ -0,0 +1,22 @@ +package example + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type CustomerRouter struct{} + +func (e *CustomerRouter) InitCustomerRouter(Router *gin.RouterGroup) { + customerRouter := Router.Group("customer").Use(middleware.OperationRecord()) + customerRouterWithoutRecord := Router.Group("customer") + { + customerRouter.POST("customer", exaCustomerApi.CreateExaCustomer) // 创建客户 + customerRouter.PUT("customer", exaCustomerApi.UpdateExaCustomer) // 更新客户 + customerRouter.DELETE("customer", exaCustomerApi.DeleteExaCustomer) // 删除客户 + } + { + customerRouterWithoutRecord.GET("customer", exaCustomerApi.GetExaCustomer) // 获取单一客户信息 + customerRouterWithoutRecord.GET("customerList", exaCustomerApi.GetExaCustomerList) // 获取客户列表 + } +} diff --git a/server/router/example/exa_file_upload_and_download.go b/server/router/example/exa_file_upload_and_download.go new file mode 100644 index 0000000..84f6ecd --- /dev/null +++ b/server/router/example/exa_file_upload_and_download.go @@ -0,0 +1,22 @@ +package example + +import ( + "github.com/gin-gonic/gin" +) + +type FileUploadAndDownloadRouter struct{} + +func (e *FileUploadAndDownloadRouter) InitFileUploadAndDownloadRouter(Router *gin.RouterGroup) { + fileUploadAndDownloadRouter := Router.Group("fileUploadAndDownload") + { + fileUploadAndDownloadRouter.POST("upload", exaFileUploadAndDownloadApi.UploadFile) // 上传文件 + fileUploadAndDownloadRouter.POST("getFileList", exaFileUploadAndDownloadApi.GetFileList) // 获取上传文件列表 + fileUploadAndDownloadRouter.POST("deleteFile", exaFileUploadAndDownloadApi.DeleteFile) // 删除指定文件 + fileUploadAndDownloadRouter.POST("editFileName", exaFileUploadAndDownloadApi.EditFileName) // 编辑文件名或者备注 + fileUploadAndDownloadRouter.POST("breakpointContinue", exaFileUploadAndDownloadApi.BreakpointContinue) // 断点续传 + fileUploadAndDownloadRouter.GET("findFile", exaFileUploadAndDownloadApi.FindFile) // 查询当前文件成功的切片 + fileUploadAndDownloadRouter.POST("breakpointContinueFinish", exaFileUploadAndDownloadApi.BreakpointContinueFinish) // 切片传输完成 + fileUploadAndDownloadRouter.POST("removeChunk", exaFileUploadAndDownloadApi.RemoveChunk) // 删除切片 + fileUploadAndDownloadRouter.POST("importURL", exaFileUploadAndDownloadApi.ImportURL) // 导入URL + } +} diff --git a/server/router/system/enter.go b/server/router/system/enter.go new file mode 100644 index 0000000..2810131 --- /dev/null +++ b/server/router/system/enter.go @@ -0,0 +1,52 @@ +package system + +import api "git.echol.cn/loser/st/server/api/v1" + +type RouterGroup struct { + ApiRouter + JwtRouter + SysRouter + BaseRouter + InitRouter + MenuRouter + UserRouter + CasbinRouter + AutoCodeRouter + AuthorityRouter + DictionaryRouter + OperationRecordRouter + DictionaryDetailRouter + AuthorityBtnRouter + SysExportTemplateRouter + SysParamsRouter + SysVersionRouter + SysErrorRouter + LoginLogRouter + ApiTokenRouter + SkillsRouter +} + +var ( + dbApi = api.ApiGroupApp.SystemApiGroup.DBApi + jwtApi = api.ApiGroupApp.SystemApiGroup.JwtApi + baseApi = api.ApiGroupApp.SystemApiGroup.BaseApi + casbinApi = api.ApiGroupApp.SystemApiGroup.CasbinApi + systemApi = api.ApiGroupApp.SystemApiGroup.SystemApi + sysParamsApi = api.ApiGroupApp.SystemApiGroup.SysParamsApi + autoCodeApi = api.ApiGroupApp.SystemApiGroup.AutoCodeApi + authorityApi = api.ApiGroupApp.SystemApiGroup.AuthorityApi + apiRouterApi = api.ApiGroupApp.SystemApiGroup.SystemApiApi + dictionaryApi = api.ApiGroupApp.SystemApiGroup.DictionaryApi + authorityBtnApi = api.ApiGroupApp.SystemApiGroup.AuthorityBtnApi + authorityMenuApi = api.ApiGroupApp.SystemApiGroup.AuthorityMenuApi + autoCodePluginApi = api.ApiGroupApp.SystemApiGroup.AutoCodePluginApi + autocodeHistoryApi = api.ApiGroupApp.SystemApiGroup.AutoCodeHistoryApi + operationRecordApi = api.ApiGroupApp.SystemApiGroup.OperationRecordApi + autoCodePackageApi = api.ApiGroupApp.SystemApiGroup.AutoCodePackageApi + dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi + autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi + exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi + sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi + sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi + skillsApi = api.ApiGroupApp.SystemApiGroup.SkillsApi +) diff --git a/server/router/system/sys_api.go b/server/router/system/sys_api.go new file mode 100644 index 0000000..908e590 --- /dev/null +++ b/server/router/system/sys_api.go @@ -0,0 +1,33 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type ApiRouter struct{} + +func (s *ApiRouter) InitApiRouter(Router *gin.RouterGroup, RouterPub *gin.RouterGroup) { + apiRouter := Router.Group("api").Use(middleware.OperationRecord()) + apiRouterWithoutRecord := Router.Group("api") + + apiPublicRouterWithoutRecord := RouterPub.Group("api") + { + apiRouter.GET("getApiGroups", apiRouterApi.GetApiGroups) // 获取路由组 + apiRouter.GET("syncApi", apiRouterApi.SyncApi) // 同步Api + apiRouter.POST("ignoreApi", apiRouterApi.IgnoreApi) // 忽略Api + apiRouter.POST("enterSyncApi", apiRouterApi.EnterSyncApi) // 确认同步Api + apiRouter.POST("createApi", apiRouterApi.CreateApi) // 创建Api + apiRouter.POST("deleteApi", apiRouterApi.DeleteApi) // 删除Api + apiRouter.POST("getApiById", apiRouterApi.GetApiById) // 获取单条Api消息 + apiRouter.POST("updateApi", apiRouterApi.UpdateApi) // 更新api + apiRouter.DELETE("deleteApisByIds", apiRouterApi.DeleteApisByIds) // 删除选中api + } + { + apiRouterWithoutRecord.POST("getAllApis", apiRouterApi.GetAllApis) // 获取所有api + apiRouterWithoutRecord.POST("getApiList", apiRouterApi.GetApiList) // 获取Api列表 + } + { + apiPublicRouterWithoutRecord.GET("freshCasbin", apiRouterApi.FreshCasbin) // 刷新casbin权限 + } +} diff --git a/server/router/system/sys_api_token.go b/server/router/system/sys_api_token.go new file mode 100644 index 0000000..4244f0c --- /dev/null +++ b/server/router/system/sys_api_token.go @@ -0,0 +1,19 @@ +package system + +import ( + "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type ApiTokenRouter struct{} + +func (s *ApiTokenRouter) InitApiTokenRouter(Router *gin.RouterGroup) { + apiTokenRouter := Router.Group("sysApiToken").Use(middleware.OperationRecord()) + apiTokenApi := v1.ApiGroupApp.SystemApiGroup.ApiTokenApi + { + apiTokenRouter.POST("createApiToken", apiTokenApi.CreateApiToken) // 签发Token + apiTokenRouter.POST("getApiTokenList", apiTokenApi.GetApiTokenList) // 获取列表 + apiTokenRouter.POST("deleteApiToken", apiTokenApi.DeleteApiToken) // 作废Token + } +} diff --git a/server/router/system/sys_authority.go b/server/router/system/sys_authority.go new file mode 100644 index 0000000..884be08 --- /dev/null +++ b/server/router/system/sys_authority.go @@ -0,0 +1,23 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type AuthorityRouter struct{} + +func (s *AuthorityRouter) InitAuthorityRouter(Router *gin.RouterGroup) { + authorityRouter := Router.Group("authority").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authority") + { + authorityRouter.POST("createAuthority", authorityApi.CreateAuthority) // 创建角色 + authorityRouter.POST("deleteAuthority", authorityApi.DeleteAuthority) // 删除角色 + authorityRouter.PUT("updateAuthority", authorityApi.UpdateAuthority) // 更新角色 + authorityRouter.POST("copyAuthority", authorityApi.CopyAuthority) // 拷贝角色 + authorityRouter.POST("setDataAuthority", authorityApi.SetDataAuthority) // 设置角色资源权限 + } + { + authorityRouterWithoutRecord.POST("getAuthorityList", authorityApi.GetAuthorityList) // 获取角色列表 + } +} diff --git a/server/router/system/sys_authority_btn.go b/server/router/system/sys_authority_btn.go new file mode 100644 index 0000000..370db85 --- /dev/null +++ b/server/router/system/sys_authority_btn.go @@ -0,0 +1,19 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AuthorityBtnRouter struct{} + +var AuthorityBtnRouterApp = new(AuthorityBtnRouter) + +func (s *AuthorityBtnRouter) InitAuthorityBtnRouterRouter(Router *gin.RouterGroup) { + // authorityRouter := Router.Group("authorityBtn").Use(middleware.OperationRecord()) + authorityRouterWithoutRecord := Router.Group("authorityBtn") + { + authorityRouterWithoutRecord.POST("getAuthorityBtn", authorityBtnApi.GetAuthorityBtn) + authorityRouterWithoutRecord.POST("setAuthorityBtn", authorityBtnApi.SetAuthorityBtn) + authorityRouterWithoutRecord.POST("canRemoveAuthorityBtn", authorityBtnApi.CanRemoveAuthorityBtn) + } +} diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go new file mode 100644 index 0000000..0f19dd9 --- /dev/null +++ b/server/router/system/sys_auto_code.go @@ -0,0 +1,47 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AutoCodeRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPublic *gin.RouterGroup) { + autoCodeRouter := Router.Group("autoCode") + publicAutoCodeRouter := RouterPublic.Group("autoCode") + { + autoCodeRouter.GET("getDB", autoCodeApi.GetDB) // 获取数据库 + autoCodeRouter.GET("getTables", autoCodeApi.GetTables) // 获取对应数据库的表 + autoCodeRouter.GET("getColumn", autoCodeApi.GetColumn) // 获取指定表所有字段信息 + } + { + autoCodeRouter.POST("preview", autoCodeTemplateApi.Preview) // 获取自动创建代码预览 + autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) // 创建自动化代码 + autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) // 为代码插入方法 + } + { + autoCodeRouter.POST("mcp", autoCodeTemplateApi.MCP) // 自动创建Mcp Tool模板 + autoCodeRouter.POST("mcpList", autoCodeTemplateApi.MCPList) // 获取MCP ToolList + autoCodeRouter.POST("mcpTest", autoCodeTemplateApi.MCPTest) // MCP 工具测试 + } + { + autoCodeRouter.POST("getPackage", autoCodePackageApi.All) // 获取package包 + autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) // 删除package包 + autoCodeRouter.POST("createPackage", autoCodePackageApi.Create) // 创建package包 + } + { + autoCodeRouter.GET("getTemplates", autoCodePackageApi.Templates) // 创建package包 + } + { + autoCodeRouter.POST("pubPlug", autoCodePluginApi.Packaged) // 打包插件 + autoCodeRouter.POST("installPlugin", autoCodePluginApi.Install) // 自动安装插件 + autoCodeRouter.POST("removePlugin", autoCodePluginApi.Remove) // 自动删除插件 + autoCodeRouter.GET("getPluginList", autoCodePluginApi.GetPluginList) // 获取插件列表 + } + { + publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto) + publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单 + publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API + publicAutoCodeRouter.POST("initDictionary", autoCodePluginApi.InitDictionary) // 同步插件字典 + } +} diff --git a/server/router/system/sys_auto_code_history.go b/server/router/system/sys_auto_code_history.go new file mode 100644 index 0000000..42a2bef --- /dev/null +++ b/server/router/system/sys_auto_code_history.go @@ -0,0 +1,17 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type AutoCodeHistoryRouter struct{} + +func (s *AutoCodeRouter) InitAutoCodeHistoryRouter(Router *gin.RouterGroup) { + autoCodeHistoryRouter := Router.Group("autoCode") + { + autoCodeHistoryRouter.POST("getMeta", autocodeHistoryApi.First) // 根据id获取meta信息 + autoCodeHistoryRouter.POST("rollback", autocodeHistoryApi.RollBack) // 回滚 + autoCodeHistoryRouter.POST("delSysHistory", autocodeHistoryApi.Delete) // 删除回滚记录 + autoCodeHistoryRouter.POST("getSysHistory", autocodeHistoryApi.GetList) // 获取回滚记录分页 + } +} diff --git a/server/router/system/sys_base.go b/server/router/system/sys_base.go new file mode 100644 index 0000000..7d959bb --- /dev/null +++ b/server/router/system/sys_base.go @@ -0,0 +1,16 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type BaseRouter struct{} + +func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + baseRouter := Router.Group("base") + { + baseRouter.POST("login", baseApi.Login) + baseRouter.POST("captcha", baseApi.Captcha) + } + return baseRouter +} diff --git a/server/router/system/sys_casbin.go b/server/router/system/sys_casbin.go new file mode 100644 index 0000000..e3cb665 --- /dev/null +++ b/server/router/system/sys_casbin.go @@ -0,0 +1,19 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type CasbinRouter struct{} + +func (s *CasbinRouter) InitCasbinRouter(Router *gin.RouterGroup) { + casbinRouter := Router.Group("casbin").Use(middleware.OperationRecord()) + casbinRouterWithoutRecord := Router.Group("casbin") + { + casbinRouter.POST("updateCasbin", casbinApi.UpdateCasbin) + } + { + casbinRouterWithoutRecord.POST("getPolicyPathByAuthorityId", casbinApi.GetPolicyPathByAuthorityId) + } +} diff --git a/server/router/system/sys_dictionary.go b/server/router/system/sys_dictionary.go new file mode 100644 index 0000000..01b7ec2 --- /dev/null +++ b/server/router/system/sys_dictionary.go @@ -0,0 +1,24 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryRouter struct{} + +func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) { + sysDictionaryRouter := Router.Group("sysDictionary").Use(middleware.OperationRecord()) + sysDictionaryRouterWithoutRecord := Router.Group("sysDictionary") + { + sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary + sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary + sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary + sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary + sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary + } + { + sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary + sysDictionaryRouterWithoutRecord.GET("getSysDictionaryList", dictionaryApi.GetSysDictionaryList) // 获取SysDictionary列表 + } +} diff --git a/server/router/system/sys_dictionary_detail.go b/server/router/system/sys_dictionary_detail.go new file mode 100644 index 0000000..af1a32b --- /dev/null +++ b/server/router/system/sys_dictionary_detail.go @@ -0,0 +1,26 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type DictionaryDetailRouter struct{} + +func (s *DictionaryDetailRouter) InitSysDictionaryDetailRouter(Router *gin.RouterGroup) { + dictionaryDetailRouter := Router.Group("sysDictionaryDetail").Use(middleware.OperationRecord()) + dictionaryDetailRouterWithoutRecord := Router.Group("sysDictionaryDetail") + { + dictionaryDetailRouter.POST("createSysDictionaryDetail", dictionaryDetailApi.CreateSysDictionaryDetail) // 新建SysDictionaryDetail + dictionaryDetailRouter.DELETE("deleteSysDictionaryDetail", dictionaryDetailApi.DeleteSysDictionaryDetail) // 删除SysDictionaryDetail + dictionaryDetailRouter.PUT("updateSysDictionaryDetail", dictionaryDetailApi.UpdateSysDictionaryDetail) // 更新SysDictionaryDetail + } + { + dictionaryDetailRouterWithoutRecord.GET("findSysDictionaryDetail", dictionaryDetailApi.FindSysDictionaryDetail) // 根据ID获取SysDictionaryDetail + dictionaryDetailRouterWithoutRecord.GET("getSysDictionaryDetailList", dictionaryDetailApi.GetSysDictionaryDetailList) // 获取SysDictionaryDetail列表 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeList", dictionaryDetailApi.GetDictionaryTreeList) // 获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryTreeListByType", dictionaryDetailApi.GetDictionaryTreeListByType) // 根据字典类型获取字典详情树形结构 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryDetailsByParent", dictionaryDetailApi.GetDictionaryDetailsByParent) // 根据父级ID获取字典详情 + dictionaryDetailRouterWithoutRecord.GET("getDictionaryPath", dictionaryDetailApi.GetDictionaryPath) // 获取字典详情的完整路径 + } +} diff --git a/server/router/system/sys_error.go b/server/router/system/sys_error.go new file mode 100644 index 0000000..168f390 --- /dev/null +++ b/server/router/system/sys_error.go @@ -0,0 +1,28 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysErrorRouter struct{} + +// InitSysErrorRouter 初始化 错误日志 路由信息 +func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord()) + sysErrorRouterWithoutRecord := Router.Group("sysError") + sysErrorRouterWithoutAuth := PublicRouter.Group("sysError") + { + sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志 + sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志 + sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志 + sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理 + } + { + sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志 + sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表 + } + { + sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志 + } +} diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go new file mode 100644 index 0000000..3f54a85 --- /dev/null +++ b/server/router/system/sys_export_template.go @@ -0,0 +1,35 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysExportTemplateRouter struct { +} + +// InitSysExportTemplateRouter 初始化 导出模板 路由信息 +func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { + sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord()) + sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate") + sysExportTemplateRouterWithoutAuth := pubRouter.Group("sysExportTemplate") + + { + sysExportTemplateRouter.POST("createSysExportTemplate", exportTemplateApi.CreateSysExportTemplate) // 新建导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplate", exportTemplateApi.DeleteSysExportTemplate) // 删除导出模板 + sysExportTemplateRouter.DELETE("deleteSysExportTemplateByIds", exportTemplateApi.DeleteSysExportTemplateByIds) // 批量删除导出模板 + sysExportTemplateRouter.PUT("updateSysExportTemplate", exportTemplateApi.UpdateSysExportTemplate) // 更新导出模板 + sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 导入excel模板数据 + } + { + sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板 + sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 + sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token + sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 + sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL + } + { + sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格 + sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板 + } +} diff --git a/server/router/system/sys_initdb.go b/server/router/system/sys_initdb.go new file mode 100644 index 0000000..3a6de50 --- /dev/null +++ b/server/router/system/sys_initdb.go @@ -0,0 +1,15 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type InitRouter struct{} + +func (s *InitRouter) InitInitRouter(Router *gin.RouterGroup) { + initRouter := Router.Group("init") + { + initRouter.POST("initdb", dbApi.InitDB) // 初始化数据库 + initRouter.POST("checkdb", dbApi.CheckDB) // 检测是否需要初始化数据库 + } +} diff --git a/server/router/system/sys_jwt.go b/server/router/system/sys_jwt.go new file mode 100644 index 0000000..4716031 --- /dev/null +++ b/server/router/system/sys_jwt.go @@ -0,0 +1,14 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type JwtRouter struct{} + +func (s *JwtRouter) InitJwtRouter(Router *gin.RouterGroup) { + jwtRouter := Router.Group("jwt") + { + jwtRouter.POST("jsonInBlacklist", jwtApi.JsonInBlacklist) // jwt加入黑名单 + } +} diff --git a/server/router/system/sys_login_log.go b/server/router/system/sys_login_log.go new file mode 100644 index 0000000..c7d5a8c --- /dev/null +++ b/server/router/system/sys_login_log.go @@ -0,0 +1,23 @@ +package system + +import ( + "git.echol.cn/loser/st/server/api/v1" + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type LoginLogRouter struct{} + +func (s *LoginLogRouter) InitLoginLogRouter(Router *gin.RouterGroup) { + loginLogRouter := Router.Group("sysLoginLog").Use(middleware.OperationRecord()) + loginLogRouterWithoutRecord := Router.Group("sysLoginLog") + sysLoginLogApi := v1.ApiGroupApp.SystemApiGroup.LoginLogApi + { + loginLogRouter.DELETE("deleteLoginLog", sysLoginLogApi.DeleteLoginLog) // 删除登录日志 + loginLogRouter.DELETE("deleteLoginLogByIds", sysLoginLogApi.DeleteLoginLogByIds) // 批量删除登录日志 + } + { + loginLogRouterWithoutRecord.GET("findLoginLog", sysLoginLogApi.FindLoginLog) // 根据ID获取登录日志(详情) + loginLogRouterWithoutRecord.GET("getLoginLogList", sysLoginLogApi.GetLoginLogList) // 获取登录日志列表 + } +} diff --git a/server/router/system/sys_menu.go b/server/router/system/sys_menu.go new file mode 100644 index 0000000..5b4779e --- /dev/null +++ b/server/router/system/sys_menu.go @@ -0,0 +1,27 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type MenuRouter struct{} + +func (s *MenuRouter) InitMenuRouter(Router *gin.RouterGroup) (R gin.IRoutes) { + menuRouter := Router.Group("menu").Use(middleware.OperationRecord()) + menuRouterWithoutRecord := Router.Group("menu") + { + menuRouter.POST("addBaseMenu", authorityMenuApi.AddBaseMenu) // 新增菜单 + menuRouter.POST("addMenuAuthority", authorityMenuApi.AddMenuAuthority) // 增加menu和角色关联关系 + menuRouter.POST("deleteBaseMenu", authorityMenuApi.DeleteBaseMenu) // 删除菜单 + menuRouter.POST("updateBaseMenu", authorityMenuApi.UpdateBaseMenu) // 更新菜单 + } + { + menuRouterWithoutRecord.POST("getMenu", authorityMenuApi.GetMenu) // 获取菜单树 + menuRouterWithoutRecord.POST("getMenuList", authorityMenuApi.GetMenuList) // 分页获取基础menu列表 + menuRouterWithoutRecord.POST("getBaseMenuTree", authorityMenuApi.GetBaseMenuTree) // 获取用户动态路由 + menuRouterWithoutRecord.POST("getMenuAuthority", authorityMenuApi.GetMenuAuthority) // 获取指定角色menu + menuRouterWithoutRecord.POST("getBaseMenuById", authorityMenuApi.GetBaseMenuById) // 根据id获取菜单 + } + return menuRouter +} diff --git a/server/router/system/sys_operation_record.go b/server/router/system/sys_operation_record.go new file mode 100644 index 0000000..d158d5e --- /dev/null +++ b/server/router/system/sys_operation_record.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" +) + +type OperationRecordRouter struct{} + +func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { + operationRecordRouter := Router.Group("sysOperationRecord") + { + operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord + operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord + operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord + operationRecordRouter.GET("getSysOperationRecordList", operationRecordApi.GetSysOperationRecordList) // 获取SysOperationRecord列表 + + } +} diff --git a/server/router/system/sys_params.go b/server/router/system/sys_params.go new file mode 100644 index 0000000..215a975 --- /dev/null +++ b/server/router/system/sys_params.go @@ -0,0 +1,25 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysParamsRouter struct{} + +// InitSysParamsRouter 初始化 参数 路由信息 +func (s *SysParamsRouter) InitSysParamsRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + sysParamsRouter := Router.Group("sysParams").Use(middleware.OperationRecord()) + sysParamsRouterWithoutRecord := Router.Group("sysParams") + { + sysParamsRouter.POST("createSysParams", sysParamsApi.CreateSysParams) // 新建参数 + sysParamsRouter.DELETE("deleteSysParams", sysParamsApi.DeleteSysParams) // 删除参数 + sysParamsRouter.DELETE("deleteSysParamsByIds", sysParamsApi.DeleteSysParamsByIds) // 批量删除参数 + sysParamsRouter.PUT("updateSysParams", sysParamsApi.UpdateSysParams) // 更新参数 + } + { + sysParamsRouterWithoutRecord.GET("findSysParams", sysParamsApi.FindSysParams) // 根据ID获取参数 + sysParamsRouterWithoutRecord.GET("getSysParamsList", sysParamsApi.GetSysParamsList) // 获取参数列表 + sysParamsRouterWithoutRecord.GET("getSysParam", sysParamsApi.GetSysParam) // 根据Key获取参数 + } +} diff --git a/server/router/system/sys_skills.go b/server/router/system/sys_skills.go new file mode 100644 index 0000000..9529e66 --- /dev/null +++ b/server/router/system/sys_skills.go @@ -0,0 +1,29 @@ +package system + +import "github.com/gin-gonic/gin" + +type SkillsRouter struct{} + +func (s *SkillsRouter) InitSkillsRouter(Router *gin.RouterGroup) { + skillsRouter := Router.Group("skills") + { + skillsRouter.GET("getTools", skillsApi.GetTools) + skillsRouter.POST("getSkillList", skillsApi.GetSkillList) + skillsRouter.POST("getSkillDetail", skillsApi.GetSkillDetail) + skillsRouter.POST("saveSkill", skillsApi.SaveSkill) + skillsRouter.POST("createScript", skillsApi.CreateScript) + skillsRouter.POST("getScript", skillsApi.GetScript) + skillsRouter.POST("saveScript", skillsApi.SaveScript) + skillsRouter.POST("createResource", skillsApi.CreateResource) + skillsRouter.POST("getResource", skillsApi.GetResource) + skillsRouter.POST("saveResource", skillsApi.SaveResource) + skillsRouter.POST("createReference", skillsApi.CreateReference) + skillsRouter.POST("getReference", skillsApi.GetReference) + skillsRouter.POST("saveReference", skillsApi.SaveReference) + skillsRouter.POST("createTemplate", skillsApi.CreateTemplate) + skillsRouter.POST("getTemplate", skillsApi.GetTemplate) + skillsRouter.POST("saveTemplate", skillsApi.SaveTemplate) + skillsRouter.POST("getGlobalConstraint", skillsApi.GetGlobalConstraint) + skillsRouter.POST("saveGlobalConstraint", skillsApi.SaveGlobalConstraint) + } +} diff --git a/server/router/system/sys_system.go b/server/router/system/sys_system.go new file mode 100644 index 0000000..f85a356 --- /dev/null +++ b/server/router/system/sys_system.go @@ -0,0 +1,22 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysRouter struct{} + +func (s *SysRouter) InitSystemRouter(Router *gin.RouterGroup) { + sysRouter := Router.Group("system").Use(middleware.OperationRecord()) + sysRouterWithoutRecord := Router.Group("system") + + { + sysRouter.POST("setSystemConfig", systemApi.SetSystemConfig) // 设置配置文件内容 + sysRouter.POST("reloadSystem", systemApi.ReloadSystem) // 重启服务 + } + { + sysRouterWithoutRecord.POST("getSystemConfig", systemApi.GetSystemConfig) // 获取配置文件内容 + sysRouterWithoutRecord.POST("getServerInfo", systemApi.GetServerInfo) // 获取服务器信息 + } +} diff --git a/server/router/system/sys_user.go b/server/router/system/sys_user.go new file mode 100644 index 0000000..a7dda19 --- /dev/null +++ b/server/router/system/sys_user.go @@ -0,0 +1,28 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type UserRouter struct{} + +func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { + userRouter := Router.Group("user").Use(middleware.OperationRecord()) + userRouterWithoutRecord := Router.Group("user") + { + userRouter.POST("admin_register", baseApi.Register) // 管理员注册账号 + userRouter.POST("changePassword", baseApi.ChangePassword) // 用户修改密码 + userRouter.POST("setUserAuthority", baseApi.SetUserAuthority) // 设置用户权限 + userRouter.DELETE("deleteUser", baseApi.DeleteUser) // 删除用户 + userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 + userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 + userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 + userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码 + userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 + } + { + userRouterWithoutRecord.POST("getUserList", baseApi.GetUserList) // 分页获取用户列表 + userRouterWithoutRecord.GET("getUserInfo", baseApi.GetUserInfo) // 获取自身信息 + } +} diff --git a/server/router/system/sys_version.go b/server/router/system/sys_version.go new file mode 100644 index 0000000..57cae64 --- /dev/null +++ b/server/router/system/sys_version.go @@ -0,0 +1,25 @@ +package system + +import ( + "git.echol.cn/loser/st/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysVersionRouter struct{} + +// InitSysVersionRouter 初始化 版本管理 路由信息 +func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) { + sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord()) + sysVersionRouterWithoutRecord := Router.Group("sysVersion") + { + sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理 + sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理 + sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据 + sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据 + } + { + sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理 + sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表 + sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据 + } +} diff --git a/server/service/app/ai_config.go b/server/service/app/ai_config.go new file mode 100644 index 0000000..660d5dd --- /dev/null +++ b/server/service/app/ai_config.go @@ -0,0 +1,316 @@ +package app + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/app/response" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type AIConfigService struct{} + +// CreateAIConfig 创建AI配置 +func (s *AIConfigService) CreateAIConfig(req *request.CreateAIConfigRequest) (*response.AIConfigResponse, error) { + // 序列化 JSON 字段 + settingsJSON, _ := json.Marshal(req.Settings) + + config := app.AIConfig{ + Name: req.Name, + Provider: req.Provider, + BaseURL: req.BaseURL, + APIKey: req.APIKey, + DefaultModel: req.DefaultModel, + Settings: datatypes.JSON(settingsJSON), + Models: datatypes.JSON("[]"), + IsActive: true, + IsDefault: false, + } + + err := global.GVA_DB.Create(&config).Error + if err != nil { + return nil, err + } + + resp := response.ToAIConfigResponse(&config) + return &resp, nil +} + +// GetAIConfigList 获取AI配置列表 +func (s *AIConfigService) GetAIConfigList() (*response.AIConfigListResponse, error) { + var configs []app.AIConfig + var total int64 + + db := global.GVA_DB.Model(&app.AIConfig{}) + + err := db.Count(&total).Error + if err != nil { + return nil, err + } + + err = db.Order("is_default DESC, created_at DESC").Find(&configs).Error + if err != nil { + return nil, err + } + + list := make([]response.AIConfigResponse, len(configs)) + for i, config := range configs { + list[i] = response.ToAIConfigResponse(&config) + } + + return &response.AIConfigListResponse{ + List: list, + Total: total, + }, nil +} + +// UpdateAIConfig 更新AI配置 +func (s *AIConfigService) UpdateAIConfig(id uint, req *request.UpdateAIConfigRequest) error { + var config app.AIConfig + + err := global.GVA_DB.First(&config, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("配置不存在") + } + return err + } + + updates := map[string]interface{}{} + + if req.Name != "" { + updates["name"] = req.Name + } + if req.BaseURL != "" { + updates["base_url"] = req.BaseURL + } + // 只有当 API Key 不是脱敏格式时才更新 + // 脱敏格式: xxxx****xxxx + if req.APIKey != "" && !isMaskedAPIKey(req.APIKey) { + updates["api_key"] = req.APIKey + } + if req.DefaultModel != "" { + updates["default_model"] = req.DefaultModel + } + if req.Settings != nil { + settingsJSON, _ := json.Marshal(req.Settings) + updates["settings"] = datatypes.JSON(settingsJSON) + } + if req.IsActive != nil { + updates["is_active"] = *req.IsActive + } + if req.IsDefault != nil && *req.IsDefault { + // 如果设置为默认,先取消其他配置的默认状态 + global.GVA_DB.Model(&app.AIConfig{}).Where("id != ?", id).Update("is_default", false) + updates["is_default"] = true + } + + return global.GVA_DB.Model(&config).Updates(updates).Error +} + +// DeleteAIConfig 删除AI配置 +func (s *AIConfigService) DeleteAIConfig(id uint) error { + result := global.GVA_DB.Delete(&app.AIConfig{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return errors.New("配置不存在") + } + return nil +} + +// GetModels 获取可用模型列表 +func (s *AIConfigService) GetModels(req *request.GetModelsRequest) (*response.GetModelsResponse, error) { + client := &http.Client{Timeout: 20 * time.Second} + + // 构建请求 + modelsURL := req.BaseURL + "/models" + httpReq, err := http.NewRequest("GET", modelsURL, nil) + if err != nil { + return nil, err + } + + // 设置认证头 + if req.Provider == "openai" || req.Provider == "custom" { + httpReq.Header.Set("Authorization", "Bearer "+req.APIKey) + } else if req.Provider == "anthropic" { + httpReq.Header.Set("x-api-key", req.APIKey) + httpReq.Header.Set("anthropic-version", "2023-06-01") + } + + // 发送请求 + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API返回错误 %d: %s", resp.StatusCode, string(body)) + } + + // 解析响应 + var result struct { + Data []struct { + ID string `json:"id"` + Object string `json:"object"` + OwnedBy string `json:"owned_by"` + } `json:"data"` + } + + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, fmt.Errorf("解析响应失败: %v", err) + } + + // 转换为模型信息 + models := make([]response.ModelInfo, 0, len(result.Data)) + for _, model := range result.Data { + models = append(models, response.ModelInfo{ + ID: model.ID, + Name: model.ID, + OwnedBy: model.OwnedBy, + }) + } + + return &response.GetModelsResponse{Models: models}, nil +} + +// GetModelsByID 通过配置ID获取模型列表(使用数据库中的完整API Key) +func (s *AIConfigService) GetModelsByID(id uint) (*response.GetModelsResponse, error) { + var config app.AIConfig + err := global.GVA_DB.First(&config, id).Error + if err != nil { + return nil, errors.New("配置不存在") + } + + // 使用数据库中的完整 API Key + req := &request.GetModelsRequest{ + Provider: config.Provider, + BaseURL: config.BaseURL, + APIKey: config.APIKey, + } + + return s.GetModels(req) +} + +// TestAIConfig 测试AI配置 +func (s *AIConfigService) TestAIConfig(req *request.TestAIConfigRequest) (*response.TestAIConfigResponse, error) { + startTime := time.Now() + client := &http.Client{Timeout: 60 * time.Second} + + // 构建测试请求 + var requestBody map[string]interface{} + var endpoint string + + if req.Provider == "openai" || req.Provider == "custom" { + endpoint = req.BaseURL + "/chat/completions" + model := req.Model + if model == "" { + model = "gpt-3.5-turbo" + } + requestBody = map[string]interface{}{ + "model": model, + "messages": []map[string]string{ + {"role": "user", "content": "Hello"}, + }, + "max_tokens": 10, + } + } else if req.Provider == "anthropic" { + endpoint = req.BaseURL + "/messages" + model := req.Model + if model == "" { + model = "claude-3-haiku-20240307" + } + requestBody = map[string]interface{}{ + "model": model, + "messages": []map[string]string{ + {"role": "user", "content": "Hello"}, + }, + "max_tokens": 10, + } + } + + bodyBytes, _ := json.Marshal(requestBody) + httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return &response.TestAIConfigResponse{ + Success: false, + Message: fmt.Sprintf("创建请求失败: %v", err), + }, nil + } + + // 设置请求头 + httpReq.Header.Set("Content-Type", "application/json") + if req.Provider == "openai" || req.Provider == "custom" { + httpReq.Header.Set("Authorization", "Bearer "+req.APIKey) + } else if req.Provider == "anthropic" { + httpReq.Header.Set("x-api-key", req.APIKey) + httpReq.Header.Set("anthropic-version", "2023-06-01") + } + + // 发送请求 + resp, err := client.Do(httpReq) + latency := time.Since(startTime).Milliseconds() + + if err != nil { + return &response.TestAIConfigResponse{ + Success: false, + Message: fmt.Sprintf("连接失败: %v", err), + Latency: latency, + }, nil + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + return &response.TestAIConfigResponse{ + Success: false, + Message: fmt.Sprintf("API返回错误 %d: %s", resp.StatusCode, string(body)), + Latency: latency, + }, nil + } + + return &response.TestAIConfigResponse{ + Success: true, + Message: "连接成功,AI响应正常", + Latency: latency, + }, nil +} + +// TestAIConfigByID 通过ID测试AI配置(使用数据库中的完整API Key) +func (s *AIConfigService) TestAIConfigByID(id uint) (*response.TestAIConfigResponse, error) { + var config app.AIConfig + err := global.GVA_DB.First(&config, id).Error + if err != nil { + return nil, errors.New("配置不存在") + } + + // 使用数据库中的完整 API Key 进行测试 + req := &request.TestAIConfigRequest{ + Provider: config.Provider, + BaseURL: config.BaseURL, + APIKey: config.APIKey, // 使用完整的 API Key,而不是脱敏后的 + Model: config.DefaultModel, + } + + return s.TestAIConfig(req) +} + +// isMaskedAPIKey 检查是否是脱敏的 API Key +func isMaskedAPIKey(apiKey string) bool { + // 脱敏格式: xxxx****xxxx 或 **** + return len(apiKey) > 0 && (apiKey == "****" || (len(apiKey) > 8 && apiKey[4:len(apiKey)-4] == "****")) +} diff --git a/server/service/app/auth.go b/server/service/app/auth.go new file mode 100644 index 0000000..8118401 --- /dev/null +++ b/server/service/app/auth.go @@ -0,0 +1,254 @@ +package app + +import ( + "errors" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/app/response" + "git.echol.cn/loser/st/server/utils" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type AuthService struct{} + +// Register 用户注册 +func (s *AuthService) Register(req *request.RegisterRequest) error { + // 检查用户名是否已存在 + var count int64 + err := global.GVA_DB.Model(&app.AppUser{}).Where("username = ?", req.Username).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return errors.New("用户名已存在") + } + + // 检查邮箱是否已存在 + if req.Email != "" { + err = global.GVA_DB.Model(&app.AppUser{}).Where("email = ?", req.Email).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return errors.New("邮箱已被使用") + } + } + + // 密码加密 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return errors.New("密码加密失败") + } + + // 创建用户 + user := app.AppUser{ + UUID: uuid.New().String(), + Username: req.Username, + Password: string(hashedPassword), + NickName: req.NickName, + Email: req.Email, + Phone: req.Phone, + Status: "active", + Enable: true, + } + + if user.NickName == "" { + user.NickName = req.Username + } + + return global.GVA_DB.Create(&user).Error +} + +// Login 用户登录 +func (s *AuthService) Login(req *request.LoginRequest, ip string) (*response.LoginResponse, error) { + // 查询用户 + var user app.AppUser + err := global.GVA_DB.Where("username = ?", req.Username).First(&user).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户名或密码错误") + } + return nil, err + } + + // 检查用户状态 + if !user.Enable { + return nil, errors.New("账户已被禁用") + } + if user.Status != "active" { + return nil, errors.New("账户状态异常") + } + + // 验证密码 + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) + if err != nil { + return nil, errors.New("用户名或密码错误") + } + + // 生成 Token + token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username) + if err != nil { + return nil, errors.New("Token 生成失败") + } + + // 生成刷新 Token + refreshToken, refreshExpiresAt, err := utils.CreateAppRefreshToken(user.ID, user.Username) + if err != nil { + return nil, errors.New("刷新 Token 生成失败") + } + + // 更新最后登录信息 + now := time.Now() + global.GVA_DB.Model(&user).Updates(map[string]interface{}{ + "last_login_at": now, + "last_login_ip": ip, + }) + + // 保存会话信息(可选) + session := app.AppUserSession{ + UserID: user.ID, + SessionToken: token, + RefreshToken: refreshToken, + ExpiresAt: time.Unix(expiresAt, 0), + RefreshExpiresAt: func() *time.Time { t := time.Unix(refreshExpiresAt, 0); return &t }(), + IPAddress: ip, + } + global.GVA_DB.Create(&session) + + return &response.LoginResponse{ + User: response.ToAppUserResponse(&user), + Token: token, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, + }, nil +} + +// RefreshToken 刷新 Token +func (s *AuthService) RefreshToken(req *request.RefreshTokenRequest) (*response.LoginResponse, error) { + // 解析刷新 Token + claims, err := utils.ParseAppToken(req.RefreshToken) + if err != nil { + return nil, errors.New("刷新 Token 无效") + } + + // 查询用户 + var user app.AppUser + err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error + if err != nil { + return nil, errors.New("用户不存在") + } + + // 检查用户状态 + if !user.Enable { + return nil, errors.New("账户已被禁用") + } + + // 生成新的 Token + token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username) + if err != nil { + return nil, errors.New("Token 生成失败") + } + + // 生成新的刷新 Token + refreshToken, _, err := utils.CreateAppRefreshToken(user.ID, user.Username) + if err != nil { + return nil, errors.New("刷新 Token 生成失败") + } + + return &response.LoginResponse{ + User: response.ToAppUserResponse(&user), + Token: token, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, + }, nil +} + +// Logout 用户登出 +func (s *AuthService) Logout(userID uint, token string) error { + // 删除会话记录 + return global.GVA_DB.Where("user_id = ? AND session_token = ?", userID, token). + Delete(&app.AppUserSession{}).Error +} + +// GetUserInfo 获取用户信息 +func (s *AuthService) GetUserInfo(userID uint) (*response.AppUserResponse, error) { + var user app.AppUser + err := global.GVA_DB.Where("id = ?", userID).First(&user).Error + if err != nil { + return nil, err + } + + resp := response.ToAppUserResponse(&user) + return &resp, nil +} + +// UpdateProfile 更新用户信息 +func (s *AuthService) UpdateProfile(userID uint, req *request.UpdateProfileRequest) error { + updates := make(map[string]interface{}) + + if req.NickName != "" { + updates["nick_name"] = req.NickName + } + if req.Email != "" { + // 检查邮箱是否已被其他用户使用 + var count int64 + err := global.GVA_DB.Model(&app.AppUser{}). + Where("email = ? AND id != ?", req.Email, userID). + Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return errors.New("邮箱已被使用") + } + updates["email"] = req.Email + } + if req.Phone != "" { + updates["phone"] = req.Phone + } + if req.Avatar != "" { + updates["avatar"] = req.Avatar + } + if req.Preferences != "" { + updates["preferences"] = req.Preferences + } + if req.AISettings != "" { + updates["ai_settings"] = req.AISettings + } + + if len(updates) == 0 { + return nil + } + + return global.GVA_DB.Model(&app.AppUser{}).Where("id = ?", userID).Updates(updates).Error +} + +// ChangePassword 修改密码 +func (s *AuthService) ChangePassword(userID uint, req *request.ChangePasswordRequest) error { + // 查询用户 + var user app.AppUser + err := global.GVA_DB.Where("id = ?", userID).First(&user).Error + if err != nil { + return err + } + + // 验证旧密码 + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)) + if err != nil { + return errors.New("原密码错误") + } + + // 加密新密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + return errors.New("密码加密失败") + } + + // 更新密码 + return global.GVA_DB.Model(&user).Update("password", string(hashedPassword)).Error +} diff --git a/server/service/app/character.go b/server/service/app/character.go new file mode 100644 index 0000000..9b58e99 --- /dev/null +++ b/server/service/app/character.go @@ -0,0 +1,366 @@ +package app + +import ( + "encoding/base64" + "encoding/json" + "errors" + "mime/multipart" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/app/response" + "git.echol.cn/loser/st/server/utils" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type CharacterService struct{} + +// CreateCharacter 创建角色卡 +func (s *CharacterService) CreateCharacter(userID uint, req *request.CreateCharacterRequest) (*response.CharacterResponse, error) { + // 序列化 JSON 字段 + tagsJSON, _ := json.Marshal(req.Tags) + alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings) + characterBookJSON, _ := json.Marshal(req.CharacterBook) + extensionsJSON, _ := json.Marshal(req.Extensions) + + character := app.AICharacter{ + UserID: userID, + Name: req.Name, + Avatar: req.Avatar, + Creator: req.Creator, + Version: req.Version, + Description: req.Description, + Personality: req.Personality, + Scenario: req.Scenario, + FirstMes: req.FirstMes, + MesExample: req.MesExample, + CreatorNotes: req.CreatorNotes, + SystemPrompt: req.SystemPrompt, + PostHistoryInstructions: req.PostHistoryInstructions, + Tags: datatypes.JSON(tagsJSON), + AlternateGreetings: datatypes.JSON(alternateGreetingsJSON), + CharacterBook: datatypes.JSON(characterBookJSON), + Extensions: datatypes.JSON(extensionsJSON), + IsPublic: req.IsPublic, + Spec: "chara_card_v2", + SpecVersion: "2.0", + } + + err := global.GVA_DB.Create(&character).Error + if err != nil { + return nil, err + } + + resp := response.ToCharacterResponse(&character) + return &resp, nil +} + +// GetCharacterList 获取角色卡列表 +func (s *CharacterService) GetCharacterList(userID uint, req *request.GetCharacterListRequest) (*response.CharacterListResponse, error) { + var characters []app.AICharacter + var total int64 + + db := global.GVA_DB.Model(&app.AICharacter{}) + + // 筛选条件 + if req.IsPublic != nil { + if *req.IsPublic { + db = db.Where("is_public = ?", true) + } else { + db = db.Where("user_id = ?", userID) + } + } else { + db = db.Where("user_id = ? OR is_public = ?", userID, true) + } + + // 关键词搜索 + if req.Keyword != "" { + db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") + } + + // 标签筛选 + if req.Tag != "" { + db = db.Where("tags @> ?", datatypes.JSON(`["`+req.Tag+`"]`)) + } + + // 统计总数 + err := db.Count(&total).Error + if err != nil { + return nil, err + } + + // 分页查询 + offset := (req.Page - 1) * req.PageSize + err = db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&characters).Error + if err != nil { + return nil, err + } + + // 转换响应 + list := make([]response.CharacterResponse, len(characters)) + for i, char := range characters { + list[i] = response.ToCharacterResponse(&char) + } + + return &response.CharacterListResponse{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// GetCharacterByID 获取角色卡详情 +func (s *CharacterService) GetCharacterByID(userID, characterID uint) (*response.CharacterResponse, error) { + var character app.AICharacter + + err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true). + First(&character).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("角色卡不存在或无权访问") + } + return nil, err + } + + resp := response.ToCharacterResponse(&character) + return &resp, nil +} + +// UpdateCharacter 更新角色卡 +func (s *CharacterService) UpdateCharacter(userID, characterID uint, req *request.UpdateCharacterRequest) error { + var character app.AICharacter + + // 检查权限 + err := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).First(&character).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("角色卡不存在或无权修改") + } + return err + } + + // 更新字段 + updates := map[string]interface{}{} + + if req.Name != "" { + updates["name"] = req.Name + } + if req.Avatar != "" { + updates["avatar"] = req.Avatar + } + if req.Creator != "" { + updates["creator"] = req.Creator + } + if req.Version != "" { + updates["version"] = req.Version + } + if req.Description != "" { + updates["description"] = req.Description + } + if req.Personality != "" { + updates["personality"] = req.Personality + } + if req.Scenario != "" { + updates["scenario"] = req.Scenario + } + if req.FirstMes != "" { + updates["first_mes"] = req.FirstMes + } + if req.MesExample != "" { + updates["mes_example"] = req.MesExample + } + if req.CreatorNotes != "" { + updates["creator_notes"] = req.CreatorNotes + } + if req.SystemPrompt != "" { + updates["system_prompt"] = req.SystemPrompt + } + if req.PostHistoryInstructions != "" { + updates["post_history_instructions"] = req.PostHistoryInstructions + } + + if req.Tags != nil { + tagsJSON, _ := json.Marshal(req.Tags) + updates["tags"] = datatypes.JSON(tagsJSON) + } + if req.AlternateGreetings != nil { + alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings) + updates["alternate_greetings"] = datatypes.JSON(alternateGreetingsJSON) + } + if req.CharacterBook != nil { + characterBookJSON, _ := json.Marshal(req.CharacterBook) + updates["character_book"] = datatypes.JSON(characterBookJSON) + } + if req.Extensions != nil { + extensionsJSON, _ := json.Marshal(req.Extensions) + updates["extensions"] = datatypes.JSON(extensionsJSON) + } + + updates["is_public"] = req.IsPublic + + return global.GVA_DB.Model(&character).Updates(updates).Error +} + +// DeleteCharacter 删除角色卡 +func (s *CharacterService) DeleteCharacter(userID, characterID uint) error { + result := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).Delete(&app.AICharacter{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return errors.New("角色卡不存在或无权删除") + } + return nil +} + +// ImportCharacterFromPNG 从 PNG 文件导入角色卡 +func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) { + // 读取文件内容 + src, err := file.Open() + if err != nil { + return nil, errors.New("打开文件失败") + } + defer src.Close() + + // 读取文件数据 + fileData := make([]byte, file.Size) + _, err = src.Read(fileData) + if err != nil { + return nil, errors.New("读取文件失败") + } + + // 提取角色卡数据 + card, err := utils.ExtractCharacterFromPNG(fileData) + if err != nil { + return nil, err + } + + // 上传 PNG 图片到 OSS(替代 Base64) + var uploadService UploadService + avatarURL, err := uploadService.UploadImage(file) + if err != nil { + // 如果上传失败,回退到 Base64(向后兼容) + avatarURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fileData) + } + + // 转换为创建请求 + req := &request.CreateCharacterRequest{ + Name: card.Data.Name, + Avatar: avatarURL, + Creator: card.Data.Creator, + Version: card.Data.CharacterVersion, + Description: card.Data.Description, + Personality: card.Data.Personality, + Scenario: card.Data.Scenario, + FirstMes: card.Data.FirstMes, + MesExample: card.Data.MesExample, + CreatorNotes: card.Data.CreatorNotes, + SystemPrompt: card.Data.SystemPrompt, + PostHistoryInstructions: card.Data.PostHistoryInstructions, + Tags: card.Data.Tags, + AlternateGreetings: card.Data.AlternateGreetings, + CharacterBook: card.Data.CharacterBook, + Extensions: card.Data.Extensions, + IsPublic: false, + } + + return s.CreateCharacter(userID, req) +} + +// ImportCharacterFromJSON 从 JSON 文件导入角色卡 +func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) { + // 读取文件内容 + src, err := file.Open() + if err != nil { + return nil, errors.New("打开文件失败") + } + defer src.Close() + + // 读取文件数据 + fileData := make([]byte, file.Size) + _, err = src.Read(fileData) + if err != nil { + return nil, errors.New("读取文件失败") + } + + // 解析 JSON + card, err := utils.ParseCharacterCardJSON(fileData) + if err != nil { + return nil, err + } + + // 转换为创建请求 + req := &request.CreateCharacterRequest{ + Name: card.Data.Name, + Creator: card.Data.Creator, + Version: card.Data.CharacterVersion, + Description: card.Data.Description, + Personality: card.Data.Personality, + Scenario: card.Data.Scenario, + FirstMes: card.Data.FirstMes, + MesExample: card.Data.MesExample, + CreatorNotes: card.Data.CreatorNotes, + SystemPrompt: card.Data.SystemPrompt, + PostHistoryInstructions: card.Data.PostHistoryInstructions, + Tags: card.Data.Tags, + AlternateGreetings: card.Data.AlternateGreetings, + CharacterBook: card.Data.CharacterBook, + Extensions: card.Data.Extensions, + IsPublic: false, + } + + return s.CreateCharacter(userID, req) +} + +// ExportCharacterToJSON 导出角色卡为 JSON +func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*utils.CharacterCardV2, error) { + var character app.AICharacter + + err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true). + First(&character).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("角色卡不存在或无权访问") + } + return nil, err + } + + // 解析 JSON 字段 + var tags []string + var alternateGreetings []string + var characterBook map[string]interface{} + var extensions map[string]interface{} + + json.Unmarshal(character.Tags, &tags) + json.Unmarshal(character.AlternateGreetings, &alternateGreetings) + json.Unmarshal(character.CharacterBook, &characterBook) + json.Unmarshal(character.Extensions, &extensions) + + // 构建 V2 格式 + card := &utils.CharacterCardV2{ + Spec: character.Spec, + SpecVersion: character.SpecVersion, + Data: utils.CharacterCardV2Data{ + Name: character.Name, + Description: character.Description, + Personality: character.Personality, + Scenario: character.Scenario, + FirstMes: character.FirstMes, + MesExample: character.MesExample, + CreatorNotes: character.CreatorNotes, + SystemPrompt: character.SystemPrompt, + PostHistoryInstructions: character.PostHistoryInstructions, + Tags: tags, + Creator: character.Creator, + CharacterVersion: character.Version, + AlternateGreetings: alternateGreetings, + CharacterBook: characterBook, + Extensions: extensions, + }, + } + + return card, nil +} diff --git a/server/service/app/conversation.go b/server/service/app/conversation.go new file mode 100644 index 0000000..2ed3d6b --- /dev/null +++ b/server/service/app/conversation.go @@ -0,0 +1,1172 @@ +package app + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/app/request" + "git.echol.cn/loser/st/server/model/app/response" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type ConversationService struct{} + +// CreateConversation 创建对话 +func (s *ConversationService) CreateConversation(userID uint, req *request.CreateConversationRequest) (*response.ConversationResponse, error) { + // 验证角色卡是否存在且有权访问 + var character app.AICharacter + err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", req.CharacterID, userID, true). + First(&character).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("角色卡不存在或无权访问") + } + return nil, err + } + + // 生成对话标题 + title := req.Title + if title == "" { + title = "与 " + character.Name + " 的对话" + } + + // 获取默认 AI 配置 + var aiConfig app.AIConfig + err = global.GVA_DB.Where("is_active = ?", true). + Order("is_default DESC, created_at DESC"). + First(&aiConfig).Error + + // 设置 AI 配置 + aiProvider := req.AIProvider + model := req.Model + + if err == nil { + // 如果找到了默认配置,使用它 + if aiProvider == "" { + aiProvider = aiConfig.Provider + } + if model == "" { + model = aiConfig.DefaultModel + } + global.GVA_LOG.Info(fmt.Sprintf("创建对话使用 AI 配置: %s (Provider: %s, Model: %s)", aiConfig.Name, aiProvider, model)) + } else { + // 如果没有找到配置,使用默认值 + if aiProvider == "" { + aiProvider = "openai" + } + if model == "" { + model = "gpt-4" + } + global.GVA_LOG.Warn("未找到默认 AI 配置,使用硬编码默认值") + } + + // 创建对话 + conversation := app.Conversation{ + UserID: userID, + CharacterID: req.CharacterID, + Title: title, + PresetID: req.PresetID, + AIProvider: aiProvider, + Model: model, + Settings: datatypes.JSON("{}"), + } + + err = global.GVA_DB.Create(&conversation).Error + if err != nil { + return nil, err + } + + // 如果角色有开场白,创建开场白消息 + if character.FirstMes != "" { + firstMessage := app.Message{ + ConversationID: conversation.ID, + Role: "assistant", + Content: character.FirstMes, + TokenCount: len(character.FirstMes) / 4, + } + err = global.GVA_DB.Create(&firstMessage).Error + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("创建开场白消息失败: %v", err)) + } else { + // 更新对话统计 + conversation.MessageCount = 1 + conversation.TokenCount = firstMessage.TokenCount + global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{ + "message_count": 1, + "token_count": firstMessage.TokenCount, + }) + } + } + + resp := response.ToConversationResponse(&conversation) + return &resp, nil +} + +// GetConversationList 获取对话列表 +func (s *ConversationService) GetConversationList(userID uint, req *request.GetConversationListRequest) (*response.ConversationListResponse, error) { + var conversations []app.Conversation + var total int64 + + db := global.GVA_DB.Model(&app.Conversation{}).Where("user_id = ?", userID) + + // 统计总数 + err := db.Count(&total).Error + if err != nil { + return nil, err + } + + // 分页查询 + offset := (req.Page - 1) * req.PageSize + err = db.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&conversations).Error + if err != nil { + return nil, err + } + + // 收集所有角色ID + characterIDs := make([]uint, 0, len(conversations)) + for _, conv := range conversations { + characterIDs = append(characterIDs, conv.CharacterID) + } + + // 批量查询角色信息(只查询必要字段) + var characters []app.AICharacter + if len(characterIDs) > 0 { + err = global.GVA_DB.Select("id, name, avatar, description, created_at, updated_at"). + Where("id IN ?", characterIDs). + Find(&characters).Error + if err != nil { + return nil, err + } + } + + // 创建角色ID到角色的映射 + characterMap := make(map[uint]*app.AICharacter) + for i := range characters { + characterMap[characters[i].ID] = &characters[i] + } + + // 转换响应(使用轻量级结构) + list := make([]response.ConversationListItemResponse, len(conversations)) + for i, conv := range conversations { + character := characterMap[conv.CharacterID] + list[i] = response.ToConversationListItemResponse(&conv, character) + } + + return &response.ConversationListResponse{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// GetConversationByID 获取对话详情 +func (s *ConversationService) GetConversationByID(userID, conversationID uint) (*response.ConversationResponse, error) { + var conversation app.Conversation + + err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID). + First(&conversation).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("对话不存在或无权访问") + } + return nil, err + } + + resp := response.ToConversationResponse(&conversation) + return &resp, nil +} + +// UpdateConversationSettings 更新对话设置 +func (s *ConversationService) UpdateConversationSettings(userID, conversationID uint, settings map[string]interface{}) error { + var conversation app.Conversation + err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID).First(&conversation).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("对话不存在或无权访问") + } + return err + } + + // 序列化设置 + settingsJSON, err := json.Marshal(settings) + if err != nil { + return err + } + + return global.GVA_DB.Model(&conversation).Update("settings", datatypes.JSON(settingsJSON)).Error +} + +// DeleteConversation 删除对话 +func (s *ConversationService) DeleteConversation(userID, conversationID uint) error { + // 开启事务 + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 删除对话的所有消息 + err := tx.Where("conversation_id = ?", conversationID).Delete(&app.Message{}).Error + if err != nil { + return err + } + + // 删除对话 + result := tx.Where("id = ? AND user_id = ?", conversationID, userID).Delete(&app.Conversation{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return errors.New("对话不存在或无权删除") + } + + return nil + }) +} + +// GetMessageList 获取消息列表 +func (s *ConversationService) GetMessageList(userID, conversationID uint, req *request.GetMessageListRequest) (*response.MessageListResponse, error) { + // 验证对话权限 + var conversation app.Conversation + err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID). + First(&conversation).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("对话不存在或无权访问") + } + return nil, err + } + + var messages []app.Message + var total int64 + + db := global.GVA_DB.Model(&app.Message{}).Where("conversation_id = ?", conversationID) + + // 统计总数 + err = db.Count(&total).Error + if err != nil { + return nil, err + } + + // 分页查询 + offset := (req.Page - 1) * req.PageSize + err = db.Order("created_at ASC").Offset(offset).Limit(req.PageSize).Find(&messages).Error + if err != nil { + return nil, err + } + + // 转换响应 + list := make([]response.MessageResponse, len(messages)) + for i, msg := range messages { + list[i] = response.ToMessageResponse(&msg) + } + + return &response.MessageListResponse{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, nil +} + +// SendMessage 发送消息并获取 AI 回复 +func (s *ConversationService) SendMessage(userID, conversationID uint, req *request.SendMessageRequest) (*response.MessageResponse, error) { + // 验证对话权限 + var conversation app.Conversation + err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID). + First(&conversation).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("对话不存在或无权访问") + } + return nil, err + } + + // 获取角色卡信息 + var character app.AICharacter + err = global.GVA_DB.Where("id = ?", conversation.CharacterID).First(&character).Error + if err != nil { + return nil, errors.New("角色卡不存在") + } + + // 保存用户消息 + userMessage := app.Message{ + ConversationID: conversationID, + Role: "user", + Content: req.Content, + TokenCount: len(req.Content) / 4, // 简单估算 + } + + err = global.GVA_DB.Create(&userMessage).Error + if err != nil { + return nil, err + } + + // 获取对话历史(最近10条) + var messages []app.Message + err = global.GVA_DB.Where("conversation_id = ?", conversationID). + Order("created_at DESC"). + Limit(10). + Find(&messages).Error + if err != nil { + return nil, err + } + + // 反转消息顺序(从旧到新) + for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 { + messages[i], messages[j] = messages[j], messages[i] + } + + // 调用 AI 服务获取回复 + aiResponse, err := s.callAIService(conversation, character, messages) + if err != nil { + return nil, err + } + + // 保存 AI 回复 + assistantMessage := app.Message{ + ConversationID: conversationID, + Role: "assistant", + Content: aiResponse, + TokenCount: len(aiResponse) / 4, // 简单估算 + } + + err = global.GVA_DB.Create(&assistantMessage).Error + if err != nil { + return nil, err + } + + // 更新对话统计 + err = global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{ + "message_count": gorm.Expr("message_count + ?", 2), + "token_count": gorm.Expr("token_count + ?", userMessage.TokenCount+assistantMessage.TokenCount), + }).Error + if err != nil { + return nil, err + } + + resp := response.ToMessageResponse(&assistantMessage) + return &resp, nil +} + +// callAIService 调用 AI 服务 +func (s *ConversationService) callAIService(conversation app.Conversation, character app.AICharacter, messages []app.Message) (string, error) { + // 获取 AI 配置 + var aiConfig app.AIConfig + var err error + + // 1. 尝试从对话设置中获取指定的 AI 配置 ID + var configID uint + if len(conversation.Settings) > 0 { + var settings map[string]interface{} + if err := json.Unmarshal(conversation.Settings, &settings); err == nil { + if id, ok := settings["aiConfigId"].(float64); ok { + configID = uint(id) + } + } + } + + if configID > 0 { + // 使用用户指定的 AI 配置 + global.GVA_LOG.Info(fmt.Sprintf("使用用户指定的 AI 配置 ID: %d", configID)) + err = global.GVA_DB.Where("id = ? AND is_active = ?", configID, true).First(&aiConfig).Error + if err != nil { + global.GVA_LOG.Error(fmt.Sprintf("未找到指定的 AI 配置 ID: %d, 错误: %v", configID, err)) + } + } + + if err != nil || configID == 0 { + // 使用默认 AI 配置 + global.GVA_LOG.Info("尝试使用默认 AI 配置") + err = global.GVA_DB.Where("is_active = ?", true). + Order("is_default DESC, created_at DESC"). + First(&aiConfig).Error + if err != nil { + global.GVA_LOG.Error(fmt.Sprintf("未找到默认 AI 配置, 错误: %v", err)) + } + } + + if err != nil { + return "", errors.New("未找到可用的 AI 配置,请在管理后台添加并激活 AI 配置") + } + + global.GVA_LOG.Info(fmt.Sprintf("使用 AI 配置: %s (Provider: %s, Model: %s)", aiConfig.Name, aiConfig.Provider, aiConfig.DefaultModel)) + + // 2. 尝试从对话设置中获取预设 ID 并加载预设参数 + var preset *app.AIPreset + var presetID uint + if len(conversation.Settings) > 0 { + var settings map[string]interface{} + if err := json.Unmarshal(conversation.Settings, &settings); err == nil { + if id, ok := settings["presetId"].(float64); ok { + presetID = uint(id) + } + } + } + + // 加载预设 + if presetID > 0 { + var loadedPreset app.AIPreset + if err := global.GVA_DB.First(&loadedPreset, presetID).Error; err == nil { + preset = &loadedPreset + global.GVA_LOG.Info(fmt.Sprintf("使用预设: %s (Temperature: %.2f, TopP: %.2f)", preset.Name, preset.Temperature, preset.TopP)) + + // 增加预设使用次数 + global.GVA_DB.Model(&preset).Update("use_count", gorm.Expr("use_count + ?", 1)) + } else { + global.GVA_LOG.Warn(fmt.Sprintf("未找到预设 ID: %d, 使用默认参数", presetID)) + } + } + + // 构建系统提示词(如果预设有系统提示词,则追加到角色卡提示词后) + systemPrompt := s.buildSystemPrompt(character) + if preset != nil && preset.SystemPrompt != "" { + systemPrompt = systemPrompt + "\n\n" + preset.SystemPrompt + global.GVA_LOG.Info("已追加预设的系统提示词") + } + + // 构建消息列表 + apiMessages := s.buildAPIMessages(messages, systemPrompt) + + // 打印发送给AI的完整内容 + global.GVA_LOG.Info("========== 发送给AI的完整内容 ==========") + global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt)) + global.GVA_LOG.Info("消息列表:") + for i, msg := range apiMessages { + global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"])) + } + global.GVA_LOG.Info("==========================================") + + // 确定使用的模型:如果用户在设置中指定了AI配置,则使用该配置的默认模型 + // 否则使用对话创建时的模型(向后兼容) + model := aiConfig.DefaultModel + if model == "" { + // 如果AI配置没有默认模型,才使用对话表中的模型 + model = conversation.Model + } + if model == "" { + // 最后的兜底 + model = "gpt-4" + } + + global.GVA_LOG.Info(fmt.Sprintf("使用模型: %s (来源: AI配置 %s)", model, aiConfig.Name)) + + // 根据提供商调用不同的 API + var aiResponse string + + switch aiConfig.Provider { + case "openai", "custom": + aiResponse, err = s.callOpenAIAPI(&aiConfig, model, apiMessages, preset) + case "anthropic": + aiResponse, err = s.callAnthropicAPI(&aiConfig, model, apiMessages, systemPrompt, preset) + default: + return "", fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider) + } + + // 打印AI返回的完整内容 + if err != nil { + global.GVA_LOG.Error(fmt.Sprintf("========== AI返回错误 ==========\n%v\n==========================================", err)) + return "", err + } + global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse)) + + return aiResponse, nil +} + +// buildSystemPrompt 构建系统提示词 +func (s *ConversationService) buildSystemPrompt(character app.AICharacter) string { + prompt := fmt.Sprintf("你是 %s。", character.Name) + + if character.Description != "" { + prompt += fmt.Sprintf("\n\n描述:%s", character.Description) + } + + if character.Personality != "" { + prompt += fmt.Sprintf("\n\n性格:%s", character.Personality) + } + + if character.Scenario != "" { + prompt += fmt.Sprintf("\n\n场景:%s", character.Scenario) + } + + if character.FirstMes != "" { + prompt += fmt.Sprintf("\n\n开场白:%s", character.FirstMes) + } + + if character.MesExample != "" { + prompt += fmt.Sprintf("\n\n对话示例:\n%s", character.MesExample) + } + + if character.SystemPrompt != "" { + prompt += fmt.Sprintf("\n\n系统提示:%s", character.SystemPrompt) + } + + // 处理世界书 (Character Book) + if len(character.CharacterBook) > 0 { + var characterBook map[string]interface{} + if err := json.Unmarshal(character.CharacterBook, &characterBook); err == nil { + if entries, ok := characterBook["entries"].([]interface{}); ok && len(entries) > 0 { + prompt += "\n\n世界设定:" + for _, entry := range entries { + if entryMap, ok := entry.(map[string]interface{}); ok { + // 默认启用,除非明确设置为false + enabled := true + if enabledVal, ok := entryMap["enabled"].(bool); ok { + enabled = enabledVal + } + if !enabled { + continue + } + // 添加世界书条目内容 + if content, ok := entryMap["content"].(string); ok && content != "" { + prompt += fmt.Sprintf("\n- %s", content) + } + } + } + } + } + } + + prompt += "\n\n请根据以上设定进行角色扮演,保持角色的性格和说话方式。" + + // 应用MVU变量替换 + prompt = s.applyMacroVariables(prompt, character) + + return prompt +} + +// applyMacroVariables 应用宏变量替换 (MVU功能) +func (s *ConversationService) applyMacroVariables(text string, character app.AICharacter) string { + // 获取当前时间 + now := time.Now() + + // 基础变量 + replacements := map[string]string{ + "{{char}}": character.Name, + "{{user}}": "用户", // 可以从用户信息中获取 + "{{time}}": now.Format("15:04"), + "{{date}}": now.Format("2006-01-02"), + "{{datetime}}": now.Format("2006-01-02 15:04:05"), + "{{weekday}}": s.getWeekdayInChinese(now.Weekday()), + "{{idle_duration}}": "0分钟", + } + + // 执行替换 + result := text + for macro, value := range replacements { + result = strings.ReplaceAll(result, macro, value) + } + + return result +} + +// getWeekdayInChinese 获取中文星期 +func (s *ConversationService) getWeekdayInChinese(weekday time.Weekday) string { + weekdays := map[time.Weekday]string{ + time.Sunday: "星期日", + time.Monday: "星期一", + time.Tuesday: "星期二", + time.Wednesday: "星期三", + time.Thursday: "星期四", + time.Friday: "星期五", + time.Saturday: "星期六", + } + return weekdays[weekday] +} + +// SendMessageStream 流式发送消息并获取 AI 回复 +func (s *ConversationService) SendMessageStream(userID, conversationID uint, req *request.SendMessageRequest, streamChan chan string, doneChan chan bool) error { + defer close(streamChan) + defer close(doneChan) + + // 验证对话权限 + var conversation app.Conversation + err := global.GVA_DB.Where("id = ? AND user_id = ?", conversationID, userID). + First(&conversation).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("对话不存在或无权访问") + } + return err + } + + // 获取角色卡信息 + var character app.AICharacter + err = global.GVA_DB.Where("id = ?", conversation.CharacterID).First(&character).Error + if err != nil { + return errors.New("角色卡不存在") + } + + // 保存用户消息 + userMessage := app.Message{ + ConversationID: conversationID, + Role: "user", + Content: req.Content, + TokenCount: len(req.Content) / 4, + } + + err = global.GVA_DB.Create(&userMessage).Error + if err != nil { + return err + } + + // 获取对话历史(最近10条) + var messages []app.Message + err = global.GVA_DB.Where("conversation_id = ?", conversationID). + Order("created_at DESC"). + Limit(10). + Find(&messages).Error + if err != nil { + return err + } + + // 反转消息顺序(从旧到新) + for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 { + messages[i], messages[j] = messages[j], messages[i] + } + + // 获取 AI 配置 + var aiConfig app.AIConfig + var configID uint + if len(conversation.Settings) > 0 { + var settings map[string]interface{} + if err := json.Unmarshal(conversation.Settings, &settings); err == nil { + if id, ok := settings["aiConfigId"].(float64); ok { + configID = uint(id) + } + } + } + + if configID > 0 { + err = global.GVA_DB.Where("id = ? AND is_active = ?", configID, true).First(&aiConfig).Error + } + + if err != nil || configID == 0 { + err = global.GVA_DB.Where("is_active = ?", true). + Order("is_default DESC, created_at DESC"). + First(&aiConfig).Error + } + + if err != nil { + return errors.New("未找到可用的 AI 配置") + } + + // 构建系统提示词和消息列表 + systemPrompt := s.buildSystemPrompt(character) + apiMessages := s.buildAPIMessages(messages, systemPrompt) + + // 打印发送给AI的完整内容(流式传输) + global.GVA_LOG.Info("========== [流式传输] 发送给AI的完整内容 ==========") + global.GVA_LOG.Info(fmt.Sprintf("系统提示词: %s", systemPrompt)) + global.GVA_LOG.Info("消息列表:") + for i, msg := range apiMessages { + global.GVA_LOG.Info(fmt.Sprintf(" [%d] Role: %s, Content: %s", i, msg["role"], msg["content"])) + } + global.GVA_LOG.Info("==========================================") + + // 确定使用的模型 + model := aiConfig.DefaultModel + if model == "" { + model = conversation.Model + } + if model == "" { + model = "gpt-4" + } + + global.GVA_LOG.Info(fmt.Sprintf("[流式传输] 使用模型: %s (Provider: %s)", model, aiConfig.Provider)) + + // 调用流式 API + var fullContent string + switch aiConfig.Provider { + case "openai", "custom": + fullContent, err = s.callOpenAIAPIStream(&aiConfig, model, apiMessages, streamChan) + case "anthropic": + fullContent, err = s.callAnthropicAPIStream(&aiConfig, model, apiMessages, systemPrompt, streamChan) + default: + return fmt.Errorf("不支持的 AI 提供商: %s", aiConfig.Provider) + } + + if err != nil { + global.GVA_LOG.Error(fmt.Sprintf("========== [流式传输] AI返回错误 ==========\n%v\n==========================================", err)) + return err + } + + // 打印AI返回的完整内容 + global.GVA_LOG.Info(fmt.Sprintf("========== [流式传输] AI返回的完整内容 ==========\n%s\n==========================================", fullContent)) + + // 保存 AI 回复 + assistantMessage := app.Message{ + ConversationID: conversationID, + Role: "assistant", + Content: fullContent, + TokenCount: len(fullContent) / 4, + } + + err = global.GVA_DB.Create(&assistantMessage).Error + if err != nil { + return err + } + + // 更新对话统计 + err = global.GVA_DB.Model(&conversation).Updates(map[string]interface{}{ + "message_count": gorm.Expr("message_count + ?", 2), + "token_count": gorm.Expr("token_count + ?", userMessage.TokenCount+assistantMessage.TokenCount), + }).Error + + doneChan <- true + return err +} + +// callOpenAIAPIStream 调用 OpenAI API 流式传输 +func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model string, messages []map[string]string, streamChan chan string) (string, error) { + client := &http.Client{Timeout: 120 * time.Second} + + if model == "" { + model = config.DefaultModel + } + if model == "" { + model = "gpt-4" + } + + // 构建请求体,启用流式传输 + requestBody := map[string]interface{}{ + "model": model, + "messages": messages, + "temperature": 0.7, + "max_tokens": 2000, + "stream": true, + } + + bodyBytes, err := json.Marshal(requestBody) + if err != nil { + return "", fmt.Errorf("序列化请求失败: %v", err) + } + + endpoint := config.BaseURL + "/chat/completions" + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+config.APIKey) + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body)) + } + + // 读取流式响应 + var fullContent strings.Builder + reader := bufio.NewReader(resp.Body) + + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + return "", fmt.Errorf("读取流失败: %v", err) + } + + line = strings.TrimSpace(line) + if line == "" || line == "data: [DONE]" { + continue + } + + if strings.HasPrefix(line, "data: ") { + data := strings.TrimPrefix(line, "data: ") + + var streamResp struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + + if err := json.Unmarshal([]byte(data), &streamResp); err != nil { + continue + } + + if len(streamResp.Choices) > 0 { + content := streamResp.Choices[0].Delta.Content + if content != "" { + fullContent.WriteString(content) + streamChan <- content + } + } + } + } + + return fullContent.String(), nil +} + +// callAnthropicAPIStream 调用 Anthropic API 流式传输 +func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, streamChan chan string) (string, error) { + client := &http.Client{Timeout: 120 * time.Second} + + if model == "" { + model = config.DefaultModel + } + if model == "" { + model = "claude-3-sonnet-20240229" + } + + // Anthropic API 不支持 system role + apiMessages := make([]map[string]string, 0) + for _, msg := range messages { + if msg["role"] != "system" { + apiMessages = append(apiMessages, msg) + } + } + + requestBody := map[string]interface{}{ + "model": model, + "messages": apiMessages, + "system": systemPrompt, + "max_tokens": 2000, + "stream": true, + } + + bodyBytes, err := json.Marshal(requestBody) + if err != nil { + return "", fmt.Errorf("序列化请求失败: %v", err) + } + + endpoint := config.BaseURL + "/messages" + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", config.APIKey) + req.Header.Set("anthropic-version", "2023-06-01") + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body)) + } + + // 读取流式响应 + var fullContent strings.Builder + reader := bufio.NewReader(resp.Body) + + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + return "", fmt.Errorf("读取流失败: %v", err) + } + + line = strings.TrimSpace(line) + if line == "" { + continue + } + + if strings.HasPrefix(line, "data: ") { + data := strings.TrimPrefix(line, "data: ") + + var streamResp struct { + Type string `json:"type"` + Delta struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"delta"` + } + + if err := json.Unmarshal([]byte(data), &streamResp); err != nil { + continue + } + + if streamResp.Type == "content_block_delta" && streamResp.Delta.Text != "" { + fullContent.WriteString(streamResp.Delta.Text) + streamChan <- streamResp.Delta.Text + } + } + } + + return fullContent.String(), nil +} + +func (s *ConversationService) buildAPIMessages(messages []app.Message, systemPrompt string) []map[string]string { + apiMessages := make([]map[string]string, 0, len(messages)+1) + + // 添加系统消息(OpenAI 格式) + apiMessages = append(apiMessages, map[string]string{ + "role": "system", + "content": systemPrompt, + }) + + // 添加历史消息 + for _, msg := range messages { + if msg.Role == "system" { + continue // 跳过已有的系统消息 + } + apiMessages = append(apiMessages, map[string]string{ + "role": msg.Role, + "content": msg.Content, + }) + } + + return apiMessages +} + +// callOpenAIAPI 调用 OpenAI API +func (s *ConversationService) callOpenAIAPI(config *app.AIConfig, model string, messages []map[string]string, preset *app.AIPreset) (string, error) { + client := &http.Client{Timeout: 120 * time.Second} + + // 使用配置的模型或默认模型 + if model == "" { + model = config.DefaultModel + } + if model == "" { + model = "gpt-4" + } + + // 应用预设参数(如果有预设) + temperature := 0.7 + maxTokens := 2000 + var topP *float64 + var frequencyPenalty *float64 + var presencePenalty *float64 + var stopSequences []string + + if preset != nil { + temperature = preset.Temperature + maxTokens = preset.MaxTokens + if preset.TopP > 0 { + topP = &preset.TopP + } + if preset.FrequencyPenalty != 0 { + frequencyPenalty = &preset.FrequencyPenalty + } + if preset.PresencePenalty != 0 { + presencePenalty = &preset.PresencePenalty + } + // 解析停止序列 + if len(preset.StopSequences) > 0 { + json.Unmarshal(preset.StopSequences, &stopSequences) + } + global.GVA_LOG.Info(fmt.Sprintf("应用预设参数: Temperature=%.2f, MaxTokens=%d, TopP=%.2f", temperature, maxTokens, preset.TopP)) + } + + // 构建请求体 + requestBody := map[string]interface{}{ + "model": model, + "messages": messages, + "temperature": temperature, + "max_tokens": maxTokens, + } + + // 添加可选参数 + if topP != nil { + requestBody["top_p"] = *topP + } + if frequencyPenalty != nil { + requestBody["frequency_penalty"] = *frequencyPenalty + } + if presencePenalty != nil { + requestBody["presence_penalty"] = *presencePenalty + } + if len(stopSequences) > 0 { + requestBody["stop"] = stopSequences + } + + bodyBytes, err := json.Marshal(requestBody) + if err != nil { + return "", fmt.Errorf("序列化请求失败: %v", err) + } + + // 创建请求 + endpoint := config.BaseURL + "/chat/completions" + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+config.APIKey) + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body)) + } + + // 解析响应 + var result struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + + err = json.Unmarshal(body, &result) + if err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + + if result.Error != nil { + return "", fmt.Errorf("API 错误: %s", result.Error.Message) + } + + if len(result.Choices) == 0 { + return "", errors.New("API 未返回任何回复") + } + + return result.Choices[0].Message.Content, nil +} + +// callAnthropicAPI 调用 Anthropic API +func (s *ConversationService) callAnthropicAPI(config *app.AIConfig, model string, messages []map[string]string, systemPrompt string, preset *app.AIPreset) (string, error) { + client := &http.Client{Timeout: 120 * time.Second} + + // 使用配置的模型或默认模型 + if model == "" { + model = config.DefaultModel + } + if model == "" { + model = "claude-3-sonnet-20240229" + } + + // Anthropic API 不支持 system role,需要单独传递 + apiMessages := make([]map[string]string, 0) + for _, msg := range messages { + if msg["role"] != "system" { + apiMessages = append(apiMessages, msg) + } + } + + // 应用预设参数(如果有预设) + maxTokens := 2000 + var temperature *float64 + var topP *float64 + var stopSequences []string + + if preset != nil { + maxTokens = preset.MaxTokens + if preset.Temperature > 0 { + temperature = &preset.Temperature + } + if preset.TopP > 0 { + topP = &preset.TopP + } + // 解析停止序列 + if len(preset.StopSequences) > 0 { + json.Unmarshal(preset.StopSequences, &stopSequences) + } + global.GVA_LOG.Info(fmt.Sprintf("应用预设参数: Temperature=%.2f, MaxTokens=%d, TopP=%.2f", preset.Temperature, maxTokens, preset.TopP)) + } + + // 构建请求体 + requestBody := map[string]interface{}{ + "model": model, + "messages": apiMessages, + "system": systemPrompt, + "max_tokens": maxTokens, + } + + // 添加可选参数 + if temperature != nil { + requestBody["temperature"] = *temperature + } + if topP != nil { + requestBody["top_p"] = *topP + } + if len(stopSequences) > 0 { + requestBody["stop_sequences"] = stopSequences + } + + bodyBytes, err := json.Marshal(requestBody) + if err != nil { + return "", fmt.Errorf("序列化请求失败: %v", err) + } + + // 创建请求 + endpoint := config.BaseURL + "/messages" + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes)) + if err != nil { + return "", fmt.Errorf("创建请求失败: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", config.APIKey) + req.Header.Set("anthropic-version", "2023-06-01") + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取响应失败: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("API 返回错误 %d: %s", resp.StatusCode, string(body)) + } + + // 解析响应 + var result struct { + Content []struct { + Text string `json:"text"` + } `json:"content"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + + err = json.Unmarshal(body, &result) + if err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + + if result.Error != nil { + return "", fmt.Errorf("API 错误: %s", result.Error.Message) + } + + if len(result.Content) == 0 { + return "", errors.New("API 未返回任何回复") + } + + return result.Content[0].Text, nil +} diff --git a/server/service/app/enter.go b/server/service/app/enter.go new file mode 100644 index 0000000..e475060 --- /dev/null +++ b/server/service/app/enter.go @@ -0,0 +1,10 @@ +package app + +type AppServiceGroup struct { + AuthService + CharacterService + ConversationService + AIConfigService + PresetService + UploadService +} diff --git a/server/service/app/preset.go b/server/service/app/preset.go new file mode 100644 index 0000000..057158b --- /dev/null +++ b/server/service/app/preset.go @@ -0,0 +1,353 @@ +package app + +import ( + "encoding/json" + "errors" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/app" + "git.echol.cn/loser/st/server/model/app/request" + "go.uber.org/zap" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type PresetService struct{} + +// CreatePreset 创建预设 +func (s *PresetService) CreatePreset(userID uint, req *request.CreatePresetRequest) (*app.AIPreset, error) { + // 序列化 StopSequences + var stopSequencesJSON datatypes.JSON + if len(req.StopSequences) > 0 { + data, err := json.Marshal(req.StopSequences) + if err != nil { + global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err)) + return nil, err + } + stopSequencesJSON = data + } + + // 序列化 Extensions + var extensionsJSON datatypes.JSON + if len(req.Extensions) > 0 { + data, err := json.Marshal(req.Extensions) + if err != nil { + global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err)) + return nil, err + } + extensionsJSON = data + } + + preset := &app.AIPreset{ + UserID: userID, + Name: req.Name, + Description: req.Description, + IsPublic: req.IsPublic, + Temperature: req.Temperature, + TopP: req.TopP, + TopK: req.TopK, + FrequencyPenalty: req.FrequencyPenalty, + PresencePenalty: req.PresencePenalty, + MaxTokens: req.MaxTokens, + RepetitionPenalty: req.RepetitionPenalty, + MinP: req.MinP, + TopA: req.TopA, + SystemPrompt: req.SystemPrompt, + StopSequences: stopSequencesJSON, + Extensions: extensionsJSON, + } + + if err := global.GVA_DB.Create(preset).Error; err != nil { + global.GVA_LOG.Error("创建预设失败", zap.Error(err)) + return nil, err + } + + return preset, nil +} + +// GetPresetList 获取预设列表 +func (s *PresetService) GetPresetList(userID uint, req *request.GetPresetListRequest) ([]app.AIPreset, int64, error) { + var presets []app.AIPreset + var total int64 + + db := global.GVA_DB.Model(&app.AIPreset{}) + + // 权限过滤:只能看到自己的预设或公开的预设 + db = db.Where("user_id = ? OR is_public = ?", userID, true) + + // 关键词搜索 + if req.Keyword != "" { + db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") + } + + // 公开/私有过滤 + if req.IsPublic != nil { + db = db.Where("is_public = ?", *req.IsPublic) + } + + // 获取总数 + if err := db.Count(&total).Error; err != nil { + global.GVA_LOG.Error("获取预设总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.PageSize + if err := db.Order("is_default DESC, updated_at DESC"). + Offset(offset). + Limit(req.PageSize). + Find(&presets).Error; err != nil { + global.GVA_LOG.Error("获取预设列表失败", zap.Error(err)) + return nil, 0, err + } + + return presets, total, nil +} + +// GetPresetByID 根据ID获取预设 +func (s *PresetService) GetPresetByID(userID uint, id uint) (*app.AIPreset, error) { + var preset app.AIPreset + + // 权限检查:只能访问自己的预设或公开的预设 + if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", id, userID, true). + First(&preset).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("预设不存在或无权访问") + } + global.GVA_LOG.Error("获取预设失败", zap.Error(err)) + return nil, err + } + + return &preset, nil +} + +// UpdatePreset 更新预设 +func (s *PresetService) UpdatePreset(userID uint, id uint, req *request.UpdatePresetRequest) error { + var preset app.AIPreset + + // 权限检查:只能更新自己的预设 + if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("预设不存在或无权修改") + } + global.GVA_LOG.Error("查询预设失败", zap.Error(err)) + return err + } + + // 构建更新数据 + updates := make(map[string]interface{}) + + if req.Name != "" { + updates["name"] = req.Name + } + if req.Description != "" { + updates["description"] = req.Description + } + if req.IsPublic != nil { + updates["is_public"] = *req.IsPublic + } + if req.Temperature != nil { + updates["temperature"] = *req.Temperature + } + if req.TopP != nil { + updates["top_p"] = *req.TopP + } + if req.TopK != nil { + updates["top_k"] = *req.TopK + } + if req.FrequencyPenalty != nil { + updates["frequency_penalty"] = *req.FrequencyPenalty + } + if req.PresencePenalty != nil { + updates["presence_penalty"] = *req.PresencePenalty + } + if req.MaxTokens != nil { + updates["max_tokens"] = *req.MaxTokens + } + if req.RepetitionPenalty != nil { + updates["repetition_penalty"] = *req.RepetitionPenalty + } + if req.MinP != nil { + updates["min_p"] = *req.MinP + } + if req.TopA != nil { + updates["top_a"] = *req.TopA + } + if req.SystemPrompt != nil { + updates["system_prompt"] = *req.SystemPrompt + } + + // 更新 StopSequences + if req.StopSequences != nil { + data, err := json.Marshal(req.StopSequences) + if err != nil { + global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err)) + return err + } + updates["stop_sequences"] = datatypes.JSON(data) + } + + // 更新 Extensions + if req.Extensions != nil { + data, err := json.Marshal(req.Extensions) + if err != nil { + global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err)) + return err + } + updates["extensions"] = datatypes.JSON(data) + } + + if err := global.GVA_DB.Model(&preset).Updates(updates).Error; err != nil { + global.GVA_LOG.Error("更新预设失败", zap.Error(err)) + return err + } + + return nil +} + +// DeletePreset 删除预设 +func (s *PresetService) DeletePreset(userID uint, id uint) error { + // 权限检查:只能删除自己的预设 + result := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AIPreset{}) + if result.Error != nil { + global.GVA_LOG.Error("删除预设失败", zap.Error(result.Error)) + return result.Error + } + + if result.RowsAffected == 0 { + return errors.New("预设不存在或无权删除") + } + + return nil +} + +// SetDefaultPreset 设置默认预设 +func (s *PresetService) SetDefaultPreset(userID uint, id uint) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 检查预设是否存在且属于当前用户 + var preset app.AIPreset + if err := tx.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("预设不存在或无权访问") + } + return err + } + + // 取消当前用户的所有默认预设 + if err := tx.Model(&app.AIPreset{}). + Where("user_id = ? AND is_default = ?", userID, true). + Update("is_default", false).Error; err != nil { + return err + } + + // 设置新的默认预设 + if err := tx.Model(&preset).Update("is_default", true).Error; err != nil { + return err + } + + return nil + }) +} + +// ImportPresetFromJSON 从JSON导入预设 +func (s *PresetService) ImportPresetFromJSON(userID uint, jsonData []byte, filename string) (*app.AIPreset, error) { + // 尝试解析为 SillyTavern 格式 + var stPreset struct { + Temperature float64 `json:"temperature"` + TopP float64 `json:"top_p"` + TopK int `json:"top_k"` + FrequencyPenalty float64 `json:"frequency_penalty"` + PresencePenalty float64 `json:"presence_penalty"` + MaxTokens int `json:"openai_max_tokens"` + RepetitionPenalty float64 `json:"repetition_penalty"` + MinP float64 `json:"min_p"` + TopA float64 `json:"top_a"` + StopSequences []string `json:"stop_sequences"` + Prompts []map[string]interface{} `json:"prompts"` + PromptOrder []map[string]interface{} `json:"prompt_order"` + } + + if err := json.Unmarshal(jsonData, &stPreset); err != nil { + global.GVA_LOG.Error("解析预设JSON失败", zap.Error(err)) + return nil, errors.New("无效的预设格式") + } + + // 从文件名提取预设名称(去掉 .json 后缀) + name := filename + if len(name) > 5 && name[len(name)-5:] == ".json" { + name = name[:len(name)-5] + } + + // 构建 extensions 对象,包含 prompts 和 prompt_order + extensions := map[string]interface{}{ + "prompts": stPreset.Prompts, + "prompt_order": stPreset.PromptOrder, + } + + // 转换为创建请求 + req := &request.CreatePresetRequest{ + Name: name, + Description: "从 SillyTavern 导入", + Temperature: stPreset.Temperature, + TopP: stPreset.TopP, + TopK: stPreset.TopK, + FrequencyPenalty: stPreset.FrequencyPenalty, + PresencePenalty: stPreset.PresencePenalty, + MaxTokens: stPreset.MaxTokens, + RepetitionPenalty: stPreset.RepetitionPenalty, + MinP: stPreset.MinP, + TopA: stPreset.TopA, + SystemPrompt: "", + StopSequences: stPreset.StopSequences, + Extensions: extensions, + } + + return s.CreatePreset(userID, req) +} + +// ExportPresetToJSON 导出预设为JSON +func (s *PresetService) ExportPresetToJSON(userID uint, id uint) ([]byte, error) { + preset, err := s.GetPresetByID(userID, id) + if err != nil { + return nil, err + } + + // 解析 StopSequences + var stopSequences []string + if len(preset.StopSequences) > 0 { + json.Unmarshal(preset.StopSequences, &stopSequences) + } + + // 解析 Extensions + var extensions map[string]interface{} + if len(preset.Extensions) > 0 { + json.Unmarshal(preset.Extensions, &extensions) + } + + // 转换为 SillyTavern 格式 + stPreset := map[string]interface{}{ + "name": preset.Name, + "description": preset.Description, + "temperature": preset.Temperature, + "top_p": preset.TopP, + "top_k": preset.TopK, + "frequency_penalty": preset.FrequencyPenalty, + "presence_penalty": preset.PresencePenalty, + "max_tokens": preset.MaxTokens, + "repetition_penalty": preset.RepetitionPenalty, + "min_p": preset.MinP, + "top_a": preset.TopA, + "system_prompt": preset.SystemPrompt, + "stop_sequences": stopSequences, + "extensions": extensions, + } + + return json.MarshalIndent(stPreset, "", " ") +} + +// IncrementUseCount 增加使用次数 +func (s *PresetService) IncrementUseCount(id uint) error { + return global.GVA_DB.Model(&app.AIPreset{}). + Where("id = ?", id). + Update("use_count", gorm.Expr("use_count + ?", 1)).Error +} diff --git a/server/service/app/upload.go b/server/service/app/upload.go new file mode 100644 index 0000000..2e2741b --- /dev/null +++ b/server/service/app/upload.go @@ -0,0 +1,49 @@ +package app + +import ( + "errors" + "mime/multipart" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/utils/upload" + "go.uber.org/zap" +) + +type UploadService struct{} + +// UploadImage 上传图片到 OSS +// 返回图片的访问 URL +func (s *UploadService) UploadImage(header *multipart.FileHeader) (string, error) { + // 验证文件类型 + if !isImageFile(header.Filename) { + return "", errors.New("只支持图片格式:jpg, jpeg, png, gif, webp") + } + + // 验证文件大小(限制 10MB) + if header.Size > 10*1024*1024 { + return "", errors.New("图片大小不能超过 10MB") + } + + // 使用 OSS 上传 + oss := upload.NewOss() + filePath, _, uploadErr := oss.UploadFile(header) + if uploadErr != nil { + global.GVA_LOG.Error("图片上传失败", zap.Error(uploadErr)) + return "", errors.New("图片上传失败") + } + + return filePath, nil +} + +// isImageFile 检查是否为图片文件 +func isImageFile(filename string) bool { + ext := strings.ToLower(filename[strings.LastIndex(filename, ".")+1:]) + imageExts := []string{"jpg", "jpeg", "png", "gif", "webp"} + for _, validExt := range imageExts { + if ext == validExt { + return true + } + } + return false +} diff --git a/server/service/enter.go b/server/service/enter.go new file mode 100644 index 0000000..fd8640b --- /dev/null +++ b/server/service/enter.go @@ -0,0 +1,15 @@ +package service + +import ( + "git.echol.cn/loser/st/server/service/app" + "git.echol.cn/loser/st/server/service/example" + "git.echol.cn/loser/st/server/service/system" +) + +var ServiceGroupApp = new(ServiceGroup) + +type ServiceGroup struct { + SystemServiceGroup system.ServiceGroup + ExampleServiceGroup example.ServiceGroup + AppServiceGroup app.AppServiceGroup +} diff --git a/server/service/example/enter.go b/server/service/example/enter.go new file mode 100644 index 0000000..f7198da --- /dev/null +++ b/server/service/example/enter.go @@ -0,0 +1,7 @@ +package example + +type ServiceGroup struct { + CustomerService + FileUploadAndDownloadService + AttachmentCategoryService +} diff --git a/server/service/example/exa_attachment_category.go b/server/service/example/exa_attachment_category.go new file mode 100644 index 0000000..7e5c1de --- /dev/null +++ b/server/service/example/exa_attachment_category.go @@ -0,0 +1,67 @@ +package example + +import ( + "errors" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/example" + "gorm.io/gorm" +) + +type AttachmentCategoryService struct{} + +// AddCategory 创建/更新的分类 +func (a *AttachmentCategoryService) AddCategory(req *example.ExaAttachmentCategory) (err error) { + // 检查是否已存在相同名称的分类 + if (!errors.Is(global.GVA_DB.Take(&example.ExaAttachmentCategory{}, "name = ? and pid = ?", req.Name, req.Pid).Error, gorm.ErrRecordNotFound)) { + return errors.New("分类名称已存在") + } + if req.ID > 0 { + if err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("id = ?", req.ID).Updates(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } else { + if err = global.GVA_DB.Create(&example.ExaAttachmentCategory{ + Name: req.Name, + Pid: req.Pid, + }).Error; err != nil { + return err + } + } + return nil +} + +// DeleteCategory 删除分类 +func (a *AttachmentCategoryService) DeleteCategory(id *int) error { + var childCount int64 + global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Where("pid = ?", id).Count(&childCount) + if childCount > 0 { + return errors.New("请先删除子级") + } + return global.GVA_DB.Where("id = ?", id).Unscoped().Delete(&example.ExaAttachmentCategory{}).Error +} + +// GetCategoryList 分类列表 +func (a *AttachmentCategoryService) GetCategoryList() (res []*example.ExaAttachmentCategory, err error) { + var fileLists []example.ExaAttachmentCategory + err = global.GVA_DB.Model(&example.ExaAttachmentCategory{}).Find(&fileLists).Error + if err != nil { + return res, err + } + return a.getChildrenList(fileLists, 0), nil +} + +// getChildrenList 子类 +func (a *AttachmentCategoryService) getChildrenList(categories []example.ExaAttachmentCategory, parentID uint) []*example.ExaAttachmentCategory { + var tree []*example.ExaAttachmentCategory + for _, category := range categories { + if category.Pid == parentID { + category.Children = a.getChildrenList(categories, category.ID) + tree = append(tree, &category) + } + } + return tree +} diff --git a/server/service/example/exa_breakpoint_continue.go b/server/service/example/exa_breakpoint_continue.go new file mode 100644 index 0000000..048bdef --- /dev/null +++ b/server/service/example/exa_breakpoint_continue.go @@ -0,0 +1,71 @@ +package example + +import ( + "errors" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/example" + "gorm.io/gorm" +) + +type FileUploadAndDownloadService struct{} + +var FileUploadAndDownloadServiceApp = new(FileUploadAndDownloadService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindOrCreateFile +//@description: 上传文件时检测当前文件属性,如果没有文件则创建,有则返回文件的当前切片 +//@param: fileMd5 string, fileName string, chunkTotal int +//@return: file model.ExaFile, err error + +func (e *FileUploadAndDownloadService) FindOrCreateFile(fileMd5 string, fileName string, chunkTotal int) (file example.ExaFile, err error) { + var cfile example.ExaFile + cfile.FileMd5 = fileMd5 + cfile.FileName = fileName + cfile.ChunkTotal = chunkTotal + + if errors.Is(global.GVA_DB.Where("file_md5 = ? AND file_name = ? AND is_finish = ?", fileMd5, fileName, true).First(&file).Error, gorm.ErrRecordNotFound) { + err = global.GVA_DB.Where("file_md5 = ? AND file_name = ?", fileMd5, fileName).Preload("ExaFileChunk").FirstOrCreate(&file, cfile).Error + return file, err + } + cfile.IsFinish = true + cfile.FilePath = file.FilePath + err = global.GVA_DB.Create(&cfile).Error + return cfile, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateFileChunk +//@description: 创建文件切片记录 +//@param: id uint, fileChunkPath string, fileChunkNumber int +//@return: error + +func (e *FileUploadAndDownloadService) CreateFileChunk(id uint, fileChunkPath string, fileChunkNumber int) error { + var chunk example.ExaFileChunk + chunk.FileChunkPath = fileChunkPath + chunk.ExaFileID = id + chunk.FileChunkNumber = fileChunkNumber + err := global.GVA_DB.Create(&chunk).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除文件切片记录 +//@param: fileMd5 string, fileName string, filePath string +//@return: error + +func (e *FileUploadAndDownloadService) DeleteFileChunk(fileMd5 string, filePath string) error { + var chunks []example.ExaFileChunk + var file example.ExaFile + err := global.GVA_DB.Where("file_md5 = ?", fileMd5).First(&file). + Updates(map[string]interface{}{ + "IsFinish": true, + "file_path": filePath, + }).Error + if err != nil { + return err + } + err = global.GVA_DB.Where("exa_file_id = ?", file.ID).Delete(&chunks).Unscoped().Error + return err +} diff --git a/server/service/example/exa_customer.go b/server/service/example/exa_customer.go new file mode 100644 index 0000000..3e24325 --- /dev/null +++ b/server/service/example/exa_customer.go @@ -0,0 +1,87 @@ +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/example" + "git.echol.cn/loser/st/server/model/system" + systemService "git.echol.cn/loser/st/server/service/system" +) + +type CustomerService struct{} + +var CustomerServiceApp = new(CustomerService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateExaCustomer +//@description: 创建客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) CreateExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Create(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFileChunk +//@description: 删除客户 +//@param: e model.ExaCustomer +//@return: err error + +func (exa *CustomerService) DeleteExaCustomer(e example.ExaCustomer) (err error) { + err = global.GVA_DB.Delete(&e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateExaCustomer +//@description: 更新客户 +//@param: e *model.ExaCustomer +//@return: err error + +func (exa *CustomerService) UpdateExaCustomer(e *example.ExaCustomer) (err error) { + err = global.GVA_DB.Save(e).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetExaCustomer +//@description: 获取客户信息 +//@param: id uint +//@return: customer model.ExaCustomer, err error + +func (exa *CustomerService) GetExaCustomer(id uint) (customer example.ExaCustomer, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&customer).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetCustomerInfoList +//@description: 分页获取客户列表 +//@param: sysUserAuthorityID string, info request.PageInfo +//@return: list interface{}, total int64, err error + +func (exa *CustomerService) GetCustomerInfoList(sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaCustomer{}) + var a system.SysAuthority + a.AuthorityId = sysUserAuthorityID + auth, err := systemService.AuthorityServiceApp.GetAuthorityInfo(a) + if err != nil { + return + } + var dataId []uint + for _, v := range auth.DataAuthorityId { + dataId = append(dataId, v.AuthorityId) + } + var CustomerList []example.ExaCustomer + err = db.Where("sys_user_authority_id in ?", dataId).Count(&total).Error + if err != nil { + return CustomerList, total, err + } else { + err = db.Limit(limit).Offset(offset).Preload("SysUser").Where("sys_user_authority_id in ?", dataId).Find(&CustomerList).Error + } + return CustomerList, total, err +} diff --git a/server/service/example/exa_file_upload_download.go b/server/service/example/exa_file_upload_download.go new file mode 100644 index 0000000..2ca37d6 --- /dev/null +++ b/server/service/example/exa_file_upload_download.go @@ -0,0 +1,130 @@ +package example + +import ( + "errors" + "mime/multipart" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/example" + "git.echol.cn/loser/st/server/model/example/request" + "git.echol.cn/loser/st/server/utils/upload" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Upload +//@description: 创建文件上传记录 +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) Upload(file example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: FindFile +//@description: 查询文件记录 +//@param: id uint +//@return: model.ExaFileUploadAndDownload, error + +func (e *FileUploadAndDownloadService) FindFile(id uint) (example.ExaFileUploadAndDownload, error) { + var file example.ExaFileUploadAndDownload + err := global.GVA_DB.Where("id = ?", id).First(&file).Error + return file, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteFile +//@description: 删除文件记录 +//@param: file model.ExaFileUploadAndDownload +//@return: err error + +func (e *FileUploadAndDownloadService) DeleteFile(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + fileFromDb, err = e.FindFile(file.ID) + if err != nil { + return + } + oss := upload.NewOss() + if err = oss.DeleteFile(fileFromDb.Key); err != nil { + return errors.New("文件删除失败") + } + err = global.GVA_DB.Where("id = ?", file.ID).Unscoped().Delete(&file).Error + return err +} + +// EditFileName 编辑文件名或者备注 +func (e *FileUploadAndDownloadService) EditFileName(file example.ExaFileUploadAndDownload) (err error) { + var fileFromDb example.ExaFileUploadAndDownload + return global.GVA_DB.Where("id = ?", file.ID).First(&fileFromDb).Update("name", file.Name).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetFileRecordInfoList +//@description: 分页获取数据 +//@param: info request.ExaAttachmentCategorySearch +//@return: list interface{}, total int64, err error + +func (e *FileUploadAndDownloadService) GetFileRecordInfoList(info request.ExaAttachmentCategorySearch) (list []example.ExaFileUploadAndDownload, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&example.ExaFileUploadAndDownload{}) + + if len(info.Keyword) > 0 { + db = db.Where("name LIKE ?", "%"+info.Keyword+"%") + } + + if info.ClassId > 0 { + db = db.Where("class_id = ?", info.ClassId) + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error + return list, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UploadFile +//@description: 根据配置文件判断是文件上传到本地或者七牛云 +//@param: header *multipart.FileHeader, noSave string +//@return: file model.ExaFileUploadAndDownload, err error + +func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, noSave string, classId int) (file example.ExaFileUploadAndDownload, err error) { + oss := upload.NewOss() + filePath, key, uploadErr := oss.UploadFile(header) + if uploadErr != nil { + return file, uploadErr + } + s := strings.Split(header.Filename, ".") + f := example.ExaFileUploadAndDownload{ + Url: filePath, + Name: header.Filename, + ClassId: classId, + Tag: s[len(s)-1], + Key: key, + } + if noSave == "0" { + // 检查是否已存在相同key的记录 + var existingFile example.ExaFileUploadAndDownload + err = global.GVA_DB.Where(&example.ExaFileUploadAndDownload{Key: key}).First(&existingFile).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return f, e.Upload(f) + } + return f, err + } + return f, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ImportURL +//@description: 导入URL +//@param: file model.ExaFileUploadAndDownload +//@return: error + +func (e *FileUploadAndDownloadService) ImportURL(file *[]example.ExaFileUploadAndDownload) error { + return global.GVA_DB.Create(&file).Error +} diff --git a/server/service/system/auto_code_history.go b/server/service/system/auto_code_history.go new file mode 100644 index 0000000..419d4a9 --- /dev/null +++ b/server/service/system/auto_code_history.go @@ -0,0 +1,218 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "path" + "path/filepath" + "strconv" + "strings" + "time" + + "git.echol.cn/loser/st/server/utils/ast" + "github.com/pkg/errors" + + "git.echol.cn/loser/st/server/global" + common "git.echol.cn/loser/st/server/model/common/request" + model "git.echol.cn/loser/st/server/model/system" + request "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + + "go.uber.org/zap" +) + +var AutocodeHistory = new(autoCodeHistory) + +type autoCodeHistory struct{} + +// Create 创建代码生成器历史记录 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Create(ctx context.Context, info request.SysAutoHistoryCreate) error { + create := info.Create() + err := global.GVA_DB.WithContext(ctx).Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + return nil +} + +// First 根据id获取代码生成器历史的数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) First(ctx context.Context, info common.GetById) (string, error) { + var meta string + err := global.GVA_DB.WithContext(ctx).Model(model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Pluck("request", &meta).Error + if err != nil { + return "", errors.Wrap(err, "获取失败!") + } + return meta, nil +} + +// Repeat 检测重复 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Repeat(businessDB, structName, abbreviation, Package string) bool { + var count int64 + global.GVA_DB.Model(&model.SysAutoCodeHistory{}).Where("business_db = ? and (struct_name = ? OR abbreviation = ?) and package = ? and flag = ?", businessDB, structName, abbreviation, Package, 0).Count(&count).Debug() + return count > 0 +} + +// RollBack 回滚 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) RollBack(ctx context.Context, info request.SysAutoHistoryRollBack) error { + var history model.SysAutoCodeHistory + err := global.GVA_DB.Where("id = ?", info.ID).First(&history).Error + if err != nil { + return err + } + if history.ExportTemplateID != 0 { + err = global.GVA_DB.Delete(&model.SysExportTemplate{}, "id = ?", history.ExportTemplateID).Error + if err != nil { + return err + } + } + if info.DeleteApi { + ids := info.ApiIds(history) + err = ApiServiceApp.DeleteApisByIds(ids) + if err != nil { + global.GVA_LOG.Error("ClearTag DeleteApiByIds:", zap.Error(err)) + } + } // 清除API表 + if info.DeleteMenu { + err = BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)) + if err != nil { + return errors.Wrap(err, "删除菜单失败!") + } + } // 清除菜单表 + if info.DeleteTable { + err = s.DropTable(history.BusinessDB, history.Table) + if err != nil { + return errors.Wrap(err, "删除表失败!") + } + } // 删除表 + templates := make(map[string]string, len(history.Templates)) + for key, template := range history.Templates { + { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(key, "/") + key = filepath.Join(keys...) + key = strings.TrimPrefix(key, server) + } // key + { + web := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot()) + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + slices := strings.Split(template, "/") + template = filepath.Join(slices...) + ext := path.Ext(template) + switch ext { + case ".js", ".vue": + template = filepath.Join(web, template) + case ".go": + template = filepath.Join(server, template) + } + } // value + templates[key] = template + } + history.Templates = templates + for key, value := range history.Injections { + var injection ast.Ast + switch key { + case ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + + case ast.TypePackageApiModuleEnter, ast.TypePackageRouterModuleEnter, ast.TypePackageServiceModuleEnter: + var entity ast.PackageModuleEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeGorm: + var entity ast.PackageInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePackageInitializeRouter: + var entity ast.PackageInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginGen: + var entity ast.PluginGen + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginApiEnter, ast.TypePluginRouterEnter, ast.TypePluginServiceEnter: + var entity ast.PluginEnter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeGorm: + var entity ast.PluginInitializeGorm + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + case ast.TypePluginInitializeRouter: + var entity ast.PluginInitializeRouter + _ = json.Unmarshal([]byte(value), &entity) + injection = &entity + } + if injection == nil { + continue + } + file, _ := injection.Parse("", nil) + if file != nil { + _ = injection.Rollback(file) + err = injection.Format("", nil, file) + if err != nil { + return err + } + fmt.Printf("[filepath:%s]回滚注入代码成功!\n", key) + } + } // 清除注入代码 + removeBasePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, "rm_file", strconv.FormatInt(int64(time.Now().Nanosecond()), 10)) + for _, value := range history.Templates { + if !filepath.IsAbs(value) { + continue + } + removePath := filepath.Join(removeBasePath, strings.TrimPrefix(value, global.GVA_CONFIG.AutoCode.Root)) + err = utils.FileMove(value, removePath) + if err != nil { + return errors.Wrapf(err, "[src:%s][dst:%s]文件移动失败!", value, removePath) + } + } // 移动文件 + err = global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}).Where("id = ?", info.ID).Update("flag", 1).Error + if err != nil { + return errors.Wrap(err, "更新失败!") + } + return nil +} + +// Delete 删除历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Where("id = ?", info.Uint()).Delete(&model.SysAutoCodeHistory{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// GetList 获取系统历史数据 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [songzhibin97](https://github.com/songzhibin97) +func (s *autoCodeHistory) GetList(ctx context.Context, info common.PageInfo) (list []model.SysAutoCodeHistory, total int64, err error) { + var entities []model.SysAutoCodeHistory + db := global.GVA_DB.WithContext(ctx).Model(&model.SysAutoCodeHistory{}) + err = db.Count(&total).Error + if err != nil { + return nil, total, err + } + err = db.Scopes(info.Paginate()).Order("updated_at desc").Find(&entities).Error + return entities, total, err +} + +// DropTable 获取指定数据库和指定数据表的所有字段名,类型值等 +// @author: [piexlmax](https://github.com/piexlmax) +func (s *autoCodeHistory) DropTable(BusinessDb, tableName string) error { + if BusinessDb != "" { + return global.MustGetGlobalDBByDBName(BusinessDb).Exec("DROP TABLE " + tableName).Error + } else { + return global.GVA_DB.Exec("DROP TABLE " + tableName).Error + } +} diff --git a/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go new file mode 100644 index 0000000..9bd7570 --- /dev/null +++ b/server/service/system/auto_code_llm.go @@ -0,0 +1,52 @@ +package system + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common" + commonResp "git.echol.cn/loser/st/server/model/common/response" + "git.echol.cn/loser/st/server/utils/request" + "github.com/goccy/go-json" +) + +// LLMAuto 调用大模型服务,返回生成结果数据 +// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload +func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) { + if global.GVA_CONFIG.AutoCode.AiPath == "" { + return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中") + } + + // 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换 + mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常 + path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", mode) + + res, err := request.HttpRequest( + path, + "POST", + nil, + nil, + llm, + ) + if err != nil { + return nil, fmt.Errorf("大模型生成失败: %w", err) + } + defer res.Body.Close() + + var resStruct commonResp.Response + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("读取大模型响应失败: %w", err) + } + if err = json.Unmarshal(b, &resStruct); err != nil { + return nil, fmt.Errorf("解析大模型响应失败: %w", err) + } + if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败 + return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg) + } + return resStruct.Data, nil +} diff --git a/server/service/system/auto_code_mcp.go b/server/service/system/auto_code_mcp.go new file mode 100644 index 0000000..8c94d00 --- /dev/null +++ b/server/service/system/auto_code_mcp.go @@ -0,0 +1,46 @@ +package system + +import ( + "context" + "os" + "path/filepath" + "text/template" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + "git.echol.cn/loser/st/server/utils/autocode" +) + +func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) { + mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl") + mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp") + + var files *template.Template + + templateName := filepath.Base(mcpTemplatePath) + + files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath) + if err != nil { + return + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath = filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return + } + + return + +} diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go new file mode 100644 index 0000000..ab33d28 --- /dev/null +++ b/server/service/system/auto_code_package.go @@ -0,0 +1,743 @@ +package system + +import ( + "context" + "fmt" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" + + "git.echol.cn/loser/st/server/global" + common "git.echol.cn/loser/st/server/model/common/request" + model "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + "git.echol.cn/loser/st/server/utils/ast" + "git.echol.cn/loser/st/server/utils/autocode" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var AutoCodePackage = new(autoCodePackage) + +type autoCodePackage struct{} + +// Create 创建包信息 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodePackageCreate) error { + switch { + case info.Template == "": + return errors.New("模板不能为空!") + case info.Template == "page": + return errors.New("page为表单生成器!") + case info.PackageName == "": + return errors.New("PackageName不能为空!") + case token.IsKeyword(info.PackageName): + return errors.Errorf("%s为go的关键字!", info.PackageName) + case info.Template == "package": + if info.PackageName == "system" || info.PackageName == "example" { + return errors.New("不能使用已保留的package name") + } + default: + break + } + if !errors.Is(global.GVA_DB.Where("package_name = ? and template = ?", info.PackageName, info.Template).First(&model.SysAutoCodePackage{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同PackageName") + } + create := info.Create() + return global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Create(&create).Error + if err != nil { + return errors.Wrap(err, "创建失败!") + } + code := info.AutoCode() + _, asts, creates, err := s.templates(ctx, create, code, true) + if err != nil { + return err + } + for key, value := range creates { // key 为 模版绝对路径 + var files *template.Template + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", key) + } + err = os.MkdirAll(filepath.Dir(value), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + var file *os.File + file, err = os.Create(value) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", value) + } + err = files.Execute(file, code) + _ = file.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]生成失败!", value) + } + fmt.Printf("[template:%s][filepath:%s]生成成功!\n", key, value) + } + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + switch keys[1] { + case ast.TypePluginInitializeV2, ast.TypePackageApiEnter, ast.TypePackageRouterEnter, ast.TypePackageServiceEnter: + file, _ := value.Parse("", nil) + if file != nil { + err = value.Injection(file) + if err != nil { + return err + } + err = value.Format("", nil, file) + if err != nil { + return err + } + } + fmt.Printf("[type:%s]注入成功!\n", key) + } + } + } + return nil + }) +} + +// Delete 删除包记录 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error { + err := global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, info.Uint()).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// DeleteByNames +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error { + if len(names) == 0 { + return nil + } + err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + +// All 获取所有包 +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) All(ctx context.Context) (entities []model.SysAutoCodePackage, err error) { + server := make([]model.SysAutoCodePackage, 0) + plugin := make([]model.SysAutoCodePackage, 0) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service") + pluginPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin") + serverDir, err := os.ReadDir(serverPath) + if err != nil { + return nil, errors.Wrap(err, "读取service文件夹失败!") + } + pluginDir, err := os.ReadDir(pluginPath) + if err != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + for i := 0; i < len(serverDir); i++ { + if serverDir[i].IsDir() { + serverPackage := model.SysAutoCodePackage{ + PackageName: serverDir[i].Name(), + Template: "package", + Label: serverDir[i].Name() + "包", + Desc: "系统自动读取" + serverDir[i].Name() + "包", + Module: global.GVA_CONFIG.AutoCode.Module, + } + server = append(server, serverPackage) + } + } + for i := 0; i < len(pluginDir); i++ { + if pluginDir[i].IsDir() { + dirNameMap := map[string]bool{ + "api": true, + "config": true, + "initialize": true, + "plugin": true, + "router": true, + "service": true, + } + dir, e := os.ReadDir(filepath.Join(pluginPath, pluginDir[i].Name())) + if e != nil { + return nil, errors.Wrap(err, "读取plugin文件夹失败!") + } + //dir目录需要包含所有的dirNameMap + for k := 0; k < len(dir); k++ { + if dir[k].IsDir() { + if ok := dirNameMap[dir[k].Name()]; ok { + delete(dirNameMap, dir[k].Name()) + } + } + } + + var desc string + if len(dirNameMap) == 0 { + // 完全符合标准结构 + desc = "系统自动读取" + pluginDir[i].Name() + "插件,使用前请确认是否为v2版本插件" + } else { + // 缺少某些结构,生成警告描述 + var missingDirs []string + for dirName := range dirNameMap { + missingDirs = append(missingDirs, dirName) + } + desc = fmt.Sprintf("系统自动读取,但是缺少 %s 结构,不建议自动化和mcp使用", strings.Join(missingDirs, "、")) + } + + pluginPackage := model.SysAutoCodePackage{ + PackageName: pluginDir[i].Name(), + Template: "plugin", + Label: pluginDir[i].Name() + "插件", + Desc: desc, + Module: global.GVA_CONFIG.AutoCode.Module, + } + plugin = append(plugin, pluginPackage) + } + } + + err = global.GVA_DB.WithContext(ctx).Find(&entities).Error + if err != nil { + return nil, errors.Wrap(err, "获取所有包失败!") + } + entitiesMap := make(map[string]model.SysAutoCodePackage) + for i := 0; i < len(entities); i++ { + entitiesMap[entities[i].PackageName] = entities[i] + } + createEntity := []model.SysAutoCodePackage{} + for i := 0; i < len(server); i++ { + if _, ok := entitiesMap[server[i].PackageName]; !ok { + if server[i].Template == "package" { + createEntity = append(createEntity, server[i]) + } + } + } + for i := 0; i < len(plugin); i++ { + if _, ok := entitiesMap[plugin[i].PackageName]; !ok { + if plugin[i].Template == "plugin" { + createEntity = append(createEntity, plugin[i]) + } + } + } + + if len(createEntity) > 0 { + err = global.GVA_DB.WithContext(ctx).Create(&createEntity).Error + if err != nil { + return nil, errors.Wrap(err, "同步失败!") + } + entities = append(entities, createEntity...) + } + + // 处理数据库存在但实体文件不存在的情况 - 删除数据库中对应的数据 + existingPackageNames := make(map[string]bool) + // 收集所有存在的包名 + for i := 0; i < len(server); i++ { + existingPackageNames[server[i].PackageName] = true + } + for i := 0; i < len(plugin); i++ { + existingPackageNames[plugin[i].PackageName] = true + } + + // 找出需要删除的数据库记录 + deleteEntityIDs := []uint{} + for i := 0; i < len(entities); i++ { + if !existingPackageNames[entities[i].PackageName] { + deleteEntityIDs = append(deleteEntityIDs, entities[i].ID) + } + } + + // 删除数据库中不存在文件的记录 + if len(deleteEntityIDs) > 0 { + err = global.GVA_DB.WithContext(ctx).Delete(&model.SysAutoCodePackage{}, deleteEntityIDs).Error + if err != nil { + return nil, errors.Wrap(err, "删除不存在的包记录失败!") + } + // 从返回结果中移除已删除的记录 + filteredEntities := []model.SysAutoCodePackage{} + for i := 0; i < len(entities); i++ { + if existingPackageNames[entities[i].PackageName] { + filteredEntities = append(filteredEntities, entities[i]) + } + } + entities = filteredEntities + } + + return entities, nil +} + +// Templates 获取所有模版文件夹 +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { + templates := make([]string, 0) + entries, err := os.ReadDir("resource") + if err != nil { + return nil, errors.Wrap(err, "读取模版文件夹失败!") + } + for i := 0; i < len(entries); i++ { + if entries[i].IsDir() { + if entries[i].Name() == "page" { + continue + } // page 为表单生成器 + if entries[i].Name() == "function" { + continue + } // function 为函数生成器 + if entries[i].Name() == "preview" { + continue + } // preview 为预览代码生成器的代码 + if entries[i].Name() == "mcp" { + continue + } // preview 为mcp生成器的代码 + templates = append(templates, entries[i].Name()) + } + } + return templates, nil +} + +func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCodePackage, info request.AutoCode, isPackage bool) (code map[string]string, asts map[string]ast.Ast, creates map[string]string, err error) { + code = make(map[string]string) + asts = make(map[string]ast.Ast) + creates = make(map[string]string) + templateDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", entity.Template) + templateDirs, err := os.ReadDir(templateDir) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", templateDir) + } + for i := 0; i < len(templateDirs); i++ { + second := filepath.Join(templateDir, templateDirs[i].Name()) + switch templateDirs[i].Name() { + case "server": + if !info.GenerateServer && !isPackage { + break + } + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + ext := filepath.Ext(secondDirs[j].Name()) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", three) + } + name := strings.TrimSuffix(secondDirs[j].Name(), ext) + if name == "main.go" || name == "plugin.go" { + pluginInitialize := &ast.PluginInitializeV2{ + Type: ast.TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, name), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"), + ImportPath: fmt.Sprintf(`"%s/plugin/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + PackageName: entity.PackageName, + } + asts[pluginInitialize.PluginPath+"=>"+pluginInitialize.Type.String()] = pluginInitialize + creates[three] = pluginInitialize.Path + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "router", "service": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + hasEnter := strings.Index(threeDirs[k].Name(), "enter") + router := strings.Index(threeDirs[k].Name(), "router") + service := strings.Index(threeDirs[k].Name(), "service") + if router == -1 && api == -1 && service == -1 && hasEnter == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, info.HumpPackageName+".go") + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isApi != -1 { + packageApiEnter := &ast.PackageEnter{ + Type: ast.TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, "api", "v1", entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + PackageName: entity.PackageName, + PackageStructName: "ApiGroup", + } + asts[packageApiEnter.Path+"=>"+packageApiEnter.Type.String()] = packageApiEnter + packageApiModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "v1", entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`"%s/service"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Api", + AppName: "ServiceGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + ModuleName: info.Abbreviation + "Service", + PackageName: "service", + ServiceName: info.StructName + "Service", + } + asts[packageApiModuleEnter.Path+"=>"+packageApiModuleEnter.Type.String()] = packageApiModuleEnter + creates[four] = packageApiModuleEnter.Path + } + if isRouter != -1 { + packageRouterEnter := &ast.PackageEnter{ + Type: ast.TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), "enter.go"), + ImportPath: fmt.Sprintf(`"%s/%s/%s"`, global.GVA_CONFIG.AutoCode.Module, secondDirs[j].Name(), entity.PackageName), + StructName: utils.FirstUpper(entity.PackageName), + PackageName: entity.PackageName, + PackageStructName: "RouterGroup", + } + asts[packageRouterEnter.Path+"=>"+packageRouterEnter.Type.String()] = packageRouterEnter + packageRouterModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + ImportPath: fmt.Sprintf(`api "%s/api/v1"`, global.GVA_CONFIG.AutoCode.Module), + StructName: info.StructName + "Router", + AppName: "ApiGroupApp", + GroupName: utils.FirstUpper(entity.PackageName) + "ApiGroup", + ModuleName: info.Abbreviation + "Api", + PackageName: "api", + ServiceName: info.StructName + "Api", + } + creates[four] = packageRouterModuleEnter.Path + asts[packageRouterModuleEnter.Path+"=>"+packageRouterModuleEnter.Type.String()] = packageRouterModuleEnter + packageInitializeRouter := &ast.PackageInitializeRouter{ + Type: ast.TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: fmt.Sprintf(`"%s/router"`, global.GVA_CONFIG.AutoCode.Module), + AppName: "RouterGroupApp", + GroupName: utils.FirstUpper(entity.PackageName), + ModuleName: entity.PackageName + "Router", + PackageName: "router", + FunctionName: "Init" + info.StructName + "Router", + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + asts[packageInitializeRouter.Path+"=>"+packageInitializeRouter.Type.String()] = packageInitializeRouter + } + if isService != -1 { + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) + importPath := fmt.Sprintf(`"%s/service/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName) + packageServiceEnter := &ast.PackageEnter{ + Type: ast.TypePackageServiceEnter, + Path: path, + ImportPath: importPath, + StructName: utils.FirstUpper(entity.PackageName) + "ServiceGroup", + PackageName: entity.PackageName, + PackageStructName: "ServiceGroup", + } + asts[packageServiceEnter.Path+"=>"+packageServiceEnter.Type.String()] = packageServiceEnter + packageServiceModuleEnter := &ast.PackageModuleEnter{ + Type: ast.TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, "enter.go"), + StructName: info.StructName + "Service", + } + asts[packageServiceModuleEnter.Path+"=>"+packageServiceModuleEnter.Type.String()] = packageServiceModuleEnter + creates[four] = packageServiceModuleEnter.Path + } + continue + } + code[four] = create + continue + } + if hasEnter != -1 { + isApi := strings.Index(secondDirs[j].Name(), "api") + isRouter := strings.Index(secondDirs[j].Name(), "router") + isService := strings.Index(secondDirs[j].Name(), "service") + if isRouter != -1 { + pluginRouterEnter := &ast.PluginEnter{ + Type: ast.TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/api"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "api" + info.StructName, + GroupName: "Api", + PackageName: "api", + ServiceName: info.StructName, + } + asts[pluginRouterEnter.Path+"=>"+pluginRouterEnter.Type.String()] = pluginRouterEnter + creates[four] = pluginRouterEnter.Path + } + if isApi != -1 { + pluginApiEnter := &ast.PluginEnter{ + Type: ast.TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/service"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + ModuleName: "service" + info.StructName, + GroupName: "Service", + PackageName: "service", + ServiceName: info.StructName, + } + asts[pluginApiEnter.Path+"=>"+pluginApiEnter.Type.String()] = pluginApiEnter + creates[four] = pluginApiEnter.Path + } + if isService != -1 { + pluginServiceEnter := &ast.PluginEnter{ + Type: ast.TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + StructName: info.StructName, + StructCamelName: info.Abbreviation, + } + asts[pluginServiceEnter.Path+"=>"+pluginServiceEnter.Type.String()] = pluginServiceEnter + creates[four] = pluginServiceEnter.Path + } + continue + } // enter.go + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + code[four] = create + } + case "gen", "config", "initialize", "plugin", "response": + if entity.Template == "package" { + continue + } // package模板不需要生成gen, config, initialize + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + gen := strings.Index(threeDirs[k].Name(), "gen") + api := strings.Index(threeDirs[k].Name(), "api") + menu := strings.Index(threeDirs[k].Name(), "menu") + viper := strings.Index(threeDirs[k].Name(), "viper") + plugin := strings.Index(threeDirs[k].Name(), "plugin") + config := strings.Index(threeDirs[k].Name(), "config") + router := strings.Index(threeDirs[k].Name(), "router") + hasGorm := strings.Index(threeDirs[k].Name(), "gorm") + response := strings.Index(threeDirs[k].Name(), "response") + dictionary := strings.Index(threeDirs[k].Name(), "dictionary") + if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 && dictionary != -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 || dictionary != -1 { + creates[four] = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) + } + if gen != -1 { + pluginGen := &ast.PluginGen{ + Type: ast.TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginGen.Path+"=>"+pluginGen.Type.String()] = pluginGen + creates[four] = pluginGen.Path + } + if hasGorm != -1 { + pluginInitializeGorm := &ast.PluginInitializeGorm{ + Type: ast.TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/model"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + StructName: info.StructName, + PackageName: "model", + IsNew: true, + } + asts[pluginInitializeGorm.Path+"=>"+pluginInitializeGorm.Type.String()] = pluginInitializeGorm + creates[four] = pluginInitializeGorm.Path + } + if router != -1 { + pluginInitializeRouter := &ast.PluginInitializeRouter{ + Type: ast.TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)), + ImportPath: fmt.Sprintf(`"%s/plugin/%s/router"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + AppName: "Router", + GroupName: info.StructName, + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + } + asts[pluginInitializeRouter.Path+"=>"+pluginInitializeRouter.Type.String()] = pluginInitializeRouter + creates[four] = pluginInitializeRouter.Path + } + } + case "model": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + var fourDirs []os.DirEntry + fourDirs, err = os.ReadDir(four) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", four) + } + for l := 0; l < len(fourDirs); l++ { + if fourDirs[l].Name() == ".DS_Store" { + continue + } + five := filepath.Join(four, fourDirs[l].Name()) + if fourDirs[l].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", five) + } + ext := filepath.Ext(five) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", five) + } + hasRequest := strings.Index(fourDirs[l].Name(), "request") + if hasRequest == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", five) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), threeDirs[k].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, threeDirs[k].Name(), info.HumpPackageName+".go") + } + code[five] = create + } + continue + } + ext := filepath.Ext(threeDirs[k].Name()) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + hasModel := strings.Index(threeDirs[k].Name(), "model") + if hasModel == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), info.HumpPackageName+".go") + if entity.Template == "package" { + packageInitializeGorm := &ast.PackageInitializeGorm{ + Type: ast.TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: fmt.Sprintf(`"%s/model/%s"`, global.GVA_CONFIG.AutoCode.Module, entity.PackageName), + Business: info.BusinessDB, + StructName: info.StructName, + PackageName: entity.PackageName, + IsNew: true, + } + code[four] = packageInitializeGorm.Path + asts[packageInitializeGorm.Path+"=>"+packageInitializeGorm.Type.String()] = packageInitializeGorm + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, secondDirs[j].Name(), entity.PackageName, info.HumpPackageName+".go") + } + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "web": + if !info.GenerateWeb && !isPackage { + break + } + var secondDirs []os.DirEntry + secondDirs, err = os.ReadDir(second) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", second) + } + for j := 0; j < len(secondDirs); j++ { + if secondDirs[j].Name() == ".DS_Store" { + continue + } + three := filepath.Join(second, secondDirs[j].Name()) + if !secondDirs[j].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", three) + } + switch secondDirs[j].Name() { + case "api", "form", "view", "table": + var threeDirs []os.DirEntry + threeDirs, err = os.ReadDir(three) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "读取模版文件夹[%s]失败!", three) + } + for k := 0; k < len(threeDirs); k++ { + if threeDirs[k].Name() == ".DS_Store" { + continue + } + four := filepath.Join(three, threeDirs[k].Name()) + if threeDirs[k].IsDir() { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) + } + ext := filepath.Ext(four) + if ext != ".tpl" { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) + } + api := strings.Index(threeDirs[k].Name(), "api") + form := strings.Index(threeDirs[k].Name(), "form") + view := strings.Index(threeDirs[k].Name(), "view") + table := strings.Index(threeDirs[k].Name(), "table") + if api == -1 && form == -1 && view == -1 && table == -1 { + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) + } + if entity.Template == "package" { + if view != -1 || table != -1 { + formPath := filepath.Join(three, "form.vue"+ext) + value, ok := code[formPath] + if ok { + value = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+"Form"+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[formPath] = value + } + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + if api != -1 { + create = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), secondDirs[j].Name(), entity.PackageName, info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + } + code[four] = create + continue + } + create := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.WebRoot(), "plugin", entity.PackageName, secondDirs[j].Name(), info.PackageName+filepath.Ext(strings.TrimSuffix(threeDirs[k].Name(), ext))) + code[four] = create + } + default: + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", three) + } + } + case "readme.txt.tpl", "readme.txt.template": + continue + default: + if templateDirs[i].Name() == ".DS_Store" { + continue + } + return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", second) + } + } + return code, asts, creates, nil +} diff --git a/server/service/system/auto_code_package_test.go b/server/service/system/auto_code_package_test.go new file mode 100644 index 0000000..2dc459b --- /dev/null +++ b/server/service/system/auto_code_package_test.go @@ -0,0 +1,108 @@ +package system + +import ( + "context" + "reflect" + "testing" + + model "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" +) + +func Test_autoCodePackage_Create(t *testing.T) { + type args struct { + ctx context.Context + info *request.SysAutoCodePackageCreate + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "package", + PackageName: "gva", + }, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: &request.SysAutoCodePackageCreate{ + Template: "plugin", + PackageName: "gva", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &autoCodePackage{} + if err := a.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodePackage_templates(t *testing.T) { + type args struct { + ctx context.Context + entity model.SysAutoCodePackage + info request.AutoCode + isPackage bool + } + tests := []struct { + name string + args args + wantCode map[string]string + wantEnter map[string]map[string]string + wantErr bool + }{ + { + name: "测试1", + args: args{ + ctx: context.Background(), + entity: model.SysAutoCodePackage{ + Desc: "描述", + Label: "展示名", + Template: "plugin", + PackageName: "preview", + }, + info: request.AutoCode{ + Abbreviation: "user", + HumpPackageName: "user", + }, + isPackage: false, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodePackage{} + gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info, tt.args.isPackage) + if (err != nil) != tt.wantErr { + t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr) + return + } + for key, value := range gotCode { + t.Logf("\n") + t.Logf(key) + t.Logf(value) + t.Logf("\n") + } + t.Log(gotCreates) + if !reflect.DeepEqual(gotEnter, tt.wantEnter) { + t.Errorf("templates() gotEnter = %v, want %v", gotEnter, tt.wantEnter) + } + }) + } +} diff --git a/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go new file mode 100644 index 0000000..e3172d2 --- /dev/null +++ b/server/service/system/auto_code_plugin.go @@ -0,0 +1,512 @@ +package system + +import ( + "bytes" + "context" + "fmt" + goast "go/ast" + "go/parser" + "go/printer" + "go/token" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" + pluginUtils "git.echol.cn/loser/st/server/plugin/plugin-tool/utils" + "git.echol.cn/loser/st/server/utils" + ast "git.echol.cn/loser/st/server/utils/ast" + "github.com/mholt/archives" + cp "github.com/otiai10/copy" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +var AutoCodePlugin = new(autoCodePlugin) + +type autoCodePlugin struct{} + +// Install 插件安装 +func (s *autoCodePlugin) Install(file *multipart.FileHeader) (web, server int, err error) { + const GVAPLUGPINATH = "./gva-plug-temp/" + defer os.RemoveAll(GVAPLUGPINATH) + _, err = os.Stat(GVAPLUGPINATH) + if os.IsNotExist(err) { + os.Mkdir(GVAPLUGPINATH, os.ModePerm) + } + + src, err := file.Open() + if err != nil { + return -1, -1, err + } + defer src.Close() + + // 在临时目录创建目标文件 + // 使用完整路径拼接的好处:明确文件位置,避免路径混乱 + out, err := os.Create(GVAPLUGPINATH + file.Filename) + if err != nil { + return -1, -1, err + } + + // 将上传的文件内容复制到临时文件 + // 使用io.Copy的好处:高效处理大文件,自动管理缓冲区,避免内存溢出 + _, err = io.Copy(out, src) + if err != nil { + out.Close() + return -1, -1, err + } + + // 立即关闭文件,确保数据写入磁盘并释放文件句柄 + // 必须在解压前关闭,否则在Windows系统上会导致文件被占用无法解压 + err = out.Close() + if err != nil { + return -1, -1, err + } + + paths, err := utils.Unzip(GVAPLUGPINATH+file.Filename, GVAPLUGPINATH) + paths = filterFile(paths) + var webIndex = -1 + var serverIndex = -1 + webPlugin := "" + serverPlugin := "" + serverPackage := "" + serverRootName := "" + + for i := range paths { + paths[i] = filepath.ToSlash(paths[i]) + pathArr := strings.Split(paths[i], "/") + ln := len(pathArr) + + if ln < 4 { + continue + } + if pathArr[2]+"/"+pathArr[3] == `server/plugin` { + if len(serverPlugin) == 0 { + serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + } + if serverRootName == "" && ln > 1 && pathArr[1] != "" { + serverRootName = pathArr[1] + } + if ln > 4 && serverPackage == "" && pathArr[4] != "" { + serverPackage = pathArr[4] + } + } + if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 { + webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3]) + } + } + if len(serverPlugin) == 0 && len(webPlugin) == 0 { + zap.L().Error("非标准插件,请按照文档自动迁移使用") + return webIndex, serverIndex, errors.New("非标准插件,请按照文档自动迁移使用") + } + + if len(serverPlugin) != 0 { + if serverPackage == "" { + serverPackage = serverRootName + } + err = installation(serverPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Server) + if err != nil { + return webIndex, serverIndex, err + } + err = ensurePluginRegisterImport(serverPackage) + if err != nil { + return webIndex, serverIndex, err + } + } + + if len(webPlugin) != 0 { + err = installation(webPlugin, global.GVA_CONFIG.AutoCode.Server, global.GVA_CONFIG.AutoCode.Web) + if err != nil { + return webIndex, serverIndex, err + } + } + + return 1, 1, err +} + +func installation(path string, formPath string, toPath string) error { + arr := strings.Split(filepath.ToSlash(path), "/") + ln := len(arr) + if ln < 3 { + return errors.New("arr") + } + name := arr[ln-3] + + var form = filepath.Join(global.GVA_CONFIG.AutoCode.Root, formPath, path) + var to = filepath.Join(global.GVA_CONFIG.AutoCode.Root, toPath, "plugin") + _, err := os.Stat(to + name) + if err == nil { + zap.L().Error("autoPath 已存在同名插件,请自行手动安装", zap.String("to", to)) + return errors.New(toPath + "已存在同名插件,请自行手动安装") + } + return cp.Copy(form, to, cp.Options{Skip: skipMacSpecialDocument}) +} + +func ensurePluginRegisterImport(packageName string) error { + module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module) + if module == "" { + return errors.New("autocode module is empty") + } + if packageName == "" { + return errors.New("plugin package is empty") + } + + registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go") + src, err := os.ReadFile(registerPath) + if err != nil { + return err + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments) + if err != nil { + return err + } + + importPath := fmt.Sprintf("%s/plugin/%s", module, packageName) + if ast.CheckImport(astFile, importPath) { + return nil + } + + importSpec := &goast.ImportSpec{ + Name: goast.NewIdent("_"), + Path: &goast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", importPath)}, + } + var importDecl *goast.GenDecl + for _, decl := range astFile.Decls { + genDecl, ok := decl.(*goast.GenDecl) + if !ok { + continue + } + if genDecl.Tok == token.IMPORT { + importDecl = genDecl + break + } + } + if importDecl == nil { + astFile.Decls = append([]goast.Decl{ + &goast.GenDecl{ + Tok: token.IMPORT, + Specs: []goast.Spec{importSpec}, + }, + }, astFile.Decls...) + } else { + importDecl.Specs = append(importDecl.Specs, importSpec) + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + return os.WriteFile(registerPath, bf.Bytes(), 0666) +} + +func filterFile(paths []string) []string { + np := make([]string, 0, len(paths)) + for _, path := range paths { + if ok, _ := skipMacSpecialDocument(nil, path, ""); ok { + continue + } + np = append(np, path) + } + return np +} + +func skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) { + if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") { + return true, nil + } + return false, nil +} + +func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { + if plugName == "" { + return "", errors.New("插件名称不能为空") + } + + // 防止路径穿越 + plugName = filepath.Clean(plugName) + + webPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", plugName) + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", plugName) + // 创建一个新的zip文件 + + // 判断目录是否存在 + _, err = os.Stat(webPath) + if err != nil { + return "", errors.New("web路径不存在") + } + _, err = os.Stat(serverPath) + if err != nil { + return "", errors.New("server路径不存在") + } + + fileName := plugName + ".zip" + // 创建一个新的zip文件 + files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{ + webPath: plugName + "/web/plugin/" + plugName, + serverPath: plugName + "/server/plugin/" + plugName, + }) + + // create the output file we'll write to + out, err := os.Create(fileName) + if err != nil { + return + } + defer out.Close() + + // we can use the CompressedArchive type to gzip a tarball + // (compression is not required; you could use Tar directly) + format := archives.CompressedArchive{ + //Compression: archives.Gz{}, + Archival: archives.Zip{}, + } + + // create the archive + err = format.Archive(context.Background(), out, files) + if err != nil { + return + } + + return filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, fileName), nil +} + +func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) { + menuPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go") + src, err := os.ReadFile(menuPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysBaseMenu") + var menus []system.SysBaseMenu + + parentMenu := []system.SysBaseMenu{ + { + ParentId: 0, + Path: menuInfo.PlugName + "Menu", + Name: menuInfo.PlugName + "Menu", + Hidden: false, + Component: "view/routerHolder.vue", + Sort: 0, + Meta: system.Meta{ + Title: menuInfo.ParentMenu, + Icon: "school", + }, + }, + } + + // 查询菜单及其关联的参数和按钮 + err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error + if err != nil { + return err + } + menus = append(parentMenu, menus...) + menuExpr := ast.CreateMenuStructAst(menus) + arrayAst.Elts = *menuExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(menuPath, bf.Bytes(), 0666) + return nil +} + +func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) { + apiPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", apiInfo.PlugName, "initialize", "api.go") + src, err := os.ReadFile(apiPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysApi") + var apis []system.SysApi + err = global.GVA_DB.Find(&apis, "id in (?)", apiInfo.APIs).Error + if err != nil { + return err + } + apisExpr := ast.CreateApiStructAst(apis) + arrayAst.Elts = *apisExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(apiPath, bf.Bytes(), 0666) + return nil +} + +func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) { + dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go") + src, err := os.ReadFile(dictPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysDictionary") + var dictionaries []system.SysDictionary + err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error + if err != nil { + return err + } + dictExpr := ast.CreateDictionaryStructAst(dictionaries) + arrayAst.Elts = *dictExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(dictPath, bf.Bytes(), 0666) + return nil +} + +func (s *autoCodePlugin) Remove(pluginName string, pluginType string) (err error) { + // 1. 删除前端代码 + if pluginType == "web" || pluginType == "full" { + webDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", pluginName) + err = os.RemoveAll(webDir) + if err != nil { + return errors.Wrap(err, "删除前端插件目录失败") + } + } + + // 2. 删除后端代码 + if pluginType == "server" || pluginType == "full" { + serverDir := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", pluginName) + err = os.RemoveAll(serverDir) + if err != nil { + return errors.Wrap(err, "删除后端插件目录失败") + } + + // 移除注册 + removePluginRegisterImport(pluginName) + } + + // 通过utils 获取 api 菜单 字典 + apis, menus, dicts := pluginUtils.GetPluginData(pluginName) + + // 3. 删除菜单 (递归删除) + if len(menus) > 0 { + for _, menu := range menus { + var dbMenu system.SysBaseMenu + if err := global.GVA_DB.Where("name = ?", menu.Name).First(&dbMenu).Error; err == nil { + // 获取该菜单及其所有子菜单的ID + var menuIds []int + GetMenuIds(dbMenu, &menuIds) + // 逆序删除,先删除子菜单 + for i := len(menuIds) - 1; i >= 0; i-- { + err := BaseMenuServiceApp.DeleteBaseMenu(menuIds[i]) + if err != nil { + zap.L().Error("删除菜单失败", zap.Int("id", menuIds[i]), zap.Error(err)) + } + } + } + } + } + + // 4. 删除API + if len(apis) > 0 { + for _, api := range apis { + var dbApi system.SysApi + if err := global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&dbApi).Error; err == nil { + err := ApiServiceApp.DeleteApi(dbApi) + if err != nil { + zap.L().Error("删除API失败", zap.String("path", api.Path), zap.Error(err)) + } + } + } + } + + // 5. 删除字典 + if len(dicts) > 0 { + for _, dict := range dicts { + var dbDict system.SysDictionary + if err := global.GVA_DB.Where("type = ?", dict.Type).First(&dbDict).Error; err == nil { + err := DictionaryServiceApp.DeleteSysDictionary(dbDict) + if err != nil { + zap.L().Error("删除字典失败", zap.String("type", dict.Type), zap.Error(err)) + } + } + } + } + + return nil +} + +func GetMenuIds(menu system.SysBaseMenu, ids *[]int) { + *ids = append(*ids, int(menu.ID)) + var children []system.SysBaseMenu + global.GVA_DB.Where("parent_id = ?", menu.ID).Find(&children) + for _, child := range children { + // 先递归收集子菜单 + GetMenuIds(child, ids) + } +} + +func removePluginRegisterImport(packageName string) error { + module := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Module) + if module == "" { + return errors.New("autocode module is empty") + } + if packageName == "" { + return errors.New("plugin package is empty") + } + + registerPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go") + src, err := os.ReadFile(registerPath) + if err != nil { + return err + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, registerPath, src, parser.ParseComments) + if err != nil { + return err + } + + importPath := fmt.Sprintf("%s/plugin/%s", module, packageName) + importLit := fmt.Sprintf("%q", importPath) + + // 移除 import + var newDecls []goast.Decl + for _, decl := range astFile.Decls { + genDecl, ok := decl.(*goast.GenDecl) + if !ok { + newDecls = append(newDecls, decl) + continue + } + if genDecl.Tok == token.IMPORT { + var newSpecs []goast.Spec + for _, spec := range genDecl.Specs { + importSpec, ok := spec.(*goast.ImportSpec) + if !ok { + newSpecs = append(newSpecs, spec) + continue + } + if importSpec.Path.Value != importLit { + newSpecs = append(newSpecs, spec) + } + } + // 如果还有其他import,保留该 decl + if len(newSpecs) > 0 { + genDecl.Specs = newSpecs + newDecls = append(newDecls, genDecl) + } + } else { + newDecls = append(newDecls, decl) + } + } + astFile.Decls = newDecls + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + return os.WriteFile(registerPath, bf.Bytes(), 0666) +} diff --git a/server/service/system/auto_code_template.go b/server/service/system/auto_code_template.go new file mode 100644 index 0000000..62f2383 --- /dev/null +++ b/server/service/system/auto_code_template.go @@ -0,0 +1,454 @@ +package system + +import ( + "context" + "encoding/json" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "text/template" + + "git.echol.cn/loser/st/server/utils/autocode" + + "git.echol.cn/loser/st/server/global" + model "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" + utilsAst "git.echol.cn/loser/st/server/utils/ast" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +var AutoCodeTemplate = new(autoCodeTemplate) + +type autoCodeTemplate struct{} + +func (s *autoCodeTemplate) checkPackage(Pkg string, template string) (err error) { + switch template { + case "package": + apiEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", Pkg, "enter.go") + _, err = os.Stat(apiEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少api/v1/%s/enter.go", Pkg) + } + serviceEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", Pkg, "enter.go") + _, err = os.Stat(serviceEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少service/%s/enter.go", Pkg) + } + routerEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", Pkg, "enter.go") + _, err = os.Stat(routerEnter) + if err != nil { + return fmt.Errorf("package结构异常,缺少router/%s/enter.go", Pkg) + } + case "plugin": + pluginEnter := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", Pkg, "plugin.go") + _, err = os.Stat(pluginEnter) + if err != nil { + return fmt.Errorf("plugin结构异常,缺少plugin/%s/plugin.go", Pkg) + } + } + return nil +} + +// Create 创建生成自动化代码 +func (s *autoCodeTemplate) Create(ctx context.Context, info request.AutoCode) error { + history := info.History() + var autoPkg model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&autoPkg).Error + if err != nil { + return errors.Wrap(err, "查询包失败!") + } + err = s.checkPackage(info.Package, autoPkg.Template) + if err != nil { + return err + } + // 增加判断: 重复创建struct 或者重复的简称 + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) { + return errors.New("已经创建过此数据结构,请勿重复创建!") + } + + generate, templates, injections, err := s.generate(ctx, info, autoPkg) + if err != nil { + return err + } + for key, builder := range generate { + err = os.MkdirAll(filepath.Dir(key), os.ModePerm) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]创建文件夹失败!", key) + } + err = os.WriteFile(key, []byte(builder.String()), 0666) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]写入文件失败!", key) + } + } + + // 自动创建api + if info.AutoCreateApiToSql && !info.OnlyTemplate { + apis := info.Apis() + err := global.GVA_DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for _, v := range apis { + var api model.SysApi + var id uint + err := tx.Where("path = ? AND method = ?", v.Path, v.Method).First(&api).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + if err = tx.Create(&v).Error; err != nil { // 遇到错误时回滚事务 + return err + } + id = v.ID + } else { + id = api.ID + } + history.ApiIDs = append(history.ApiIDs, id) + } + return nil + }) + if err != nil { + return err + } + } + + // 自动创建menu + if info.AutoCreateMenuToSql { + var entity model.SysBaseMenu + var id uint + err := global.GVA_DB.WithContext(ctx).First(&entity, "name = ?", info.Abbreviation).Error + if err == nil { + id = entity.ID + } else { + entity = info.Menu(autoPkg.Template) + if info.AutoCreateBtnAuth && !info.OnlyTemplate { + entity.MenuBtn = []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "add", Desc: "新增"}, + {SysBaseMenuID: entity.ID, Name: "batchDelete", Desc: "批量删除"}, + {SysBaseMenuID: entity.ID, Name: "delete", Desc: "删除"}, + {SysBaseMenuID: entity.ID, Name: "edit", Desc: "编辑"}, + {SysBaseMenuID: entity.ID, Name: "info", Desc: "详情"}, + } + if info.HasExcel { + excelBtn := []model.SysBaseMenuBtn{ + {SysBaseMenuID: entity.ID, Name: "exportTemplate", Desc: "导出模板"}, + {SysBaseMenuID: entity.ID, Name: "exportExcel", Desc: "导出Excel"}, + {SysBaseMenuID: entity.ID, Name: "importExcel", Desc: "导入Excel"}, + } + entity.MenuBtn = append(entity.MenuBtn, excelBtn...) + } + } + err = global.GVA_DB.WithContext(ctx).Create(&entity).Error + id = entity.ID + if err != nil { + return errors.Wrap(err, "创建菜单失败!") + } + } + history.MenuID = id + } + + if info.HasExcel { + dbName := info.BusinessDB + name := info.Package + "_" + info.StructName + tableName := info.TableName + fieldsMap := make(map[string]string, len(info.Fields)) + for _, field := range info.Fields { + if field.Excel { + fieldsMap[field.ColumnName] = field.FieldDesc + } + } + templateInfo, _ := json.Marshal(fieldsMap) + sysExportTemplate := model.SysExportTemplate{ + DBName: dbName, + Name: name, + TableName: tableName, + TemplateID: name, + TemplateInfo: string(templateInfo), + } + err = SysExportTemplateServiceApp.CreateSysExportTemplate(&sysExportTemplate) + if err != nil { + return err + } + history.ExportTemplateID = sysExportTemplate.ID + } + + // 创建历史记录 + history.Templates = templates + history.Injections = make(map[string]string, len(injections)) + for key, value := range injections { + bytes, _ := json.Marshal(value) + history.Injections[key] = string(bytes) + } + err = AutocodeHistory.Create(ctx, history) + if err != nil { + return err + } + return nil +} + +// Preview 预览自动化代码 +func (s *autoCodeTemplate) Preview(ctx context.Context, info request.AutoCode) (map[string]string, error) { + var entity model.SysAutoCodePackage + err := global.GVA_DB.WithContext(ctx).Where("package_name = ?", info.Package).First(&entity).Error + if err != nil { + return nil, errors.Wrap(err, "查询包失败!") + } + // 增加判断: 重复创建struct 或者重复的简称 + if AutocodeHistory.Repeat(info.BusinessDB, info.StructName, info.Abbreviation, info.Package) && !info.IsAdd { + return nil, errors.New("已经创建过此数据结构或重复简称,请勿重复创建!") + } + + preview := make(map[string]string) + codes, _, _, err := s.generate(ctx, info, entity) + if err != nil { + return nil, err + } + for key, writer := range codes { + if len(key) > len(global.GVA_CONFIG.AutoCode.Root) { + key, _ = filepath.Rel(global.GVA_CONFIG.AutoCode.Root, key) + } + // 获取key的后缀 取消. + suffix := filepath.Ext(key)[1:] + var builder strings.Builder + builder.WriteString("```" + suffix + "\n\n") + builder.WriteString(writer.String()) + builder.WriteString("\n\n```") + preview[key] = builder.String() + } + return preview, nil +} + +func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, entity model.SysAutoCodePackage) (map[string]strings.Builder, map[string]string, map[string]utilsAst.Ast, error) { + templates, asts, _, err := AutoCodePackage.templates(ctx, entity, info, false) + if err != nil { + return nil, nil, nil, err + } + code := make(map[string]strings.Builder) + for key, create := range templates { + var files *template.Template + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]生成文件失败!", create) + } + code[create] = builder + } // 生成文件 + injections := make(map[string]utilsAst.Ast, len(asts)) + for key, value := range asts { + keys := strings.Split(key, "=>") + if len(keys) == 2 { + if keys[1] == utilsAst.TypePluginInitializeV2 { + continue + } + if info.OnlyTemplate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + if !info.AutoMigrate { + if keys[1] == utilsAst.TypePackageInitializeGorm || keys[1] == utilsAst.TypePluginInitializeGorm { + continue + } + } + var builder strings.Builder + parse, _ := value.Parse("", &builder) + if parse != nil { + _ = value.Injection(parse) + err = value.Format("", &builder, parse) + if err != nil { + return nil, nil, nil, err + } + code[keys[0]] = builder + injections[keys[1]] = value + fmt.Println(keys[0], "注入成功!") + } + } + } + // 注入代码 + return code, templates, injections, nil +} + +func (s *autoCodeTemplate) AddFunc(info request.AutoFunc) error { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + err = s.addTemplateToFile("api.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("server.go", info) + if err != nil { + return err + } + err = s.addTemplateToFile("api.js", info) + if err != nil { + return err + } + return s.addTemplateToAst("router", info) +} + +func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]string, error) { + autoPkg := model.SysAutoCodePackage{} + err := global.GVA_DB.First(&autoPkg, "package_name = ?", info.Package).Error + if err != nil { + return nil, err + } + if autoPkg.Template != "package" { + info.IsPlugin = true + } + + apiStr, err := s.getTemplateStr("api.go", info) + if err != nil { + return nil, err + } + serverStr, err := s.getTemplateStr("server.go", info) + if err != nil { + return nil, err + } + jsStr, err := s.getTemplateStr("api.js", info) + if err != nil { + return nil, err + } + return map[string]string{"api": apiStr, "server": serverStr, "js": jsStr}, nil + +} + +func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) { + tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl") + files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath) + if err != nil { + return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) + } + var builder strings.Builder + err = files.Execute(&builder, info) + if err != nil { + fmt.Println(err.Error()) + return "", errors.Wrapf(err, "[filpath:%s]生成文件失败!", tempPath) + } + return builder.String(), nil +} + +func (s *autoCodeTemplate) addTemplateToAst(t string, info request.AutoFunc) error { + tPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", info.Package, info.HumpPackageName+".go") + funcName := fmt.Sprintf("Init%sRouter", info.StructName) + + routerStr := "RouterWithoutAuth" + if info.IsAuth { + routerStr = "Router" + } + + stmtStr := fmt.Sprintf("%s%s.%s(\"%s\", %sApi.%s)", info.Abbreviation, routerStr, info.Method, info.Router, info.Abbreviation, info.FuncName) + if info.IsPlugin { + tPath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "router", info.HumpPackageName+".go") + stmtStr = fmt.Sprintf("group.%s(\"%s\", api%s.%s)", info.Method, info.Router, info.StructName, info.FuncName) + funcName = "Init" + } + + src, err := os.ReadFile(tPath) + if err != nil { + return err + } + + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + return err + } + funcDecl := utilsAst.FindFunction(astFile, funcName) + stmtNode := utilsAst.CreateStmt(stmtStr) + + if info.IsAuth { + for i := 0; i < len(funcDecl.Body.List); i++ { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } else { + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + st := funcDecl.Body.List[i] + // 使用类型断言来检查stmt是否是一个块语句 + if blockStmt, ok := st.(*ast.BlockStmt); ok { + // 如果是,插入代码 跳出 + blockStmt.List = append(blockStmt.List, stmtNode) + break + } + } + } + + // 创建一个新的文件 + f, err := os.Create(tPath) + if err != nil { + return err + } + defer f.Close() + + if err := format.Node(f, fileSet, astFile); err != nil { + return err + } + return err +} + +func (s *autoCodeTemplate) addTemplateToFile(t string, info request.AutoFunc) error { + getTemplateStr, err := s.getTemplateStr(t, info) + if err != nil { + return err + } + var target string + + switch t { + case "api.go": + if info.IsAi && info.ApiFunc != "" { + getTemplateStr = info.ApiFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", info.Package, info.HumpPackageName+".go") + case "server.go": + if info.IsAi && info.ServerFunc != "" { + getTemplateStr = info.ServerFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", info.Package, info.HumpPackageName+".go") + case "api.js": + if info.IsAi && info.JsFunc != "" { + getTemplateStr = info.JsFunc + } + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "api", info.Package, info.PackageName+".js") + } + if info.IsPlugin { + switch t { + case "api.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "api", info.HumpPackageName+".go") + case "server.go": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", info.Package, "service", info.HumpPackageName+".go") + case "api.js": + target = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Web, "plugin", info.Package, "api", info.PackageName+".js") + } + } + + // 打开文件,如果不存在则返回错误 + file, err := os.OpenFile(target, os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + defer file.Close() + + // 写入内容 + _, err = fmt.Fprintln(file, getTemplateStr) + if err != nil { + fmt.Printf("写入文件失败: %s\n", err.Error()) + return err + } + + return nil +} diff --git a/server/service/system/auto_code_template_test.go b/server/service/system/auto_code_template_test.go new file mode 100644 index 0000000..12889a7 --- /dev/null +++ b/server/service/system/auto_code_template_test.go @@ -0,0 +1,85 @@ +package system + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + "git.echol.cn/loser/st/server/model/system/request" +) + +func Test_autoCodeTemplate_Create(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &autoCodeTemplate{} + if err := s.Create(tt.args.ctx, tt.args.info); (err != nil) != tt.wantErr { + t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_autoCodeTemplate_Preview(t *testing.T) { + type args struct { + ctx context.Context + info request.AutoCode + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + { + name: "测试 package", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + { + name: "测试 plugin", + args: args{ + ctx: context.Background(), + info: request.AutoCode{}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testJson := `{"structName":"SysUser","tableName":"sys_users","packageName":"sysUsers","package":"gva","abbreviation":"sysUsers","description":"sysUsers表","businessDB":"","autoCreateApiToSql":true,"autoCreateMenuToSql":true,"autoMigrate":true,"gvaModel":true,"autoCreateResource":false,"fields":[{"fieldName":"Uuid","fieldDesc":"用户UUID","fieldType":"string","dataType":"varchar","fieldJson":"uuid","primaryKey":false,"dataTypeLong":"191","columnName":"uuid","comment":"用户UUID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Username","fieldDesc":"用户登录名","fieldType":"string","dataType":"varchar","fieldJson":"username","primaryKey":false,"dataTypeLong":"191","columnName":"username","comment":"用户登录名","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Password","fieldDesc":"用户登录密码","fieldType":"string","dataType":"varchar","fieldJson":"password","primaryKey":false,"dataTypeLong":"191","columnName":"password","comment":"用户登录密码","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"NickName","fieldDesc":"用户昵称","fieldType":"string","dataType":"varchar","fieldJson":"nickName","primaryKey":false,"dataTypeLong":"191","columnName":"nick_name","comment":"用户昵称","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"SideMode","fieldDesc":"用户侧边主题","fieldType":"string","dataType":"varchar","fieldJson":"sideMode","primaryKey":false,"dataTypeLong":"191","columnName":"side_mode","comment":"用户侧边主题","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"HeaderImg","fieldDesc":"用户头像","fieldType":"string","dataType":"varchar","fieldJson":"headerImg","primaryKey":false,"dataTypeLong":"191","columnName":"header_img","comment":"用户头像","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"BaseColor","fieldDesc":"基础颜色","fieldType":"string","dataType":"varchar","fieldJson":"baseColor","primaryKey":false,"dataTypeLong":"191","columnName":"base_color","comment":"基础颜色","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"AuthorityId","fieldDesc":"用户角色ID","fieldType":"int","dataType":"bigint","fieldJson":"authorityId","primaryKey":false,"dataTypeLong":"20","columnName":"authority_id","comment":"用户角色ID","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Phone","fieldDesc":"用户手机号","fieldType":"string","dataType":"varchar","fieldJson":"phone","primaryKey":false,"dataTypeLong":"191","columnName":"phone","comment":"用户手机号","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Email","fieldDesc":"用户邮箱","fieldType":"string","dataType":"varchar","fieldJson":"email","primaryKey":false,"dataTypeLong":"191","columnName":"email","comment":"用户邮箱","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}},{"fieldName":"Enable","fieldDesc":"用户是否被冻结 1正常 2冻结","fieldType":"int","dataType":"bigint","fieldJson":"enable","primaryKey":false,"dataTypeLong":"19","columnName":"enable","comment":"用户是否被冻结 1正常 2冻结","require":false,"errorText":"","clearable":true,"fieldSearchType":"","fieldIndexType":"","dictType":"","front":true,"dataSource":{"association":1,"table":"","label":"","value":""}}],"humpPackageName":"sys_users"}` + err := json.Unmarshal([]byte(testJson), &tt.args.info) + if err != nil { + t.Error(err) + return + } + err = tt.args.info.Pretreatment() + if err != nil { + t.Error(err) + return + } + got, err := AutoCodeTemplate.Preview(tt.args.ctx, tt.args.info) + if (err != nil) != tt.wantErr { + t.Errorf("Preview() error = %+v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Preview() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/service/system/enter.go b/server/service/system/enter.go new file mode 100644 index 0000000..6d68bb7 --- /dev/null +++ b/server/service/system/enter.go @@ -0,0 +1,29 @@ +package system + +type ServiceGroup struct { + JwtService + ApiService + MenuService + UserService + CasbinService + InitDBService + AutoCodeService + BaseMenuService + AuthorityService + DictionaryService + SystemConfigService + OperationRecordService + DictionaryDetailService + AuthorityBtnService + SysExportTemplateService + SysParamsService + SysVersionService + SkillsService + AutoCodePlugin autoCodePlugin + AutoCodePackage autoCodePackage + AutoCodeHistory autoCodeHistory + AutoCodeTemplate autoCodeTemplate + SysErrorService + LoginLogService + ApiTokenService +} diff --git a/server/service/system/jwt_black_list.go b/server/service/system/jwt_black_list.go new file mode 100644 index 0000000..4954c0c --- /dev/null +++ b/server/service/system/jwt_black_list.go @@ -0,0 +1,52 @@ +package system + +import ( + "context" + + "go.uber.org/zap" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" +) + +type JwtService struct{} + +var JwtServiceApp = new(JwtService) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: JsonInBlacklist +//@description: 拉黑jwt +//@param: jwtList model.JwtBlacklist +//@return: err error + +func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err error) { + err = global.GVA_DB.Create(&jwtList).Error + if err != nil { + return + } + global.BlackCache.SetDefault(jwtList.Jwt, struct{}{}) + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetRedisJWT +//@description: 从redis取jwt +//@param: userName string +//@return: redisJWT string, err error + +func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err error) { + redisJWT, err = global.GVA_REDIS.Get(context.Background(), userName).Result() + return redisJWT, err +} + +func LoadAll() { + var data []string + err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error + if err != nil { + global.GVA_LOG.Error("加载数据库jwt黑名单失败!", zap.Error(err)) + return + } + for i := 0; i < len(data); i++ { + global.BlackCache.SetDefault(data[i], struct{}{}) + } // jwt黑名单 加入 BlackCache 中 +} diff --git a/server/service/system/sys_api.go b/server/service/system/sys_api.go new file mode 100644 index 0000000..60ecd7b --- /dev/null +++ b/server/service/system/sys_api.go @@ -0,0 +1,326 @@ +package system + +import ( + "errors" + "fmt" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" + systemRes "git.echol.cn/loser/st/server/model/system/response" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateApi +//@description: 新增基础api +//@param: api model.SysApi +//@return: err error + +type ApiService struct{} + +var ApiServiceApp = new(ApiService) + +func (apiService *ApiService) CreateApi(api system.SysApi) (err error) { + if !errors.Is(global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&system.SysApi{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同api") + } + return global.GVA_DB.Create(&api).Error +} + +func (apiService *ApiService) GetApiGroups() (groups []string, groupApiMap map[string]string, err error) { + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + groupApiMap = make(map[string]string, 0) + for i := range apis { + pathArr := strings.Split(apis[i].Path, "/") + newGroup := true + for i2 := range groups { + if groups[i2] == apis[i].ApiGroup { + newGroup = false + } + } + if newGroup { + groups = append(groups, apis[i].ApiGroup) + } + groupApiMap[pathArr[1]] = apis[i].ApiGroup + } + return +} + +func (apiService *ApiService) SyncApi() (newApis, deleteApis, ignoreApis []system.SysApi, err error) { + newApis = make([]system.SysApi, 0) + deleteApis = make([]system.SysApi, 0) + ignoreApis = make([]system.SysApi, 0) + var apis []system.SysApi + err = global.GVA_DB.Find(&apis).Error + if err != nil { + return + } + var ignores []system.SysIgnoreApi + err = global.GVA_DB.Find(&ignores).Error + if err != nil { + return + } + + for i := range ignores { + ignoreApis = append(ignoreApis, system.SysApi{ + Path: ignores[i].Path, + Description: "", + ApiGroup: "", + Method: ignores[i].Method, + }) + } + + var cacheApis []system.SysApi + for i := range global.GVA_ROUTERS { + ignoresFlag := false + for j := range ignores { + if ignores[j].Path == global.GVA_ROUTERS[i].Path && ignores[j].Method == global.GVA_ROUTERS[i].Method { + ignoresFlag = true + } + } + if !ignoresFlag { + cacheApis = append(cacheApis, system.SysApi{ + Path: global.GVA_ROUTERS[i].Path, + Method: global.GVA_ROUTERS[i].Method, + }) + } + } + + //对比数据库中的api和内存中的api,如果数据库中的api不存在于内存中,则把api放入删除数组,如果内存中的api不存在于数据库中,则把api放入新增数组 + for i := range cacheApis { + var flag bool + // 如果存在于内存不存在于api数组中 + for j := range apis { + if cacheApis[i].Path == apis[j].Path && cacheApis[i].Method == apis[j].Method { + flag = true + } + } + if !flag { + newApis = append(newApis, system.SysApi{ + Path: cacheApis[i].Path, + Description: "", + ApiGroup: "", + Method: cacheApis[i].Method, + }) + } + } + + for i := range apis { + var flag bool + // 如果存在于api数组不存在于内存 + for j := range cacheApis { + if cacheApis[j].Path == apis[i].Path && cacheApis[j].Method == apis[i].Method { + flag = true + } + } + if !flag { + deleteApis = append(deleteApis, apis[i]) + } + } + return +} + +func (apiService *ApiService) IgnoreApi(ignoreApi system.SysIgnoreApi) (err error) { + if ignoreApi.Flag { + return global.GVA_DB.Create(&ignoreApi).Error + } + return global.GVA_DB.Unscoped().Delete(&ignoreApi, "path = ? AND method = ?", ignoreApi.Path, ignoreApi.Method).Error +} + +func (apiService *ApiService) EnterSyncApi(syncApis systemRes.SysSyncApis) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var txErr error + if len(syncApis.NewApis) > 0 { + txErr = tx.Create(&syncApis.NewApis).Error + if txErr != nil { + return txErr + } + } + for i := range syncApis.DeleteApis { + CasbinServiceApp.ClearCasbin(1, syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method) + txErr = tx.Delete(&system.SysApi{}, "path = ? AND method = ?", syncApis.DeleteApis[i].Path, syncApis.DeleteApis[i].Method).Error + if txErr != nil { + return txErr + } + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApi +//@description: 删除基础api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApi(api system.SysApi) (err error) { + var entity system.SysApi + err = global.GVA_DB.First(&entity, "id = ?", api.ID).Error // 根据id查询api记录 + if errors.Is(err, gorm.ErrRecordNotFound) { // api记录不存在 + return err + } + err = global.GVA_DB.Delete(&entity).Error + if err != nil { + return err + } + CasbinServiceApp.ClearCasbin(1, entity.Path, entity.Method) + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAPIInfoList +//@description: 分页获取数据, +//@param: api model.SysApi, info request.PageInfo, order string, desc bool +//@return: list interface{}, total int64, err error + +func (apiService *ApiService) GetAPIInfoList(api system.SysApi, info request.PageInfo, order string, desc bool) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysApi{}) + var apiList []system.SysApi + + if api.Path != "" { + db = db.Where("path LIKE ?", "%"+api.Path+"%") + } + + if api.Description != "" { + db = db.Where("description LIKE ?", "%"+api.Description+"%") + } + + if api.Method != "" { + db = db.Where("method = ?", api.Method) + } + + if api.ApiGroup != "" { + db = db.Where("api_group = ?", api.ApiGroup) + } + + err = db.Count(&total).Error + + if err != nil { + return apiList, total, err + } + + db = db.Limit(limit).Offset(offset) + OrderStr := "id desc" + if order != "" { + orderMap := make(map[string]bool, 5) + orderMap["id"] = true + orderMap["path"] = true + orderMap["api_group"] = true + orderMap["description"] = true + orderMap["method"] = true + if !orderMap[order] { + err = fmt.Errorf("非法的排序字段: %v", order) + return apiList, total, err + } + OrderStr = order + if desc { + OrderStr = order + " desc" + } + } + err = db.Order(OrderStr).Find(&apiList).Error + return apiList, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAllApis +//@description: 获取所有的api +//@return: apis []model.SysApi, err error + +func (apiService *ApiService) GetAllApis(authorityID uint) (apis []system.SysApi, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + err = global.GVA_DB.Order("id desc").Find(&apis).Error + if parentAuthorityID == 0 || !global.GVA_CONFIG.System.UseStrictAuth { + return + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(authorityID) + // 挑选 apis里面的path和method也在paths里面的api + var authApis []system.SysApi + for i := range apis { + for j := range paths { + if paths[j].Path == apis[i].Path && paths[j].Method == apis[i].Method { + authApis = append(authApis, apis[i]) + } + } + } + return authApis, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetApiById +//@description: 根据id获取api +//@param: id float64 +//@return: api model.SysApi, err error + +func (apiService *ApiService) GetApiById(id int) (api system.SysApi, err error) { + err = global.GVA_DB.First(&api, "id = ?", id).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateApi +//@description: 根据id更新api +//@param: api model.SysApi +//@return: err error + +func (apiService *ApiService) UpdateApi(api system.SysApi) (err error) { + var oldA system.SysApi + err = global.GVA_DB.First(&oldA, "id = ?", api.ID).Error + if oldA.Path != api.Path || oldA.Method != api.Method { + var duplicateApi system.SysApi + if ferr := global.GVA_DB.First(&duplicateApi, "path = ? AND method = ?", api.Path, api.Method).Error; ferr != nil { + if !errors.Is(ferr, gorm.ErrRecordNotFound) { + return ferr + } + } else { + if duplicateApi.ID != api.ID { + return errors.New("存在相同api路径") + } + } + + } + if err != nil { + return err + } + + err = CasbinServiceApp.UpdateCasbinApi(oldA.Path, api.Path, oldA.Method, api.Method) + if err != nil { + return err + } + + return global.GVA_DB.Save(&api).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteApisByIds +//@description: 删除选中API +//@param: apis []model.SysApi +//@return: err error + +func (apiService *ApiService) DeleteApisByIds(ids request.IdsReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var apis []system.SysApi + err = tx.Find(&apis, "id in ?", ids.Ids).Error + if err != nil { + return err + } + err = tx.Delete(&[]system.SysApi{}, "id in ?", ids.Ids).Error + if err != nil { + return err + } + for _, sysApi := range apis { + CasbinServiceApp.ClearCasbin(1, sysApi.Path, sysApi.Method) + } + return err + }) +} diff --git a/server/service/system/sys_api_token.go b/server/service/system/sys_api_token.go new file mode 100644 index 0000000..6132731 --- /dev/null +++ b/server/service/system/sys_api_token.go @@ -0,0 +1,107 @@ +package system + +import ( + "errors" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + sysReq "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + "github.com/golang-jwt/jwt/v5" +) + +type ApiTokenService struct{} + +func (apiVersion *ApiTokenService) CreateApiToken(apiToken system.SysApiToken, days int) (string, error) { + var user system.SysUser + if err := global.GVA_DB.Where("id = ?", apiToken.UserID).First(&user).Error; err != nil { + return "", errors.New("用户不存在") + } + + hasAuth := false + for _, auth := range user.Authorities { + if auth.AuthorityId == apiToken.AuthorityID { + hasAuth = true + break + } + } + if !hasAuth && user.AuthorityId != apiToken.AuthorityID { + return "", errors.New("用户不具备该角色权限") + } + + j := &utils.JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一不同的部分是过期时间 + + expireTime := time.Duration(days) * 24 * time.Hour + if days == -1 { + expireTime = 100 * 365 * 24 * time.Hour + } + + bf, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + + claims := sysReq.CustomClaims{ + BaseClaims: sysReq.BaseClaims{ + UUID: user.UUID, + ID: user.ID, + Username: user.Username, + NickName: user.NickName, + AuthorityId: apiToken.AuthorityID, + }, + BufferTime: int64(bf / time.Second), // 缓冲时间 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireTime)), + Issuer: global.GVA_CONFIG.JWT.Issuer, + }, + } + + token, err := j.CreateToken(claims) + if err != nil { + return "", err + } + + apiToken.Token = token + apiToken.Status = true + apiToken.ExpiresAt = time.Now().Add(expireTime) + err = global.GVA_DB.Create(&apiToken).Error + return token, err +} + +func (apiVersion *ApiTokenService) GetApiTokenList(info sysReq.SysApiTokenSearch) (list []system.SysApiToken, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysApiToken{}) + + db = db.Preload("User") + + if info.UserID != 0 { + db = db.Where("user_id = ?", info.UserID) + } + if info.Status != nil { + db = db.Where("status = ?", *info.Status) + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&list).Error + return list, total, err +} + +func (apiVersion *ApiTokenService) DeleteApiToken(id uint) error { + var apiToken system.SysApiToken + err := global.GVA_DB.First(&apiToken, id).Error + if err != nil { + return err + } + + jwtService := JwtService{} + err = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: apiToken.Token}) + if err != nil { + return err + } + + return global.GVA_DB.Model(&apiToken).Update("status", false).Error +} diff --git a/server/service/system/sys_authority.go b/server/service/system/sys_authority.go new file mode 100644 index 0000000..5da40c4 --- /dev/null +++ b/server/service/system/sys_authority.go @@ -0,0 +1,333 @@ +package system + +import ( + "errors" + "strconv" + + systemReq "git.echol.cn/loser/st/server/model/system/request" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/response" + "gorm.io/gorm" +) + +var ErrRoleExistence = errors.New("存在相同角色id") + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateAuthority +//@description: 创建一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +type AuthorityService struct{} + +var AuthorityServiceApp = new(AuthorityService) + +func (authorityService *AuthorityService) CreateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + + if err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error; !errors.Is(err, gorm.ErrRecordNotFound) { + return auth, ErrRoleExistence + } + + e := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + if err = tx.Create(&auth).Error; err != nil { + return err + } + + auth.SysBaseMenus = systemReq.DefaultMenu() + if err = tx.Model(&auth).Association("SysBaseMenus").Replace(&auth.SysBaseMenus); err != nil { + return err + } + casbinInfos := systemReq.DefaultCasbin() + authorityId := strconv.Itoa(int(auth.AuthorityId)) + rules := [][]string{} + for _, v := range casbinInfos { + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + return CasbinServiceApp.AddPolicies(tx, rules) + }) + + return auth, e +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CopyAuthority +//@description: 复制一个角色 +//@param: copyInfo response.SysAuthorityCopyResponse +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) CopyAuthority(adminAuthorityID uint, copyInfo response.SysAuthorityCopyResponse) (authority system.SysAuthority, err error) { + var authorityBox system.SysAuthority + if !errors.Is(global.GVA_DB.Where("authority_id = ?", copyInfo.Authority.AuthorityId).First(&authorityBox).Error, gorm.ErrRecordNotFound) { + return authority, ErrRoleExistence + } + copyInfo.Authority.Children = []system.SysAuthority{} + menus, err := MenuServiceApp.GetMenuAuthority(&request.GetAuthorityId{AuthorityId: copyInfo.OldAuthorityId}) + if err != nil { + return + } + var baseMenu []system.SysBaseMenu + for _, v := range menus { + intNum := v.MenuId + v.SysBaseMenu.ID = uint(intNum) + baseMenu = append(baseMenu, v.SysBaseMenu) + } + copyInfo.Authority.SysBaseMenus = baseMenu + err = global.GVA_DB.Create(©Info.Authority).Error + if err != nil { + return + } + + var btns []system.SysAuthorityBtn + + err = global.GVA_DB.Find(&btns, "authority_id = ?", copyInfo.OldAuthorityId).Error + if err != nil { + return + } + if len(btns) > 0 { + for i := range btns { + btns[i].AuthorityId = copyInfo.Authority.AuthorityId + } + err = global.GVA_DB.Create(&btns).Error + + if err != nil { + return + } + } + paths := CasbinServiceApp.GetPolicyPathByAuthorityId(copyInfo.OldAuthorityId) + err = CasbinServiceApp.UpdateCasbin(adminAuthorityID, copyInfo.Authority.AuthorityId, paths) + if err != nil { + _ = authorityService.DeleteAuthority(©Info.Authority) + } + return copyInfo.Authority, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateAuthority +//@description: 更改一个角色 +//@param: auth model.SysAuthority +//@return: authority system.SysAuthority, err error + +func (authorityService *AuthorityService) UpdateAuthority(auth system.SysAuthority) (authority system.SysAuthority, err error) { + var oldAuthority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&oldAuthority).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return system.SysAuthority{}, errors.New("查询角色数据失败") + } + err = global.GVA_DB.Model(&oldAuthority).Updates(&auth).Error + return auth, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteAuthority +//@description: 删除角色 +//@param: auth *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) DeleteAuthority(auth *system.SysAuthority) error { + if errors.Is(global.GVA_DB.Debug().Preload("Users").First(&auth).Error, gorm.ErrRecordNotFound) { + return errors.New("该角色不存在") + } + if len(auth.Users) != 0 { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("authority_id = ?", auth.AuthorityId).First(&system.SysUser{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色有用户正在使用禁止删除") + } + if !errors.Is(global.GVA_DB.Where("parent_id = ?", auth.AuthorityId).First(&system.SysAuthority{}).Error, gorm.ErrRecordNotFound) { + return errors.New("此角色存在子角色不允许删除") + } + + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var err error + if err = tx.Preload("SysBaseMenus").Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(auth).Unscoped().Delete(auth).Error; err != nil { + return err + } + + if len(auth.SysBaseMenus) > 0 { + if err = tx.Model(auth).Association("SysBaseMenus").Delete(auth.SysBaseMenus); err != nil { + return err + } + // err = db.Association("SysBaseMenus").Delete(&auth) + } + if len(auth.DataAuthorityId) > 0 { + if err = tx.Model(auth).Association("DataAuthorityId").Delete(auth.DataAuthorityId); err != nil { + return err + } + } + + if err = tx.Delete(&system.SysUserAuthority{}, "sys_authority_authority_id = ?", auth.AuthorityId).Error; err != nil { + return err + } + if err = tx.Where("authority_id = ?", auth.AuthorityId).Delete(&[]system.SysAuthorityBtn{}).Error; err != nil { + return err + } + + authorityId := strconv.Itoa(int(auth.AuthorityId)) + + if err = CasbinServiceApp.RemoveFilteredPolicy(tx, authorityId); err != nil { + return err + } + + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetAuthorityInfoList(authorityID uint) (list []system.SysAuthority, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + if err != nil { + return nil, err + } + var authorities []system.SysAuthority + db := global.GVA_DB.Model(&system.SysAuthority{}) + if global.GVA_CONFIG.System.UseStrictAuth { + // 当开启了严格树形结构后 + if *authority.ParentId == 0 { + // 只有顶级角色可以修改自己的权限和以下权限 + err = db.Preload("DataAuthorityId").Where("authority_id = ?", authorityID).Find(&authorities).Error + } else { + // 非顶级角色只能修改以下权限 + err = db.Debug().Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + } + } else { + err = db.Preload("DataAuthorityId").Where("parent_id = ?", "0").Find(&authorities).Error + } + + for k := range authorities { + err = authorityService.findChildrenAuthority(&authorities[k]) + } + return authorities, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: list interface{}, total int64, err error + +func (authorityService *AuthorityService) GetStructAuthorityList(authorityID uint) (list []uint, err error) { + var auth system.SysAuthority + _ = global.GVA_DB.First(&auth, "authority_id = ?", authorityID).Error + var authorities []system.SysAuthority + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authorityID).Find(&authorities).Error + if len(authorities) > 0 { + for k := range authorities { + list = append(list, authorities[k].AuthorityId) + childrenList, err := authorityService.GetStructAuthorityList(authorities[k].AuthorityId) + if err == nil { + list = append(list, childrenList...) + } + } + } + if *auth.ParentId == 0 { + list = append(list, authorityID) + } + return list, err +} + +func (authorityService *AuthorityService) CheckAuthorityIDAuth(authorityID, targetID uint) (err error) { + if !global.GVA_CONFIG.System.UseStrictAuth { + return nil + } + authIDS, err := authorityService.GetStructAuthorityList(authorityID) + if err != nil { + return err + } + hasAuth := false + for _, v := range authIDS { + if v == targetID { + hasAuth = true + break + } + } + if !hasAuth { + return errors.New("您提交的角色ID不合法") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetAuthorityInfo +//@description: 获取所有角色信息 +//@param: auth model.SysAuthority +//@return: sa system.SysAuthority, err error + +func (authorityService *AuthorityService) GetAuthorityInfo(auth system.SysAuthority) (sa system.SysAuthority, err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("authority_id = ?", auth.AuthorityId).First(&sa).Error + return sa, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetDataAuthority +//@description: 设置角色资源权限 +//@param: auth model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetDataAuthority(adminAuthorityID uint, auth system.SysAuthority) error { + var checkIDs []uint + checkIDs = append(checkIDs, auth.AuthorityId) + for i := range auth.DataAuthorityId { + checkIDs = append(checkIDs, auth.DataAuthorityId[i].AuthorityId) + } + + for i := range checkIDs { + err := authorityService.CheckAuthorityIDAuth(adminAuthorityID, checkIDs[i]) + if err != nil { + return err + } + } + + var s system.SysAuthority + global.GVA_DB.Preload("DataAuthorityId").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("DataAuthorityId").Replace(&auth.DataAuthorityId) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetMenuAuthority +//@description: 菜单与角色绑定 +//@param: auth *model.SysAuthority +//@return: error + +func (authorityService *AuthorityService) SetMenuAuthority(auth *system.SysAuthority) error { + var s system.SysAuthority + global.GVA_DB.Preload("SysBaseMenus").First(&s, "authority_id = ?", auth.AuthorityId) + err := global.GVA_DB.Model(&s).Association("SysBaseMenus").Replace(&auth.SysBaseMenus) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: findChildrenAuthority +//@description: 查询子角色 +//@param: authority *model.SysAuthority +//@return: err error + +func (authorityService *AuthorityService) findChildrenAuthority(authority *system.SysAuthority) (err error) { + err = global.GVA_DB.Preload("DataAuthorityId").Where("parent_id = ?", authority.AuthorityId).Find(&authority.Children).Error + if len(authority.Children) > 0 { + for k := range authority.Children { + err = authorityService.findChildrenAuthority(&authority.Children[k]) + } + } + return err +} + +func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) (parentID uint, err error) { + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error + if err != nil { + return + } + return *authority.ParentId, nil +} diff --git a/server/service/system/sys_authority_btn.go b/server/service/system/sys_authority_btn.go new file mode 100644 index 0000000..f470d84 --- /dev/null +++ b/server/service/system/sys_authority_btn.go @@ -0,0 +1,61 @@ +package system + +import ( + "errors" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/model/system/response" + "gorm.io/gorm" +) + +type AuthorityBtnService struct{} + +var AuthorityBtnServiceApp = new(AuthorityBtnService) + +func (a *AuthorityBtnService) GetAuthorityBtn(req request.SysAuthorityBtnReq) (res response.SysAuthorityBtnRes, err error) { + var authorityBtn []system.SysAuthorityBtn + err = global.GVA_DB.Find(&authorityBtn, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return + } + var selected []uint + for _, v := range authorityBtn { + selected = append(selected, v.SysBaseMenuBtnID) + } + res.Selected = selected + return res, err +} + +func (a *AuthorityBtnService) SetAuthorityBtn(req request.SysAuthorityBtnReq) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var authorityBtn []system.SysAuthorityBtn + err = tx.Delete(&[]system.SysAuthorityBtn{}, "authority_id = ? and sys_menu_id = ?", req.AuthorityId, req.MenuID).Error + if err != nil { + return err + } + for _, v := range req.Selected { + authorityBtn = append(authorityBtn, system.SysAuthorityBtn{ + AuthorityId: req.AuthorityId, + SysMenuID: req.MenuID, + SysBaseMenuBtnID: v, + }) + } + if len(authorityBtn) > 0 { + err = tx.Create(&authorityBtn).Error + } + if err != nil { + return err + } + return err + }) +} + +func (a *AuthorityBtnService) CanRemoveAuthorityBtn(ID string) (err error) { + fErr := global.GVA_DB.First(&system.SysAuthorityBtn{}, "sys_base_menu_btn_id = ?", ID).Error + if errors.Is(fErr, gorm.ErrRecordNotFound) { + return nil + } + return errors.New("此按钮正在被使用无法删除") +} diff --git a/server/service/system/sys_auto_code_interface.go b/server/service/system/sys_auto_code_interface.go new file mode 100644 index 0000000..c8fc0f6 --- /dev/null +++ b/server/service/system/sys_auto_code_interface.go @@ -0,0 +1,55 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +type AutoCodeService struct{} + +type Database interface { + GetDB(businessDB string) (data []response.Db, err error) + GetTables(businessDB string, dbName string) (data []response.Table, err error) + GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) +} + +func (autoCodeService *AutoCodeService) Database(businessDB string) Database { + + if businessDB == "" { + switch global.GVA_CONFIG.System.DbType { + case "mysql": + return AutoCodeMysql + case "pgsql": + return AutoCodePgsql + case "mssql": + return AutoCodeMssql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } else { + for _, info := range global.GVA_CONFIG.DBList { + if info.AliasName == businessDB { + switch info.Type { + case "mysql": + return AutoCodeMysql + case "mssql": + return AutoCodeMssql + case "pgsql": + return AutoCodePgsql + case "oracle": + return AutoCodeOracle + case "sqlite": + return AutoCodeSqlite + default: + return AutoCodeMysql + } + } + } + return AutoCodeMysql + } + +} diff --git a/server/service/system/sys_auto_code_mssql.go b/server/service/system/sys_auto_code_mssql.go new file mode 100644 index 0000000..a9cc92c --- /dev/null +++ b/server/service/system/sys_auto_code_mssql.go @@ -0,0 +1,84 @@ +package system + +import ( + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +var AutoCodeMssql = new(autoCodeMssql) + +type autoCodeMssql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "select name AS 'database' from sys.databases;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + + sql := fmt.Sprintf(`select name as 'table_name' from %s.DBO.sysobjects where xtype='U'`, dbName) + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMssql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf(` +SELECT + sc.name AS column_name, + st.name AS data_type, + sc.max_length AS data_type_long, + CASE + WHEN pk.object_id IS NOT NULL THEN 1 + ELSE 0 + END AS primary_key, + sc.column_id +FROM + %s.sys.columns sc +JOIN + sys.types st ON sc.user_type_id=st.user_type_id +LEFT JOIN + %s.sys.objects so ON so.name='%s' AND so.type='U' +LEFT JOIN + %s.sys.indexes si ON si.object_id = so.object_id AND si.is_primary_key = 1 +LEFT JOIN + %s.sys.index_columns sic ON sic.object_id = si.object_id AND sic.index_id = si.index_id AND sic.column_id = sc.column_id +LEFT JOIN + %s.sys.key_constraints pk ON pk.object_id = si.object_id +WHERE + st.is_user_defined=0 AND sc.object_id = so.object_id +ORDER BY + sc.column_id +`, dbName, dbName, tableName, dbName, dbName, dbName) + + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} diff --git a/server/service/system/sys_auto_code_mysql.go b/server/service/system/sys_auto_code_mysql.go new file mode 100644 index 0000000..01af927 --- /dev/null +++ b/server/service/system/sys_auto_code_mysql.go @@ -0,0 +1,83 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +var AutoCodeMysql = new(autoCodeMysql) + +type autoCodeMysql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "SELECT SCHEMA_NAME AS `database` FROM INFORMATION_SCHEMA.SCHEMATA;" + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_schema = ?` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + } + + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeMysql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + c.COLUMN_NAME column_name, + c.DATA_TYPE data_type, + CASE c.DATA_TYPE + WHEN 'longtext' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'varchar' THEN c.CHARACTER_MAXIMUM_LENGTH + WHEN 'double' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'decimal' THEN CONCAT_WS(',', c.NUMERIC_PRECISION, c.NUMERIC_SCALE) + WHEN 'int' THEN c.NUMERIC_PRECISION + WHEN 'bigint' THEN c.NUMERIC_PRECISION + ELSE '' + END AS data_type_long, + c.COLUMN_COMMENT column_comment, + CASE WHEN kcu.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS primary_key, + c.ORDINAL_POSITION +FROM + INFORMATION_SCHEMA.COLUMNS c +LEFT JOIN + INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu +ON + c.TABLE_SCHEMA = kcu.TABLE_SCHEMA + AND c.TABLE_NAME = kcu.TABLE_NAME + AND c.COLUMN_NAME = kcu.COLUMN_NAME + AND kcu.CONSTRAINT_NAME = 'PRIMARY' +WHERE + c.TABLE_NAME = ? + AND c.TABLE_SCHEMA = ? +ORDER BY + c.ORDINAL_POSITION;` + if businessDB == "" { + err = global.GVA_DB.Raw(sql, tableName, dbName).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + } + + return entities, err +} diff --git a/server/service/system/sys_auto_code_oracle.go b/server/service/system/sys_auto_code_oracle.go new file mode 100644 index 0000000..5f1c055 --- /dev/null +++ b/server/service/system/sys_auto_code_oracle.go @@ -0,0 +1,72 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +var AutoCodeOracle = new(autoCodeOracle) + +type autoCodeOracle struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT lower(username) AS "database" FROM all_users` + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select lower(table_name) as "table_name" from all_tables where lower(owner) = ?` + + err = global.GVA_DBList[businessDB].Raw(sql, dbName).Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodeOracle) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := ` + SELECT + lower(a.COLUMN_NAME) as "column_name", + (CASE WHEN a.DATA_TYPE = 'NUMBER' AND a.DATA_SCALE=0 THEN 'int' else lower(a.DATA_TYPE) end) as "data_type", + (CASE WHEN a.DATA_TYPE = 'NUMBER' THEN a.DATA_PRECISION else a.DATA_LENGTH end) as "data_type_long", + b.COMMENTS as "column_comment", + (CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END) as "primary_key", + a.COLUMN_ID +FROM + all_tab_columns a +JOIN + all_col_comments b ON a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME +LEFT JOIN + ( + SELECT + acc.OWNER, + acc.TABLE_NAME, + acc.COLUMN_NAME + FROM + all_cons_columns acc + JOIN + all_constraints ac ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME + WHERE + ac.CONSTRAINT_TYPE = 'P' + ) pk ON a.OWNER = pk.OWNER AND a.TABLE_NAME = pk.TABLE_NAME AND a.COLUMN_NAME = pk.COLUMN_NAME +WHERE + lower(a.table_name) = ? + AND lower(a.OWNER) = ? +ORDER BY + a.COLUMN_ID +` + + err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error + return entities, err +} diff --git a/server/service/system/sys_auto_code_pgsql.go b/server/service/system/sys_auto_code_pgsql.go new file mode 100644 index 0000000..c605f08 --- /dev/null +++ b/server/service/system/sys_auto_code_pgsql.go @@ -0,0 +1,135 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +var AutoCodePgsql = new(autoCodePgsql) + +type autoCodePgsql struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := `SELECT datname as database FROM pg_database WHERE datistemplate = false` + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&entities).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&entities).Error + } + + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `select table_name as table_name from information_schema.tables where table_catalog = ? and table_schema = ?` + + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, "public").Scan(&entities).Error + return entities, err +} + +// GetColumn 获取指定数据库和指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodePgsql) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + // todo 数据获取不全, 待完善sql + sql := ` +SELECT + psc.COLUMN_NAME AS COLUMN_NAME, + psc.udt_name AS data_type, + CASE + psc.udt_name + WHEN 'text' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'varchar' THEN + concat_ws ( '', '', psc.CHARACTER_MAXIMUM_LENGTH ) + WHEN 'smallint' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'decimal' THEN + concat_ws ( ',', psc.NUMERIC_PRECISION, psc.NUMERIC_SCALE ) + WHEN 'integer' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int4' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'int8' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'bigint' THEN + concat_ws ( '', '', psc.NUMERIC_PRECISION ) + WHEN 'timestamp' THEN + concat_ws ( '', '', psc.datetime_precision ) + ELSE '' + END AS data_type_long, + ( + SELECT + pd.description + FROM + pg_description pd + WHERE + (pd.objoid,pd.objsubid) in ( + SELECT pa.attrelid,pa.attnum + FROM + pg_attribute pa + WHERE pa.attrelid = ( SELECT oid FROM pg_class pc WHERE + pc.relname = psc.table_name + ) + and attname = psc.column_name + ) + ) AS column_comment, + ( + SELECT + COUNT(*) + FROM + pg_constraint + WHERE + contype = 'p' + AND conrelid = ( + SELECT + oid + FROM + pg_class + WHERE + relname = psc.table_name + ) + AND conkey::int[] @> ARRAY[( + SELECT + attnum::integer + FROM + pg_attribute + WHERE + attrelid = conrelid + AND attname = psc.column_name + )] + ) > 0 AS primary_key, + psc.ordinal_position +FROM + INFORMATION_SCHEMA.COLUMNS psc +WHERE + table_catalog = ? + AND table_schema = 'public' + AND TABLE_NAME = ? +ORDER BY + psc.ordinal_position; +` + var entities []response.Column + //sql = strings.ReplaceAll(sql, "@table_catalog", dbName) + //sql = strings.ReplaceAll(sql, "@table_name", tableName) + db := global.GVA_DB + if businessDB != "" { + db = global.GVA_DBList[businessDB] + } + + err = db.Raw(sql, dbName, tableName).Scan(&entities).Error + return entities, err +} diff --git a/server/service/system/sys_auto_code_sqlite.go b/server/service/system/sys_auto_code_sqlite.go new file mode 100644 index 0000000..3987230 --- /dev/null +++ b/server/service/system/sys_auto_code_sqlite.go @@ -0,0 +1,85 @@ +package system + +import ( + "fmt" + "path/filepath" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/response" +) + +var AutoCodeSqlite = new(autoCodeSqlite) + +type autoCodeSqlite struct{} + +// GetDB 获取数据库的所有数据库名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetDB(businessDB string) (data []response.Db, err error) { + var entities []response.Db + sql := "PRAGMA database_list;" + var databaseList []struct { + File string `gorm:"column:file"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&databaseList).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&databaseList).Error + } + for _, database := range databaseList { + if database.File != "" { + fileName := filepath.Base(database.File) + fileExt := filepath.Ext(fileName) + fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt) + + entities = append(entities, response.Db{fileNameWithoutExt}) + } + } + // entities = append(entities, response.Db{global.GVA_CONFIG.Sqlite.Dbname}) + return entities, err +} + +// GetTables 获取数据库的所有表名 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetTables(businessDB string, dbName string) (data []response.Table, err error) { + var entities []response.Table + sql := `SELECT name FROM sqlite_master WHERE type='table'` + tabelNames := []string{} + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Find(&tabelNames).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Find(&tabelNames).Error + } + for _, tabelName := range tabelNames { + entities = append(entities, response.Table{tabelName}) + } + return entities, err +} + +// GetColumn 获取指定数据表的所有字段名,类型值等 +// Author [piexlmax](https://github.com/piexlmax) +// Author [SliverHorn](https://github.com/SliverHorn) +func (a *autoCodeSqlite) GetColumn(businessDB string, tableName string, dbName string) (data []response.Column, err error) { + var entities []response.Column + sql := fmt.Sprintf("PRAGMA table_info(%s);", tableName) + var columnInfos []struct { + Name string `gorm:"column:name"` + Type string `gorm:"column:type"` + Pk int `gorm:"column:pk"` + } + if businessDB == "" { + err = global.GVA_DB.Raw(sql).Scan(&columnInfos).Error + } else { + err = global.GVA_DBList[businessDB].Raw(sql).Scan(&columnInfos).Error + } + for _, columnInfo := range columnInfos { + entities = append(entities, response.Column{ + ColumnName: columnInfo.Name, + DataType: columnInfo.Type, + PrimaryKey: columnInfo.Pk == 1, + }) + } + return entities, err +} diff --git a/server/service/system/sys_base_menu.go b/server/service/system/sys_base_menu.go new file mode 100644 index 0000000..034a766 --- /dev/null +++ b/server/service/system/sys_base_menu.go @@ -0,0 +1,147 @@ +package system + +import ( + "errors" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "gorm.io/gorm" +) + +type BaseMenuService struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteBaseMenu +//@description: 删除基础路由 +//@param: id float64 +//@return: err error + +var BaseMenuServiceApp = new(BaseMenuService) + +func (baseMenuService *BaseMenuService) DeleteBaseMenu(id int) (err error) { + err = global.GVA_DB.First(&system.SysBaseMenu{}, "parent_id = ?", id).Error + if err == nil { + return errors.New("此菜单存在子菜单不可删除") + } + var menu system.SysBaseMenu + err = global.GVA_DB.First(&menu, id).Error + if err != nil { + return errors.New("记录不存在") + } + err = global.GVA_DB.First(&system.SysAuthority{}, "default_router = ?", menu.Name).Error + if err == nil { + return errors.New("此菜单有角色正在作为首页,不可删除") + } + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + + err = tx.Delete(&system.SysBaseMenu{}, "id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + err = tx.Delete(&system.SysAuthorityBtn{}, "sys_menu_id = ?", id).Error + if err != nil { + return err + } + + err = tx.Delete(&system.SysAuthorityMenu{}, "sys_base_menu_id = ?", id).Error + if err != nil { + return err + } + return nil + }) + +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateBaseMenu +//@description: 更新路由 +//@param: menu model.SysBaseMenu +//@return: err error + +func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) (err error) { + var oldMenu system.SysBaseMenu + upDateMap := make(map[string]interface{}) + upDateMap["keep_alive"] = menu.KeepAlive + upDateMap["transition_type"] = menu.TransitionType + upDateMap["close_tab"] = menu.CloseTab + upDateMap["default_menu"] = menu.DefaultMenu + upDateMap["parent_id"] = menu.ParentId + upDateMap["path"] = menu.Path + upDateMap["name"] = menu.Name + upDateMap["hidden"] = menu.Hidden + upDateMap["component"] = menu.Component + upDateMap["title"] = menu.Title + upDateMap["active_name"] = menu.ActiveName + upDateMap["icon"] = menu.Icon + upDateMap["sort"] = menu.Sort + + err = global.GVA_DB.Transaction(func(tx *gorm.DB) error { + tx.Where("id = ?", menu.ID).Find(&oldMenu) + if oldMenu.Name != menu.Name { + if !errors.Is(tx.Where("id <> ? AND name = ?", menu.ID, menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + global.GVA_LOG.Debug("存在相同name修改失败") + return errors.New("存在相同name修改失败") + } + } + txErr := tx.Unscoped().Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + txErr = tx.Unscoped().Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", menu.ID).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + if len(menu.Parameters) > 0 { + for k := range menu.Parameters { + menu.Parameters[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.Parameters).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + if len(menu.MenuBtn) > 0 { + for k := range menu.MenuBtn { + menu.MenuBtn[k].SysBaseMenuID = menu.ID + } + txErr = tx.Create(&menu.MenuBtn).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + } + + txErr = tx.Model(&oldMenu).Updates(upDateMap).Error + if txErr != nil { + global.GVA_LOG.Debug(txErr.Error()) + return txErr + } + return nil + }) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuById +//@description: 返回当前选中menu +//@param: id float64 +//@return: menu system.SysBaseMenu, err error + +func (baseMenuService *BaseMenuService) GetBaseMenuById(id int) (menu system.SysBaseMenu, err error) { + err = global.GVA_DB.Preload("MenuBtn").Preload("Parameters").Where("id = ?", id).First(&menu).Error + return +} diff --git a/server/service/system/sys_casbin.go b/server/service/system/sys_casbin.go new file mode 100644 index 0000000..89148ae --- /dev/null +++ b/server/service/system/sys_casbin.go @@ -0,0 +1,173 @@ +package system + +import ( + "errors" + "strconv" + + "gorm.io/gorm" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + gormadapter "github.com/casbin/gorm-adapter/v3" + _ "github.com/go-sql-driver/mysql" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbin +//@description: 更新casbin权限 +//@param: authorityId string, casbinInfos []request.CasbinInfo +//@return: error + +type CasbinService struct{} + +var CasbinServiceApp = new(CasbinService) + +func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID uint, casbinInfos []request.CasbinInfo) error { + + err := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, AuthorityID) + if err != nil { + return err + } + + if global.GVA_CONFIG.System.UseStrictAuth { + apis, e := ApiServiceApp.GetAllApis(adminAuthorityID) + if e != nil { + return e + } + + for i := range casbinInfos { + hasApi := false + for j := range apis { + if apis[j].Path == casbinInfos[i].Path && apis[j].Method == casbinInfos[i].Method { + hasApi = true + break + } + } + if !hasApi { + return errors.New("存在api不在权限列表中") + } + } + } + + authorityId := strconv.Itoa(int(AuthorityID)) + casbinService.ClearCasbin(0, authorityId) + rules := [][]string{} + //做权限去重处理 + deduplicateMap := make(map[string]bool) + for _, v := range casbinInfos { + key := authorityId + v.Path + v.Method + if _, ok := deduplicateMap[key]; !ok { + deduplicateMap[key] = true + rules = append(rules, []string{authorityId, v.Path, v.Method}) + } + } + if len(rules) == 0 { + return nil + } // 设置空权限无需调用 AddPolicies 方法 + e := utils.GetCasbin() + success, _ := e.AddPolicies(rules) + if !success { + return errors.New("存在相同api,添加失败,请联系管理员") + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateCasbinApi +//@description: API更新随动 +//@param: oldPath string, newPath string, oldMethod string, newMethod string +//@return: error + +func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath string, oldMethod string, newMethod string) error { + err := global.GVA_DB.Model(&gormadapter.CasbinRule{}).Where("v1 = ? AND v2 = ?", oldPath, oldMethod).Updates(map[string]interface{}{ + "v1": newPath, + "v2": newMethod, + }).Error + if err != nil { + return err + } + + e := utils.GetCasbin() + return e.LoadPolicy() +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetPolicyPathByAuthorityId +//@description: 获取权限列表 +//@param: authorityId string +//@return: pathMaps []request.CasbinInfo + +func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { + e := utils.GetCasbin() + authorityId := strconv.Itoa(int(AuthorityID)) + list, _ := e.GetFilteredPolicy(0, authorityId) + for _, v := range list { + pathMaps = append(pathMaps, request.CasbinInfo{ + Path: v[1], + Method: v[2], + }) + } + return pathMaps +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ClearCasbin +//@description: 清除匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { + e := utils.GetCasbin() + success, _ := e.RemoveFilteredPolicy(v, p...) + return success +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveFilteredPolicy +//@description: 使用数据库方法清理筛选的politicy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string +//@return: error + +func (casbinService *CasbinService) RemoveFilteredPolicy(db *gorm.DB, authorityId string) error { + return db.Delete(&gormadapter.CasbinRule{}, "v0 = ?", authorityId).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SyncPolicy +//@description: 同步目前数据库的policy 此方法需要调用FreshCasbin方法才可以在系统中即刻生效 +//@param: db *gorm.DB, authorityId string, rules [][]string +//@return: error + +func (casbinService *CasbinService) SyncPolicy(db *gorm.DB, authorityId string, rules [][]string) error { + err := casbinService.RemoveFilteredPolicy(db, authorityId) + if err != nil { + return err + } + return casbinService.AddPolicies(db, rules) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddPolicies +//@description: 添加匹配的权限 +//@param: v int, p ...string +//@return: bool + +func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) error { + var casbinRules []gormadapter.CasbinRule + for i := range rules { + casbinRules = append(casbinRules, gormadapter.CasbinRule{ + Ptype: "p", + V0: rules[i][0], + V1: rules[i][1], + V2: rules[i][2], + }) + } + return db.Create(&casbinRules).Error +} + +func (casbinService *CasbinService) FreshCasbin() (err error) { + e := utils.GetCasbin() + err = e.LoadPolicy() + return err +} diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go new file mode 100644 index 0000000..c52d174 --- /dev/null +++ b/server/service/system/sys_dictionary.go @@ -0,0 +1,297 @@ +package system + +import ( + "encoding/json" + "errors" + + "git.echol.cn/loser/st/server/model/system/request" + "github.com/gin-gonic/gin" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionary +//@description: 创建字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +type DictionaryService struct{} + +var DictionaryServiceApp = new(DictionaryService) + +func (dictionaryService *DictionaryService) CreateSysDictionary(sysDictionary system.SysDictionary) (err error) { + if (!errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound)) { + return errors.New("存在相同的type,不允许创建") + } + err = global.GVA_DB.Create(&sysDictionary).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionary +//@description: 删除字典数据 +//@param: sysDictionary model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) DeleteSysDictionary(sysDictionary system.SysDictionary) (err error) { + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).Preload("SysDictionaryDetails").First(&sysDictionary).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("请不要搞事") + } + if err != nil { + return err + } + err = global.GVA_DB.Delete(&sysDictionary).Error + if err != nil { + return err + } + + if sysDictionary.SysDictionaryDetails != nil { + return global.GVA_DB.Where("sys_dictionary_id=?", sysDictionary.ID).Delete(sysDictionary.SysDictionaryDetails).Error + } + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionary +//@description: 更新字典数据 +//@param: sysDictionary *model.SysDictionary +//@return: err error + +func (dictionaryService *DictionaryService) UpdateSysDictionary(sysDictionary *system.SysDictionary) (err error) { + var dict system.SysDictionary + sysDictionaryMap := map[string]interface{}{ + "Name": sysDictionary.Name, + "Type": sysDictionary.Type, + "Status": sysDictionary.Status, + "Desc": sysDictionary.Desc, + "ParentID": sysDictionary.ParentID, + } + err = global.GVA_DB.Where("id = ?", sysDictionary.ID).First(&dict).Error + if err != nil { + global.GVA_LOG.Debug(err.Error()) + return errors.New("查询字典数据失败") + } + if dict.Type != sysDictionary.Type { + if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", sysDictionary.Type).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同的type,不允许创建") + } + } + + // 检查是否会形成循环引用 + if sysDictionary.ParentID != nil && *sysDictionary.ParentID != 0 { + if err := dictionaryService.checkCircularReference(sysDictionary.ID, *sysDictionary.ParentID); err != nil { + return err + } + } + + err = global.GVA_DB.Model(&dict).Updates(sysDictionaryMap).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionary +//@description: 根据id或者type获取字典单条数据 +//@param: Type string, Id uint +//@return: err error, sysDictionary model.SysDictionary + +func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uint, status *bool) (sysDictionary system.SysDictionary, err error) { + var flag = false + if status == nil { + flag = true + } else { + flag = *status + } + err = global.GVA_DB.Where("(type = ? OR id = ?) and status = ?", Type, Id, flag).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + return db.Where("status = ? and deleted_at is null", true).Order("sort") + }).First(&sysDictionary).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetSysDictionaryInfoList +//@description: 分页获取字典列表 +//@param: info request.SysDictionarySearch +//@return: err error, list interface{}, total int64 + +func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) { + var sysDictionarys []system.SysDictionary + query := global.GVA_DB.WithContext(c) + if req.Name != "" { + query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%") + } + // 预加载子字典 + query = query.Preload("Children") + err = query.Find(&sysDictionarys).Error + return sysDictionarys, err +} + +// checkCircularReference 检查是否会形成循环引用 +func (dictionaryService *DictionaryService) checkCircularReference(currentID uint, parentID uint) error { + if currentID == parentID { + return errors.New("不能将字典设置为自己的父级") + } + + // 递归检查父级链条 + var parent system.SysDictionary + err := global.GVA_DB.Where("id = ?", parentID).First(&parent).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // 父级不存在,允许设置 + } + return err + } + + // 如果父级还有父级,继续检查 + if parent.ParentID != nil && *parent.ParentID != 0 { + return dictionaryService.checkCircularReference(currentID, *parent.ParentID) + } + + return nil +} + +//@author: [pixelMax] +//@function: ExportSysDictionary +//@description: 导出字典JSON(包含字典详情) +//@param: id uint +//@return: exportData map[string]interface{}, err error + +func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) { + var dictionary system.SysDictionary + // 查询字典及其所有详情 + err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + return db.Order("sort") + }).First(&dictionary).Error + if err != nil { + return nil, err + } + + // 清空字典详情中的ID、创建时间、更新时间等字段 + var cleanDetails []map[string]interface{} + for _, detail := range dictionary.SysDictionaryDetails { + cleanDetail := map[string]interface{}{ + "label": detail.Label, + "value": detail.Value, + "extend": detail.Extend, + "status": detail.Status, + "sort": detail.Sort, + "level": detail.Level, + "path": detail.Path, + } + cleanDetails = append(cleanDetails, cleanDetail) + } + + // 构造导出数据 + exportData = map[string]interface{}{ + "name": dictionary.Name, + "type": dictionary.Type, + "status": dictionary.Status, + "desc": dictionary.Desc, + "sysDictionaryDetails": cleanDetails, + } + + return exportData, nil +} + +//@author: [pixelMax] +//@function: ImportSysDictionary +//@description: 导入字典JSON(包含字典详情) +//@param: jsonStr string +//@return: err error + +func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error { + // 直接解析到 SysDictionary 结构体 + var importData system.SysDictionary + if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil { + return errors.New("JSON 格式错误: " + err.Error()) + } + + // 验证必填字段 + if importData.Name == "" { + return errors.New("字典名称不能为空") + } + if importData.Type == "" { + return errors.New("字典类型不能为空") + } + + // 检查字典类型是否已存在 + if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同的type,不允许导入") + } + + // 创建字典(清空导入数据的ID和时间戳) + dictionary := system.SysDictionary{ + Name: importData.Name, + Type: importData.Type, + Status: importData.Status, + Desc: importData.Desc, + } + + // 开启事务 + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 创建字典 + if err := tx.Create(&dictionary).Error; err != nil { + return err + } + + // 处理字典详情 + if len(importData.SysDictionaryDetails) > 0 { + // 创建一个映射来跟踪旧ID到新ID的对应关系 + idMap := make(map[uint]uint) + + // 第一遍:创建所有详情记录 + for _, detail := range importData.SysDictionaryDetails { + // 验证必填字段 + if detail.Label == "" || detail.Value == "" { + continue + } + + // 记录旧ID + oldID := detail.ID + + // 创建新的详情记录(ID会被GORM自动设置) + detailRecord := system.SysDictionaryDetail{ + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + Level: detail.Level, + Path: detail.Path, + SysDictionaryID: int(dictionary.ID), + } + + // 创建详情记录 + if err := tx.Create(&detailRecord).Error; err != nil { + return err + } + + // 记录旧ID到新ID的映射 + if oldID > 0 { + idMap[oldID] = detailRecord.ID + } + } + + // 第二遍:更新parent_id关系 + for _, detail := range importData.SysDictionaryDetails { + if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 { + if newID, exists := idMap[detail.ID]; exists { + if newParentID, parentExists := idMap[*detail.ParentID]; parentExists { + if err := tx.Model(&system.SysDictionaryDetail{}). + Where("id = ?", newID). + Update("parent_id", newParentID).Error; err != nil { + return err + } + } + } + } + } + } + + return nil + }) +} diff --git a/server/service/system/sys_dictionary_detail.go b/server/service/system/sys_dictionary_detail.go new file mode 100644 index 0000000..cc5942a --- /dev/null +++ b/server/service/system/sys_dictionary_detail.go @@ -0,0 +1,392 @@ +package system + +import ( + "fmt" + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateSysDictionaryDetail +//@description: 创建字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +type DictionaryDetailService struct{} + +var DictionaryDetailServiceApp = new(DictionaryDetailService) + +func (dictionaryDetailService *DictionaryDetailService) CreateSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + + err = global.GVA_DB.Create(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysDictionaryDetail +//@description: 删除字典详情数据 +//@param: sysDictionaryDetail model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) DeleteSysDictionaryDetail(sysDictionaryDetail system.SysDictionaryDetail) (err error) { + // 检查是否有子项 + var count int64 + err = global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("parent_id = ?", sysDictionaryDetail.ID).Count(&count).Error + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("该字典详情下还有子项,无法删除") + } + + err = global.GVA_DB.Delete(&sysDictionaryDetail).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: UpdateSysDictionaryDetail +//@description: 更新字典详情数据 +//@param: sysDictionaryDetail *model.SysDictionaryDetail +//@return: err error + +func (dictionaryDetailService *DictionaryDetailService) UpdateSysDictionaryDetail(sysDictionaryDetail *system.SysDictionaryDetail) (err error) { + // 如果更新了父级ID,需要重新计算层级和路径 + if sysDictionaryDetail.ParentID != nil { + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, *sysDictionaryDetail.ParentID).Error + if err != nil { + return err + } + + // 检查循环引用 + if dictionaryDetailService.checkCircularReference(sysDictionaryDetail.ID, *sysDictionaryDetail.ParentID) { + return fmt.Errorf("不能将字典详情设置为自己或其子项的父级") + } + + sysDictionaryDetail.Level = parent.Level + 1 + if parent.Path == "" { + sysDictionaryDetail.Path = strconv.Itoa(int(parent.ID)) + } else { + sysDictionaryDetail.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + } else { + sysDictionaryDetail.Level = 0 + sysDictionaryDetail.Path = "" + } + + err = global.GVA_DB.Save(sysDictionaryDetail).Error + if err != nil { + return err + } + + // 更新所有子项的层级和路径 + return dictionaryDetailService.updateChildrenLevelAndPath(sysDictionaryDetail.ID) +} + +// checkCircularReference 检查循环引用 +func (dictionaryDetailService *DictionaryDetailService) checkCircularReference(id, parentID uint) bool { + if id == parentID { + return true + } + + var parent system.SysDictionaryDetail + err := global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return false + } + + if parent.ParentID == nil { + return false + } + + return dictionaryDetailService.checkCircularReference(id, *parent.ParentID) +} + +// updateChildrenLevelAndPath 更新子项的层级和路径 +func (dictionaryDetailService *DictionaryDetailService) updateChildrenLevelAndPath(parentID uint) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", parentID).Find(&children).Error + if err != nil { + return err + } + + var parent system.SysDictionaryDetail + err = global.GVA_DB.First(&parent, parentID).Error + if err != nil { + return err + } + + for _, child := range children { + child.Level = parent.Level + 1 + if parent.Path == "" { + child.Path = strconv.Itoa(int(parent.ID)) + } else { + child.Path = parent.Path + "," + strconv.Itoa(int(parent.ID)) + } + + err = global.GVA_DB.Save(&child).Error + if err != nil { + return err + } + + // 递归更新子项的子项 + err = dictionaryDetailService.updateChildrenLevelAndPath(child.ID) + if err != nil { + return err + } + } + + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetail +//@description: 根据id获取字典详情单条数据 +//@param: id uint +//@return: sysDictionaryDetail system.SysDictionaryDetail, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetail(id uint) (sysDictionaryDetail system.SysDictionaryDetail, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysDictionaryDetail).Error + return +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysDictionaryDetailInfoList +//@description: 分页获取字典详情列表 +//@param: info request.SysDictionaryDetailSearch +//@return: list interface{}, total int64, err error + +func (dictionaryDetailService *DictionaryDetailService) GetSysDictionaryDetailInfoList(info request.SysDictionaryDetailSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}) + var sysDictionaryDetails []system.SysDictionaryDetail + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Label != "" { + db = db.Where("label LIKE ?", "%"+info.Label+"%") + } + if info.Value != "" { + db = db.Where("value = ?", info.Value) + } + if info.Status != nil { + db = db.Where("status = ?", info.Status) + } + if info.SysDictionaryID != 0 { + db = db.Where("sys_dictionary_id = ?", info.SysDictionaryID) + } + if info.ParentID != nil { + db = db.Where("parent_id = ?", *info.ParentID) + } + if info.Level != nil { + db = db.Where("level = ?", *info.Level) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("sort").Order("id").Find(&sysDictionaryDetails).Error + return sysDictionaryDetails, total, err +} + +// 按照字典id获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + err = global.GVA_DB.Find(&sysDictionaryDetails, "sys_dictionary_id = ?", dictionaryID).Error + return sysDictionaryDetails, err +} + +// GetDictionaryTreeList 获取字典树形结构列表 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeList(dictionaryID uint) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + // 只获取顶级项目(parent_id为空) + err = global.GVA_DB.Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + +// loadChildren 递归加载子项 +func (dictionaryDetailService *DictionaryDetailService) loadChildren(detail *system.SysDictionaryDetail) error { + var children []system.SysDictionaryDetail + err := global.GVA_DB.Where("parent_id = ?", detail.ID).Order("sort").Find(&children).Error + if err != nil { + return err + } + + for i := range children { + // 设置disabled属性:当status为false时,disabled为true + if children[i].Status != nil { + children[i].Disabled = !*children[i].Status + } else { + children[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&children[i]) + if err != nil { + return err + } + } + + detail.Children = children + return nil +} + +// GetDictionaryDetailsByParent 根据父级ID获取字典详情 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryDetailsByParent(req request.GetDictionaryDetailsByParentRequest) (list []system.SysDictionaryDetail, err error) { + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Where("sys_dictionary_id = ?", req.SysDictionaryID) + + if req.ParentID != nil { + db = db.Where("parent_id = ?", *req.ParentID) + } else { + db = db.Where("parent_id IS NULL") + } + + err = db.Order("sort").Find(&list).Error + if err != nil { + return list, err + } + + // 设置disabled属性 + for i := range list { + if list[i].Status != nil { + list[i].Disabled = !*list[i].Status + } else { + list[i].Disabled = false // 默认不禁用 + } + } + + // 如果需要包含子级数据,使用递归方式加载所有层级的子项 + if req.IncludeChildren { + for i := range list { + err = dictionaryDetailService.loadChildren(&list[i]) + if err != nil { + return list, err + } + } + } + + return list, err +} + +// 按照字典type获取字典全部内容的方法 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.Find(&sysDictionaryDetails, "type = ?", t).Error + return sysDictionaryDetails, err +} + +// GetDictionaryTreeListByType 根据字典类型获取树形结构 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryTreeListByType(t string) (list []system.SysDictionaryDetail, err error) { + var sysDictionaryDetails []system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}). + Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id"). + Where("sys_dictionaries.type = ? AND sys_dictionary_details.parent_id IS NULL", t). + Order("sys_dictionary_details.sort") + + err = db.Find(&sysDictionaryDetails).Error + if err != nil { + return nil, err + } + + // 递归加载子项并设置disabled属性 + for i := range sysDictionaryDetails { + // 设置disabled属性:当status为false时,disabled为true + if sysDictionaryDetails[i].Status != nil { + sysDictionaryDetails[i].Disabled = !*sysDictionaryDetails[i].Status + } else { + sysDictionaryDetails[i].Disabled = false // 默认不禁用 + } + + err = dictionaryDetailService.loadChildren(&sysDictionaryDetails[i]) + if err != nil { + return nil, err + } + } + + return sysDictionaryDetails, nil +} + +// 按照字典id+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByValue(dictionaryID uint, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetail system.SysDictionaryDetail + err = global.GVA_DB.First(&sysDictionaryDetail, "sys_dictionary_id = ? and value = ?", dictionaryID, value).Error + return sysDictionaryDetail, err +} + +// 按照字典type+字典内容value获取单条字典内容 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryInfoByTypeValue(t string, value string) (detail system.SysDictionaryDetail, err error) { + var sysDictionaryDetails system.SysDictionaryDetail + db := global.GVA_DB.Model(&system.SysDictionaryDetail{}).Joins("JOIN sys_dictionaries ON sys_dictionaries.id = sys_dictionary_details.sys_dictionary_id") + err = db.First(&sysDictionaryDetails, "sys_dictionaries.type = ? and sys_dictionary_details.value = ?", t, value).Error + return sysDictionaryDetails, err +} + +// GetDictionaryPath 获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPath(id uint) (path []system.SysDictionaryDetail, err error) { + var detail system.SysDictionaryDetail + err = global.GVA_DB.First(&detail, id).Error + if err != nil { + return nil, err + } + + path = append(path, detail) + + if detail.ParentID != nil { + parentPath, err := dictionaryDetailService.GetDictionaryPath(*detail.ParentID) + if err != nil { + return nil, err + } + path = append(parentPath, path...) + } + + return path, nil +} + +// GetDictionaryPathByValue 根据值获取字典详情的完整路径 +func (dictionaryDetailService *DictionaryDetailService) GetDictionaryPathByValue(dictionaryID uint, value string) (path []system.SysDictionaryDetail, err error) { + detail, err := dictionaryDetailService.GetDictionaryInfoByValue(dictionaryID, value) + if err != nil { + return nil, err + } + + return dictionaryDetailService.GetDictionaryPath(detail.ID) +} diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go new file mode 100644 index 0000000..efaf95b --- /dev/null +++ b/server/service/system/sys_error.go @@ -0,0 +1,127 @@ +package system + +import ( + "context" + "fmt" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common" + "git.echol.cn/loser/st/server/model/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" +) + +type SysErrorService struct{} + +// CreateSysError 创建错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) { + if global.GVA_DB == nil { + return nil + } + err = global.GVA_DB.Create(sysError).Error + return err +} + +// DeleteSysError 删除错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) DeleteSysError(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysError{}, "id = ?", ID).Error + return err +} + +// DeleteSysErrorByIds 批量删除错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) DeleteSysErrorByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]system.SysError{}, "id in ?", IDs).Error + return err +} + +// UpdateSysError 更新错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) UpdateSysError(ctx context.Context, sysError system.SysError) (err error) { + err = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", sysError.ID).Updates(&sysError).Error + return err +} + +// GetSysError 根据ID获取错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysError(ctx context.Context, ID string) (sysError system.SysError, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysError).Error + return +} + +// GetSysErrorInfoList 分页获取错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, info systemReq.SysErrorSearch) (list []system.SysError, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysError{}).Order("created_at desc") + var sysErrors []system.SysError + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.Form != nil && *info.Form != "" { + db = db.Where("form = ?", *info.Form) + } + if info.Info != nil && *info.Info != "" { + db = db.Where("info LIKE ?", "%"+*info.Info+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysErrors).Error + return sysErrors, total, err +} + +// GetSysErrorSolution 异步处理错误 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) { + // 立即更新为处理中 + err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error + if err != nil { + return err + } + + // 异步协程在一分钟后更新为处理完成 + go func(id string) { + // 查询当前错误信息用于生成方案 + var se system.SysError + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error + + // 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案 + var form, info string + if se.Form != nil { + form = *se.Form + } + if se.Info != nil { + info = *se.Info + } + + llmReq := common.JSONMap{ + "mode": "solution", + "info": info, + "form": form, + } + + // 调用服务层 LLMAuto,忽略错误但尽量写入方案 + var solution string + if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil { + solution = fmt.Sprintf("%v", data.(map[string]interface{})["text"]) + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error + } else { + // 即使生成失败也标记为完成,避免任务卡住 + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error + } + }(ID) + + return nil +} diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go new file mode 100644 index 0000000..a39cc5e --- /dev/null +++ b/server/service/system/sys_export_template.go @@ -0,0 +1,724 @@ +package system + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "mime/multipart" + "net/url" + "strconv" + "strings" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/request" + "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/xuri/excelize/v2" + "gorm.io/gorm" +) + +type SysExportTemplateService struct { +} + +var SysExportTemplateServiceApp = new(SysExportTemplateService) + +// CreateSysExportTemplate 创建导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) CreateSysExportTemplate(sysExportTemplate *system.SysExportTemplate) (err error) { + err = global.GVA_DB.Create(sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplate 删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + err = global.GVA_DB.Delete(&sysExportTemplate).Error + return err +} + +// DeleteSysExportTemplateByIds 批量删除导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) DeleteSysExportTemplateByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysExportTemplate{}, "id in ?", ids.Ids).Error + return err +} + +// UpdateSysExportTemplate 更新导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) UpdateSysExportTemplate(sysExportTemplate system.SysExportTemplate) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + conditions := sysExportTemplate.Conditions + e := tx.Delete(&[]system.Condition{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.Conditions = nil + + joins := sysExportTemplate.JoinTemplate + e = tx.Delete(&[]system.JoinTemplate{}, "template_id = ?", sysExportTemplate.TemplateID).Error + if e != nil { + return e + } + sysExportTemplate.JoinTemplate = nil + + e = tx.Updates(&sysExportTemplate).Error + if e != nil { + return e + } + if len(conditions) > 0 { + for i := range conditions { + conditions[i].ID = 0 + } + e = tx.Create(&conditions).Error + } + if len(joins) > 0 { + for i := range joins { + joins[i].ID = 0 + } + e = tx.Create(&joins).Error + } + return e + }) +} + +// GetSysExportTemplate 根据id获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplate(id uint) (sysExportTemplate system.SysExportTemplate, err error) { + err = global.GVA_DB.Where("id = ?", id).Preload("JoinTemplate").Preload("Conditions").First(&sysExportTemplate).Error + return +} + +// GetSysExportTemplateInfoList 分页获取导出模板记录 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateInfoList(info systemReq.SysExportTemplateSearch) (list []system.SysExportTemplate, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysExportTemplate{}) + var sysExportTemplates []system.SysExportTemplate + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.TableName != "" { + db = db.Where("table_name = ?", info.TableName) + } + if info.TemplateID != "" { + db = db.Where("template_id = ?", info.TemplateID) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysExportTemplates).Error + return sysExportTemplates, total, err +} + +// ExportExcel 导出Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) { + var params = values.Get("params") + paramsValues, err := url.ParseQuery(params) + if err != nil { + return nil, "", fmt.Errorf("解析 params 参数失败: %v", err) + } + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return nil, "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + selects := strings.Join(selectKeyFmt, ", ") + var tableMap []map[string]interface{} + db := global.GVA_DB + if template.DBName != "" { + db = global.MustGetGlobalDBByDBName(template.DBName) + } + + // 如果有自定义SQL,则优先使用自定义SQL + if template.SQL != "" { + // 将 url.Values 转换为 map[string]interface{} 以支持 GORM 的命名参数 + sqlParams := make(map[string]interface{}) + for k, v := range paramsValues { + if len(v) > 0 { + sqlParams[k] = v[0] + } + } + + // 执行原生 SQL,支持 @key 命名参数 + err = db.Raw(template.SQL, sqlParams).Scan(&tableMap).Error + if err != nil { + return nil, "", err + } + } else { + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON) + } + } + + db = db.Select(selects).Table(template.TableName) + + filterDeleted := false + + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + + if filterDeleted { + // 自动过滤主表的软删除 + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + + // 过滤关联表的软删除(如果有) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + // 检查关联表是否有deleted_at字段 + hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table) + if hasDeletedAt { + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator) + value := paramsValues.Get(condition.From) + + if condition.Operator == "IN" || condition.Operator == "NOT IN" { + sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) + } + + if condition.Operator == "BETWEEN" { + sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column) + startValue := paramsValues.Get("start" + condition.From) + endValue := paramsValues.Get("end" + condition.From) + if startValue != "" && endValue != "" { + db = db.Where(sql, startValue, endValue) + } + continue + } + + if value != "" { + if condition.Operator == "LIKE" { + value = "%" + value + "%" + } + db = db.Where(sql, value) + } + } + } + // 通过参数传入limit + limit := paramsValues.Get("limit") + if limit != "" { + l, e := strconv.Atoi(limit) + if e == nil { + db = db.Limit(l) + } + } + // 模板的默认limit + if limit == "" && template.Limit != nil && *template.Limit != 0 { + db = db.Limit(*template.Limit) + } + + // 通过参数传入offset + offset := paramsValues.Get("offset") + if offset != "" { + o, e := strconv.Atoi(offset) + if e == nil { + db = db.Offset(o) + } + } + + // 获取当前表的所有字段 + table := template.TableName + orderColumns, err := db.Migrator().ColumnTypes(table) + if err != nil { + return nil, "", err + } + + // 创建一个 map 来存储字段名 + fields := make(map[string]bool) + + for _, column := range orderColumns { + fields[column.Name()] = true + } + + // 通过参数传入order + order := paramsValues.Get("order") + + if order == "" && template.Order != "" { + // 如果没有order入参,这里会使用模板的默认排序 + order = template.Order + } + + if order != "" { + checkOrderArr := strings.Split(order, " ") + orderStr := "" + // 检查请求的排序字段是否在字段列表中 + if _, ok := fields[checkOrderArr[0]]; !ok { + return nil, "", fmt.Errorf("order by %s is not in the fields", order) + } + orderStr = checkOrderArr[0] + if len(checkOrderArr) > 1 { + if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" { + return nil, "", fmt.Errorf("order by %s is not secure", order) + } + orderStr = orderStr + " " + checkOrderArr[1] + } + db = db.Order(orderStr) + } + + err = db.Debug().Find(&tableMap).Error + if err != nil { + return nil, "", err + } + } + + var rows [][]string + rows = append(rows, tableTitle) + for _, exTable := range tableMap { + var row []string + for _, column := range columns { + column = strings.ReplaceAll(column, "\"", "") + column = strings.ReplaceAll(column, "`", "") + if len(template.JoinTemplate) > 0 { + columnAs := strings.Split(column, " as ") + if len(columnAs) > 1 { + column = strings.TrimSpace(strings.Split(column, " as ")[1]) + } else { + columnArr := strings.Split(column, ".") + if len(columnArr) > 1 { + column = strings.Split(column, ".")[1] + } + } + } + // 需要对时间类型特殊处理 + if t, ok := exTable[column].(time.Time); ok { + row = append(row, t.Format("2006-01-02 15:04:05")) + } else { + row = append(row, fmt.Sprintf("%v", exTable[column])) + } + } + rows = append(rows, row) + } + for i, row := range rows { + for j, colCell := range row { + cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1) + + var sErr error + if v, err := strconv.ParseFloat(colCell, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else { + sErr = f.SetCellValue("Sheet1", cell, colCell) + } + + if sErr != nil { + return nil, "", sErr + } + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + return file, template.Name, nil +} + +// PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串) +// Author [piexlmax](https://github.com/piexlmax) & [trae-ai] +func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) { + // 解析 params(与导出逻辑保持一致) + var params = values.Get("params") + paramsValues, _ := url.ParseQuery(params) + + // 加载模板 + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return "", err + } + + // 解析模板列 + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return "", err + } + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + } + selects := strings.Join(selectKeyFmt, ", ") + + // 生成 FROM 与 JOIN 片段 + var sb strings.Builder + sb.WriteString("SELECT ") + sb.WriteString(selects) + sb.WriteString(" FROM ") + sb.WriteString(template.TableName) + + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + sb.WriteString(" ") + sb.WriteString(join.JOINS) + sb.WriteString(" ") + sb.WriteString(join.Table) + sb.WriteString(" ON ") + sb.WriteString(join.ON) + } + } + + // WHERE 条件 + var wheres []string + + // 软删除过滤 + filterDeleted := false + if paramsValues != nil { + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + } + if filterDeleted { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + if sysExportTemplateService.hasDeletedAtColumn(join.Table) { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + + // 模板条件(保留与 ExportExcel 同步的解析规则) + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + op := strings.ToUpper(strings.TrimSpace(condition.Operator)) + col := strings.TrimSpace(condition.Column) + + // 预览优先展示传入值,没有则展示占位符 + val := "" + if paramsValues != nil { + val = paramsValues.Get(condition.From) + } + + switch op { + case "BETWEEN": + startValue := "" + endValue := "" + if paramsValues != nil { + startValue = paramsValues.Get("start" + condition.From) + endValue = paramsValues.Get("end" + condition.From) + } + if startValue != "" && endValue != "" { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue)) + } else { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From)) + } + case "IN", "NOT IN": + if val != "" { + // 逗号分隔值做简单展示 + parts := strings.Split(val, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','"))) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From)) + } + case "LIKE": + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From)) + } + default: + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From)) + } + } + } + } + + if len(wheres) > 0 { + sb.WriteString(" WHERE ") + sb.WriteString(strings.Join(wheres, " AND ")) + } + + // 排序 + order := "" + if paramsValues != nil { + order = paramsValues.Get("order") + } + if order == "" && template.Order != "" { + order = template.Order + } + if order != "" { + sb.WriteString(" ORDER BY ") + sb.WriteString(order) + } + + // limit/offset(如果传入或默认值为0,则不生成) + limitStr := "" + offsetStr := "" + if paramsValues != nil { + limitStr = paramsValues.Get("limit") + offsetStr = paramsValues.Get("offset") + } + + // 处理模板默认limit(仅当非0时) + if limitStr == "" && template.Limit != nil && *template.Limit != 0 { + limitStr = strconv.Itoa(*template.Limit) + } + + // 解析为数值,用于判断是否生成 + limitInt := 0 + offsetInt := 0 + if limitStr != "" { + if v, e := strconv.Atoi(limitStr); e == nil { + limitInt = v + } + } + if offsetStr != "" { + if v, e := strconv.Atoi(offsetStr); e == nil { + offsetInt = v + } + } + + if limitInt > 0 { + sb.WriteString(" LIMIT ") + sb.WriteString(strconv.Itoa(limitInt)) + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } else { + // 当limit未设置或为0时,仅当offset>0才生成OFFSET + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } + + return sb.String(), nil +} + +// ExportTemplate 导出Excel模板 +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return nil, "", err + } + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + // Create a new sheet. + index, err := f.NewSheet("Sheet1") + if err != nil { + fmt.Println(err) + return + } + var templateInfoMap = make(map[string]string) + + columns, err := utils.GetJSONKeys(template.TemplateInfo) + + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return nil, "", err + } + var tableTitle []string + for _, key := range columns { + tableTitle = append(tableTitle, templateInfoMap[key]) + } + + for i := range tableTitle { + fErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(i+1), 1), tableTitle[i]) + if fErr != nil { + return nil, "", fErr + } + } + f.SetActiveSheet(index) + file, err = f.WriteToBuffer() + if err != nil { + return nil, "", err + } + + return file, template.Name, nil +} + +// 辅助函数:检查表是否有deleted_at列 +func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool { + var count int64 + global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count) + return count > 0 +} + +// ImportExcel 导入Excel +// Author [piexlmax](https://github.com/piexlmax) +func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) { + var template system.SysExportTemplate + err = global.GVA_DB.First(&template, "template_id = ?", templateID).Error + if err != nil { + return err + } + + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + f, err := excelize.OpenReader(src) + if err != nil { + return err + } + + rows, err := f.GetRows("Sheet1") + if err != nil { + return err + } + if len(rows) < 2 { + return errors.New("Excel data is not enough.\nIt should contain title row and data") + } + + var templateInfoMap = make(map[string]string) + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return err + } + + db := global.GVA_DB + if template.DBName != "" { + db = global.MustGetGlobalDBByDBName(template.DBName) + } + + items, err := sysExportTemplateService.parseExcelToMap(rows, templateInfoMap) + if err != nil { + return err + } + + return db.Transaction(func(tx *gorm.DB) error { + if template.ImportSQL != "" { + return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items) + } + return sysExportTemplateService.importByGORM(tx, template.TableName, items) + }) +} + +func (sysExportTemplateService *SysExportTemplateService) parseExcelToMap(rows [][]string, templateInfoMap map[string]string) ([]map[string]interface{}, error) { + var titleKeyMap = make(map[string]string) + for key, title := range templateInfoMap { + titleKeyMap[title] = key + } + + excelTitle := rows[0] + for i, str := range excelTitle { + excelTitle[i] = strings.TrimSpace(str) + } + values := rows[1:] + items := make([]map[string]interface{}, 0, len(values)) + for _, row := range values { + var item = make(map[string]interface{}) + for ii, value := range row { + if ii >= len(excelTitle) { + continue + } + if _, ok := titleKeyMap[excelTitle[ii]]; !ok { + continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过 + } + key := titleKeyMap[excelTitle[ii]] + item[key] = value + } + items = append(items, item) + } + return items, nil +} + +func (sysExportTemplateService *SysExportTemplateService) importBySQL(tx *gorm.DB, sql string, items []map[string]interface{}) error { + for _, item := range items { + if err := tx.Exec(sql, item).Error; err != nil { + return err + } + } + return nil +} + +func (sysExportTemplateService *SysExportTemplateService) importByGORM(tx *gorm.DB, tableName string, items []map[string]interface{}) error { + needCreated := tx.Migrator().HasColumn(tableName, "created_at") + needUpdated := tx.Migrator().HasColumn(tableName, "updated_at") + + for _, item := range items { + if item["created_at"] == nil && needCreated { + item["created_at"] = time.Now() + } + if item["updated_at"] == nil && needUpdated { + item["updated_at"] = time.Now() + } + } + return tx.Table(tableName).CreateInBatches(&items, 1000).Error +} + +func getColumnName(n int) string { + columnName := "" + for n > 0 { + n-- + columnName = string(rune('A'+n%26)) + columnName + n /= 26 + } + return columnName +} diff --git a/server/service/system/sys_initdb.go b/server/service/system/sys_initdb.go new file mode 100644 index 0000000..4cf07b0 --- /dev/null +++ b/server/service/system/sys_initdb.go @@ -0,0 +1,190 @@ +package system + +import ( + "context" + "database/sql" + "errors" + "fmt" + "sort" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "gorm.io/gorm" +) + +const ( + Mysql = "mysql" + Pgsql = "pgsql" + Sqlite = "sqlite" + Mssql = "mssql" + InitSuccess = "\n[%v] --> 初始数据成功!\n" + InitDataExist = "\n[%v] --> %v 的初始数据已存在!\n" + InitDataFailed = "\n[%v] --> %v 初始数据失败! \nerr: %+v\n" + InitDataSuccess = "\n[%v] --> %v 初始数据成功!\n" +) + +const ( + InitOrderSystem = 10 + InitOrderInternal = 1000 + InitOrderExternal = 100000 +) + +var ( + ErrMissingDBContext = errors.New("missing db in context") + ErrMissingDependentContext = errors.New("missing dependent value in context") + ErrDBTypeMismatch = errors.New("db type mismatch") +) + +// SubInitializer 提供 source/*/init() 使用的接口,每个 initializer 完成一个初始化过程 +type SubInitializer interface { + InitializerName() string // 不一定代表单独一个表,所以改成了更宽泛的语义 + MigrateTable(ctx context.Context) (next context.Context, err error) + InitializeData(ctx context.Context) (next context.Context, err error) + TableCreated(ctx context.Context) bool + DataInserted(ctx context.Context) bool +} + +// TypedDBInitHandler 执行传入的 initializer +type TypedDBInitHandler interface { + EnsureDB(ctx context.Context, conf *request.InitDB) (context.Context, error) // 建库,失败属于 fatal error,因此让它 panic + WriteConfig(ctx context.Context) error // 回写配置 + InitTables(ctx context.Context, inits initSlice) error // 建表 handler + InitData(ctx context.Context, inits initSlice) error // 建数据 handler +} + +// orderedInitializer 组合一个顺序字段,以供排序 +type orderedInitializer struct { + order int + SubInitializer +} + +// initSlice 供 initializer 排序依赖时使用 +type initSlice []*orderedInitializer + +var ( + initializers initSlice + cache map[string]*orderedInitializer +) + +// RegisterInit 注册要执行的初始化过程,会在 InitDB() 时调用 +func RegisterInit(order int, i SubInitializer) { + if initializers == nil { + initializers = initSlice{} + } + if cache == nil { + cache = map[string]*orderedInitializer{} + } + name := i.InitializerName() + if _, existed := cache[name]; existed { + panic(fmt.Sprintf("Name conflict on %s", name)) + } + ni := orderedInitializer{order, i} + initializers = append(initializers, &ni) + cache[name] = &ni +} + +/* ---- * service * ---- */ + +type InitDBService struct{} + +// InitDB 创建数据库并初始化 总入口 +func (initDBService *InitDBService) InitDB(conf request.InitDB) (err error) { + ctx := context.TODO() + ctx = context.WithValue(ctx, "adminPassword", conf.AdminPassword) + if len(initializers) == 0 { + return errors.New("无可用初始化过程,请检查初始化是否已执行完成") + } + sort.Sort(&initializers) // 保证有依赖的 initializer 排在后面执行 + // Note: 若 initializer 只有单一依赖,可以写为 B=A+1, C=A+1; 由于 BC 之间没有依赖关系,所以谁先谁后并不影响初始化 + // 若存在多个依赖,可以写为 C=A+B, D=A+B+C, E=A+1; + // C必然>A|B,因此在AB之后执行,D必然>A|B|C,因此在ABC后执行,而E只依赖A,顺序与CD无关,因此E与CD哪个先执行并不影响 + var initHandler TypedDBInitHandler + switch conf.DBType { + case "mysql": + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + case "pgsql": + initHandler = NewPgsqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "pgsql") + case "sqlite": + initHandler = NewSqliteInitHandler() + ctx = context.WithValue(ctx, "dbtype", "sqlite") + case "mssql": + initHandler = NewMssqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mssql") + default: + initHandler = NewMysqlInitHandler() + ctx = context.WithValue(ctx, "dbtype", "mysql") + } + ctx, err = initHandler.EnsureDB(ctx, &conf) + if err != nil { + return err + } + + db := ctx.Value("db").(*gorm.DB) + global.GVA_DB = db + + if err = initHandler.InitTables(ctx, initializers); err != nil { + return err + } + if err = initHandler.InitData(ctx, initializers); err != nil { + return err + } + + if err = initHandler.WriteConfig(ctx); err != nil { + return err + } + initializers = initSlice{} + cache = map[string]*orderedInitializer{} + return nil +} + +// createDatabase 创建数据库( EnsureDB() 中调用 ) +func createDatabase(dsn string, driver string, createSql string) error { + db, err := sql.Open(driver, dsn) + if err != nil { + return err + } + defer func(db *sql.DB) { + err = db.Close() + if err != nil { + fmt.Println(err) + } + }(db) + if err = db.Ping(); err != nil { + return err + } + _, err = db.Exec(createSql) + return err +} + +// createTables 创建表(默认 dbInitHandler.initTables 行为) +func createTables(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.TableCreated(next) { + continue + } + if n, err := init.MigrateTable(next); err != nil { + return err + } else { + next = n + } + } + return nil +} + +/* -- sortable interface -- */ + +func (a initSlice) Len() int { + return len(a) +} + +func (a initSlice) Less(i, j int) bool { + return a[i].order < a[j].order +} + +func (a initSlice) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} diff --git a/server/service/system/sys_initdb_mssql.go b/server/service/system/sys_initdb_mssql.go new file mode 100644 index 0000000..b5b75ca --- /dev/null +++ b/server/service/system/sys_initdb_mssql.go @@ -0,0 +1,93 @@ +package system + +import ( + "context" + "errors" + "path/filepath" + + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" + "github.com/google/uuid" + "github.com/gookit/color" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" +) + +type MssqlInitHandler struct{} + +func NewMssqlInitHandler() *MssqlInitHandler { + return &MssqlInitHandler{} +} + +// WriteConfig mssql回写配置 +func (h MssqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mssql) + if !ok { + return errors.New("mssql config invalid") + } + global.GVA_CONFIG.System.DbType = "mssql" + global.GVA_CONFIG.Mssql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mssql +func (h MssqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mssql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMssqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MssqlEmptyDsn() + + mssqlConfig := sqlserver.Config{ + DSN: dsn, // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + } + + var db *gorm.DB + + if db, err = gorm.Open(sqlserver.New(mssqlConfig), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return nil, err + } + + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MssqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MssqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mssql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mssql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mssql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mssql) + return nil +} diff --git a/server/service/system/sys_initdb_mysql.go b/server/service/system/sys_initdb_mysql.go new file mode 100644 index 0000000..b620324 --- /dev/null +++ b/server/service/system/sys_initdb_mysql.go @@ -0,0 +1,97 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "git.echol.cn/loser/st/server/config" + "github.com/gookit/color" + + "git.echol.cn/loser/st/server/utils" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "github.com/google/uuid" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type MysqlInitHandler struct{} + +func NewMysqlInitHandler() *MysqlInitHandler { + return &MysqlInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h MysqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Mysql) + if !ok { + return errors.New("mysql config invalid") + } + global.GVA_CONFIG.System.DbType = "mysql" + global.GVA_CONFIG.Mysql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 mysql +func (h MysqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "mysql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToMysqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.MysqlEmptyDsn() + createSql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;", c.Dbname) + if err = createDatabase(dsn, "mysql", createSql); err != nil { + return nil, err + } // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(mysql.New(mysql.Config{ + DSN: c.Dsn(), // DSN data source name + DefaultStringSize: 191, // string 类型字段的默认长度 + SkipInitializeWithVersion: true, // 根据版本自动配置 + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h MysqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h MysqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Mysql, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Mysql, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Mysql, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Mysql) + return nil +} diff --git a/server/service/system/sys_initdb_pgsql.go b/server/service/system/sys_initdb_pgsql.go new file mode 100644 index 0000000..d7039a4 --- /dev/null +++ b/server/service/system/sys_initdb_pgsql.go @@ -0,0 +1,101 @@ +package system + +import ( + "context" + "errors" + "fmt" + "path/filepath" + + "git.echol.cn/loser/st/server/config" + "github.com/gookit/color" + + "git.echol.cn/loser/st/server/utils" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "github.com/google/uuid" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type PgsqlInitHandler struct{} + +func NewPgsqlInitHandler() *PgsqlInitHandler { + return &PgsqlInitHandler{} +} + +// WriteConfig pgsql 回写配置 +func (h PgsqlInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Pgsql) + if !ok { + return errors.New("postgresql config invalid") + } + global.GVA_CONFIG.System.DbType = "pgsql" + global.GVA_CONFIG.Pgsql = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 pg +func (h PgsqlInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "pgsql" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToPgsqlConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.PgsqlEmptyDsn() + var createSql string + if conf.Template != "" { + createSql = fmt.Sprintf("CREATE DATABASE %s WITH TEMPLATE %s;", c.Dbname, conf.Template) + } else { + createSql = fmt.Sprintf("CREATE DATABASE %s;", c.Dbname) + } + if err = createDatabase(dsn, "pgx", createSql); err != nil { + return nil, err + } // 创建数据库 + + var db *gorm.DB + if db, err = gorm.Open(postgres.New(postgres.Config{ + DSN: c.Dsn(), // DSN data source name + PreferSimpleProtocol: false, + }), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h PgsqlInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h PgsqlInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for i := 0; i < len(inits); i++ { + if inits[i].DataInserted(next) { + color.Info.Printf(InitDataExist, Pgsql, inits[i].InitializerName()) + continue + } + if n, err := inits[i].InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Pgsql, inits[i].InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Pgsql, inits[i].InitializerName()) + } + } + color.Info.Printf(InitSuccess, Pgsql) + return nil +} diff --git a/server/service/system/sys_initdb_sqlite.go b/server/service/system/sys_initdb_sqlite.go new file mode 100644 index 0000000..b70c907 --- /dev/null +++ b/server/service/system/sys_initdb_sqlite.go @@ -0,0 +1,89 @@ +package system + +import ( + "context" + "errors" + "path/filepath" + + "github.com/glebarez/sqlite" + "github.com/google/uuid" + "github.com/gookit/color" + "gorm.io/gorm" + + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + "git.echol.cn/loser/st/server/utils" +) + +type SqliteInitHandler struct{} + +func NewSqliteInitHandler() *SqliteInitHandler { + return &SqliteInitHandler{} +} + +// WriteConfig mysql回写配置 +func (h SqliteInitHandler) WriteConfig(ctx context.Context) error { + c, ok := ctx.Value("config").(config.Sqlite) + if !ok { + return errors.New("sqlite config invalid") + } + global.GVA_CONFIG.System.DbType = "sqlite" + global.GVA_CONFIG.Sqlite = c + global.GVA_CONFIG.JWT.SigningKey = uuid.New().String() + cs := utils.StructToMap(global.GVA_CONFIG) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + global.GVA_ACTIVE_DBNAME = &c.Dbname + return global.GVA_VP.WriteConfig() +} + +// EnsureDB 创建数据库并初始化 sqlite +func (h SqliteInitHandler) EnsureDB(ctx context.Context, conf *request.InitDB) (next context.Context, err error) { + if s, ok := ctx.Value("dbtype").(string); !ok || s != "sqlite" { + return ctx, ErrDBTypeMismatch + } + + c := conf.ToSqliteConfig() + next = context.WithValue(ctx, "config", c) + if c.Dbname == "" { + return ctx, nil + } // 如果没有数据库名, 则跳出初始化数据 + + dsn := conf.SqliteEmptyDsn() + + var db *gorm.DB + if db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }); err != nil { + return ctx, err + } + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") + next = context.WithValue(next, "db", db) + return next, err +} + +func (h SqliteInitHandler) InitTables(ctx context.Context, inits initSlice) error { + return createTables(ctx, inits) +} + +func (h SqliteInitHandler) InitData(ctx context.Context, inits initSlice) error { + next, cancel := context.WithCancel(ctx) + defer cancel() + for _, init := range inits { + if init.DataInserted(next) { + color.Info.Printf(InitDataExist, Sqlite, init.InitializerName()) + continue + } + if n, err := init.InitializeData(next); err != nil { + color.Info.Printf(InitDataFailed, Sqlite, init.InitializerName(), err) + return err + } else { + next = n + color.Info.Printf(InitDataSuccess, Sqlite, init.InitializerName()) + } + } + color.Info.Printf(InitSuccess, Sqlite) + return nil +} diff --git a/server/service/system/sys_login_log.go b/server/service/system/sys_login_log.go new file mode 100644 index 0000000..c71e5ff --- /dev/null +++ b/server/service/system/sys_login_log.go @@ -0,0 +1,53 @@ +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/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" +) + +type LoginLogService struct{} + +var LoginLogServiceApp = new(LoginLogService) + +func (loginLogService *LoginLogService) CreateLoginLog(loginLog system.SysLoginLog) (err error) { + err = global.GVA_DB.Create(&loginLog).Error + return err +} + +func (loginLogService *LoginLogService) DeleteLoginLogByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysLoginLog{}, "id in (?)", ids.Ids).Error + return err +} + +func (loginLogService *LoginLogService) DeleteLoginLog(loginLog system.SysLoginLog) (err error) { + err = global.GVA_DB.Delete(&loginLog).Error + return err +} + +func (loginLogService *LoginLogService) GetLoginLog(id uint) (loginLog system.SysLoginLog, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&loginLog).Error + return +} + +func (loginLogService *LoginLogService) GetLoginLogInfoList(info systemReq.SysLoginLogSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysLoginLog{}) + var loginLogs []system.SysLoginLog + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Username != "" { + db = db.Where("username LIKE ?", "%"+info.Username+"%") + } + if info.Status != false { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("id desc").Preload("User").Find(&loginLogs).Error + return loginLogs, total, err +} diff --git a/server/service/system/sys_menu.go b/server/service/system/sys_menu.go new file mode 100644 index 0000000..7c84af5 --- /dev/null +++ b/server/service/system/sys_menu.go @@ -0,0 +1,332 @@ +package system + +import ( + "errors" + "strconv" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/common/request" + "git.echol.cn/loser/st/server/model/system" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getMenuTreeMap +//@description: 获取路由总树map +//@param: authorityId string +//@return: treeMap map[string][]system.SysMenu, err error + +type MenuService struct{} + +var MenuServiceApp = new(MenuService) + +func (menuService *MenuService) getMenuTreeMap(authorityId uint) (treeMap map[uint][]system.SysMenu, err error) { + var allMenus []system.SysMenu + var baseMenu []system.SysBaseMenu + var btns []system.SysAuthorityBtn + treeMap = make(map[uint][]system.SysMenu) + + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?)", MenuIds).Order("sort").Preload("Parameters").Find(&baseMenu).Error + if err != nil { + return + } + + for i := range baseMenu { + allMenus = append(allMenus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: authorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + + err = global.GVA_DB.Where("authority_id = ?", authorityId).Preload("SysBaseMenuBtn").Find(&btns).Error + if err != nil { + return + } + var btnMap = make(map[uint]map[string]uint) + for _, v := range btns { + if btnMap[v.SysMenuID] == nil { + btnMap[v.SysMenuID] = make(map[string]uint) + } + btnMap[v.SysMenuID][v.SysBaseMenuBtn.Name] = authorityId + } + for _, v := range allMenus { + v.Btns = btnMap[v.SysBaseMenu.ID] + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuTree +//@description: 获取动态菜单树 +//@param: authorityId string +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuTree(authorityId uint) (menus []system.SysMenu, err error) { + menuTree, err := menuService.getMenuTreeMap(authorityId) + menus = menuTree[0] + for i := 0; i < len(menus); i++ { + err = menuService.getChildrenList(&menus[i], menuTree) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getChildrenList +//@description: 获取子菜单 +//@param: menu *model.SysMenu, treeMap map[string][]model.SysMenu +//@return: err error + +func (menuService *MenuService) getChildrenList(menu *system.SysMenu, treeMap map[uint][]system.SysMenu) (err error) { + menu.Children = treeMap[menu.MenuId] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetInfoList +//@description: 获取路由分页 +//@return: list interface{}, total int64,err error + +func (menuService *MenuService) GetInfoList(authorityID uint) (list interface{}, err error) { + var menuList []system.SysBaseMenu + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menuList = treeMap[0] + for i := 0; i < len(menuList); i++ { + err = menuService.getBaseChildrenList(&menuList[i], treeMap) + } + return menuList, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseChildrenList +//@description: 获取菜单的子菜单 +//@param: menu *model.SysBaseMenu, treeMap map[string][]model.SysBaseMenu +//@return: err error + +func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, treeMap map[uint][]system.SysBaseMenu) (err error) { + menu.Children = treeMap[menu.ID] + for i := 0; i < len(menu.Children); i++ { + err = menuService.getBaseChildrenList(&menu.Children[i], treeMap) + } + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddBaseMenu +//@description: 添加基础路由 +//@param: menu model.SysBaseMenu +//@return: error + +func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 检查name是否重复 + if !errors.Is(tx.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复name,请修改name") + } + + if menu.ParentId != 0 { + // 检查父菜单是否存在 + var parentMenu system.SysBaseMenu + if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("父菜单不存在") + } + return err + } + + // 检查父菜单下现有子菜单数量 + var existingChildrenCount int64 + err := tx.Model(&system.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error + if err != nil { + return err + } + + // 如果父菜单原本是叶子菜单(没有子菜单),现在要变成枝干菜单,需要清空其权限分配 + if existingChildrenCount == 0 { + // 检查父菜单是否被其他角色设置为首页 + var defaultRouterCount int64 + err := tx.Model(&system.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error + if err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限") + } + + // 清空父菜单的所有权限分配 + err = tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&system.SysAuthorityMenu{}).Error + if err != nil { + return err + } + } + } + + // 创建菜单 + return tx.Create(&menu).Error + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: getBaseMenuTreeMap +//@description: 获取路由总树map +//@return: treeMap map[string][]system.SysBaseMenu, err error + +func (menuService *MenuService) getBaseMenuTreeMap(authorityID uint) (treeMap map[uint][]system.SysBaseMenu, err error) { + parentAuthorityID, err := AuthorityServiceApp.GetParentAuthorityID(authorityID) + if err != nil { + return nil, err + } + + var allMenus []system.SysBaseMenu + treeMap = make(map[uint][]system.SysBaseMenu) + db := global.GVA_DB.Order("sort").Preload("MenuBtn").Preload("Parameters") + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && parentAuthorityID != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityID).Find(&authorityMenus).Error + if err != nil { + return nil, err + } + var menuIds []string + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + db = db.Where("id in (?)", menuIds) + } + + err = db.Find(&allMenus).Error + for _, v := range allMenus { + treeMap[v.ParentId] = append(treeMap[v.ParentId], v) + } + return treeMap, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetBaseMenuTree +//@description: 获取基础路由树 +//@return: menus []system.SysBaseMenu, err error + +func (menuService *MenuService) GetBaseMenuTree(authorityID uint) (menus []system.SysBaseMenu, err error) { + treeMap, err := menuService.getBaseMenuTreeMap(authorityID) + menus = treeMap[0] + for i := 0; i < len(menus); i++ { + err = menuService.getBaseChildrenList(&menus[i], treeMap) + } + return menus, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: AddMenuAuthority +//@description: 为角色增加menu树 +//@param: menus []model.SysBaseMenu, authorityId string +//@return: err error + +func (menuService *MenuService) AddMenuAuthority(menus []system.SysBaseMenu, adminAuthorityID, authorityId uint) (err error) { + var auth system.SysAuthority + auth.AuthorityId = authorityId + auth.SysBaseMenus = menus + + err = AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, authorityId) + if err != nil { + return err + } + + var authority system.SysAuthority + _ = global.GVA_DB.First(&authority, "authority_id = ?", adminAuthorityID).Error + var menuIds []string + + // 当开启了严格的树角色并且父角色不为0时需要进行菜单筛选 + if global.GVA_CONFIG.System.UseStrictAuth && *authority.ParentId != 0 { + var authorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", adminAuthorityID).Find(&authorityMenus).Error + if err != nil { + return err + } + for i := range authorityMenus { + menuIds = append(menuIds, authorityMenus[i].MenuId) + } + + for i := range menus { + hasMenu := false + for j := range menuIds { + idStr := strconv.Itoa(int(menus[i].ID)) + if idStr == menuIds[j] { + hasMenu = true + } + } + if !hasMenu { + return errors.New("添加失败,请勿跨级操作") + } + } + } + + err = AuthorityServiceApp.SetMenuAuthority(&auth) + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetMenuAuthority +//@description: 查看当前角色树 +//@param: info *request.GetAuthorityId +//@return: menus []system.SysMenu, err error + +func (menuService *MenuService) GetMenuAuthority(info *request.GetAuthorityId) (menus []system.SysMenu, err error) { + var baseMenu []system.SysBaseMenu + var SysAuthorityMenus []system.SysAuthorityMenu + err = global.GVA_DB.Where("sys_authority_authority_id = ?", info.AuthorityId).Find(&SysAuthorityMenus).Error + if err != nil { + return + } + + var MenuIds []string + + for i := range SysAuthorityMenus { + MenuIds = append(MenuIds, SysAuthorityMenus[i].MenuId) + } + + err = global.GVA_DB.Where("id in (?) ", MenuIds).Order("sort").Find(&baseMenu).Error + + for i := range baseMenu { + menus = append(menus, system.SysMenu{ + SysBaseMenu: baseMenu[i], + AuthorityId: info.AuthorityId, + MenuId: baseMenu[i].ID, + Parameters: baseMenu[i].Parameters, + }) + } + return menus, err +} + +// UserAuthorityDefaultRouter 用户角色默认路由检查 +// +// Author [SliverHorn](https://github.com/SliverHorn) +func (menuService *MenuService) UserAuthorityDefaultRouter(user *system.SysUser) { + var menuIds []string + err := global.GVA_DB.Model(&system.SysAuthorityMenu{}).Where("sys_authority_authority_id = ?", user.AuthorityId).Pluck("sys_base_menu_id", &menuIds).Error + if err != nil { + return + } + var am system.SysBaseMenu + err = global.GVA_DB.First(&am, "name = ? and id in (?)", user.Authority.DefaultRouter, menuIds).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + user.Authority.DefaultRouter = "404" + } +} diff --git a/server/service/system/sys_operation_record.go b/server/service/system/sys_operation_record.go new file mode 100644 index 0000000..20b37a5 --- /dev/null +++ b/server/service/system/sys_operation_record.go @@ -0,0 +1,83 @@ +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/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" +) + +//@author: [granty1](https://github.com/granty1) +//@function: CreateSysOperationRecord +//@description: 创建记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +type OperationRecordService struct{} + +var OperationRecordServiceApp = new(OperationRecordService) + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteSysOperationRecordByIds +//@description: 批量删除记录 +//@param: ids request.IdsReq +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecordByIds(ids request.IdsReq) (err error) { + err = global.GVA_DB.Delete(&[]system.SysOperationRecord{}, "id in (?)", ids.Ids).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: DeleteSysOperationRecord +//@description: 删除操作记录 +//@param: sysOperationRecord model.SysOperationRecord +//@return: err error + +func (operationRecordService *OperationRecordService) DeleteSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { + err = global.GVA_DB.Delete(&sysOperationRecord).Error + return err +} + +//@author: [granty1](https://github.com/granty1) +//@function: GetSysOperationRecord +//@description: 根据id获取单条操作记录 +//@param: id uint +//@return: sysOperationRecord system.SysOperationRecord, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecord(id uint) (sysOperationRecord system.SysOperationRecord, err error) { + err = global.GVA_DB.Where("id = ?", id).First(&sysOperationRecord).Error + return +} + +//@author: [granty1](https://github.com/granty1) +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSysOperationRecordInfoList +//@description: 分页获取操作记录列表 +//@param: info systemReq.SysOperationRecordSearch +//@return: list interface{}, total int64, err error + +func (operationRecordService *OperationRecordService) GetSysOperationRecordInfoList(info systemReq.SysOperationRecordSearch) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysOperationRecord{}) + var sysOperationRecords []system.SysOperationRecord + // 如果有条件搜索 下方会自动创建搜索语句 + if info.Method != "" { + db = db.Where("method = ?", info.Method) + } + if info.Path != "" { + db = db.Where("path LIKE ?", "%"+info.Path+"%") + } + if info.Status != 0 { + db = db.Where("status = ?", info.Status) + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Order("id desc").Limit(limit).Offset(offset).Preload("User").Find(&sysOperationRecords).Error + return sysOperationRecords, total, err +} diff --git a/server/service/system/sys_params.go b/server/service/system/sys_params.go new file mode 100644 index 0000000..953f004 --- /dev/null +++ b/server/service/system/sys_params.go @@ -0,0 +1,82 @@ +package system + +import ( + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" +) + +type SysParamsService struct{} + +// CreateSysParams 创建参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) { + err = global.GVA_DB.Create(sysParams).Error + return err +} + +// DeleteSysParams 删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error + return err +} + +// DeleteSysParamsByIds 批量删除参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error + return err +} + +// UpdateSysParams 更新参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) { + err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error + return err +} + +// GetSysParams 根据ID获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParams(ID string) (sysParams system.SysParams, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysParams).Error + return +} + +// GetSysParamsInfoList 分页获取参数记录 +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParamsInfoList(info systemReq.SysParamsSearch) (list []system.SysParams, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysParams{}) + var sysParamss []system.SysParams + // 如果有条件搜索 下方会自动创建搜索语句 + if info.StartCreatedAt != nil && info.EndCreatedAt != nil { + db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + } + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + if info.Key != "" { + db = db.Where("key LIKE ?", "%"+info.Key+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysParamss).Error + return sysParamss, total, err +} + +// GetSysParam 根据key获取参数value +// Author [Mr.奇淼](https://github.com/pixelmaxQm) +func (sysParamsService *SysParamsService) GetSysParam(key string) (param system.SysParams, err error) { + err = global.GVA_DB.Where(system.SysParams{Key: key}).First(¶m).Error + return +} diff --git a/server/service/system/sys_skills.go b/server/service/system/sys_skills.go new file mode 100644 index 0000000..673f957 --- /dev/null +++ b/server/service/system/sys_skills.go @@ -0,0 +1,549 @@ +package system + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/model/system/request" + "gopkg.in/yaml.v3" +) + +const ( + skillFileName = "SKILL.md" + globalConstraintFileName = "README.md" +) + +var skillToolOrder = []string{"copilot", "claude", "cursor", "trae", "codex"} + +var skillToolDirs = map[string]string{ + "copilot": ".aone_copilot", + "claude": ".claude", + "trae": ".trae", + "codex": ".codex", + "cursor": ".cursor", +} + +var skillToolLabels = map[string]string{ + "copilot": "Copilot", + "claude": "Claude", + "trae": "Trae", + "codex": "Codex", + "cursor": "Cursor", +} + +const defaultSkillMarkdown = "## 技能用途\n请在这里描述技能的目标、适用场景与限制条件。\n\n## 输入\n- 请补充输入格式与示例。\n\n## 输出\n- 请补充输出格式与示例。\n\n## 关键步骤\n1. 第一步\n2. 第二步\n\n## 示例\n在此补充一到两个典型示例。\n" + +const defaultResourceMarkdown = "# 资源说明\n请在这里补充资源内容。\n" + +const defaultReferenceMarkdown = "# 参考资料\n请在这里补充参考资料内容。\n" + +const defaultTemplateMarkdown = "# 模板\n请在这里补充模板内容。\n" + +const defaultGlobalConstraintMarkdown = "# 全局约束\n请在这里补充该工具的统一约束与使用规范。\n" + +type SkillsService struct{} + +func (s *SkillsService) Tools(_ context.Context) ([]system.SkillTool, error) { + tools := make([]system.SkillTool, 0, len(skillToolOrder)) + for _, key := range skillToolOrder { + if _, err := s.toolSkillsDir(key); err != nil { + return nil, err + } + tools = append(tools, system.SkillTool{Key: key, Label: skillToolLabels[key]}) + } + return tools, nil +} + +func (s *SkillsService) List(_ context.Context, tool string) ([]string, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return nil, err + } + entries, err := os.ReadDir(skillsDir) + if err != nil { + return nil, err + } + var skills []string + for _, entry := range entries { + if entry.IsDir() { + skills = append(skills, entry.Name()) + } + } + sort.Strings(skills) + return skills, nil +} + +func (s *SkillsService) Detail(_ context.Context, tool, skill string) (system.SkillDetail, error) { + var detail system.SkillDetail + if !isSafeName(skill) { + return detail, errors.New("技能名称不合法") + } + detail.Tool = tool + detail.Skill = skill + + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return detail, err + } + + skillFilePath := filepath.Join(skillDir, skillFileName) + content, err := os.ReadFile(skillFilePath) + if err != nil { + if !os.IsNotExist(err) { + return detail, err + } + detail.Meta = system.SkillMeta{Name: skill} + detail.Markdown = defaultSkillMarkdown + } else { + meta, body, parseErr := parseSkillContent(string(content)) + if parseErr != nil { + meta = system.SkillMeta{Name: skill} + body = string(content) + } + if meta.Name == "" { + meta.Name = skill + } + detail.Meta = meta + detail.Markdown = body + } + + detail.Scripts = listFiles(filepath.Join(skillDir, "scripts")) + detail.Resources = listFiles(filepath.Join(skillDir, "resources")) + detail.References = listFiles(filepath.Join(skillDir, "references")) + detail.Templates = listFiles(filepath.Join(skillDir, "templates")) + return detail, nil +} + +func (s *SkillsService) Save(_ context.Context, req request.SkillSaveRequest) error { + if !isSafeName(req.Skill) { + return errors.New("技能名称不合法") + } + skillDir, err := s.ensureSkillDir(req.Tool, req.Skill) + if err != nil { + return err + } + if req.Meta.Name == "" { + req.Meta.Name = req.Skill + } + content, err := buildSkillContent(req.Meta, req.Markdown) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(skillDir, skillFileName), []byte(content), 0644); err != nil { + return err + } + + if len(req.SyncTools) > 0 { + for _, tool := range req.SyncTools { + if tool == req.Tool { + continue + } + targetDir, err := s.ensureSkillDir(tool, req.Skill) + if err != nil { + return err + } + if err := copySkillDir(skillDir, targetDir); err != nil { + return err + } + } + } + return nil +} + +func (s *SkillsService) CreateScript(_ context.Context, req request.SkillScriptCreateRequest) (string, string, error) { + if !isSafeName(req.Skill) { + return "", "", errors.New("技能名称不合法") + } + fileName, lang, err := buildScriptFileName(req.FileName, req.ScriptType) + if err != nil { + return "", "", err + } + if lang == "" { + return "", "", errors.New("脚本类型不支持") + } + skillDir, err := s.ensureSkillDir(req.Tool, req.Skill) + if err != nil { + return "", "", err + } + filePath := filepath.Join(skillDir, "scripts", fileName) + if _, err := os.Stat(filePath); err == nil { + return "", "", errors.New("脚本已存在") + } + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return "", "", err + } + content := scriptTemplate(lang) + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", "", err + } + return fileName, content, nil +} + +func (s *SkillsService) GetScript(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "scripts", req.FileName) +} + +func (s *SkillsService) SaveScript(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "scripts", req.FileName, req.Content) +} + +func (s *SkillsService) CreateResource(_ context.Context, req request.SkillResourceCreateRequest) (string, string, error) { + return s.createMarkdownFile(req.Tool, req.Skill, "resources", req.FileName, defaultResourceMarkdown, "资源") +} + +func (s *SkillsService) GetResource(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "resources", req.FileName) +} + +func (s *SkillsService) SaveResource(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "resources", req.FileName, req.Content) +} + +func (s *SkillsService) CreateReference(_ context.Context, req request.SkillReferenceCreateRequest) (string, string, error) { + return s.createMarkdownFile(req.Tool, req.Skill, "references", req.FileName, defaultReferenceMarkdown, "参考") +} + +func (s *SkillsService) GetReference(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "references", req.FileName) +} + +func (s *SkillsService) SaveReference(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "references", req.FileName, req.Content) +} + +func (s *SkillsService) CreateTemplate(_ context.Context, req request.SkillTemplateCreateRequest) (string, string, error) { + return s.createMarkdownFile(req.Tool, req.Skill, "templates", req.FileName, defaultTemplateMarkdown, "模板") +} + +func (s *SkillsService) GetTemplate(_ context.Context, req request.SkillFileRequest) (string, error) { + return s.readSkillFile(req.Tool, req.Skill, "templates", req.FileName) +} + +func (s *SkillsService) SaveTemplate(_ context.Context, req request.SkillFileSaveRequest) error { + return s.writeSkillFile(req.Tool, req.Skill, "templates", req.FileName, req.Content) +} + +func (s *SkillsService) GetGlobalConstraint(_ context.Context, tool string) (string, bool, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return "", false, err + } + filePath := filepath.Join(skillsDir, globalConstraintFileName) + content, err := os.ReadFile(filePath) + if err != nil { + if os.IsNotExist(err) { + return defaultGlobalConstraintMarkdown, false, nil + } + return "", false, err + } + return string(content), true, nil +} + +func (s *SkillsService) SaveGlobalConstraint(_ context.Context, req request.SkillGlobalConstraintSaveRequest) error { + if strings.TrimSpace(req.Tool) == "" { + return errors.New("工具类型不能为空") + } + writeConstraint := func(tool, content string) error { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return err + } + filePath := filepath.Join(skillsDir, globalConstraintFileName) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + return os.WriteFile(filePath, []byte(content), 0644) + } + if err := writeConstraint(req.Tool, req.Content); err != nil { + return err + } + if len(req.SyncTools) == 0 { + return nil + } + for _, tool := range req.SyncTools { + if tool == "" || tool == req.Tool { + continue + } + if err := writeConstraint(tool, req.Content); err != nil { + return err + } + } + return nil +} + +func (s *SkillsService) toolSkillsDir(tool string) (string, error) { + toolDir, ok := skillToolDirs[tool] + if !ok { + return "", errors.New("工具类型不支持") + } + root := strings.TrimSpace(global.GVA_CONFIG.AutoCode.Root) + if root == "" { + root = "." + } + skillsDir := filepath.Join(root, toolDir, "skills") + if err := os.MkdirAll(skillsDir, os.ModePerm); err != nil { + return "", err + } + return skillsDir, nil +} + +func (s *SkillsService) skillDir(tool, skill string) (string, error) { + skillsDir, err := s.toolSkillsDir(tool) + if err != nil { + return "", err + } + return filepath.Join(skillsDir, skill), nil +} + +func (s *SkillsService) ensureSkillDir(tool, skill string) (string, error) { + if !isSafeName(skill) { + return "", errors.New("技能名称不合法") + } + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return "", err + } + if err := os.MkdirAll(skillDir, os.ModePerm); err != nil { + return "", err + } + return skillDir, nil +} + +func (s *SkillsService) createMarkdownFile(tool, skill, subDir, fileName, defaultContent, label string) (string, string, error) { + if !isSafeName(skill) { + return "", "", errors.New("技能名称不合法") + } + cleanName, err := buildResourceFileName(fileName) + if err != nil { + return "", "", err + } + skillDir, err := s.ensureSkillDir(tool, skill) + if err != nil { + return "", "", err + } + filePath := filepath.Join(skillDir, subDir, cleanName) + if _, err := os.Stat(filePath); err == nil { + if label == "" { + label = "文件" + } + return "", "", fmt.Errorf("%s已存在", label) + } + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return "", "", err + } + content := defaultContent + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", "", err + } + return cleanName, content, nil +} + +func (s *SkillsService) readSkillFile(tool, skill, subDir, fileName string) (string, error) { + if !isSafeName(skill) { + return "", errors.New("技能名称不合法") + } + if !isSafeFileName(fileName) { + return "", errors.New("文件名不合法") + } + skillDir, err := s.skillDir(tool, skill) + if err != nil { + return "", err + } + filePath := filepath.Join(skillDir, subDir, fileName) + content, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return string(content), nil +} + +func (s *SkillsService) writeSkillFile(tool, skill, subDir, fileName, content string) error { + if !isSafeName(skill) { + return errors.New("技能名称不合法") + } + if !isSafeFileName(fileName) { + return errors.New("文件名不合法") + } + skillDir, err := s.ensureSkillDir(tool, skill) + if err != nil { + return err + } + filePath := filepath.Join(skillDir, subDir, fileName) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + return os.WriteFile(filePath, []byte(content), 0644) +} + +func parseSkillContent(content string) (system.SkillMeta, string, error) { + clean := strings.TrimPrefix(content, "\ufeff") + lines := strings.Split(clean, "\n") + if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" { + return system.SkillMeta{}, clean, nil + } + end := -1 + for i := 1; i < len(lines); i++ { + if strings.TrimSpace(lines[i]) == "---" { + end = i + break + } + } + if end == -1 { + return system.SkillMeta{}, clean, nil + } + yamlText := strings.Join(lines[1:end], "\n") + body := strings.Join(lines[end+1:], "\n") + var meta system.SkillMeta + if err := yaml.Unmarshal([]byte(yamlText), &meta); err != nil { + return system.SkillMeta{}, body, err + } + return meta, body, nil +} + +func buildSkillContent(meta system.SkillMeta, markdown string) (string, error) { + if meta.Name == "" { + return "", errors.New("name不能为空") + } + data, err := yaml.Marshal(meta) + if err != nil { + return "", err + } + yamlText := strings.TrimRight(string(data), "\n") + body := strings.TrimLeft(markdown, "\n") + if body != "" { + body = body + "\n" + } + return fmt.Sprintf("---\n%s\n---\n%s", yamlText, body), nil +} + +func listFiles(dir string) []string { + entries, err := os.ReadDir(dir) + if err != nil { + return []string{} + } + files := make([]string, 0, len(entries)) + for _, entry := range entries { + if entry.Type().IsRegular() { + files = append(files, entry.Name()) + } + } + sort.Strings(files) + return files +} + +func isSafeName(name string) bool { + if strings.TrimSpace(name) == "" { + return false + } + if strings.Contains(name, "..") { + return false + } + if strings.ContainsAny(name, "/\\") { + return false + } + return name == filepath.Base(name) +} + +func isSafeFileName(name string) bool { + if strings.TrimSpace(name) == "" { + return false + } + if strings.Contains(name, "..") { + return false + } + if strings.ContainsAny(name, "/\\") { + return false + } + return name == filepath.Base(name) +} + +func buildScriptFileName(fileName, scriptType string) (string, string, error) { + clean := strings.TrimSpace(fileName) + if clean == "" { + return "", "", errors.New("文件名不能为空") + } + if !isSafeFileName(clean) { + return "", "", errors.New("文件名不合法") + } + base := strings.TrimSuffix(clean, filepath.Ext(clean)) + if base == "" { + return "", "", errors.New("文件名不合法") + } + + switch strings.ToLower(scriptType) { + case "py", "python": + return base + ".py", "python", nil + case "js", "javascript", "script": + return base + ".js", "javascript", nil + case "sh", "shell", "bash": + return base + ".sh", "sh", nil + default: + return "", "", errors.New("脚本类型不支持") + } +} + +func buildResourceFileName(fileName string) (string, error) { + clean := strings.TrimSpace(fileName) + if clean == "" { + return "", errors.New("文件名不能为空") + } + if !isSafeFileName(clean) { + return "", errors.New("文件名不合法") + } + base := strings.TrimSuffix(clean, filepath.Ext(clean)) + if base == "" { + return "", errors.New("文件名不合法") + } + return base + ".md", nil +} + +func scriptTemplate(lang string) string { + switch lang { + case "python": + return "# -*- coding: utf-8 -*-\n# TODO: 在这里实现脚本逻辑\n" + case "javascript": + return "// TODO: 在这里实现脚本逻辑\n" + case "sh": + return "#!/usr/bin/env bash\nset -euo pipefail\n\n# TODO: 在这里实现脚本逻辑\n" + default: + return "" + } +} + +func copySkillDir(src, dst string) error { + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + target := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(target, os.ModePerm) + } + if !d.Type().IsRegular() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil { + return err + } + return os.WriteFile(target, data, 0644) + }) +} diff --git a/server/service/system/sys_system.go b/server/service/system/sys_system.go new file mode 100644 index 0000000..5db7fde --- /dev/null +++ b/server/service/system/sys_system.go @@ -0,0 +1,62 @@ +package system + +import ( + "git.echol.cn/loser/st/server/config" + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/utils" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetSystemConfig +//@description: 读取配置文件 +//@return: conf config.Server, err error + +type SystemConfigService struct{} + +var SystemConfigServiceApp = new(SystemConfigService) + +func (systemConfigService *SystemConfigService) GetSystemConfig() (conf config.Server, err error) { + return global.GVA_CONFIG, nil +} + +// @description set system config, +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSystemConfig +//@description: 设置配置文件 +//@param: system model.System +//@return: err error + +func (systemConfigService *SystemConfigService) SetSystemConfig(system system.System) (err error) { + cs := utils.StructToMap(system.Config) + for k, v := range cs { + global.GVA_VP.Set(k, v) + } + err = global.GVA_VP.WriteConfig() + return err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetServerInfo +//@description: 获取服务器信息 +//@return: server *utils.Server, err error + +func (systemConfigService *SystemConfigService) GetServerInfo() (server *utils.Server, err error) { + var s utils.Server + s.Os = utils.InitOS() + if s.Cpu, err = utils.InitCPU(); err != nil { + global.GVA_LOG.Error("func utils.InitCPU() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Ram, err = utils.InitRAM(); err != nil { + global.GVA_LOG.Error("func utils.InitRAM() Failed", zap.String("err", err.Error())) + return &s, err + } + if s.Disk, err = utils.InitDisk(); err != nil { + global.GVA_LOG.Error("func utils.InitDisk() Failed", zap.String("err", err.Error())) + return &s, err + } + + return &s, nil +} diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go new file mode 100644 index 0000000..bd79a32 --- /dev/null +++ b/server/service/system/sys_user.go @@ -0,0 +1,318 @@ +package system + +import ( + "errors" + "fmt" + "time" + + "git.echol.cn/loser/st/server/model/common" + systemReq "git.echol.cn/loser/st/server/model/system/request" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Register +//@description: 用户注册 +//@param: u model.SysUser +//@return: userInter system.SysUser, err error + +type UserService struct{} + +var UserServiceApp = new(UserService) + +func (userService *UserService) Register(u system.SysUser) (userInter system.SysUser, err error) { + var user system.SysUser + if !errors.Is(global.GVA_DB.Where("username = ?", u.Username).First(&user).Error, gorm.ErrRecordNotFound) { // 判断用户名是否注册 + return userInter, errors.New("用户名已注册") + } + // 否则 附加uuid 密码hash加密 注册 + u.Password = utils.BcryptHash(u.Password) + u.UUID = uuid.New() + err = global.GVA_DB.Create(&u).Error + return u, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: Login +//@description: 用户登录 +//@param: u *model.SysUser +//@return: err error, userInter *model.SysUser + +func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysUser, err error) { + if nil == global.GVA_DB { + return nil, fmt.Errorf("db not init") + } + + var user system.SysUser + err = global.GVA_DB.Where("username = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error + if err == nil { + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return nil, errors.New("密码错误") + } + MenuServiceApp.UserAuthorityDefaultRouter(&user) + } + return &user, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ChangePassword +//@description: 修改用户密码 +//@param: u *model.SysUser, newPassword string +//@return: err error + +func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (err error) { + var user system.SysUser + err = global.GVA_DB.Select("id, password").Where("id = ?", u.ID).First(&user).Error + if err != nil { + return err + } + if ok := utils.BcryptCheck(u.Password, user.Password); !ok { + return errors.New("原密码错误") + } + pwd := utils.BcryptHash(newPassword) + err = global.GVA_DB.Model(&user).Update("password", pwd).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: GetUserInfoList +//@description: 分页获取数据 +//@param: info request.PageInfo +//@return: err error, list interface{}, total int64 + +func (userService *UserService) GetUserInfoList(info systemReq.GetUserList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + db := global.GVA_DB.Model(&system.SysUser{}) + var userList []system.SysUser + + if info.NickName != "" { + db = db.Where("nick_name LIKE ?", "%"+info.NickName+"%") + } + if info.Phone != "" { + db = db.Where("phone LIKE ?", "%"+info.Phone+"%") + } + if info.Username != "" { + db = db.Where("username LIKE ?", "%"+info.Username+"%") + } + if info.Email != "" { + db = db.Where("email LIKE ?", "%"+info.Email+"%") + } + + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Preload("Authorities").Preload("Authority").Find(&userList).Error + return userList, total, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthority +//@description: 设置一个用户的权限 +//@param: uuid uuid.UUID, authorityId string +//@return: err error + +func (userService *UserService) SetUserAuthority(id uint, authorityId uint) (err error) { + + assignErr := global.GVA_DB.Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityId).First(&system.SysUserAuthority{}).Error + if errors.Is(assignErr, gorm.ErrRecordNotFound) { + return errors.New("该用户无此角色") + } + + var authority system.SysAuthority + err = global.GVA_DB.Where("authority_id = ?", authorityId).First(&authority).Error + if err != nil { + return err + } + var authorityMenu []system.SysAuthorityMenu + var authorityMenuIDs []string + err = global.GVA_DB.Where("sys_authority_authority_id = ?", authorityId).Find(&authorityMenu).Error + if err != nil { + return err + } + + for i := range authorityMenu { + authorityMenuIDs = append(authorityMenuIDs, authorityMenu[i].MenuId) + } + + var authorityMenus []system.SysBaseMenu + err = global.GVA_DB.Preload("Parameters").Where("id in (?)", authorityMenuIDs).Find(&authorityMenus).Error + if err != nil { + return err + } + hasMenu := false + for i := range authorityMenus { + if authorityMenus[i].Name == authority.DefaultRouter { + hasMenu = true + break + } + } + if !hasMenu { + return errors.New("找不到默认路由,无法切换本角色") + } + + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", id).Update("authority_id", authorityId).Error + return err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserAuthorities +//@description: 设置一个用户的权限 +//@param: id uint, authorityIds []string +//@return: err error + +func (userService *UserService) SetUserAuthorities(adminAuthorityID, id uint, authorityIds []uint) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + var user system.SysUser + TxErr := tx.Where("id = ?", id).First(&user).Error + if TxErr != nil { + global.GVA_LOG.Debug(TxErr.Error()) + return errors.New("查询用户数据失败") + } + TxErr = tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error + if TxErr != nil { + return TxErr + } + var useAuthority []system.SysUserAuthority + for _, v := range authorityIds { + e := AuthorityServiceApp.CheckAuthorityIDAuth(adminAuthorityID, v) + if e != nil { + return e + } + useAuthority = append(useAuthority, system.SysUserAuthority{ + SysUserId: id, SysAuthorityAuthorityId: v, + }) + } + TxErr = tx.Create(&useAuthority).Error + if TxErr != nil { + return TxErr + } + TxErr = tx.Model(&user).Update("authority_id", authorityIds[0]).Error + if TxErr != nil { + return TxErr + } + // 返回 nil 提交事务 + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: DeleteUser +//@description: 删除用户 +//@param: id float64 +//@return: err error + +func (userService *UserService) DeleteUser(id int) (err error) { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Where("id = ?", id).Delete(&system.SysUser{}).Error; err != nil { + return err + } + if err := tx.Delete(&[]system.SysUserAuthority{}, "sys_user_id = ?", id).Error; err != nil { + return err + } + return nil + }) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetUserInfo +//@description: 设置用户信息 +//@param: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetUserInfo(req system.SysUser) error { + return global.GVA_DB.Model(&system.SysUser{}). + Select("updated_at", "nick_name", "header_img", "phone", "email", "enable"). + Where("id=?", req.ID). + Updates(map[string]interface{}{ + "updated_at": time.Now(), + "nick_name": req.NickName, + "header_img": req.HeaderImg, + "phone": req.Phone, + "email": req.Email, + "enable": req.Enable, + }).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfInfo +//@description: 设置用户信息 +//@param: reqUser model.SysUser +//@return: err error, user model.SysUser + +func (userService *UserService) SetSelfInfo(req system.SysUser) error { + return global.GVA_DB.Model(&system.SysUser{}). + Where("id=?", req.ID). + Updates(req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetSelfSetting +//@description: 设置用户配置 +//@param: req datatypes.JSON, uid uint +//@return: err error + +func (userService *UserService) SetSelfSetting(req common.JSONMap, uid uint) error { + return global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", uid).Update("origin_setting", req).Error +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: GetUserInfo +//@description: 获取用户信息 +//@param: uuid uuid.UUID +//@return: err error, user system.SysUser + +func (userService *UserService) GetUserInfo(uuid uuid.UUID) (user system.SysUser, err error) { + var reqUser system.SysUser + err = global.GVA_DB.Preload("Authorities").Preload("Authority").First(&reqUser, "uuid = ?", uuid).Error + if err != nil { + return reqUser, err + } + MenuServiceApp.UserAuthorityDefaultRouter(&reqUser) + return reqUser, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserById +//@description: 通过id获取用户信息 +//@param: id int +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserById(id int) (user *system.SysUser, err error) { + var u system.SysUser + err = global.GVA_DB.Where("id = ?", id).First(&u).Error + return &u, err +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: FindUserByUuid +//@description: 通过uuid获取用户信息 +//@param: uuid string +//@return: err error, user *model.SysUser + +func (userService *UserService) FindUserByUuid(uuid string) (user *system.SysUser, err error) { + var u system.SysUser + if err = global.GVA_DB.Where("uuid = ?", uuid).First(&u).Error; err != nil { + return &u, errors.New("用户不存在") + } + return &u, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ResetPassword +//@description: 修改用户密码 +//@param: ID uint +//@return: err error + +func (userService *UserService) ResetPassword(ID uint, password string) (err error) { + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash(password)).Error + return err +} diff --git a/server/service/system/sys_version.go b/server/service/system/sys_version.go new file mode 100644 index 0000000..37f2a6b --- /dev/null +++ b/server/service/system/sys_version.go @@ -0,0 +1,231 @@ +package system + +import ( + "context" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" + "gorm.io/gorm" +) + +type SysVersionService struct{} + +// CreateSysVersion 创建版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) { + err = global.GVA_DB.Create(sysVersion).Error + return err +} + +// DeleteSysVersion 删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error + return err +} + +// DeleteSysVersionByIds 批量删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error + return err +} + +// GetSysVersion 根据ID获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error + return +} + +// GetSysVersionInfoList 分页获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysVersion{}) + var sysVersions []system.SysVersion + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.VersionName != nil && *info.VersionName != "" { + db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%") + } + if info.VersionCode != nil && *info.VersionCode != "" { + db = db.Where("version_code = ?", *info.VersionCode) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysVersions).Error + return sysVersions, total, err +} +func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} + +// GetMenusByIds 根据ID列表获取菜单数据 +func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + return +} + +// GetApisByIds 根据ID列表获取API数据 +func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) { + err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error + return +} + +// GetDictionariesByIds 根据ID列表获取字典数据 +func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error + return +} + +// ImportMenus 导入菜单数据 +func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 递归创建菜单 + return sysVersionService.createMenusRecursively(tx, menus, 0) + }) +} + +// createMenusRecursively 递归创建菜单 +func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error { + for _, menu := range menus { + // 检查菜单是否已存在 + var existingMenu system.SysBaseMenu + if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil { + // 菜单已存在,使用现有菜单ID继续处理子菜单 + if len(menu.Children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil { + return err + } + } + continue + } + + // 保存参数和按钮数据,稍后处理 + parameters := menu.Parameters + menuBtns := menu.MenuBtn + children := menu.Children + + // 创建新菜单(不包含关联数据) + newMenu := system.SysBaseMenu{ + ParentId: parentId, + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + if err := tx.Create(&newMenu).Error; err != nil { + return err + } + + // 创建参数 + if len(parameters) > 0 { + for _, param := range parameters { + newParam := system.SysBaseMenuParameter{ + SysBaseMenuID: newMenu.ID, + Type: param.Type, + Key: param.Key, + Value: param.Value, + } + if err := tx.Create(&newParam).Error; err != nil { + return err + } + } + } + + // 创建菜单按钮 + if len(menuBtns) > 0 { + for _, btn := range menuBtns { + newBtn := system.SysBaseMenuBtn{ + SysBaseMenuID: newMenu.ID, + Name: btn.Name, + Desc: btn.Desc, + } + if err := tx.Create(&newBtn).Error; err != nil { + return err + } + } + } + + // 递归处理子菜单 + if len(children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil { + return err + } + } + } + return nil +} + +// ImportApis 导入API数据 +func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + // 检查API是否已存在 + var existingApi system.SysApi + if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil { + // API已存在,跳过 + continue + } + + // 创建新API + newApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + + if err := tx.Create(&newApi).Error; err != nil { + return err + } + } + return nil + }) +} + +// ImportDictionaries 导入字典数据 +func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + // 检查字典是否已存在 + var existingDict system.SysDictionary + if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil { + // 字典已存在,跳过 + continue + } + + // 创建新字典 + newDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + SysDictionaryDetails: dict.SysDictionaryDetails, + } + + if err := tx.Create(&newDict).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/server/source/example/file_upload_download.go b/server/source/example/file_upload_download.go new file mode 100644 index 0000000..2a6cb3c --- /dev/null +++ b/server/source/example/file_upload_download.go @@ -0,0 +1,66 @@ +package example + +import ( + "context" + + "git.echol.cn/loser/st/server/model/example" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderExaFile = system.InitOrderInternal + 1 + +type initExaFileMysql struct{} + +// auto run +func init() { + system.RegisterInit(initOrderExaFile, &initExaFileMysql{}) +} + +func (i *initExaFileMysql) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&example.ExaFileUploadAndDownload{}) +} + +func (i *initExaFileMysql) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&example.ExaFileUploadAndDownload{}) +} + +func (i *initExaFileMysql) InitializerName() string { + return example.ExaFileUploadAndDownload{}.TableName() +} + +func (i *initExaFileMysql) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []example.ExaFileUploadAndDownload{ + {Name: "10.png", Url: "https://qmplusimg.henrongyi.top/gvalogo.png", Tag: "png", Key: "158787308910.png"}, + {Name: "logo.png", Url: "https://qmplusimg.henrongyi.top/1576554439myAvatar.png", Tag: "png", Key: "1587973709logo.png"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, example.ExaFileUploadAndDownload{}.TableName()+"表数据初始化失败!") + } + return ctx, nil +} + +func (i *initExaFileMysql) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + lookup := example.ExaFileUploadAndDownload{Name: "logo.png", Key: "1587973709logo.png"} + if errors.Is(db.First(&lookup, &lookup).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/api.go b/server/source/system/api.go new file mode 100644 index 0000000..e7e35ab --- /dev/null +++ b/server/source/system/api.go @@ -0,0 +1,264 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApi struct{} + +const initOrderApi = system.InitOrderSystem + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApi, &initApi{}) +} + +func (i *initApi) InitializerName() string { + return sysModel.SysApi{}.TableName() +} + +func (i *initApi) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysApi{}) +} + +func (i *initApi) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysApi{}) +} + +func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysApi{ + {ApiGroup: "jwt", Method: "POST", Path: "/jwt/jsonInBlacklist", Description: "jwt加入黑名单(退出,必选)"}, + + {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLog", Description: "删除登录日志"}, + {ApiGroup: "登录日志", Method: "DELETE", Path: "/sysLoginLog/deleteLoginLogByIds", Description: "批量删除登录日志"}, + {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/findLoginLog", Description: "根据ID获取登录日志"}, + {ApiGroup: "登录日志", Method: "GET", Path: "/sysLoginLog/getLoginLogList", Description: "获取登录日志列表"}, + + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/createApiToken", Description: "签发API Token"}, + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/getApiTokenList", Description: "获取API Token列表"}, + {ApiGroup: "API Token", Method: "POST", Path: "/sysApiToken/deleteApiToken", Description: "作废API Token"}, + + {ApiGroup: "系统用户", Method: "DELETE", Path: "/user/deleteUser", Description: "删除用户"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/admin_register", Description: "用户注册"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/getUserList", Description: "获取用户列表"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setUserInfo", Description: "设置用户信息"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfInfo", Description: "设置自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "GET", Path: "/user/getUserInfo", Description: "获取自身信息(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthorities", Description: "设置权限组"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/changePassword", Description: "修改密码(建议选择)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/setUserAuthority", Description: "修改用户角色(必选)"}, + {ApiGroup: "系统用户", Method: "POST", Path: "/user/resetPassword", Description: "重置用户密码"}, + {ApiGroup: "系统用户", Method: "PUT", Path: "/user/setSelfSetting", Description: "用户界面配置"}, + + {ApiGroup: "api", Method: "POST", Path: "/api/createApi", Description: "创建api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/deleteApi", Description: "删除Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/updateApi", Description: "更新Api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiList", Description: "获取api列表"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getAllApis", Description: "获取所有api"}, + {ApiGroup: "api", Method: "POST", Path: "/api/getApiById", Description: "获取api详细信息"}, + {ApiGroup: "api", Method: "DELETE", Path: "/api/deleteApisByIds", Description: "批量删除api"}, + {ApiGroup: "api", Method: "GET", Path: "/api/syncApi", Description: "获取待同步API"}, + {ApiGroup: "api", Method: "GET", Path: "/api/getApiGroups", Description: "获取路由组"}, + {ApiGroup: "api", Method: "POST", Path: "/api/enterSyncApi", Description: "确认同步API"}, + {ApiGroup: "api", Method: "POST", Path: "/api/ignoreApi", Description: "忽略API"}, + + {ApiGroup: "角色", Method: "POST", Path: "/authority/copyAuthority", Description: "拷贝角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/createAuthority", Description: "创建角色"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/deleteAuthority", Description: "删除角色"}, + {ApiGroup: "角色", Method: "PUT", Path: "/authority/updateAuthority", Description: "更新角色信息"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/getAuthorityList", Description: "获取角色列表"}, + {ApiGroup: "角色", Method: "POST", Path: "/authority/setDataAuthority", Description: "设置角色资源权限"}, + + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/updateCasbin", Description: "更改角色api权限"}, + {ApiGroup: "casbin", Method: "POST", Path: "/casbin/getPolicyPathByAuthorityId", Description: "获取权限列表"}, + + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addBaseMenu", Description: "新增菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenu", Description: "获取菜单树(必选)"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/deleteBaseMenu", Description: "删除菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/updateBaseMenu", Description: "更新菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuById", Description: "根据id获取菜单"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuList", Description: "分页获取基础menu列表"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getBaseMenuTree", Description: "获取用户动态路由"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/getMenuAuthority", Description: "获取指定角色menu"}, + {ApiGroup: "菜单", Method: "POST", Path: "/menu/addMenuAuthority", Description: "增加menu和角色关联关系"}, + + {ApiGroup: "分片上传", Method: "GET", Path: "/fileUploadAndDownload/findFile", Description: "寻找目标文件(秒传)"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinue", Description: "断点续传"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/breakpointContinueFinish", Description: "断点续传完成"}, + {ApiGroup: "分片上传", Method: "POST", Path: "/fileUploadAndDownload/removeChunk", Description: "上传完成移除文件"}, + + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/upload", Description: "文件上传(建议选择)"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/deleteFile", Description: "删除文件"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/editFileName", Description: "文件名或者备注编辑"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/getFileList", Description: "获取上传文件列表"}, + {ApiGroup: "文件上传与下载", Method: "POST", Path: "/fileUploadAndDownload/importURL", Description: "导入URL"}, + + {ApiGroup: "系统服务", Method: "POST", Path: "/system/getServerInfo", Description: "获取服务器信息"}, + {ApiGroup: "系统服务", Method: "POST", Path: "/system/getSystemConfig", Description: "获取配置文件内容"}, + {ApiGroup: "系统服务", Method: "POST", Path: "/system/setSystemConfig", Description: "设置配置文件内容"}, + + {ApiGroup: "skills", Method: "GET", Path: "/skills/getTools", Description: "获取技能工具列表"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillList", Description: "获取技能列表"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getSkillDetail", Description: "获取技能详情"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveSkill", Description: "保存技能定义"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createScript", Description: "创建技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getScript", Description: "读取技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveScript", Description: "保存技能脚本"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createResource", Description: "创建技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getResource", Description: "读取技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveResource", Description: "保存技能资源"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createReference", Description: "创建技能参考"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getReference", Description: "读取技能参考"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveReference", Description: "保存技能参考"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/createTemplate", Description: "创建技能模板"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getTemplate", Description: "读取技能模板"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveTemplate", Description: "保存技能模板"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/getGlobalConstraint", Description: "读取全局约束"}, + {ApiGroup: "skills", Method: "POST", Path: "/skills/saveGlobalConstraint", Description: "保存全局约束"}, + + {ApiGroup: "客户", Method: "PUT", Path: "/customer/customer", Description: "更新客户"}, + {ApiGroup: "客户", Method: "POST", Path: "/customer/customer", Description: "创建客户"}, + {ApiGroup: "客户", Method: "DELETE", Path: "/customer/customer", Description: "删除客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customer", Description: "获取单一客户"}, + {ApiGroup: "客户", Method: "GET", Path: "/customer/customerList", Description: "获取客户列表"}, + + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getDB", Description: "获取所有数据库"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getTables", Description: "获取数据库表"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/createTemp", Description: "自动化代码"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/preview", Description: "预览自动化代码"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/removePlugin", Description: "卸载插件"}, + {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getPluginList", Description: "获取已安装插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"}, + + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/createPackage", Description: "配置模板"}, + {ApiGroup: "模板配置", Method: "GET", Path: "/autoCode/getTemplates", Description: "获取模板文件"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/getPackage", Description: "获取所有模板"}, + {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/delPackage", Description: "删除模板"}, + + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getMeta", Description: "获取meta信息"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/rollback", Description: "回滚自动生成代码"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/getSysHistory", Description: "查询回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/delSysHistory", Description: "删除回滚记录"}, + {ApiGroup: "代码生成器历史", Method: "POST", Path: "/autoCode/addFunc", Description: "增加模板方法"}, + + {ApiGroup: "系统字典详情", Method: "PUT", Path: "/sysDictionaryDetail/updateSysDictionaryDetail", Description: "更新字典内容"}, + {ApiGroup: "系统字典详情", Method: "POST", Path: "/sysDictionaryDetail/createSysDictionaryDetail", Description: "新增字典内容"}, + {ApiGroup: "系统字典详情", Method: "DELETE", Path: "/sysDictionaryDetail/deleteSysDictionaryDetail", Description: "删除字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/findSysDictionaryDetail", Description: "根据ID获取字典内容"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getSysDictionaryDetailList", Description: "获取字典内容列表"}, + + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeList", Description: "获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryTreeListByType", Description: "根据分类获取字典数列表"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryDetailsByParent", Description: "根据父级ID获取字典详情"}, + {ApiGroup: "系统字典详情", Method: "GET", Path: "/sysDictionaryDetail/getDictionaryPath", Description: "获取字典详情的完整路径"}, + + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/createSysDictionary", Description: "新增字典"}, + {ApiGroup: "系统字典", Method: "DELETE", Path: "/sysDictionary/deleteSysDictionary", Description: "删除字典"}, + {ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典(建议选择)"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"}, + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"}, + + {ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"}, + {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"}, + {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/getSysOperationRecordList", Description: "获取操作记录列表"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecord", Description: "删除操作记录"}, + {ApiGroup: "操作记录", Method: "DELETE", Path: "/sysOperationRecord/deleteSysOperationRecordByIds", Description: "批量删除操作历史"}, + + {ApiGroup: "断点续传(插件版)", Method: "POST", Path: "/simpleUploader/upload", Description: "插件版分片上传"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/checkFileMd5", Description: "文件完整度验证"}, + {ApiGroup: "断点续传(插件版)", Method: "GET", Path: "/simpleUploader/mergeFileMd5", Description: "上传完成合并文件"}, + + {ApiGroup: "email", Method: "POST", Path: "/email/emailTest", Description: "发送测试邮件"}, + {ApiGroup: "email", Method: "POST", Path: "/email/sendEmail", Description: "发送邮件"}, + + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/setAuthorityBtn", Description: "设置按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/getAuthorityBtn", Description: "获取已有按钮权限"}, + {ApiGroup: "按钮权限", Method: "POST", Path: "/authorityBtn/canRemoveAuthorityBtn", Description: "删除按钮"}, + + {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/createSysExportTemplate", Description: "新增导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplate", Description: "删除导出模板"}, + {ApiGroup: "导出模板", Method: "DELETE", Path: "/sysExportTemplate/deleteSysExportTemplateByIds", Description: "批量删除导出模板"}, + {ApiGroup: "导出模板", Method: "PUT", Path: "/sysExportTemplate/updateSysExportTemplate", Description: "更新导出模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/findSysExportTemplate", Description: "根据ID获取导出模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"}, + {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"}, + + {ApiGroup: "错误日志", Method: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"}, + {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"}, + {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"}, + {ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"}, + + {ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"}, + {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"}, + {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfoByIds", Description: "批量删除公告"}, + {ApiGroup: "公告", Method: "PUT", Path: "/info/updateInfo", Description: "更新公告"}, + {ApiGroup: "公告", Method: "GET", Path: "/info/findInfo", Description: "根据ID获取公告"}, + {ApiGroup: "公告", Method: "GET", Path: "/info/getInfoList", Description: "获取公告列表"}, + + {ApiGroup: "参数管理", Method: "POST", Path: "/sysParams/createSysParams", Description: "新建参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParams", Description: "删除参数"}, + {ApiGroup: "参数管理", Method: "DELETE", Path: "/sysParams/deleteSysParamsByIds", Description: "批量删除参数"}, + {ApiGroup: "参数管理", Method: "PUT", Path: "/sysParams/updateSysParams", Description: "更新参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/findSysParams", Description: "根据ID获取参数"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParamsList", Description: "获取参数列表"}, + {ApiGroup: "参数管理", Method: "GET", Path: "/sysParams/getSysParam", Description: "获取参数列表"}, + {ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"}, + {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"}, + + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApi) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/authorityBtn/canRemoveAuthorityBtn", "POST"). + First(&sysModel.SysApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/api_ignore.go b/server/source/system/api_ignore.go new file mode 100644 index 0000000..3d1805c --- /dev/null +++ b/server/source/system/api_ignore.go @@ -0,0 +1,78 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initApiIgnore struct{} + +const initOrderApiIgnore = initOrderApi + 1 + +// auto run +func init() { + system.RegisterInit(initOrderApiIgnore, &initApiIgnore{}) +} + +func (i *initApiIgnore) InitializerName() string { + return sysModel.SysIgnoreApi{}.TableName() +} + +func (i *initApiIgnore) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysIgnoreApi{}) +} + +func (i *initApiIgnore) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysIgnoreApi{ + {Method: "GET", Path: "/swagger/*any"}, + {Method: "GET", Path: "/api/freshCasbin"}, + {Method: "GET", Path: "/uploads/file/*filepath"}, + {Method: "GET", Path: "/health"}, + {Method: "HEAD", Path: "/uploads/file/*filepath"}, + {Method: "POST", Path: "/autoCode/llmAuto"}, + {Method: "POST", Path: "/system/reloadSystem"}, + {Method: "POST", Path: "/base/login"}, + {Method: "POST", Path: "/base/captcha"}, + {Method: "POST", Path: "/init/initdb"}, + {Method: "POST", Path: "/init/checkdb"}, + {Method: "GET", Path: "/info/getInfoDataSource"}, + {Method: "GET", Path: "/info/getInfoPublic"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysIgnoreApi{}.TableName()+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initApiIgnore) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ? AND method = ?", "/swagger/*any", "GET"). + First(&sysModel.SysIgnoreApi{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/authorities_menus.go b/server/source/system/authorities_menus.go new file mode 100644 index 0000000..27e47d2 --- /dev/null +++ b/server/source/system/authorities_menus.go @@ -0,0 +1,121 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenuAuthority = initOrderMenu + initOrderAuthority + +type initMenuAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenuAuthority, &initMenuAuthority{}) +} + +func (i *initMenuAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + return ctx, nil // do nothing +} + +func (i *initMenuAuthority) TableCreated(ctx context.Context) bool { + return false // always replace +} + +func (i *initMenuAuthority) InitializerName() string { + return "sys_menu_authorities" +} + +func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + initAuth := &initAuthority{} + authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") + } + + allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) + if !ok { + return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") + } + next = ctx + + // 构建菜单ID映射,方便快速查找 + menuMap := make(map[uint]sysModel.SysBaseMenu) + for _, menu := range allMenus { + menuMap[menu.ID] = menu + } + + // 为不同角色分配不同权限 + // 1. 超级管理员角色(888) - 拥有所有菜单权限 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil { + return next, errors.Wrap(err, "为超级管理员分配菜单失败") + } + + // 2. 普通用户角色(8881) - 仅拥有基础功能菜单 + // 仅选择部分父级菜单及其子菜单 + var menu8881 []sysModel.SysBaseMenu + + // 添加仪表盘、关于我们和个人信息菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") { + menu8881 = append(menu8881, menu) + } + } + + if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { + return next, errors.Wrap(err, "为普通用户分配菜单失败") + } + + // 3. 测试角色(9528) - 拥有部分菜单权限 + var menu9528 []sysModel.SysBaseMenu + + // 添加所有父级菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 { + menu9528 = append(menu9528, menu) + } + } + + // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单 + for _, menu := range allMenus { + parentName := "" + if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" { + parentName = menuMap[menu.ParentId].Name + } + + if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example") { + menu9528 = append(menu9528, menu) + } + } + + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil { + return next, errors.Wrap(err, "为测试角色分配菜单失败") + } + + return next, nil +} + +func (i *initMenuAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + auth := &sysModel.SysAuthority{} + if ret := db.Model(auth). + Where("authority_id = ?", 9528).Preload("SysBaseMenus").Find(auth); ret != nil { + if ret.Error != nil { + return false + } + return len(auth.SysBaseMenus) > 0 + } + return false +} diff --git a/server/source/system/authority.go b/server/source/system/authority.go new file mode 100644 index 0000000..95dcffd --- /dev/null +++ b/server/source/system/authority.go @@ -0,0 +1,89 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "git.echol.cn/loser/st/server/utils" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderAuthority = initOrderCasbin + 1 + +type initAuthority struct{} + +// auto run +func init() { + system.RegisterInit(initOrderAuthority, &initAuthority{}) +} + +func (i *initAuthority) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysAuthority{}) +} + +func (i *initAuthority) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysAuthority{}) +} + +func (i *initAuthority) InitializerName() string { + return sysModel.SysAuthority{}.TableName() +} + +func (i *initAuthority) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []sysModel.SysAuthority{ + {AuthorityId: 888, AuthorityName: "普通用户", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 9528, AuthorityName: "测试角色", ParentId: utils.Pointer[uint](0), DefaultRouter: "dashboard"}, + {AuthorityId: 8881, AuthorityName: "普通用户子角色", ParentId: utils.Pointer[uint](888), DefaultRouter: "dashboard"}, + } + + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", sysModel.SysAuthority{}.TableName()) + } + // data authority + if err := db.Model(&entities[0]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 888}, + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[0]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + if err := db.Model(&entities[1]).Association("DataAuthorityId").Replace( + []*sysModel.SysAuthority{ + {AuthorityId: 9528}, + {AuthorityId: 8881}, + }); err != nil { + return ctx, errors.Wrapf(err, "%s表数据初始化失败!", + db.Model(&entities[1]).Association("DataAuthorityId").Relationship.JoinTable.Name) + } + + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initAuthority) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("authority_id = ?", "8881"). + First(&sysModel.SysAuthority{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go new file mode 100644 index 0000000..6fb9446 --- /dev/null +++ b/server/source/system/casbin.go @@ -0,0 +1,348 @@ +package system + +import ( + "context" + + "git.echol.cn/loser/st/server/service/system" + adapter "github.com/casbin/gorm-adapter/v3" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderCasbin = initOrderApiIgnore + 1 + +type initCasbin struct{} + +// auto run +func init() { + system.RegisterInit(initOrderCasbin, &initCasbin{}) +} + +func (i *initCasbin) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&adapter.CasbinRule{}) +} + +func (i *initCasbin) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&adapter.CasbinRule{}) +} + +func (i *initCasbin) InitializerName() string { + var entity adapter.CasbinRule + return entity.TableName() +} + +func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + entities := []adapter.CasbinRule{ + {Ptype: "p", V0: "888", V1: "/user/admin_register", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLog", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/deleteLoginLogByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/findLoginLog", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysLoginLog/getLoginLogList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysApiToken/createApiToken", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysApiToken/getApiTokenList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysApiToken/deleteApiToken", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/deleteApisByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/api/syncApi", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/getApiGroups", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/api/enterSyncApi", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/api/ignoreApi", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/authority/copyAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/updateAuthority", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authority/setDataAuthority", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/menu/getBaseMenuById", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/user/getUserInfo", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/user/setUserInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/deleteUser", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setUserAuthorities", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/resetPassword", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/user/setSelfSetting", V2: "PUT"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/findFile", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinueFinish", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/breakpointContinue", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/removeChunk", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/system/getServerInfo", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/skills/getTools", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/skills/getSkillList", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getSkillDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveSkill", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveScript", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveResource", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createReference", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getReference", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveReference", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/createTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/getGlobalConstraint", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/skills/saveGlobalConstraint", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/customer/customerList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/autoCode/getDB", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getMeta", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/preview", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTables", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getColumn", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/rollback", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getSysHistory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getTemplates", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/delPackage", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/createPlug", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/removePlugin", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/getPluginList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpList", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/createSysDictionaryDetail", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getSysDictionaryDetailList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/deleteSysDictionaryDetail", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryTreeListByType", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryDetailsByParent", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/getDictionaryPath", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysDictionary/findSysDictionary", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/updateSysDictionary", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/importSysDictionary", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/createSysOperationRecord", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/getSysOperationRecordList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecord", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysOperationRecord/deleteSysOperationRecordByIds", V2: "DELETE"}, + + {Ptype: "p", V0: "888", V1: "/email/emailTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/email/sendEmail", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/simpleUploader/upload", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/checkFileMd5", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/simpleUploader/mergeFileMd5", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/authorityBtn/setAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/getAuthorityBtn", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/authorityBtn/canRemoveAuthorityBtn", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/createSysExportTemplate", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplate", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/deleteSysExportTemplateByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/updateSysExportTemplate", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/findSysExportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/previewSQL", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysError/createSysError", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/info/deleteInfoByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/info/updateInfo", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/info/findInfo", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/info/getInfoList", V2: "GET"}, + + {Ptype: "p", V0: "888", V1: "/sysParams/createSysParams", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParams", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/deleteSysParamsByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysParams/updateSysParams", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysParams/findSysParams", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParamsList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysParams/getSysParam", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/getCategoryList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"}, + + {Ptype: "p", V0: "888", V1: "/sysVersion/findSysVersion", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"}, + + {Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/api/getAllApis", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/authority/setDataAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "8881", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "8881", V1: "/user/getUserInfo", V2: "GET"}, + + {Ptype: "p", V0: "9528", V1: "/user/admin_register", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/createApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getApiById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/deleteApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/updateApi", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/api/getAllApis", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/authority/createAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/deleteAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/getAuthorityList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/authority/setDataAuthority", V2: "POST"}, + + {Ptype: "p", V0: "9528", V1: "/menu/getMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuTree", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/addMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getMenuAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/deleteBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/updateBaseMenu", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/menu/getBaseMenuById", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/changePassword", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/setUserAuthority", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/upload", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/getFileList", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/deleteFile", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/editFileName", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/fileUploadAndDownload/importURL", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/updateCasbin", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/casbin/getPolicyPathByAuthorityId", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/jwt/jsonInBlacklist", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/getSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/system/setSystemConfig", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "PUT"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/customer/customer", V2: "DELETE"}, + {Ptype: "p", V0: "9528", V1: "/customer/customerList", V2: "GET"}, + {Ptype: "p", V0: "9528", V1: "/autoCode/createTemp", V2: "POST"}, + {Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "Casbin 表 ("+i.InitializerName()+") 数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initCasbin) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where(adapter.CasbinRule{Ptype: "p", V0: "9528", V1: "/user/getUserInfo", V2: "GET"}). + First(&adapter.CasbinRule{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/dictionary.go b/server/source/system/dictionary.go new file mode 100644 index 0000000..071e5b1 --- /dev/null +++ b/server/source/system/dictionary.go @@ -0,0 +1,72 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDict = initOrderCasbin + 1 + +type initDict struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDict, &initDict{}) +} + +func (i *initDict) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionary{}) +} + +func (i *initDict) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionary{}) +} + +func (i *initDict) InitializerName() string { + return sysModel.SysDictionary{}.TableName() +} + +func (i *initDict) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + True := true + entities := []sysModel.SysDictionary{ + {Name: "性别", Type: "gender", Status: &True, Desc: "性别字典"}, + {Name: "数据库int类型", Type: "int", Status: &True, Desc: "int类型对应的数据库类型"}, + {Name: "数据库时间日期类型", Type: "time.Time", Status: &True, Desc: "数据库时间日期类型"}, + {Name: "数据库浮点型", Type: "float64", Status: &True, Desc: "数据库浮点型"}, + {Name: "数据库字符串", Type: "string", Status: &True, Desc: "数据库字符串"}, + {Name: "数据库bool类型", Type: "bool", Status: &True, Desc: "数据库bool类型"}, + } + + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionary{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initDict) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("type = ?", "bool").First(&sysModel.SysDictionary{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/dictionary_detail.go b/server/source/system/dictionary_detail.go new file mode 100644 index 0000000..58d130c --- /dev/null +++ b/server/source/system/dictionary_detail.go @@ -0,0 +1,122 @@ +package system + +import ( + "context" + "fmt" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderDictDetail = initOrderDict + 1 + +type initDictDetail struct{} + +// auto run +func init() { + system.RegisterInit(initOrderDictDetail, &initDictDetail{}) +} + +func (i *initDictDetail) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysDictionaryDetail{}) +} + +func (i *initDictDetail) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysDictionaryDetail{}) +} + +func (i *initDictDetail) InitializerName() string { + return sysModel.SysDictionaryDetail{}.TableName() +} + +func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + dicts, ok := ctx.Value(new(initDict).InitializerName()).([]sysModel.SysDictionary) + if !ok { + return ctx, errors.Wrap(system.ErrMissingDependentContext, + fmt.Sprintf("未找到 %s 表初始化数据", sysModel.SysDictionary{}.TableName())) + } + True := true + dicts[0].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "男", Value: "1", Status: &True, Sort: 1}, + {Label: "女", Value: "2", Status: &True, Sort: 2}, + } + + dicts[1].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "smallint", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "mediumint", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "int", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "bigint", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "int2", Value: "5", Status: &True, Extend: "pgsql", Sort: 5}, + {Label: "int4", Value: "6", Status: &True, Extend: "pgsql", Sort: 6}, + {Label: "int6", Value: "7", Status: &True, Extend: "pgsql", Sort: 7}, + {Label: "int8", Value: "8", Status: &True, Extend: "pgsql", Sort: 8}, + } + + dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "date", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "timestamp", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5}, + } + dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "float", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3}, + {Label: "smallserial", Value: "4", Status: &True, Extend: "pgsql", Sort: 4}, + } + + dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "char", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, + {Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, + {Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, + {Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, + {Label: "text", Value: "4", Status: &True, Extend: "mysql", Sort: 4}, + {Label: "blob", Value: "5", Status: &True, Extend: "mysql", Sort: 5}, + {Label: "mediumblob", Value: "6", Status: &True, Extend: "mysql", Sort: 6}, + {Label: "mediumtext", Value: "7", Status: &True, Extend: "mysql", Sort: 7}, + {Label: "longblob", Value: "8", Status: &True, Extend: "mysql", Sort: 8}, + {Label: "longtext", Value: "9", Status: &True, Extend: "mysql", Sort: 9}, + } + + dicts[5].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ + {Label: "tinyint", Value: "1", Extend: "mysql", Status: &True}, + {Label: "bool", Value: "2", Extend: "pgsql", Status: &True}, + } + for _, dict := range dicts { + if err := db.Model(&dict).Association("SysDictionaryDetails"). + Replace(dict.SysDictionaryDetails); err != nil { + return ctx, errors.Wrap(err, sysModel.SysDictionaryDetail{}.TableName()+"表数据初始化失败!") + } + } + return ctx, nil +} + +func (i *initDictDetail) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var dict sysModel.SysDictionary + if err := db.Preload("SysDictionaryDetails"). + First(&dict, &sysModel.SysDictionary{Name: "数据库bool类型"}).Error; err != nil { + return false + } + return len(dict.SysDictionaryDetails) > 0 && dict.SysDictionaryDetails[0].Label == "tinyint" +} diff --git a/server/source/system/excel_template.go b/server/source/system/excel_template.go new file mode 100644 index 0000000..fac0b13 --- /dev/null +++ b/server/source/system/excel_template.go @@ -0,0 +1,76 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type initExcelTemplate struct{} + +const initOrderExcelTemplate = initOrderDictDetail + 1 + +// auto run +func init() { + system.RegisterInit(initOrderExcelTemplate, &initExcelTemplate{}) +} + +func (i *initExcelTemplate) InitializerName() string { + return "sys_export_templates" +} + +func (i *initExcelTemplate) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysExportTemplate{}) +} + +func (i *initExcelTemplate) InitializeData(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + entities := []sysModel.SysExportTemplate{ + { + Name: "api", + TableName: "sys_apis", + TemplateID: "api", + TemplateInfo: `{ +"path":"路径", +"method":"方法(大写)", +"description":"方法介绍", +"api_group":"方法分组" +}`, + }, + } + if err := db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, "sys_export_templates"+"表数据初始化失败!") + } + next := context.WithValue(ctx, i.InitializerName(), entities) + return next, nil +} + +func (i *initExcelTemplate) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.First(&sysModel.SysExportTemplate{}).Error, gorm.ErrRecordNotFound) { + return false + } + return true +} diff --git a/server/source/system/menu.go b/server/source/system/menu.go new file mode 100644 index 0000000..b1ccbde --- /dev/null +++ b/server/source/system/menu.go @@ -0,0 +1,138 @@ +package system + +import ( + "context" + + . "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderMenu = initOrderAuthority + 1 + +type initMenu struct{} + +// auto run +func init() { + system.RegisterInit(initOrderMenu, &initMenu{}) +} + +func (i *initMenu) InitializerName() string { + return SysBaseMenu{}.TableName() +} + +func (i *initMenu) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate( + &SysBaseMenu{}, + &SysBaseMenuParameter{}, + &SysBaseMenuBtn{}, + ) +} + +func (i *initMenu) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + m := db.Migrator() + return m.HasTable(&SysBaseMenu{}) && + m.HasTable(&SysBaseMenuParameter{}) && + m.HasTable(&SysBaseMenuBtn{}) +} + +func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + // 定义所有菜单 + allMenus := []SysBaseMenu{ + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}}, + {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, + {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}}, + } + + // 先创建父级菜单(ParentId = 0 的菜单) + if err = db.Create(&allMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!") + } + + // 建立菜单映射 - 通过Name查找已创建的菜单及其ID + menuNameMap := make(map[string]uint) + for _, menu := range allMenus { + menuNameMap[menu.Name] = menu.ID + } + + // 定义子菜单,并设置正确的ParentId + childMenus := []SysBaseMenu{ + // superAdmin子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, + + // example子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, + + // systemTools子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "loginLog", Name: "loginLog", Component: "view/systemTools/loginLog/index.vue", Sort: 5, Meta: Meta{Title: "登录日志", Icon: "monitor"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "apiToken", Name: "apiToken", Component: "view/systemTools/apiToken/index.vue", Sort: 6, Meta: Meta{Title: "API Token", Icon: "key"}}, + {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "skills", Name: "skills", Component: "view/systemTools/skills/index.vue", Sort: 6, Meta: Meta{Title: "Skills管理", Icon: "document"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 9, Meta: Meta{Title: "错误日志", Icon: "warn"}}, + + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, + } + + // 创建子菜单 + if err = db.Create(&childMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!") + } + + // 组合所有菜单作为返回结果 + allEntities := append(allMenus, childMenus...) + next = context.WithValue(ctx, i.InitializerName(), allEntities) + return next, nil +} + +func (i *initMenu) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + if errors.Is(db.Where("path = ?", "autoPkg").First(&SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return true +} diff --git a/server/source/system/user.go b/server/source/system/user.go new file mode 100644 index 0000000..1563368 --- /dev/null +++ b/server/source/system/user.go @@ -0,0 +1,107 @@ +package system + +import ( + "context" + + sysModel "git.echol.cn/loser/st/server/model/system" + "git.echol.cn/loser/st/server/service/system" + "git.echol.cn/loser/st/server/utils" + "github.com/google/uuid" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +const initOrderUser = initOrderAuthority + 1 + +type initUser struct{} + +// auto run +func init() { + system.RegisterInit(initOrderUser, &initUser{}) +} + +func (i *initUser) MigrateTable(ctx context.Context) (context.Context, error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + return ctx, db.AutoMigrate(&sysModel.SysUser{}) +} + +func (i *initUser) TableCreated(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + return db.Migrator().HasTable(&sysModel.SysUser{}) +} + +func (i *initUser) InitializerName() string { + return sysModel.SysUser{}.TableName() +} + +func (i *initUser) InitializeData(ctx context.Context) (next context.Context, err error) { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return ctx, system.ErrMissingDBContext + } + + ap := ctx.Value("adminPassword") + apStr, ok := ap.(string) + if !ok { + apStr = "123456" + } + + password := utils.BcryptHash(apStr) + adminPassword := utils.BcryptHash(apStr) + + entities := []sysModel.SysUser{ + { + UUID: uuid.New(), + Username: "admin", + Password: adminPassword, + NickName: "Mr.奇淼", + HeaderImg: "https://qmplusimg.henrongyi.top/gva_header.jpg", + AuthorityId: 888, + Phone: "17611111111", + Email: "333333333@qq.com", + }, + { + UUID: uuid.New(), + Username: "a303176530", + Password: password, + NickName: "用户1", + HeaderImg: "https://qmplusimg.henrongyi.top/1572075907logo.png", + AuthorityId: 9528, + Phone: "17611111111", + Email: "333333333@qq.com"}, + } + if err = db.Create(&entities).Error; err != nil { + return ctx, errors.Wrap(err, sysModel.SysUser{}.TableName()+"表数据初始化失败!") + } + next = context.WithValue(ctx, i.InitializerName(), entities) + authorityEntities, ok := ctx.Value(new(initAuthority).InitializerName()).([]sysModel.SysAuthority) + if !ok { + return next, errors.Wrap(system.ErrMissingDependentContext, "创建 [用户-权限] 关联失败, 未找到权限表初始化数据") + } + if err = db.Model(&entities[0]).Association("Authorities").Replace(authorityEntities); err != nil { + return next, err + } + if err = db.Model(&entities[1]).Association("Authorities").Replace(authorityEntities[:1]); err != nil { + return next, err + } + return next, err +} + +func (i *initUser) DataInserted(ctx context.Context) bool { + db, ok := ctx.Value("db").(*gorm.DB) + if !ok { + return false + } + var record sysModel.SysUser + if errors.Is(db.Where("username = ?", "a303176530"). + Preload("Authorities").First(&record).Error, gorm.ErrRecordNotFound) { // 判断是否存在数据 + return false + } + return len(record.Authorities) > 0 && record.Authorities[0].AuthorityId == 888 +} diff --git a/server/task/clearTable.go b/server/task/clearTable.go new file mode 100644 index 0000000..e138a11 --- /dev/null +++ b/server/task/clearTable.go @@ -0,0 +1,52 @@ +package task + +import ( + "errors" + "fmt" + "time" + + "git.echol.cn/loser/st/server/model/common" + + "gorm.io/gorm" +) + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: ClearTable +//@description: 清理数据库表数据 +//@param: db(数据库对象) *gorm.DB, tableName(表名) string, compareField(比较字段) string, interval(间隔) string +//@return: error + +func ClearTable(db *gorm.DB) error { + var ClearTableDetail []common.ClearDB + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "sys_operation_records", + CompareField: "created_at", + Interval: "2160h", + }) + + ClearTableDetail = append(ClearTableDetail, common.ClearDB{ + TableName: "jwt_blacklists", + CompareField: "created_at", + Interval: "168h", + }) + + if db == nil { + return errors.New("db Cannot be empty") + } + + for _, detail := range ClearTableDetail { + duration, err := time.ParseDuration(detail.Interval) + if err != nil { + return err + } + if duration < 0 { + return errors.New("parse duration < 0") + } + err = db.Debug().Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", detail.TableName, detail.CompareField), time.Now().Add(-duration)).Error + if err != nil { + return err + } + } + return nil +} diff --git a/server/utils/app_jwt.go b/server/utils/app_jwt.go new file mode 100644 index 0000000..2d73851 --- /dev/null +++ b/server/utils/app_jwt.go @@ -0,0 +1,84 @@ +package utils + +import ( + "errors" + "time" + + "git.echol.cn/loser/st/server/global" + "github.com/golang-jwt/jwt/v5" +) + +const ( + UserTypeApp = "app" // 前台用户类型标识 +) + +// AppJWTClaims 前台用户 JWT Claims +type AppJWTClaims struct { + UserID uint `json:"userId"` + Username string `json:"username"` + UserType string `json:"userType"` // 用户类型标识 + jwt.RegisteredClaims +} + +// CreateAppToken 创建前台用户 Token(有效期 7 天) +func CreateAppToken(userID uint, username string) (tokenString string, expiresAt int64, err error) { + // Token 有效期为 7 天 + expiresTime := time.Now().Add(7 * 24 * time.Hour) + expiresAt = expiresTime.Unix() + + claims := AppJWTClaims{ + UserID: userID, + Username: username, + UserType: UserTypeApp, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: global.GVA_CONFIG.JWT.Issuer, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey)) + return +} + +// CreateAppRefreshToken 创建前台用户刷新 Token(有效期更长) +func CreateAppRefreshToken(userID uint, username string) (tokenString string, expiresAt int64, err error) { + // 刷新 Token 有效期为 7 天 + expiresTime := time.Now().Add(7 * 24 * time.Hour) + expiresAt = expiresTime.Unix() + + claims := AppJWTClaims{ + UserID: userID, + Username: username, + UserType: UserTypeApp, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: global.GVA_CONFIG.JWT.Issuer, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey)) + return +} + +// ParseAppToken 解析前台用户 Token +func ParseAppToken(tokenString string) (*AppJWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &AppJWTClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(global.GVA_CONFIG.JWT.SigningKey), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*AppJWTClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/server/utils/ast/ast.go b/server/utils/ast/ast.go new file mode 100644 index 0000000..8e30098 --- /dev/null +++ b/server/utils/ast/ast.go @@ -0,0 +1,410 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + + "git.echol.cn/loser/st/server/model/system" +) + +// AddImport 增加 import 方法 +func AddImport(astNode ast.Node, imp string) { + impStr := fmt.Sprintf("\"%s\"", imp) + ast.Inspect(astNode, func(node ast.Node) bool { + if genDecl, ok := node.(*ast.GenDecl); ok { + if genDecl.Tok == token.IMPORT { + for i := range genDecl.Specs { + if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok { + if impNode.Path.Value == impStr { + return false + } + } + } + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: impStr, + }, + }) + } + } + return true + }) +} + +// FindFunction 查询特定function方法 +func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl { + var funcDeclP *ast.FuncDecl + ast.Inspect(astNode, func(node ast.Node) bool { + if funcDecl, ok := node.(*ast.FuncDecl); ok { + if funcDecl.Name.String() == FunctionName { + funcDeclP = funcDecl + return false + } + } + return true + }) + return funcDeclP +} + +// FindArray 查询特定数组方法 +func FindArray(astNode ast.Node, identName, selectorExprName string) *ast.CompositeLit { + var assignStmt *ast.CompositeLit + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Rhs { + if exprType, ok := expr.(*ast.CompositeLit); ok { + if arrayType, ok := exprType.Type.(*ast.ArrayType); ok { + sel, ok1 := arrayType.Elt.(*ast.SelectorExpr) + x, ok2 := sel.X.(*ast.Ident) + if ok1 && ok2 && x.Name == identName && sel.Sel.Name == selectorExprName { + assignStmt = exprType + return false + } + } + } + } + } + return true + }) + return assignStmt +} + +func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr { + var menuElts []ast.Expr + for i := range menus { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ParentId"}, + Value: &ast.BasicLit{Kind: token.INT, Value: "0"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Hidden"}, + Value: &ast.Ident{Name: "false"}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Component"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Component)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Sort"}, + Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", menus[i].Sort)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Meta"}, + Value: &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "Meta"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Title"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Title)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Icon"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", menus[i].Icon)}, + }, + }, + }, + }, + } + + // 添加菜单参数 + if len(menus[i].Parameters) > 0 { + var paramElts []ast.Expr + for _, param := range menus[i].Parameters { + paramElts = append(paramElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Type"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Type)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Key"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Key)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Value"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Value)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Parameters"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + }, + Elts: paramElts, + }, + }) + } + + // 添加菜单按钮 + if len(menus[i].MenuBtn) > 0 { + var btnElts []ast.Expr + for _, btn := range menus[i].MenuBtn { + btnElts = append(btnElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Desc"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Desc)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "MenuBtn"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + }, + Elts: btnElts, + }, + }) + } + + menuElts = append(menuElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &menuElts +} + +func CreateApiStructAst(apis []system.SysApi) *[]ast.Expr { + var apiElts []ast.Expr + for i := range apis { + elts := []ast.Expr{ // 结构体的字段 + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Path"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Path)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Description"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Description)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "ApiGroup"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].ApiGroup)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Method"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", apis[i].Method)}, + }, + } + apiElts = append(apiElts, &ast.CompositeLit{ + Type: nil, + Elts: elts, + }) + } + return &apiElts +} + +// CheckImport 检查是否存在Import +func CheckImport(file *ast.File, importPath string) bool { + for _, imp := range file.Imports { + // Remove quotes around the import path + path := imp.Path.Value[1 : len(imp.Path.Value)-1] + + if path == importPath { + return true + } + } + + return false +} + +func clearPosition(astNode ast.Node) { + ast.Inspect(astNode, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.Ident: + // 清除位置信息 + node.NamePos = token.NoPos + case *ast.CallExpr: + // 清除位置信息 + node.Lparen = token.NoPos + node.Rparen = token.NoPos + case *ast.BasicLit: + // 清除位置信息 + node.ValuePos = token.NoPos + case *ast.SelectorExpr: + // 清除位置信息 + node.Sel.NamePos = token.NoPos + case *ast.BinaryExpr: + node.OpPos = token.NoPos + case *ast.UnaryExpr: + node.OpPos = token.NoPos + case *ast.StarExpr: + node.Star = token.NoPos + } + return true + }) +} + +func CreateStmt(statement string) *ast.ExprStmt { + expr, err := parser.ParseExpr(statement) + if err != nil { + log.Fatal(err) + } + clearPosition(expr) + return &ast.ExprStmt{X: expr} +} + +func IsBlockStmt(node ast.Node) bool { + _, ok := node.(*ast.BlockStmt) + return ok +} + +func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool { + exists := false + ast.Inspect(block, func(n ast.Node) bool { + switch node := n.(type) { + case *ast.AssignStmt: + for _, expr := range node.Lhs { + if ident, ok := expr.(*ast.Ident); ok && ident.Name == varName { + exists = true + return false + } + } + } + return true + }) + return exists +} + +func CreateDictionaryStructAst(dictionaries []system.SysDictionary) *[]ast.Expr { + var dictElts []ast.Expr + for i := range dictionaries { + statusStr := "true" + if dictionaries[i].Status != nil && !*dictionaries[i].Status { + statusStr = "false" + } + + elts := []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Type"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Type)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Status"}, + Value: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "utils"}, + Sel: &ast.Ident{Name: "Pointer"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: statusStr}, + }, + }, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Desc"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Desc)}, + }, + } + + if len(dictionaries[i].SysDictionaryDetails) > 0 { + var detailElts []ast.Expr + for _, detail := range dictionaries[i].SysDictionaryDetails { + detailStatusStr := "true" + if detail.Status != nil && !*detail.Status { + detailStatusStr = "false" + } + + detailElts = append(detailElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionaryDetail"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Label"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Label)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Value"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Value)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Extend"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Extend)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Status"}, + Value: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "utils"}, + Sel: &ast.Ident{Name: "Pointer"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: detailStatusStr}, + }, + }, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Sort"}, + Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", detail.Sort)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "SysDictionaryDetails"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionaryDetail"}, + }}, + Elts: detailElts, + }, + }) + } + + dictElts = append(dictElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionary"}, + }, + Elts: elts, + }) + } + return &dictElts +} diff --git a/server/utils/ast/ast_auto_enter.go b/server/utils/ast/ast_auto_enter.go new file mode 100644 index 0000000..382f554 --- /dev/null +++ b/server/utils/ast/ast_auto_enter.go @@ -0,0 +1,47 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +func ImportForAutoEnter(path string, funcName string, code string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + ast.Inspect(astFile, func(node ast.Node) bool { + if typeSpec, ok := node.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == funcName { + if st, ok := typeSpec.Type.(*ast.StructType); ok { + for i := range st.Fields.List { + if t, ok := st.Fields.List[i].Type.(*ast.Ident); ok { + if t.Name == code { + return false + } + } + } + sn := &ast.Field{ + Type: &ast.Ident{Name: code}, + } + st.Fields.List = append(st.Fields.List, sn) + } + } + } + return true + }) + var out []byte + bf := bytes.NewBuffer(out) + err = printer.Fprint(bf, fileSet, astFile) + if err != nil { + return + } + _ = os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/server/utils/ast/ast_enter.go b/server/utils/ast/ast_enter.go new file mode 100644 index 0000000..b7fc85a --- /dev/null +++ b/server/utils/ast/ast_enter.go @@ -0,0 +1,182 @@ +package ast + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "log" + "os" + "strconv" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Visitor struct { + ImportCode string + StructName string + PackageName string + GroupName string +} + +func (vi *Visitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.GenDecl: + // 查找有没有import context包 + // Notice:没有考虑没有import任何包的情况 + if n.Tok == token.IMPORT && vi.ImportCode != "" { + vi.addImport(n) + // 不需要再遍历子树 + return nil + } + if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" { + vi.addStruct(n) + return nil + } + case *ast.FuncDecl: + if n.Name.Name == "Routers" { + vi.addFuncBodyVar(n) + return nil + } + + } + return vi +} + +func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor { + for i := range genDecl.Specs { + switch n := genDecl.Specs[i].(type) { + case *ast.TypeSpec: + if strings.Index(n.Name.Name, "Group") > -1 { + switch t := n.Type.(type) { + case *ast.StructType: + f := &ast.Field{ + Names: []*ast.Ident{ + { + Name: vi.StructName, + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.StructName, + }, + }, + }, + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: vi.PackageName, + }, + Sel: &ast.Ident{ + Name: vi.GroupName, + }, + }, + } + t.Fields.List = append(t.Fields.List, f) + } + } + } + } + return vi +} + +func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor { + // 是否已经import + hasImported := false + for _, v := range genDecl.Specs { + importSpec := v.(*ast.ImportSpec) + // 如果已经包含 + if importSpec.Path.Value == strconv.Quote(vi.ImportCode) { + hasImported = true + } + } + if !hasImported { + genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: strconv.Quote(vi.ImportCode), + }, + }) + } + return vi +} + +func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor { + hasVar := false + for _, v := range funDecl.Body.List { + switch varSpec := v.(type) { + case *ast.AssignStmt: + for i := range varSpec.Lhs { + switch nn := varSpec.Lhs[i].(type) { + case *ast.Ident: + if nn.Name == vi.PackageName+"Router" { + hasVar = true + } + } + } + } + } + if !hasVar { + assignStmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: vi.PackageName + "Router", + Obj: &ast.Object{ + Kind: ast.Var, + Name: vi.PackageName + "Router", + }, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "router", + }, + Sel: &ast.Ident{ + Name: "RouterGroupApp", + }, + }, + Sel: &ast.Ident{ + Name: cases.Title(language.English).String(vi.PackageName), + }, + }, + }, + } + funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1]) + index := 1 + copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:]) + funDecl.Body.List[index] = assignStmt + } + return vi +} + +func ImportReference(filepath, importCode, structName, packageName, groupName string) error { + fSet := token.NewFileSet() + fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments) + if err != nil { + return err + } + importCode = strings.TrimSpace(importCode) + v := &Visitor{ + ImportCode: importCode, + StructName: structName, + PackageName: packageName, + GroupName: groupName, + } + if importCode == "" { + ast.Print(fSet, fParser) + } + + ast.Walk(v, fParser) + + var output []byte + buffer := bytes.NewBuffer(output) + err = format.Node(buffer, fSet, fParser) + if err != nil { + log.Fatal(err) + } + // 写回数据 + return os.WriteFile(filepath, buffer.Bytes(), 0o600) +} diff --git a/server/utils/ast/ast_gorm.go b/server/utils/ast/ast_gorm.go new file mode 100644 index 0000000..b975632 --- /dev/null +++ b/server/utils/ast/ast_gorm.go @@ -0,0 +1,166 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" +) + +// AddRegisterTablesAst 自动为 gorm.go 注册一个自动迁移 +func AddRegisterTablesAst(path, funcName, pk, varName, dbName, model string) { + modelPk := fmt.Sprintf("git.echol.cn/loser/st/server/model/%s", pk) + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + AddImport(astFile, modelPk) + FuncNode := FindFunction(astFile, funcName) + if FuncNode != nil { + ast.Print(fileSet, FuncNode) + } + addDBVar(FuncNode.Body, varName, dbName) + addAutoMigrate(FuncNode.Body, varName, pk, model) + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(path, bf.Bytes(), 0666) +} + +// 增加一个 db库变量 +func addDBVar(astBody *ast.BlockStmt, varName, dbName string) { + if dbName == "" { + return + } + dbStr := fmt.Sprintf("\"%s\"", dbName) + + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if ident.Name == varName { + return + } + } + } + } + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: varName, + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: dbStr, + }, + }, + }, + }, + } + astBody.List = append([]ast.Stmt{assignNode}, astBody.List...) +} + +// 为db库变量增加 AutoMigrate 方法 +func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) { + if dbname == "" { + dbname = "db" + } + flag := true + ast.Inspect(astBody, func(node ast.Node) bool { + // 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑 + switch n := node.(type) { + case *ast.CallExpr: + // 判断是否找到了AutoMigrate语句 + if s, ok := n.Fun.(*ast.SelectorExpr); ok { + if x, ok := s.X.(*ast.Ident); ok { + if s.Sel.Name == "AutoMigrate" && x.Name == dbname { + flag = false + if !NeedAppendModel(n, pk, model) { + return false + } + // 判断已经找到了AutoMigrate语句 + n.Args = append(n.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }) + return false + } + } + } + } + return true + //然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条 + }) + + if flag { + exprStmt := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: dbname, + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + Args: []ast.Expr{ + &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: pk, + }, + Sel: &ast.Ident{ + Name: model, + }, + }, + }, + }, + }} + astBody.List = append(astBody.List, exprStmt) + } +} + +// NeedAppendModel 为automigrate增加实参 +func NeedAppendModel(callNode ast.Node, pk string, model string) bool { + flag := true + ast.Inspect(callNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.SelectorExpr: + if x, ok := n.X.(*ast.Ident); ok { + if n.Sel.Name == model && x.Name == pk { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/server/utils/ast/ast_init_test.go b/server/utils/ast/ast_init_test.go new file mode 100644 index 0000000..b75b475 --- /dev/null +++ b/server/utils/ast/ast_init_test.go @@ -0,0 +1,12 @@ +package ast + +import ( + "path/filepath" + + "git.echol.cn/loser/st/server/global" +) + +func init() { + global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("../../../") + global.GVA_CONFIG.AutoCode.Server = "server" +} diff --git a/server/utils/ast/ast_rollback.go b/server/utils/ast/ast_rollback.go new file mode 100644 index 0000000..6407c72 --- /dev/null +++ b/server/utils/ast/ast_rollback.go @@ -0,0 +1,174 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" + + "git.echol.cn/loser/st/server/global" +) + +func RollBackAst(pk, model string) { + RollGormBack(pk, model) + RollRouterBack(pk, model) +} + +func RollGormBack(pk, model string) { + + // 首先分析存在多少个ttt作为调用方的node块 + // 如果多个 仅仅删除对应块即可 + // 如果单个 那么还需要剔除import + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + var n *ast.CallExpr + var k int = -1 + var pkNum = 0 + ast.Inspect(astFile, func(node ast.Node) bool { + if node, ok := node.(*ast.CallExpr); ok { + for i := range node.Args { + pkOK := false + modelOK := false + ast.Inspect(node.Args[i], func(item ast.Node) bool { + if ii, ok := item.(*ast.Ident); ok { + if ii.Name == pk { + pkOK = true + pkNum++ + } + if ii.Name == model { + modelOK = true + } + } + if pkOK && modelOK { + n = node + k = i + } + return true + }) + } + } + return true + }) + if k > -1 { + n.Args = append(append([]ast.Expr{}, n.Args[:k]...), n.Args[k+1:]...) + } + if pkNum == 1 { + var imI int = -1 + var gp *ast.GenDecl + ast.Inspect(astFile, func(node ast.Node) bool { + if gen, ok := node.(*ast.GenDecl); ok { + for i := range gen.Specs { + if imspec, ok := gen.Specs[i].(*ast.ImportSpec); ok { + if imspec.Path.Value == "\"git.echol.cn/loser/st/server/model/"+pk+"\"" { + gp = gen + imI = i + return false + } + } + } + } + return true + }) + + if imI > -1 { + gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...) + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) + +} + +func RollRouterBack(pk, model string) { + + // 首先抓到所有的代码块结构 {} + // 分析结构中是否存在一个变量叫做 pk+Router + // 然后获取到代码块指针 对内部需要回滚的代码进行剔除 + path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go") + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + if err != nil { + fmt.Println(err) + } + + var block *ast.BlockStmt + var routerStmt *ast.FuncDecl + + ast.Inspect(astFile, func(node ast.Node) bool { + if n, ok := node.(*ast.FuncDecl); ok { + if n.Name.Name == "initBizRouter" { + routerStmt = n + } + } + + if n, ok := node.(*ast.BlockStmt); ok { + ast.Inspect(n, func(bNode ast.Node) bool { + if in, ok := bNode.(*ast.Ident); ok { + if in.Name == pk+"Router" { + block = n + return false + } + } + return true + }) + return true + } + return true + }) + var k int + for i := range block.List { + if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok { + ast.Inspect(stmtNode, func(node ast.Node) bool { + if n, ok := node.(*ast.Ident); ok { + if n.Name == "Init"+model+"Router" { + k = i + return false + } + } + return true + }) + } + } + + block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...) + + if len(block.List) == 1 { + // 说明这个块就没任何意义了 + block.List = nil + } + + for i, n := range routerStmt.Body.List { + if n, ok := n.(*ast.BlockStmt); ok { + if n.List == nil { + routerStmt.Body.List = append(append([]ast.Stmt{}, routerStmt.Body.List[:i]...), routerStmt.Body.List[i+1:]...) + i-- + } + } + } + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.Remove(path) + os.WriteFile(path, bf.Bytes(), 0666) +} diff --git a/server/utils/ast/ast_router.go b/server/utils/ast/ast_router.go new file mode 100644 index 0000000..86356b8 --- /dev/null +++ b/server/utils/ast/ast_router.go @@ -0,0 +1,135 @@ +package ast + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "strings" +) + +func AppendNodeToList(stmts []ast.Stmt, stmt ast.Stmt, index int) []ast.Stmt { + return append(stmts[:index], append([]ast.Stmt{stmt}, stmts[index:]...)...) +} + +func AddRouterCode(path, funcName, pk, model string) { + src, err := os.ReadFile(path) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, parser.ParseComments) + + if err != nil { + fmt.Println(err) + } + + FuncNode := FindFunction(astFile, funcName) + + pkName := strings.ToUpper(pk[:1]) + pk[1:] + routerName := fmt.Sprintf("%sRouter", pk) + modelName := fmt.Sprintf("Init%sRouter", model) + var bloctPre *ast.BlockStmt + for i := len(FuncNode.Body.List) - 1; i >= 0; i-- { + if block, ok := FuncNode.Body.List[i].(*ast.BlockStmt); ok { + bloctPre = block + } + } + ast.Print(fileSet, FuncNode) + if ok, b := needAppendRouter(FuncNode, pk); ok { + routerNode := + &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: routerName}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: "router"}, + Sel: &ast.Ident{Name: "RouterGroupApp"}, + }, + Sel: &ast.Ident{Name: pkName}, + }, + }, + }, + }, + } + + FuncNode.Body.List = AppendNodeToList(FuncNode.Body.List, routerNode, len(FuncNode.Body.List)-1) + bloctPre = routerNode + } else { + bloctPre = b + } + + if needAppendInit(FuncNode, routerName, modelName) { + bloctPre.List = append(bloctPre.List, + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: routerName}, + Sel: &ast.Ident{Name: modelName}, + }, + Args: []ast.Expr{ + &ast.Ident{ + Name: "privateGroup", + }, + &ast.Ident{ + Name: "publicGroup", + }, + }, + }, + }) + } + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + os.WriteFile(path, bf.Bytes(), 0666) +} + +func needAppendRouter(funcNode ast.Node, pk string) (bool, *ast.BlockStmt) { + flag := true + var block *ast.BlockStmt + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := node.(type) { + case *ast.BlockStmt: + for i := range n.List { + if assignNode, ok := n.List[i].(*ast.AssignStmt); ok { + if identNode, ok := assignNode.Lhs[0].(*ast.Ident); ok { + if identNode.Name == fmt.Sprintf("%sRouter", pk) { + flag = false + block = n + return false + } + } + } + } + + } + return true + }) + return flag, block +} + +func needAppendInit(funcNode ast.Node, routerName string, modelName string) bool { + flag := true + ast.Inspect(funcNode, func(node ast.Node) bool { + switch n := funcNode.(type) { + case *ast.CallExpr: + if selectNode, ok := n.Fun.(*ast.SelectorExpr); ok { + x, xok := selectNode.X.(*ast.Ident) + if xok && x.Name == routerName && selectNode.Sel.Name == modelName { + flag = false + return false + } + } + } + return true + }) + return flag +} diff --git a/server/utils/ast/ast_test.go b/server/utils/ast/ast_test.go new file mode 100644 index 0000000..3c9f65d --- /dev/null +++ b/server/utils/ast/ast_test.go @@ -0,0 +1,33 @@ +package ast + +import ( + "go/ast" + "go/parser" + "go/printer" + "go/token" + "os" + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestAst(t *testing.T) { + filename := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go") + fileSet := token.NewFileSet() + file, err := parser.ParseFile(fileSet, filename, nil, parser.ParseComments) + if err != nil { + t.Error(err) + return + } + err = ast.Print(fileSet, file) + if err != nil { + t.Error(err) + return + } + err = printer.Fprint(os.Stdout, token.NewFileSet(), file) + if err != nil { + panic(err) + } + +} diff --git a/server/utils/ast/ast_type.go b/server/utils/ast/ast_type.go new file mode 100644 index 0000000..43285c9 --- /dev/null +++ b/server/utils/ast/ast_type.go @@ -0,0 +1,53 @@ +package ast + +type Type string + +func (r Type) String() string { + return string(r) +} + +func (r Type) Group() string { + switch r { + case TypePackageApiEnter: + return "ApiGroup" + case TypePackageRouterEnter: + return "RouterGroup" + case TypePackageServiceEnter: + return "ServiceGroup" + case TypePackageApiModuleEnter: + return "ApiGroup" + case TypePackageRouterModuleEnter: + return "RouterGroup" + case TypePackageServiceModuleEnter: + return "ServiceGroup" + case TypePluginApiEnter: + return "api" + case TypePluginRouterEnter: + return "router" + case TypePluginServiceEnter: + return "service" + default: + return "" + } +} + +const ( + TypePackageApiEnter = "PackageApiEnter" // server/api/v1/enter.go + TypePackageRouterEnter = "PackageRouterEnter" // server/router/enter.go + TypePackageServiceEnter = "PackageServiceEnter" // server/service/enter.go + TypePackageApiModuleEnter = "PackageApiModuleEnter" // server/api/v1/{package}/enter.go + TypePackageRouterModuleEnter = "PackageRouterModuleEnter" // server/router/{package}/enter.go + TypePackageServiceModuleEnter = "PackageServiceModuleEnter" // server/service/{package}/enter.go + TypePackageInitializeGorm = "PackageInitializeGorm" // server/initialize/gorm_biz.go + TypePackageInitializeRouter = "PackageInitializeRouter" // server/initialize/router_biz.go + TypePluginGen = "PluginGen" // server/plugin/{package}/gen/main.go + TypePluginApiEnter = "PluginApiEnter" // server/plugin/{package}/enter.go + TypePluginInitializeV1 = "PluginInitializeV1" // server/initialize/plugin_biz_v1.go + TypePluginInitializeV2 = "PluginInitializeV2" // server/plugin/register.go + TypePluginRouterEnter = "PluginRouterEnter" // server/plugin/{package}/enter.go + TypePluginServiceEnter = "PluginServiceEnter" // server/plugin/{package}/enter.go + TypePluginInitializeApi = "PluginInitializeApi" // server/plugin/{package}/initialize/api.go + TypePluginInitializeGorm = "PluginInitializeGorm" // server/plugin/{package}/initialize/gorm.go + TypePluginInitializeMenu = "PluginInitializeMenu" // server/plugin/{package}/initialize/menu.go + TypePluginInitializeRouter = "PluginInitializeRouter" // server/plugin/{package}/initialize/router.go +) diff --git a/server/utils/ast/extract_func.go b/server/utils/ast/extract_func.go new file mode 100644 index 0000000..35260c3 --- /dev/null +++ b/server/utils/ast/extract_func.go @@ -0,0 +1,62 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" +) + +// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码 +// 返回:方法名、完整源码、起止行号 +func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) { + // 读取源文件 + src, readErr := os.ReadFile(filePath) + if readErr != nil { + err = fmt.Errorf("read file failed: %w", readErr) + return + } + + // 解析 AST + fset := token.NewFileSet() + file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments) + if parseErr != nil { + err = fmt.Errorf("parse file failed: %w", parseErr) + return + } + + // 在 AST 中定位包含指定行号的函数声明 + var target *ast.FuncDecl + ast.Inspect(file, func(n ast.Node) bool { + fd, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + s := fset.Position(fd.Pos()).Line + e := fset.Position(fd.End()).Line + if line >= s && line <= e { + target = fd + startLine = s + endLine = e + return false + } + return true + }) + + if target == nil { + err = fmt.Errorf("no function encloses line %d in %s", line, filePath) + return + } + + // 使用字节偏移精确提取源码片段(包含注释与原始格式) + start := fset.Position(target.Pos()).Offset + end := fset.Position(target.End()).Offset + if start < 0 || end > len(src) || start >= end { + err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src)) + return + } + source = string(src[start:end]) + name = target.Name.Name + return +} diff --git a/server/utils/ast/import.go b/server/utils/ast/import.go new file mode 100644 index 0000000..5de18a3 --- /dev/null +++ b/server/utils/ast/import.go @@ -0,0 +1,94 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" + "strings" +) + +type Import struct { + Base + ImportPath string // 导包路径 +} + +func NewImport(importPath string) *Import { + return &Import{ImportPath: importPath} +} + +func (a *Import) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + return a.Base.Parse(filename, writer) +} + +func (a *Import) Rollback(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + if len(v1.Specs) == 0 { + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } // 如果没有import声明,就删除, 如果不删除则会出现import() + break + } + } + } + } + return nil +} + +func (a *Import) Injection(file *ast.File) error { + if a.ImportPath == "" { + return nil + } + var has bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok != token.IMPORT { + break + } + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.ImportSpec) + if o2 && strings.HasSuffix(a.ImportPath, v2.Path.Value) { + has = true + break + } + } + if !has { + spec := &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + } + v1.Specs = append(v1.Specs, spec) + return nil + } + } + } + if !has { + decls := file.Decls + file.Decls = make([]ast.Decl, 0, len(file.Decls)+1) + decl := &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{ + &ast.ImportSpec{ + Path: &ast.BasicLit{Kind: token.STRING, Value: a.ImportPath}, + }, + }, + } + file.Decls = append(file.Decls, decl) + file.Decls = append(file.Decls, decls...) + } // 如果没有import声明,就创建一个, 主要要放在第一个 + return nil +} + +func (a *Import) Format(filename string, writer io.Writer, file *ast.File) error { + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/interfaces.go b/server/utils/ast/interfaces.go new file mode 100644 index 0000000..33ecc47 --- /dev/null +++ b/server/utils/ast/interfaces.go @@ -0,0 +1,17 @@ +package ast + +import ( + "go/ast" + "io" +) + +type Ast interface { + // Parse 解析文件/代码 + Parse(filename string, writer io.Writer) (file *ast.File, err error) + // Rollback 回滚 + Rollback(file *ast.File) error + // Injection 注入 + Injection(file *ast.File) error + // Format 格式化输出 + Format(filename string, writer io.Writer, file *ast.File) error +} diff --git a/server/utils/ast/interfaces_base.go b/server/utils/ast/interfaces_base.go new file mode 100644 index 0000000..a470a23 --- /dev/null +++ b/server/utils/ast/interfaces_base.go @@ -0,0 +1,77 @@ +package ast + +import ( + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + "path" + "path/filepath" + "strings" + + "git.echol.cn/loser/st/server/global" + "github.com/pkg/errors" +) + +type Base struct{} + +func (a *Base) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + fileSet := token.NewFileSet() + if writer != nil { + file, err = parser.ParseFile(fileSet, filename, nil, parser.ParseComments) + } else { + file, err = parser.ParseFile(fileSet, filename, writer, parser.ParseComments) + } + if err != nil { + return nil, errors.Wrapf(err, "[filepath:%s]打开/解析文件失败!", filename) + } + return file, nil +} + +func (a *Base) Rollback(file *ast.File) error { + return nil +} + +func (a *Base) Injection(file *ast.File) error { + return nil +} + +func (a *Base) Format(filename string, writer io.Writer, file *ast.File) error { + fileSet := token.NewFileSet() + if writer == nil { + open, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, 0666) + defer open.Close() + if err != nil { + return errors.Wrapf(err, "[filepath:%s]打开文件失败!", filename) + } + writer = open + } + err := format.Node(writer, fileSet, file) + if err != nil { + return errors.Wrapf(err, "[filepath:%s]注入失败!", filename) + } + return nil +} + +// RelativePath 绝对路径转相对路径 +func (a *Base) RelativePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + hasServer := strings.Index(filePath, server) + if hasServer != -1 { + filePath = strings.TrimPrefix(filePath, server) + keys := strings.Split(filePath, string(filepath.Separator)) + filePath = path.Join(keys...) + } + return filePath +} + +// AbsolutePath 相对路径转绝对路径 +func (a *Base) AbsolutePath(filePath string) string { + server := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + keys := strings.Split(filePath, "/") + filePath = filepath.Join(keys...) + filePath = filepath.Join(server, filePath) + return filePath +} diff --git a/server/utils/ast/package_enter.go b/server/utils/ast/package_enter.go new file mode 100644 index 0000000..f4b6305 --- /dev/null +++ b/server/utils/ast/package_enter.go @@ -0,0 +1,85 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageEnter 模块化入口 +type PackageEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + PackageStructName string // 包结构体名称 +} + +func (a *PackageEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageEnter) Rollback(file *ast.File) error { + // 无需回滚 + return nil +} + +func (a *PackageEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + return true + } + + for _, spec := range genDecl.Specs { + typeSpec, specok := spec.(*ast.TypeSpec) + if !specok || typeSpec.Name.Name != a.Type.Group() { + continue + } + + structType, structTypeOK := typeSpec.Type.(*ast.StructType) + if !structTypeOK { + continue + } + + for _, field := range structType.Fields.List { + if len(field.Names) == 1 && field.Names[0].Name == a.StructName { + return true + } + } + + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.PackageStructName}, + }, + } + structType.Fields.List = append(structType.Fields.List, field) + return false + } + + return true + }) + return nil +} + +func (a *PackageEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/package_enter_test.go b/server/utils/ast/package_enter_test.go new file mode 100644 index 0000000..17cfb56 --- /dev/null +++ b/server/utils/ast/package_enter_test.go @@ -0,0 +1,155 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPackageEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup回滚", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleRouterGroup回滚", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup回滚", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + PackageStructName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试ExampleApiGroup注入", + fields: fields{ + Type: TypePackageApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/api/v1/example"`, + StructName: "ExampleApiGroup", + PackageName: "example", + PackageStructName: "ApiGroup", + }, + }, + { + name: "测试ExampleRouterGroup注入", + fields: fields{ + Type: TypePackageRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/router/example"`, + StructName: "Example", + PackageName: "example", + PackageStructName: "RouterGroup", + }, + wantErr: false, + }, + { + name: "测试ExampleServiceGroup注入", + fields: fields{ + Type: TypePackageServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/service/example"`, + StructName: "ExampleServiceGroup", + PackageName: "example", + PackageStructName: "ServiceGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + PackageStructName: tt.fields.PackageStructName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_initialize_gorm.go b/server/utils/ast/package_initialize_gorm.go new file mode 100644 index 0000000..594f714 --- /dev/null +++ b/server/utils/ast/package_initialize_gorm.go @@ -0,0 +1,196 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeGorm 包初始化gorm +type PackageInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + Business string // 业务库 gva => gva, 不要传"gva" + StructName string // 结构体名称 + PackageName string // 包名 + RelativePath string // 相对路径 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PackageInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeGorm) Rollback(file *ast.File) error { + packageNameNum := 0 + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 删除结构体参数 + for i := 0; i < len(callExpr.Args); i++ { + if com, comok := callExpr.Args[i].(*ast.CompositeLit); comok { + if selector, exprok := com.Type.(*ast.SelectorExpr); exprok { + if x, identok := selector.X.(*ast.Ident); identok { + if x.Name == a.PackageName { + packageNameNum++ + if selector.Sel.Name == a.StructName { + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + i-- + } + } + } + } + } + } + return true + }) + + if packageNameNum == 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + return nil +} + +func (a *PackageInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + bizModelDecl := FindFunction(file, "bizModel") + if bizModelDecl != nil { + a.addDbVar(bizModelDecl.Body) + } + // 寻找目标结构 + ast.Inspect(file, func(n ast.Node) bool { + // 总调用的db变量根据business来决定 + varDB := a.Business + "Db" + + if a.Business == "" { + varDB = "db" + } + + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + // 检查是不是 db.AutoMigrate() 方法 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + + // 检查调用方是不是 db + ident, ok := selExpr.X.(*ast.Ident) + if !ok || ident.Name != varDB { + return true + } + + // 添加结构体参数 + callExpr.Args = append(callExpr.Args, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: ast.NewIdent(a.PackageName), + Sel: ast.NewIdent(a.StructName), + }, + }) + return true + }) + return nil +} + +func (a *PackageInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +// 创建businessDB变量 +func (a *PackageInitializeGorm) addDbVar(astBody *ast.BlockStmt) { + for i := range astBody.List { + if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok { + if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok { + if (a.Business == "" && ident.Name == "db") || ident.Name == a.Business+"Db" { + return + } + } + } + } + + // 添加 businessDb := global.GetGlobalDBByDBName("business") 变量 + assignNode := &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{ + Name: a.Business + "Db", + }, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: "global", + }, + Sel: &ast.Ident{ + Name: "GetGlobalDBByDBName", + }, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: fmt.Sprintf("\"%s\"", a.Business), + }, + }, + }, + }, + } + + // 添加 businessDb.AutoMigrate() 方法 + autoMigrateCall := &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{ + Name: a.Business + "Db", + }, + Sel: &ast.Ident{ + Name: "AutoMigrate", + }, + }, + }, + } + + returnNode := astBody.List[len(astBody.List)-1] + astBody.List = append(astBody.List[:len(astBody.List)-1], assignNode, autoMigrateCall, returnNode) +} diff --git a/server/utils/ast/package_initialize_gorm_test.go b/server/utils/ast/package_initialize_gorm_test.go new file mode 100644 index 0000000..282df53 --- /dev/null +++ b/server/utils/ast/package_initialize_gorm_test.go @@ -0,0 +1,172 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPackageInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 注入", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &example.ExaFileUploadAndDownload{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 &example.ExaCustomer{} 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: false, + }, + }, + { + name: "测试 new(example.ExaFileUploadAndDownload) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaFileUploadAndDownload", + PackageName: "example", + IsNew: true, + }, + }, + { + name: "测试 new(example.ExaCustomer) 回滚", + fields: fields{ + Type: TypePackageInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/model/example"`, + StructName: "ExaCustomer", + PackageName: "example", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_initialize_router.go b/server/utils/ast/package_initialize_router.go new file mode 100644 index 0000000..9fe4429 --- /dev/null +++ b/server/utils/ast/package_initialize_router.go @@ -0,0 +1,150 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/token" + "io" +) + +// PackageInitializeRouter 包初始化路由 +// ModuleName := PackageName.AppName.GroupName +// ModuleName.FunctionName(RouterGroupName) +type PackageInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + FunctionName string // 函数名 + RouterGroupName string // 路由分组名称 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PackageInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + exprNum := 0 + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + for ii, stmt := range funcDecl.Body.List[i].(*ast.BlockStmt).List { + // 检查语句是否为 *ast.ExprStmt + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + // 检查表达式是否为 *ast.CallExpr + callExpr, ok := exprStmt.X.(*ast.CallExpr) + if !ok { + continue + } + // 检查是否调用了我们正在寻找的函数 + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + // 检查调用的函数是否为 systemRouter.InitApiRouter + ident, ok := selExpr.X.(*ast.Ident) + //只要存在调用则+1 + if ok && ident.Name == a.ModuleName { + exprNum++ + } + //判断是否为目标结构 + if !ok || ident.Name != a.ModuleName || selExpr.Sel.Name != a.FunctionName { + continue + } + exprNum-- + // 从语句列表中移除。 + funcDecl.Body.List[i].(*ast.BlockStmt).List = append(funcDecl.Body.List[i].(*ast.BlockStmt).List[:ii], funcDecl.Body.List[i].(*ast.BlockStmt).List[ii+1:]...) + // 如果不再存在任何调用,则删除导入和变量。 + if exprNum == 0 { + funcDecl.Body.List = append(funcDecl.Body.List[:i], funcDecl.Body.List[i+1:]...) + } + break + } + break + } + } + } + + return nil +} + +func (a *PackageInitializeRouter) Injection(file *ast.File) error { + funcDecl := FindFunction(file, "initBizRouter") + hasRouter := false + var varBlock *ast.BlockStmt + for i := range funcDecl.Body.List { + if IsBlockStmt(funcDecl.Body.List[i]) { + if VariableExistsInBlock(funcDecl.Body.List[i].(*ast.BlockStmt), a.ModuleName) { + hasRouter = true + varBlock = funcDecl.Body.List[i].(*ast.BlockStmt) + break + } + } + } + if !hasRouter { + stmt := a.CreateAssignStmt() + varBlock = &ast.BlockStmt{ + List: []ast.Stmt{ + stmt, + }, + } + } + routerStmt := CreateStmt(fmt.Sprintf("%s.%s(%s,%s)", a.ModuleName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName)) + varBlock.List = append(varBlock.List, routerStmt) + if !hasRouter { + funcDecl.Body.List = append(funcDecl.Body.List, varBlock) + } + return nil +} + +func (a *PackageInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} + +func (a *PackageInitializeRouter) CreateAssignStmt() *ast.AssignStmt { + //创建左侧变量 + ident := &ast.Ident{ + Name: a.ModuleName, + } + + //创建右侧的赋值语句 + selector := &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + } + + // 创建一个组合的赋值语句 + stmt := &ast.AssignStmt{ + Lhs: []ast.Expr{ident}, + Tok: token.DEFINE, + Rhs: []ast.Expr{selector}, + } + + return stmt +} diff --git a/server/utils/ast/package_initialize_router_test.go b/server/utils/ast/package_initialize_router_test.go new file mode 100644 index 0000000..5398615 --- /dev/null +++ b/server/utils/ast/package_initialize_router_test.go @@ -0,0 +1,159 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPackageInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 InitCustomerRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + LeftRouterGroupName: "privateGroup", + RightRouterGroupName: "publicGroup", + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + ModuleName string + PackageName string + FunctionName string + RouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + + { + name: "测试 InitCustomerRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitCustomerRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + { + name: "测试 InitFileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router_biz.go"), + ImportPath: `"git.echol.cn/loser/st/server/router"`, + AppName: "RouterGroupApp", + GroupName: "Example", + ModuleName: "exampleRouter", + PackageName: "router", + FunctionName: "InitFileUploadAndDownloadRouter", + RouterGroupName: "privateGroup", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + RouterGroupName: tt.fields.RouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/package_module_enter.go b/server/utils/ast/package_module_enter.go new file mode 100644 index 0000000..881fb3f --- /dev/null +++ b/server/utils/ast/package_module_enter.go @@ -0,0 +1,180 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PackageModuleEnter 模块化入口 +// ModuleName := PackageName.AppName.GroupName.ServiceName +type PackageModuleEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + AppName string // 应用名称 + GroupName string // 分组名称 + ModuleName string // 模块名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PackageModuleEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PackageModuleEnter) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + for j := 0; j < len(v1.Specs); j++ { + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + v3.Fields.List = append(v3.Fields.List[:k], v3.Fields.List[k+1:]...) + } + } + } + continue + } + if a.Type == TypePackageServiceModuleEnter { + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + v1.Specs = append(v1.Specs[:j], v1.Specs[j+1:]...) + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + if i == len(file.Decls) { + file.Decls = append(file.Decls[:i-1]) + break + } // 空的var(), 如果不删除则会影响的注入变量, 因为识别不到*ast.ValueSpec + file.Decls = append(file.Decls[:i], file.Decls[i+1:]...) + } + } + } + } + return nil +} + +func (a *PackageModuleEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var hasValue bool + var hasVariables bool + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.GenDecl) + if o1 { + if v1.Tok == token.VAR { + hasVariables = true + } + for j := 0; j < len(v1.Specs); j++ { + if a.Type == TypePackageServiceModuleEnter { + hasValue = true + } + v2, o2 := v1.Specs[j].(*ast.TypeSpec) + if o2 { + if v2.Name.Name != a.Type.Group() { + continue + } + v3, o3 := v2.Type.(*ast.StructType) + if o3 { + var hasStruct bool + for k := 0; k < len(v3.Fields.List); k++ { + v4, o4 := v3.Fields.List[k].Type.(*ast.Ident) + if o4 && v4.Name == a.StructName { + hasStruct = true + } + } + if !hasStruct { + field := &ast.Field{Type: &ast.Ident{Name: a.StructName}} + v3.Fields.List = append(v3.Fields.List, field) + } + } + continue + } + v3, o3 := v1.Specs[j].(*ast.ValueSpec) + if o3 { + hasVariables = true + if len(v3.Names) == 1 && v3.Names[0].Name == a.ModuleName { + hasValue = true + } + } + if v1.Tok == token.VAR && len(v1.Specs) == 0 { + hasVariables = false + } // 说明是空var() + if hasVariables && !hasValue { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + v1.Specs = append(v1.Specs, spec) + hasValue = true + } + } + } + } + if !hasValue && !hasVariables { + decl := &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{ + &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.AppName}, + }, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + }, + }, + } + file.Decls = append(file.Decls, decl) + } + return nil +} + +func (a *PackageModuleEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/package_module_enter_test.go b/server/utils/ast/package_module_enter_test.go new file mode 100644 index 0000000..d04b1f1 --- /dev/null +++ b/server/utils/ast/package_module_enter_test.go @@ -0,0 +1,186 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPackageModuleEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 回滚", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "git.echol.cn/loser/st/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 回滚", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 回滚", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPackageModuleEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + AppName string + GroupName string + ModuleName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 FileUploadAndDownloadRouter 注入", + fields: fields{ + Type: TypePackageRouterModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", "example", "enter.go"), + ImportPath: `api "git.echol.cn/loser/st/server/api/v1"`, + StructName: "FileUploadAndDownloadRouter", + AppName: "ApiGroupApp", + GroupName: "ExampleApiGroup", + ModuleName: "exaFileUploadAndDownloadApi", + PackageName: "api", + ServiceName: "FileUploadAndDownloadApi", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadApi 注入", + fields: fields{ + Type: TypePackageApiModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", "example", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/service"`, + StructName: "FileUploadAndDownloadApi", + AppName: "ServiceGroupApp", + GroupName: "ExampleServiceGroup", + ModuleName: "fileUploadAndDownloadService", + PackageName: "service", + ServiceName: "FileUploadAndDownloadService", + }, + wantErr: false, + }, + { + name: "测试 FileUploadAndDownloadService 注入", + fields: fields{ + Type: TypePackageServiceModuleEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", "example", "enter.go"), + ImportPath: ``, + StructName: "FileUploadAndDownloadService", + AppName: "", + GroupName: "", + ModuleName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PackageModuleEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + ModuleName: tt.fields.ModuleName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_enter.go b/server/utils/ast/plugin_enter.go new file mode 100644 index 0000000..df5bba4 --- /dev/null +++ b/server/utils/ast/plugin_enter.go @@ -0,0 +1,167 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +// PluginEnter 插件化入口 +// ModuleName := PackageName.GroupName.ServiceName +type PluginEnter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + StructCamelName string // 结构体小驼峰名称 + ModuleName string // 模块名称 + GroupName string // 分组名称 + PackageName string // 包名 + ServiceName string // 服务名称 +} + +func (a *PluginEnter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginEnter) Rollback(file *ast.File) error { + //回滚结构体内内容 + var structType *ast.StructType + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + structType = s + for i, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + s.Fields.List = append(s.Fields.List[:i], s.Fields.List[i+1:]...) + return false + } + } + } + } + return true + }) + + if len(structType.Fields.List) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //回滚变量内容 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for i, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + genDecl.Specs = append(genDecl.Specs[:i], genDecl.Specs[i+1:]...) + return false + } + } + } + } + } + return true + }) + + return nil +} + +func (a *PluginEnter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + + has := false + hasVar := false + var firstStruct *ast.StructType + var varSpec *ast.GenDecl + //寻找是否存在结构且定位 + ast.Inspect(file, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + if s, ok := x.Type.(*ast.StructType); ok { + firstStruct = s + for _, field := range x.Type.(*ast.StructType).Fields.List { + if len(field.Names) > 0 && field.Names[0].Name == a.StructName { + has = true + return false + } + } + } + } + return true + }) + + if !has { + field := &ast.Field{ + Names: []*ast.Ident{{Name: a.StructName}}, + Type: &ast.Ident{Name: a.StructCamelName}, + } + firstStruct.Fields.List = append(firstStruct.Fields.List, field) + } + + if a.Type == TypePluginServiceEnter { + return nil + } + + //寻找是否存在变量且定位 + ast.Inspect(file, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + if ok && genDecl.Tok == token.VAR { + for _, spec := range genDecl.Specs { + valueSpec, vsok := spec.(*ast.ValueSpec) + if vsok { + varSpec = genDecl + for _, name := range valueSpec.Names { + if name.Name == a.ModuleName { + hasVar = true + return false + } + } + } + } + } + return true + }) + + if !hasVar { + spec := &ast.ValueSpec{ + Names: []*ast.Ident{{Name: a.ModuleName}}, + Values: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.GroupName}, + }, + Sel: &ast.Ident{Name: a.ServiceName}, + }, + }, + } + varSpec.Specs = append(varSpec.Specs, spec) + } + + return nil +} + +func (a *PluginEnter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_enter_test.go b/server/utils/ast/plugin_enter_test.go new file mode 100644 index 0000000..92013f3 --- /dev/null +++ b/server/utils/ast/plugin_enter_test.go @@ -0,0 +1,201 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPluginEnter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserApi 注入", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserRouter 注入", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + { + name: "测试 gva的User 注入", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginEnter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + StructCamelName string + ModuleName string + GroupName string + PackageName string + ServiceName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件UserRouter 回滚", + fields: fields{ + Type: TypePluginRouterEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "router", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/api"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "userApi", + GroupName: "Api", + PackageName: "api", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserApi 回滚", + fields: fields{ + Type: TypePluginApiEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "api", "enter.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/service"`, + StructName: "User", + StructCamelName: "user", + ModuleName: "serviceUser", + GroupName: "Service", + PackageName: "service", + ServiceName: "User", + }, + wantErr: false, + }, + { + name: "测试 Gva插件UserService 回滚", + fields: fields{ + Type: TypePluginServiceEnter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "service", "enter.go"), + ImportPath: "", + StructName: "User", + StructCamelName: "user", + ModuleName: "", + GroupName: "", + PackageName: "", + ServiceName: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginEnter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + StructCamelName: tt.fields.StructCamelName, + ModuleName: tt.fields.ModuleName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + ServiceName: tt.fields.ServiceName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_gen.go b/server/utils/ast/plugin_gen.go new file mode 100644 index 0000000..ed7d04f --- /dev/null +++ b/server/utils/ast/plugin_gen.go @@ -0,0 +1,189 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" +) + +type PluginGen struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 +} + +func (a *PluginGen) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} +func (a *PluginGen) Rollback(file *ast.File) error { + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + } + if k >= len(v3.Args) { + break + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + v3.Args = append(v3.Args[:k], v3.Args[k+1:]...) + continue + } + } + } + } + } + if len(v3.Args) == 0 { + _ = NewImport(a.ImportPath).Rollback(file) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + for i := 0; i < len(file.Decls); i++ { + v1, o1 := file.Decls[i].(*ast.FuncDecl) + if o1 { + for j := 0; j < len(v1.Body.List); j++ { + v2, o2 := v1.Body.List[j].(*ast.ExprStmt) + if o2 { + v3, o3 := v2.X.(*ast.CallExpr) + if o3 { + v4, o4 := v3.Fun.(*ast.SelectorExpr) + if o4 { + if v4.Sel.Name != "ApplyBasic" { + continue + } + var has bool + for k := 0; k < len(v3.Args); k++ { + v5, o5 := v3.Args[k].(*ast.CallExpr) + if o5 { + v6, o6 := v5.Fun.(*ast.Ident) + if o6 { + if v6.Name != "new" { + continue + } + for l := 0; l < len(v5.Args); l++ { + v7, o7 := v5.Args[l].(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + } + v6, o6 := v3.Args[k].(*ast.CompositeLit) + if o6 { + v7, o7 := v6.Type.(*ast.SelectorExpr) + if o7 { + v8, o8 := v7.X.(*ast.Ident) + if o8 { + if v8.Name == a.PackageName && v7.Sel.Name == a.StructName { + has = true + break + } + } + } + } + } + if !has { + if a.IsNew { + arg := &ast.CallExpr{ + Fun: &ast.Ident{Name: "\n\t\tnew"}, + Args: []ast.Expr{ + &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + }, + } + v3.Args = append(v3.Args, arg) + v3.Args = append(v3.Args, &ast.BasicLit{ + Kind: token.STRING, + Value: "\n", + }) + break + } + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + v3.Args = append(v3.Args, arg) + } + } + } + } + } + } + } + return nil +} + +func (a *PluginGen) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_gen_test.go b/server/utils/ast/plugin_gen_test.go new file mode 100644 index 0000000..d88a957 --- /dev/null +++ b/server/utils/ast/plugin_gen_test.go @@ -0,0 +1,128 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPluginGenModel_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 结构体注入", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginGenModel_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + PackageName string + StructName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: false, + }, + }, + { + name: "测试 GvaUser 回滚", + fields: fields{ + Type: TypePluginGen, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "gen", "main.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + PackageName: "model", + StructName: "User", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginGen{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + PackageName: tt.fields.PackageName, + StructName: tt.fields.StructName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_gorm.go b/server/utils/ast/plugin_initialize_gorm.go new file mode 100644 index 0000000..e342251 --- /dev/null +++ b/server/utils/ast/plugin_initialize_gorm.go @@ -0,0 +1,111 @@ +package ast + +import ( + "go/ast" + "io" +) + +type PluginInitializeGorm struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + RelativePath string // 相对路径 + StructName string // 结构体名称 + PackageName string // 包名 + IsNew bool // 是否使用new关键字 true: new(PackageName.StructName) false: &PackageName.StructName{} +} + +func (a *PluginInitializeGorm) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeGorm) Rollback(file *ast.File) error { + var needRollBackImport bool + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, seok := callExpr.Fun.(*ast.SelectorExpr) + if !seok || selExpr.Sel.Name != "AutoMigrate" { + return true + } + if len(callExpr.Args) <= 1 { + needRollBackImport = true + } + // 删除指定的参数 + for i, arg := range callExpr.Args { + compLit, cok := arg.(*ast.CompositeLit) + if !cok { + continue + } + + cselExpr, sok := compLit.Type.(*ast.SelectorExpr) + if !sok { + continue + } + + ident, idok := cselExpr.X.(*ast.Ident) + if idok && ident.Name == a.PackageName && cselExpr.Sel.Name == a.StructName { + // 删除参数 + callExpr.Args = append(callExpr.Args[:i], callExpr.Args[i+1:]...) + break + } + } + + return true + }) + + if needRollBackImport { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeGorm) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + var call *ast.CallExpr + ast.Inspect(file, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if ok && selExpr.Sel.Name == "AutoMigrate" { + call = callExpr + return false + } + + return true + }) + + arg := &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: a.PackageName}, + Sel: &ast.Ident{Name: a.StructName}, + }, + } + + call.Args = append(call.Args, arg) + return nil +} + +func (a *PluginInitializeGorm) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_gorm_test.go b/server/utils/ast/plugin_initialize_gorm_test.go new file mode 100644 index 0000000..467c2df --- /dev/null +++ b/server/utils/ast/plugin_initialize_gorm_test.go @@ -0,0 +1,139 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPluginInitializeGorm_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + { + name: "测试 new(model.SysUsers) 注入", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + StructName: "SysUser", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeGorm_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + StructName string + PackageName string + IsNew bool + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 &model.User{} 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: false, + }, + }, + { + name: "测试 new(model.ExaCustomer) 回滚", + fields: fields{ + Type: TypePluginInitializeGorm, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "gorm.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/model"`, + StructName: "User", + PackageName: "model", + IsNew: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeGorm{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + StructName: tt.fields.StructName, + PackageName: tt.fields.PackageName, + IsNew: tt.fields.IsNew, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_router.go b/server/utils/ast/plugin_initialize_router.go new file mode 100644 index 0000000..6550789 --- /dev/null +++ b/server/utils/ast/plugin_initialize_router.go @@ -0,0 +1,124 @@ +package ast + +import ( + "fmt" + "go/ast" + "io" +) + +// PluginInitializeRouter 插件初始化路由 +// PackageName.AppName.GroupName.FunctionName() +type PluginInitializeRouter struct { + Base + Type Type // 类型 + Path string // 文件路径 + ImportPath string // 导包路径 + ImportGlobalPath string // 导包全局变量路径 + ImportMiddlewarePath string // 导包中间件路径 + RelativePath string // 相对路径 + AppName string // 应用名称 + GroupName string // 分组名称 + PackageName string // 包名 + FunctionName string // 函数名 + LeftRouterGroupName string // 左路由分组名称 + RightRouterGroupName string // 右路由分组名称 +} + +func (a *PluginInitializeRouter) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.Path + a.RelativePath = a.Base.RelativePath(a.Path) + return a.Base.Parse(filename, writer) + } + a.Path = a.Base.AbsolutePath(a.RelativePath) + filename = a.Path + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeRouter) Rollback(file *ast.File) error { + funcDecl := FindFunction(file, "Router") + delI := 0 + routerNum := 0 + for i := len(funcDecl.Body.List) - 1; i >= 0; i-- { + stmt, ok := funcDecl.Body.List[i].(*ast.ExprStmt) + if !ok { + continue + } + + callExpr, ok := stmt.X.(*ast.CallExpr) + if !ok { + continue + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + + if ok { + if iExpr, ieok := ident.X.(*ast.SelectorExpr); ieok { + if iden, idok := iExpr.X.(*ast.Ident); idok { + if iden.Name == "router" { + routerNum++ + } + } + } + if ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + // 删除语句 + delI = i + } + } + } + + funcDecl.Body.List = append(funcDecl.Body.List[:delI], funcDecl.Body.List[delI+1:]...) + + if routerNum <= 1 { + _ = NewImport(a.ImportPath).Rollback(file) + } + + return nil +} + +func (a *PluginInitializeRouter) Injection(file *ast.File) error { + _ = NewImport(a.ImportPath).Injection(file) + funcDecl := FindFunction(file, "Router") + + var exists bool + + ast.Inspect(funcDecl, func(n ast.Node) bool { + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + ident, ok := selExpr.X.(*ast.SelectorExpr) + if ok && ident.Sel.Name == a.GroupName && selExpr.Sel.Name == a.FunctionName { + exists = true + return false + } + return true + }) + + if !exists { + stmtStr := fmt.Sprintf("%s.%s.%s.%s(%s, %s)", a.PackageName, a.AppName, a.GroupName, a.FunctionName, a.LeftRouterGroupName, a.RightRouterGroupName) + stmt := CreateStmt(stmtStr) + funcDecl.Body.List = append(funcDecl.Body.List, stmt) + } + return nil +} + +func (a *PluginInitializeRouter) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.Path + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_router_test.go b/server/utils/ast/plugin_initialize_router_test.go new file mode 100644 index 0000000..b407d63 --- /dev/null +++ b/server/utils/ast/plugin_initialize_router_test.go @@ -0,0 +1,156 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPluginInitializeRouter_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitializeRouter_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + ImportPath string + AppName string + GroupName string + PackageName string + FunctionName string + LeftRouterGroupName string + RightRouterGroupName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件User 回滚", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "User", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + { + name: "测试 中文 注入", + fields: fields{ + Type: TypePluginInitializeRouter, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "initialize", "router.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva/router"`, + AppName: "Router", + GroupName: "U中文", + PackageName: "router", + FunctionName: "Init", + LeftRouterGroupName: "public", + RightRouterGroupName: "private", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &PluginInitializeRouter{ + Type: tt.fields.Type, + Path: tt.fields.Path, + ImportPath: tt.fields.ImportPath, + AppName: tt.fields.AppName, + GroupName: tt.fields.GroupName, + PackageName: tt.fields.PackageName, + FunctionName: tt.fields.FunctionName, + LeftRouterGroupName: tt.fields.LeftRouterGroupName, + RightRouterGroupName: tt.fields.RightRouterGroupName, + } + file, err := a.Parse(a.Path, nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format(a.Path, nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/ast/plugin_initialize_v2.go b/server/utils/ast/plugin_initialize_v2.go new file mode 100644 index 0000000..974f513 --- /dev/null +++ b/server/utils/ast/plugin_initialize_v2.go @@ -0,0 +1,82 @@ +package ast + +import ( + "go/ast" + "go/token" + "io" + "strconv" + "strings" +) + +type PluginInitializeV2 struct { + Base + Type Type // 类型 + Path string // 文件路径 + PluginPath string // 插件路径 + RelativePath string // 相对路径 + ImportPath string // 导包路径 + StructName string // 结构体名称 + PackageName string // 包名 +} + +func (a *PluginInitializeV2) Parse(filename string, writer io.Writer) (file *ast.File, err error) { + if filename == "" { + if a.RelativePath == "" { + filename = a.PluginPath + a.RelativePath = a.Base.RelativePath(a.PluginPath) + return a.Base.Parse(filename, writer) + } + a.PluginPath = a.Base.AbsolutePath(a.RelativePath) + filename = a.PluginPath + } + return a.Base.Parse(filename, writer) +} + +func (a *PluginInitializeV2) Injection(file *ast.File) error { + importPath := strings.TrimSpace(a.ImportPath) + if importPath == "" { + return nil + } + importPath = strings.Trim(importPath, "\"") + if importPath == "" || CheckImport(file, importPath) { + return nil + } + + importSpec := &ast.ImportSpec{ + Name: ast.NewIdent("_"), + Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(importPath)}, + } + var importDecl *ast.GenDecl + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + if genDecl.Tok == token.IMPORT { + importDecl = genDecl + break + } + } + if importDecl == nil { + file.Decls = append([]ast.Decl{ + &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{importSpec}, + }, + }, file.Decls...) + return nil + } + importDecl.Specs = append(importDecl.Specs, importSpec) + return nil +} + +func (a *PluginInitializeV2) Rollback(file *ast.File) error { + return nil +} + +func (a *PluginInitializeV2) Format(filename string, writer io.Writer, file *ast.File) error { + if filename == "" { + filename = a.PluginPath + } + return a.Base.Format(filename, writer, file) +} diff --git a/server/utils/ast/plugin_initialize_v2_test.go b/server/utils/ast/plugin_initialize_v2_test.go new file mode 100644 index 0000000..55dc0c9 --- /dev/null +++ b/server/utils/ast/plugin_initialize_v2_test.go @@ -0,0 +1,101 @@ +package ast + +import ( + "path/filepath" + "testing" + + "git.echol.cn/loser/st/server/global" +) + +func TestPluginInitialize_Injection(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 注册注入", + fields: fields{ + Type: TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva"`, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + } + file, err := a.Parse("", nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Injection(file) + err = a.Format("", nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Injection() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPluginInitialize_Rollback(t *testing.T) { + type fields struct { + Type Type + Path string + PluginPath string + ImportPath string + PluginName string + StructName string + PackageName string + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "测试 Gva插件 回滚", + fields: fields{ + Type: TypePluginInitializeV2, + Path: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "gva", "plugin.go"), + PluginPath: filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", "register.go"), + ImportPath: `"git.echol.cn/loser/st/server/plugin/gva"`, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := PluginInitializeV2{ + Type: tt.fields.Type, + Path: tt.fields.Path, + PluginPath: tt.fields.PluginPath, + ImportPath: tt.fields.ImportPath, + StructName: "Plugin", + PackageName: "gva", + } + file, err := a.Parse("", nil) + if err != nil { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + } + a.Rollback(file) + err = a.Format("", nil, file) + if (err != nil) != tt.wantErr { + t.Errorf("Rollback() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go new file mode 100644 index 0000000..817918b --- /dev/null +++ b/server/utils/autocode/template_funcs.go @@ -0,0 +1,714 @@ +package autocode + +import ( + "fmt" + "slices" + "strings" + "text/template" + + systemReq "git.echol.cn/loser/st/server/model/system/request" +) + +// GetTemplateFuncMap 返回模板函数映射,用于在模板中使用 +func GetTemplateFuncMap() template.FuncMap { + return template.FuncMap{ + "title": strings.Title, + "GenerateField": GenerateField, + "GenerateSearchField": GenerateSearchField, + "GenerateSearchConditions": GenerateSearchConditions, + "GenerateSearchFormItem": GenerateSearchFormItem, + "GenerateTableColumn": GenerateTableColumn, + "GenerateFormItem": GenerateFormItem, + "GenerateDescriptionItem": GenerateDescriptionItem, + "GenerateDefaultFormValue": GenerateDefaultFormValue, + } +} + +// 渲染Model中的字段 +func GenerateField(field systemReq.AutoCodeField) string { + // 构建gorm标签 + gormTag := `` + + if field.FieldIndexType != "" { + gormTag += field.FieldIndexType + ";" + } + + if field.PrimaryKey { + gormTag += "primarykey;" + } + + if field.DefaultValue != "" { + gormTag += fmt.Sprintf("default:%s;", field.DefaultValue) + } + + if field.Comment != "" { + gormTag += fmt.Sprintf("comment:%s;", field.Comment) + } + + gormTag += "column:" + field.ColumnName + ";" + + // 对于int类型,根据DataTypeLong决定具体的Go类型,不使用size标签 + if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" { + gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong) + } + + requireTag := ` binding:"required"` + "`" + + // 根据字段类型构建不同的字段定义 + var result string + switch field.FieldType { + case "enum": + result = fmt.Sprintf(`%s string `+"`"+`json:"%s" form:"%s" gorm:"%stype:enum(%s);"`+"`", + field.FieldName, field.FieldJson, field.FieldJson, gormTag, field.DataTypeLong) + case "picture", "video": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s string `+"`"+`%s`+"`"+``, field.FieldName, tagContent) + case "file", "pictures", "array": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``, + field.FieldName, tagContent) + case "richtext": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``, + field.FieldName, tagContent) + case "json": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"object"`+"`"+``, + field.FieldName, tagContent) + default: + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + // 对于int类型,根据DataTypeLong决定具体的Go类型 + var fieldType string + if field.FieldType == "int" { + switch field.DataTypeLong { + case "1", "2", "3": + fieldType = "int8" + case "4", "5": + fieldType = "int16" + case "6", "7", "8", "9", "10": + fieldType = "int32" + case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20": + fieldType = "int64" + default: + fieldType = "int64" + } + } else { + fieldType = field.FieldType + } + + result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``, + field.FieldName, fieldType, tagContent) + } + + if field.Require { + result = result[0:len(result)-1] + requireTag + } + + // 添加字段描述 + if field.FieldDesc != "" { + result += fmt.Sprintf(" //%s", field.FieldDesc) + } + + return result +} + +// 格式化搜索条件语句 +func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string { + var conditions []string + + for _, field := range fields { + if field.FieldSearchType == "" { + continue + } + + var condition string + + if slices.Contains([]string{"enum", "pictures", "picture", "video", "json", "richtext", "array"}, field.FieldType) { + if field.FieldType == "enum" { + if field.FieldSearchType == "LIKE" { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s LIKE ?", "%%"+ info.%s+"%%") + }`, + field.FieldName, field.ColumnName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s %s ?", info.%s) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName) + } + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + // TODO 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + }`, field.FieldName) + } + + } else if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + if field.FieldType == "time.Time" { + condition = fmt.Sprintf(` + if len(info.%sRange) == 2 { + db = db.Where("%s %s ? AND ? ", info.%sRange[0], info.%sRange[1]) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.Start%s != nil && info.End%s != nil { + db = db.Where("%s %s ? AND ? ", *info.Start%s, *info.End%s) + }`, + field.FieldName, field.FieldName, field.ColumnName, + field.FieldSearchType, field.FieldName, field.FieldName) + } + } else { + nullCheck := "info." + field.FieldName + " != nil" + if field.FieldType == "string" { + condition = fmt.Sprintf(` + if %s && *info.%s != "" {`, nullCheck, field.FieldName) + } else { + condition = fmt.Sprintf(` + if %s {`, nullCheck) + } + + if field.FieldSearchType == "LIKE" { + condition += fmt.Sprintf(` + db = db.Where("%s LIKE ?", "%%"+ *info.%s+"%%") + }`, + field.ColumnName, field.FieldName) + } else { + condition += fmt.Sprintf(` + db = db.Where("%s %s ?", *info.%s) + }`, + field.ColumnName, field.FieldSearchType, field.FieldName) + } + } + + conditions = append(conditions, condition) + } + + return strings.Join(conditions, "") +} + +// 格式化前端搜索条件 +func GenerateSearchFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 根据字段属性生成不同的输入类型 + if field.FieldType == "bool" { + result += fmt.Sprintf(` +`, field.FieldJson) + result += ` +` + result += ` +` + result += ` +` + } else if field.DictType != "" { + multipleAttr := "" + if field.FieldType == "array" { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable, multipleAttr) + } else if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else if field.FieldType == "float64" || field.FieldType == "int" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += fmt.Sprintf(` +`, field.FieldName) + result += ` — +` + result += fmt.Sprintf(` +`, field.FieldName) + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + } else if field.FieldType == "time.Time" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += ` +` + result += fmt.Sprintf(``, field.FieldJson) + } else { + result += fmt.Sprintf(``, field.FieldJson) + } + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + // 关闭表单项 + result += `` + + return result +} + +// GenerateTableColumn generates HTML for table column based on field properties +func GenerateTableColumn(field systemReq.AutoCodeField) string { + // Add sortable attribute if needed + sortAttr := "" + if field.Sort { + sortAttr = " sortable" + } + + // Handle different field types + if field.CheckDataSource { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.DictType != "" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "bool" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "time.Time" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "picture" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "pictures" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "video" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "richtext" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "file" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "json" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "array" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else { + return fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + } +} + +func GenerateFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 处理不同字段类型 + if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = " multiple" + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else { + switch field.FieldType { + case "bool": + result += fmt.Sprintf(` +`, + field.FieldJson) + + case "string": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.DictType, field.Clearable) + } else { + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + } + + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "json": + result += fmt.Sprintf(` // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.%s 后端会按照json的类型进行存取 +`, field.FieldJson) + result += fmt.Sprintf(` {{ formData.%s }} +`, field.FieldJson) + + case "array": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + case "int": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + + case "time.Time": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "float64": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "enum": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DataTypeLong) + result += ` +` + + case "picture": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "pictures": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "video": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "file": + result += fmt.Sprintf(` +`, field.FieldJson) + } + } + + // 关闭表单项 + result += `` + + return result +} + +func GenerateDescriptionItem(field systemReq.AutoCodeField) string { + // 开始构建描述项 + result := fmt.Sprintf(` +`, field.FieldDesc) + + if field.CheckDataSource { + result += ` +` + } else if field.FieldType != "picture" && field.FieldType != "pictures" && + field.FieldType != "file" && field.FieldType != "array" && + field.FieldType != "richtext" { + result += fmt.Sprintf(` {{ detailForm.%s }} +`, field.FieldJson) + } else { + switch field.FieldType { + case "picture": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "array": + result += fmt.Sprintf(` +`, field.FieldJson) + case "pictures": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + case "file": + result += fmt.Sprintf(`
+`, field.FieldJson) + result += ` +` + result += ` +` + result += ` {{ item.name }} +` + result += ` +` + result += `
+` + } + } + + // 关闭描述项 + result += `
` + + return result +} + +func GenerateDefaultFormValue(field systemReq.AutoCodeField) string { + // 根据字段类型确定默认值 + var defaultValue string + + switch field.FieldType { + case "bool": + defaultValue = "false" + case "string", "richtext": + defaultValue = "''" + case "int": + if field.DataSource != nil { // 检查数据源是否存在 + defaultValue = "undefined" + } else { + defaultValue = "0" + } + case "time.Time": + defaultValue = "new Date()" + case "float64": + defaultValue = "0" + case "picture", "video": + defaultValue = "\"\"" + case "pictures", "file", "array": + defaultValue = "[]" + case "json": + defaultValue = "{}" + default: + defaultValue = "null" + } + + // 返回格式化后的默认值字符串 + return fmt.Sprintf(`%s: %s,`, field.FieldJson, defaultValue) +} + +// GenerateSearchField 根据字段属性生成搜索结构体中的字段定义 +func GenerateSearchField(field systemReq.AutoCodeField) string { + var result string + + if field.FieldSearchType == "" { + return "" // 如果没有搜索类型,返回空字符串 + } + + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + // 生成范围搜索字段 + // time 的情况 + if field.FieldType == "time.Time" { + result = fmt.Sprintf("%sRange []time.Time `json:\"%sRange\" form:\"%sRange[]\"`", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + startField := fmt.Sprintf("Start%s *%s `json:\"start%s\" form:\"start%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + endField := fmt.Sprintf("End%s *%s `json:\"end%s\" form:\"end%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + result = startField + "\n" + endField + } + } else { + // 生成普通搜索字段 + if field.FieldType == "enum" || field.FieldType == "picture" || + field.FieldType == "pictures" || field.FieldType == "video" || + field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" || field.FieldType == "file" { + result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + result = fmt.Sprintf("%s *%s `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldType, field.FieldJson, field.FieldJson) + } + } + + return result +} diff --git a/server/utils/breakpoint_continue.go b/server/utils/breakpoint_continue.go new file mode 100644 index 0000000..66368e2 --- /dev/null +++ b/server/utils/breakpoint_continue.go @@ -0,0 +1,121 @@ +package utils + +import ( + "errors" + "os" + "strconv" + "strings" +) + +// 前端传来文件片与当前片为什么文件的第几片 +// 后端拿到以后比较次分片是否上传 或者是否为不完全片 +// 前端发送每片多大 +// 前端告知是否为最后一片且是否完成 + +const ( + breakpointDir = "./breakpointDir/" + finishDir = "./fileDir/" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: BreakPointContinue +//@description: 断点续传 +//@param: content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string +//@return: error, string + +func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(fileMd5, "..") { + return "", errors.New("文件名或路径不合法") + } + path := breakpointDir + fileMd5 + "/" + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + return path, err + } + pathC, err := makeFileContent(content, fileName, path, contentNumber) + return pathC, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CheckMd5 +//@description: 检查Md5 +//@param: content []byte, chunkMd5 string +//@return: CanUpload bool + +func CheckMd5(content []byte, chunkMd5 string) (CanUpload bool) { + fileMd5 := MD5V(content) + if fileMd5 == chunkMd5 { + return true // 可以继续上传 + } else { + return false // 切片不完整,废弃 + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片内容 +//@param: content []byte, fileName string, FileDir string, contentNumber int +//@return: string, error + +func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(FileDir, "..") { + return "", errors.New("文件名或路径不合法") + } + path := FileDir + fileName + "_" + strconv.Itoa(contentNumber) + f, err := os.Create(path) + if err != nil { + return path, err + } + defer f.Close() + _, err = f.Write(content) + if err != nil { + return path, err + } + + return path, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: makeFileContent +//@description: 创建切片文件 +//@param: fileName string, FileMd5 string +//@return: error, string + +func MakeFile(fileName string, FileMd5 string) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(FileMd5, "..") { + return "", errors.New("文件名或路径不合法") + } + rd, err := os.ReadDir(breakpointDir + FileMd5) + if err != nil { + return finishDir + fileName, err + } + _ = os.MkdirAll(finishDir, os.ModePerm) + fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644) + if err != nil { + return finishDir + fileName, err + } + defer fd.Close() + for k := range rd { + content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k)) + _, err = fd.Write(content) + if err != nil { + _ = os.Remove(finishDir + fileName) + return finishDir + fileName, err + } + } + return finishDir + fileName, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RemoveChunk +//@description: 移除切片 +//@param: FileMd5 string +//@return: error + +func RemoveChunk(FileMd5 string) error { + if strings.Contains(FileMd5, "..") { + return errors.New("路径不合法") + } + err := os.RemoveAll(breakpointDir + FileMd5) + return err +} diff --git a/server/utils/captcha/redis.go b/server/utils/captcha/redis.go new file mode 100644 index 0000000..52fa2b4 --- /dev/null +++ b/server/utils/captcha/redis.go @@ -0,0 +1,61 @@ +package captcha + +import ( + "context" + "time" + + "git.echol.cn/loser/st/server/global" + "go.uber.org/zap" +) + +func NewDefaultRedisStore() *RedisStore { + return &RedisStore{ + Expiration: time.Second * 180, + PreKey: "CAPTCHA_", + Context: context.TODO(), + } +} + +type RedisStore struct { + Expiration time.Duration + PreKey string + Context context.Context +} + +func (rs *RedisStore) UseWithCtx(ctx context.Context) *RedisStore { + if ctx == nil { + rs.Context = ctx + } + return rs +} + +func (rs *RedisStore) Set(id string, value string) error { + err := global.GVA_REDIS.Set(rs.Context, rs.PreKey+id, value, rs.Expiration).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreSetError!", zap.Error(err)) + return err + } + return nil +} + +func (rs *RedisStore) Get(key string, clear bool) string { + val, err := global.GVA_REDIS.Get(rs.Context, key).Result() + if err != nil { + global.GVA_LOG.Error("RedisStoreGetError!", zap.Error(err)) + return "" + } + if clear { + err := global.GVA_REDIS.Del(rs.Context, key).Err() + if err != nil { + global.GVA_LOG.Error("RedisStoreClearError!", zap.Error(err)) + return "" + } + } + return val +} + +func (rs *RedisStore) Verify(id, answer string, clear bool) bool { + key := rs.PreKey + id + v := rs.Get(key, clear) + return v == answer +} diff --git a/server/utils/casbin_util.go b/server/utils/casbin_util.go new file mode 100644 index 0000000..18de021 --- /dev/null +++ b/server/utils/casbin_util.go @@ -0,0 +1,52 @@ +package utils + +import ( + "sync" + + "git.echol.cn/loser/st/server/global" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "go.uber.org/zap" +) + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +// GetCasbin 获取casbin实例 +func GetCasbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/server/utils/character_card.go b/server/utils/character_card.go new file mode 100644 index 0000000..c124773 --- /dev/null +++ b/server/utils/character_card.go @@ -0,0 +1,285 @@ +package utils + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "image" + "image/png" + "io" +) + +// CharacterCardV2 SillyTavern 角色卡 V2 格式 +type CharacterCardV2 struct { + Spec string `json:"spec"` + SpecVersion string `json:"spec_version"` + Data CharacterCardV2Data `json:"data"` +} + +type CharacterCardV2Data struct { + Name string `json:"name"` + Description string `json:"description"` + Personality string `json:"personality"` + Scenario string `json:"scenario"` + FirstMes string `json:"first_mes"` + MesExample string `json:"mes_example"` + CreatorNotes string `json:"creator_notes"` + SystemPrompt string `json:"system_prompt"` + PostHistoryInstructions string `json:"post_history_instructions"` + Tags []string `json:"tags"` + Creator string `json:"creator"` + CharacterVersion string `json:"character_version"` + AlternateGreetings []string `json:"alternate_greetings"` + CharacterBook map[string]interface{} `json:"character_book,omitempty"` + Extensions map[string]interface{} `json:"extensions"` +} + +// ExtractCharacterFromPNG 从 PNG 图片中提取角色卡数据 +func ExtractCharacterFromPNG(pngData []byte) (*CharacterCardV2, error) { + reader := bytes.NewReader(pngData) + + // 验证 PNG 格式(解码但不保存图片) + _, err := png.Decode(reader) + if err != nil { + return nil, errors.New("无效的 PNG 文件") + } + + // 重新读取以获取 tEXt chunks + reader.Seek(0, 0) + + // 查找 tEXt chunk 中的 "chara" 字段 + charaJSON, err := extractTextChunk(reader, "chara") + if err != nil { + return nil, errors.New("PNG 中没有找到角色卡数据") + } + + // 尝试 Base64 解码 + decodedJSON, err := base64.StdEncoding.DecodeString(charaJSON) + if err != nil { + // 如果不是 Base64,直接使用原始 JSON + decodedJSON = []byte(charaJSON) + } + + // 解析 JSON + var card CharacterCardV2 + err = json.Unmarshal(decodedJSON, &card) + if err != nil { + return nil, errors.New("解析角色卡数据失败: " + err.Error()) + } + + return &card, nil +} + +// extractTextChunk 从 PNG 中提取指定 key 的 tEXt chunk +func extractTextChunk(r io.Reader, key string) (string, error) { + // 跳过 PNG signature (8 bytes) + signature := make([]byte, 8) + if _, err := io.ReadFull(r, signature); err != nil { + return "", err + } + + // 验证 PNG signature + expectedSig := []byte{137, 80, 78, 71, 13, 10, 26, 10} + if !bytes.Equal(signature, expectedSig) { + return "", errors.New("invalid PNG signature") + } + + // 读取所有 chunks + for { + // 读取 chunk length (4 bytes) + lengthBytes := make([]byte, 4) + if _, err := io.ReadFull(r, lengthBytes); err != nil { + if err == io.EOF { + break + } + return "", err + } + length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 | + uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3]) + + // 读取 chunk type (4 bytes) + chunkType := make([]byte, 4) + if _, err := io.ReadFull(r, chunkType); err != nil { + return "", err + } + + // 读取 chunk data + data := make([]byte, length) + if _, err := io.ReadFull(r, data); err != nil { + return "", err + } + + // 读取 CRC (4 bytes) + crc := make([]byte, 4) + if _, err := io.ReadFull(r, crc); err != nil { + return "", err + } + + // 检查是否是 tEXt chunk + if string(chunkType) == "tEXt" { + // tEXt chunk 格式: keyword\0text + nullIndex := bytes.IndexByte(data, 0) + if nullIndex == -1 { + continue + } + + keyword := string(data[:nullIndex]) + text := string(data[nullIndex+1:]) + + if keyword == key { + return text, nil + } + } + + // IEND chunk 表示结束 + if string(chunkType) == "IEND" { + break + } + } + + return "", errors.New("text chunk not found") +} + +// EmbedCharacterToPNG 将角色卡数据嵌入到 PNG 图片中 +func EmbedCharacterToPNG(img image.Image, card *CharacterCardV2) ([]byte, error) { + // 序列化角色卡数据 + cardJSON, err := json.Marshal(card) + if err != nil { + return nil, err + } + + // Base64 编码 + encodedJSON := base64.StdEncoding.EncodeToString(cardJSON) + + // 创建一个 buffer 来写入 PNG + var buf bytes.Buffer + + // 写入 PNG signature + buf.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10}) + + // 编码原始图片到临时 buffer + var imgBuf bytes.Buffer + if err := png.Encode(&imgBuf, img); err != nil { + return nil, err + } + + // 跳过原始 PNG 的 signature + imgData := imgBuf.Bytes()[8:] + + // 将原始图片的 chunks 复制到输出,在 IEND 之前插入 tEXt chunk + r := bytes.NewReader(imgData) + + for { + // 读取 chunk length + lengthBytes := make([]byte, 4) + if _, err := io.ReadFull(r, lengthBytes); err != nil { + if err == io.EOF { + break + } + return nil, err + } + length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 | + uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3]) + + // 读取 chunk type + chunkType := make([]byte, 4) + if _, err := io.ReadFull(r, chunkType); err != nil { + return nil, err + } + + // 读取 chunk data + data := make([]byte, length) + if _, err := io.ReadFull(r, data); err != nil { + return nil, err + } + + // 读取 CRC + crc := make([]byte, 4) + if _, err := io.ReadFull(r, crc); err != nil { + return nil, err + } + + // 如果是 IEND chunk,先写入 tEXt chunk + if string(chunkType) == "IEND" { + // 写入 tEXt chunk + writeTextChunk(&buf, "chara", encodedJSON) + } + + // 写入原始 chunk + buf.Write(lengthBytes) + buf.Write(chunkType) + buf.Write(data) + buf.Write(crc) + + if string(chunkType) == "IEND" { + break + } + } + + return buf.Bytes(), nil +} + +// writeTextChunk 写入 tEXt chunk +func writeTextChunk(w io.Writer, keyword, text string) error { + data := append([]byte(keyword), 0) + data = append(data, []byte(text)...) + + // 写入 length + length := uint32(len(data)) + lengthBytes := []byte{ + byte(length >> 24), + byte(length >> 16), + byte(length >> 8), + byte(length), + } + w.Write(lengthBytes) + + // 写入 type + w.Write([]byte("tEXt")) + + // 写入 data + w.Write(data) + + // 计算并写入 CRC + crcData := append([]byte("tEXt"), data...) + crc := calculateCRC(crcData) + crcBytes := []byte{ + byte(crc >> 24), + byte(crc >> 16), + byte(crc >> 8), + byte(crc), + } + w.Write(crcBytes) + + return nil +} + +// calculateCRC 计算 CRC32 +func calculateCRC(data []byte) uint32 { + crc := uint32(0xFFFFFFFF) + + for _, b := range data { + crc ^= uint32(b) + for i := 0; i < 8; i++ { + if crc&1 != 0 { + crc = (crc >> 1) ^ 0xEDB88320 + } else { + crc >>= 1 + } + } + } + + return crc ^ 0xFFFFFFFF +} + +// ParseCharacterCardJSON 解析 JSON 格式的角色卡 +func ParseCharacterCardJSON(jsonData []byte) (*CharacterCardV2, error) { + var card CharacterCardV2 + err := json.Unmarshal(jsonData, &card) + if err != nil { + return nil, errors.New("解析角色卡 JSON 失败: " + err.Error()) + } + + return &card, nil +} diff --git a/server/utils/claims.go b/server/utils/claims.go new file mode 100644 index 0000000..4962c17 --- /dev/null +++ b/server/utils/claims.go @@ -0,0 +1,148 @@ +package utils + +import ( + "net" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system" + systemReq "git.echol.cn/loser/st/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func ClearToken(c *gin.Context) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", "", -1, "/", "", false, false) + } else { + c.SetCookie("x-token", "", -1, "/", host, false, false) + } +} + +func SetToken(c *gin.Context, token string, maxAge int) { + // 增加cookie x-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("x-token", token, maxAge, "/", "", false, false) + } else { + c.SetCookie("x-token", token, maxAge, "/", host, false, false) + } +} + +func GetToken(c *gin.Context) string { + token := c.Request.Header.Get("x-token") + if token == "" { + j := NewJWT() + token, _ = c.Cookie("x-token") + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在x-token且claims是否为规定结构") + return token + } + SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix())) + } + return token +} + +func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) { + token := GetToken(c) + j := NewJWT() + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构") + } + return claims, err +} + +// GetUserID 从Gin的Context中获取从jwt解析出来的用户ID +func GetUserID(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.BaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.BaseClaims.ID + } +} + +// GetUserUuid 从Gin的Context中获取从jwt解析出来的用户UUID +func GetUserUuid(c *gin.Context) uuid.UUID { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return uuid.UUID{} + } else { + return cl.UUID + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.UUID + } +} + +// GetUserAuthorityId 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserAuthorityId(c *gin.Context) uint { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return 0 + } else { + return cl.AuthorityId + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.AuthorityId + } +} + +// GetUserInfo 从Gin的Context中获取从jwt解析出来的用户角色id +func GetUserInfo(c *gin.Context) *systemReq.CustomClaims { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return nil + } else { + return cl + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse + } +} + +// GetUserName 从Gin的Context中获取从jwt解析出来的用户名 +func GetUserName(c *gin.Context) string { + if claims, exists := c.Get("claims"); !exists { + if cl, err := GetClaims(c); err != nil { + return "" + } else { + return cl.Username + } + } else { + waitUse := claims.(*systemReq.CustomClaims) + return waitUse.Username + } +} + +func LoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) { + j := NewJWT() + claims = j.CreateClaims(systemReq.BaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + NickName: user.GetNickname(), + Username: user.GetUsername(), + AuthorityId: user.GetAuthorityId(), + }) + token, err = j.CreateToken(claims) + return +} diff --git a/server/utils/directory.go b/server/utils/directory.go new file mode 100644 index 0000000..95e4178 --- /dev/null +++ b/server/utils/directory.go @@ -0,0 +1,124 @@ +package utils + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "strings" + + "git.echol.cn/loser/st/server/global" + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: PathExists +//@description: 文件目录是否存在 +//@param: path string +//@return: bool, error + +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, errors.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateDir +//@description: 批量创建文件夹 +//@param: dirs ...string +//@return: err error + +func CreateDir(dirs ...string) (err error) { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + global.GVA_LOG.Debug("create directory" + v) + if err := os.MkdirAll(v, os.ModePerm); err != nil { + global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err)) + return err + } + } + } + return err +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: FileMove +//@description: 文件移动供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileMove(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + return os.Rename(src, dst) +} + +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: TrimSpace +//@description: 去除结构体空格 +//@param: target interface (target: 目标结构体,传入必须是指针类型) +//@return: null + +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} diff --git a/server/utils/fmt_plus.go b/server/utils/fmt_plus.go new file mode 100644 index 0000000..8ff29cd --- /dev/null +++ b/server/utils/fmt_plus.go @@ -0,0 +1,126 @@ +package utils + +import ( + "fmt" + "math/rand" + "reflect" + "strings" + + "git.echol.cn/loser/st/server/model/common" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: StructToMap +//@description: 利用反射将结构体转化为map +//@param: obj interface{} +//@return: map[string]interface{} + +func StructToMap(obj interface{}) map[string]interface{} { + obj1 := reflect.TypeOf(obj) + obj2 := reflect.ValueOf(obj) + + data := make(map[string]interface{}) + for i := 0; i < obj1.NumField(); i++ { + if obj1.Field(i).Tag.Get("mapstructure") != "" { + data[obj1.Field(i).Tag.Get("mapstructure")] = obj2.Field(i).Interface() + } else { + data[obj1.Field(i).Name] = obj2.Field(i).Interface() + } + } + return data +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: ArrayToString +//@description: 将数组格式化为字符串 +//@param: array []interface{} +//@return: string + +func ArrayToString(array []interface{}) string { + return strings.Replace(strings.Trim(fmt.Sprint(array), "[]"), " ", ",", -1) +} + +func Pointer[T any](in T) (out *T) { + return &in +} + +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +// MaheHump 将字符串转换为驼峰命名 +func MaheHump(s string) string { + words := strings.Split(s, "-") + + for i := 1; i < len(words); i++ { + words[i] = strings.Title(words[i]) + } + + return strings.Join(words, "") +} + +// HumpToUnderscore 将驼峰命名转换为下划线分割模式 +func HumpToUnderscore(s string) string { + var result strings.Builder + + for i, char := range s { + if i > 0 && char >= 'A' && char <= 'Z' { + // 在大写字母前添加下划线 + result.WriteRune('_') + result.WriteRune(char - 'A' + 'a') // 转小写 + } else { + result.WriteRune(char) + } + } + + return strings.ToLower(result.String()) +} + +// RandomString 随机字符串 +func RandomString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + b := make([]rune, n) + for i := range b { + b[i] = letters[RandomInt(0, len(letters))] + } + return string(b) +} + +func RandomInt(min, max int) int { + return min + rand.Intn(max-min) +} + +// BuildTree 用于构建一个树形结构 +func BuildTree[T common.TreeNode[T]](nodes []T) []T { + nodeMap := make(map[int]T) + // 创建一个基本map + for i := range nodes { + nodeMap[nodes[i].GetID()] = nodes[i] + } + + for i := range nodes { + if nodes[i].GetParentID() != 0 { + parent := nodeMap[nodes[i].GetParentID()] + parent.SetChildren(nodes[i]) + } + } + + var rootNodes []T + + for i := range nodeMap { + if nodeMap[i].GetParentID() == 0 { + rootNodes = append(rootNodes, nodeMap[i]) + } + } + return rootNodes +} diff --git a/server/utils/hash.go b/server/utils/hash.go new file mode 100644 index 0000000..e7f23aa --- /dev/null +++ b/server/utils/hash.go @@ -0,0 +1,32 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + + "golang.org/x/crypto/bcrypt" +) + +// BcryptHash 使用 bcrypt 对密码进行加密 +func BcryptHash(password string) string { + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes) +} + +// BcryptCheck 对比明文密码和数据库的哈希值 +func BcryptCheck(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: MD5V +//@description: md5加密 +//@param: str []byte +//@return: string + +func MD5V(str []byte, b ...byte) string { + h := md5.New() + h.Write(str) + return hex.EncodeToString(h.Sum(b)) +} diff --git a/server/utils/human_duration.go b/server/utils/human_duration.go new file mode 100644 index 0000000..0cdb055 --- /dev/null +++ b/server/utils/human_duration.go @@ -0,0 +1,29 @@ +package utils + +import ( + "strconv" + "strings" + "time" +) + +func ParseDuration(d string) (time.Duration, error) { + d = strings.TrimSpace(d) + dr, err := time.ParseDuration(d) + if err == nil { + return dr, nil + } + if strings.Contains(d, "d") { + index := strings.Index(d, "d") + + hour, _ := strconv.Atoi(d[:index]) + dr = time.Hour * 24 * time.Duration(hour) + ndr, err := time.ParseDuration(d[index+1:]) + if err != nil { + return dr, nil + } + return dr + ndr, nil + } + + dv, err := strconv.ParseInt(d, 10, 64) + return time.Duration(dv), err +} diff --git a/server/utils/human_duration_test.go b/server/utils/human_duration_test.go new file mode 100644 index 0000000..8a5294b --- /dev/null +++ b/server/utils/human_duration_test.go @@ -0,0 +1,49 @@ +package utils + +import ( + "testing" + "time" +) + +func TestParseDuration(t *testing.T) { + type args struct { + d string + } + tests := []struct { + name string + args args + want time.Duration + wantErr bool + }{ + { + name: "5h20m", + args: args{"5h20m"}, + want: time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d5h20m", + args: args{"1d5h20m"}, + want: 24*time.Hour + time.Hour*5 + 20*time.Minute, + wantErr: false, + }, + { + name: "1d", + args: args{"1d"}, + want: 24 * time.Hour, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseDuration(tt.args.d) + if (err != nil) != tt.wantErr { + t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseDuration() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/server/utils/json.go b/server/utils/json.go new file mode 100644 index 0000000..8c4118c --- /dev/null +++ b/server/utils/json.go @@ -0,0 +1,34 @@ +package utils + +import ( + "encoding/json" + "strings" +) + +func GetJSONKeys(jsonStr string) (keys []string, err error) { + // 使用json.Decoder,以便在解析过程中记录键的顺序 + dec := json.NewDecoder(strings.NewReader(jsonStr)) + t, err := dec.Token() + if err != nil { + return nil, err + } + // 确保数据是一个对象 + if t != json.Delim('{') { + return nil, err + } + for dec.More() { + t, err = dec.Token() + if err != nil { + return nil, err + } + keys = append(keys, t.(string)) + + // 解析值 + var value interface{} + err = dec.Decode(&value) + if err != nil { + return nil, err + } + } + return keys, nil +} diff --git a/server/utils/json_test.go b/server/utils/json_test.go new file mode 100644 index 0000000..f21a679 --- /dev/null +++ b/server/utils/json_test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "testing" +) + +func TestGetJSONKeys(t *testing.T) { + var jsonStr = ` + { + "Name": "test", + "TableName": "test", + "TemplateID": "test", + "TemplateInfo": "test", + "Limit": 0 +}` + keys, err := GetJSONKeys(jsonStr) + if err != nil { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if len(keys) != 5 { + t.Errorf("GetJSONKeys failed" + err.Error()) + return + } + if keys[0] != "Name" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[1] != "TableName" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[2] != "TemplateID" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[3] != "TemplateInfo" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + if keys[4] != "Limit" { + t.Errorf("GetJSONKeys failed" + err.Error()) + + return + } + + fmt.Println(keys) +} diff --git a/server/utils/jwt.go b/server/utils/jwt.go new file mode 100644 index 0000000..effb72c --- /dev/null +++ b/server/utils/jwt.go @@ -0,0 +1,105 @@ +package utils + +import ( + "context" + "errors" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/model/system/request" + jwt "github.com/golang-jwt/jwt/v5" +) + +type JWT struct { + SigningKey []byte +} + +var ( + TokenValid = errors.New("未知错误") + TokenExpired = errors.New("token已过期") + TokenNotValidYet = errors.New("token尚未激活") + TokenMalformed = errors.New("这不是一个token") + TokenSignatureInvalid = errors.New("无效签名") + TokenInvalid = errors.New("无法处理此token") +) + +func NewJWT() *JWT { + return &JWT{ + []byte(global.GVA_CONFIG.JWT.SigningKey), + } +} + +func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims { + bf, _ := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + ep, _ := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims := request.CustomClaims{ + BaseClaims: baseClaims, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA"}, // 受众 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateToken 创建一个token +func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 +func (j *JWT) CreateTokenByOldToken(oldToken string, claims request.CustomClaims) (string, error) { + v, err, _ := global.GVA_Concurrency_Control.Do("JWT:"+oldToken, func() (interface{}, error) { + return j.CreateToken(claims) + }) + return v.(string), err +} + +// ParseToken 解析 token +func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenValid +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} diff --git a/server/utils/plugin/plugin.go b/server/utils/plugin/plugin.go new file mode 100644 index 0000000..a59d5b5 --- /dev/null +++ b/server/utils/plugin/plugin.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +const ( + OnlyFuncName = "Plugin" +) + +// Plugin 插件模式接口化 +type Plugin interface { + // Register 注册路由 + Register(group *gin.RouterGroup) + + // RouterPath 用户返回注册路由 + RouterPath() string +} diff --git a/server/utils/plugin/v2/plugin.go b/server/utils/plugin/v2/plugin.go new file mode 100644 index 0000000..4dac0ab --- /dev/null +++ b/server/utils/plugin/v2/plugin.go @@ -0,0 +1,11 @@ +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +// Plugin 插件模式接口化v2 +type Plugin interface { + // Register 注册路由 + Register(group *gin.Engine) +} diff --git a/server/utils/plugin/v2/registry.go b/server/utils/plugin/v2/registry.go new file mode 100644 index 0000000..4ec5fce --- /dev/null +++ b/server/utils/plugin/v2/registry.go @@ -0,0 +1,27 @@ +package plugin + +import "sync" + +var ( + registryMu sync.RWMutex + registry []Plugin +) + +// Register records a plugin for auto initialization. +func Register(p Plugin) { + if p == nil { + return + } + registryMu.Lock() + registry = append(registry, p) + registryMu.Unlock() +} + +// Registered returns a snapshot of all registered plugins. +func Registered() []Plugin { + registryMu.RLock() + defer registryMu.RUnlock() + out := make([]Plugin, len(registry)) + copy(out, registry) + return out +} diff --git a/server/utils/request/http.go b/server/utils/request/http.go new file mode 100644 index 0000000..86d0d15 --- /dev/null +++ b/server/utils/request/http.go @@ -0,0 +1,62 @@ +package request + +import ( + "bytes" + "encoding/json" + "net/http" + "net/url" +) + +func HttpRequest( + urlStr string, + method string, + headers map[string]string, + params map[string]string, + data any) (*http.Response, error) { + // 创建URL + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + // 添加查询参数 + query := u.Query() + for k, v := range params { + query.Set(k, v) + } + u.RawQuery = query.Encode() + + // 将数据编码为JSON + buf := new(bytes.Buffer) + if data != nil { + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + buf = bytes.NewBuffer(b) + } + + // 创建请求 + req, err := http.NewRequest(method, u.String(), buf) + + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + if data != nil { + req.Header.Set("Content-Type", "application/json") + } + + // 发送请求 + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + // 返回响应,让调用者处理 + return resp, nil +} diff --git a/server/utils/server.go b/server/utils/server.go new file mode 100644 index 0000000..4d84cb4 --- /dev/null +++ b/server/utils/server.go @@ -0,0 +1,127 @@ +package utils + +import ( + "runtime" + "time" + + "git.echol.cn/loser/st/server/global" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +const ( + B = 1 + KB = 1024 * B + MB = 1024 * KB + GB = 1024 * MB +) + +type Server struct { + Os Os `json:"os"` + Cpu Cpu `json:"cpu"` + Ram Ram `json:"ram"` + Disk []Disk `json:"disk"` +} + +type Os struct { + GOOS string `json:"goos"` + NumCPU int `json:"numCpu"` + Compiler string `json:"compiler"` + GoVersion string `json:"goVersion"` + NumGoroutine int `json:"numGoroutine"` +} + +type Cpu struct { + Cpus []float64 `json:"cpus"` + Cores int `json:"cores"` +} + +type Ram struct { + UsedMB int `json:"usedMb"` + TotalMB int `json:"totalMb"` + UsedPercent int `json:"usedPercent"` +} + +type Disk struct { + MountPoint string `json:"mountPoint"` + UsedMB int `json:"usedMb"` + UsedGB int `json:"usedGb"` + TotalMB int `json:"totalMb"` + TotalGB int `json:"totalGb"` + UsedPercent int `json:"usedPercent"` +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: OS信息 +//@return: o Os, err error + +func InitOS() (o Os) { + o.GOOS = runtime.GOOS + o.NumCPU = runtime.NumCPU() + o.Compiler = runtime.Compiler + o.GoVersion = runtime.Version() + o.NumGoroutine = runtime.NumGoroutine() + return o +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitCPU +//@description: CPU信息 +//@return: c Cpu, err error + +func InitCPU() (c Cpu, err error) { + if cores, err := cpu.Counts(false); err != nil { + return c, err + } else { + c.Cores = cores + } + if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil { + return c, err + } else { + c.Cpus = cpus + } + return c, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitRAM +//@description: RAM信息 +//@return: r Ram, err error + +func InitRAM() (r Ram, err error) { + if u, err := mem.VirtualMemory(); err != nil { + return r, err + } else { + r.UsedMB = int(u.Used) / MB + r.TotalMB = int(u.Total) / MB + r.UsedPercent = int(u.UsedPercent) + } + return r, nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@function: InitDisk +//@description: 硬盘信息 +//@return: d Disk, err error + +func InitDisk() (d []Disk, err error) { + for i := range global.GVA_CONFIG.DiskList { + mp := global.GVA_CONFIG.DiskList[i].MountPoint + if u, err := disk.Usage(mp); err != nil { + return d, err + } else { + d = append(d, Disk{ + MountPoint: mp, + UsedMB: int(u.Used) / MB, + UsedGB: int(u.Used) / GB, + TotalMB: int(u.Total) / MB, + TotalGB: int(u.Total) / GB, + UsedPercent: int(u.UsedPercent), + }) + } + } + return d, nil +} diff --git a/server/utils/stacktrace/stacktrace.go b/server/utils/stacktrace/stacktrace.go new file mode 100644 index 0000000..e9f4dbd --- /dev/null +++ b/server/utils/stacktrace/stacktrace.go @@ -0,0 +1,79 @@ +package stacktrace + +import ( + "regexp" + "strconv" + "strings" +) + +// Frame 表示一次栈帧解析结果 +type Frame struct { + File string + Line int + Func string +} + +var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`) + +// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号 +// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件 +func FindFinalCaller(stack string) (Frame, bool) { + if stack == "" { + return Frame{}, false + } + lines := strings.Split(stack, "\n") + var currFunc string + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + if m := fileLineRe.FindStringSubmatch(line); m != nil { + file := m[1] + ln, _ := strconv.Atoi(m[2]) + if shouldSkip(file) { + // 跳过此帧,同时重置函数名以避免错误配对 + currFunc = "" + continue + } + return Frame{File: file, Line: ln, Func: currFunc}, true + } + // 记录函数名行,下一行通常是文件:行 + currFunc = line + } + return Frame{}, false +} + +func shouldSkip(file string) bool { + // 第三方库与 Go 模块缓存 + if strings.Contains(file, "/go/pkg/mod/") { + return true + } + if strings.Contains(file, "/go.uber.org/") { + return true + } + if strings.Contains(file, "/gorm.io/") { + return true + } + // 标准库 + if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go + return true + } + // 框架内不需要作为最终调用方的路径 + if strings.Contains(file, "/server/core/zap.go") { + return true + } + if strings.Contains(file, "/server/core/") { + return true + } + if strings.Contains(file, "/server/utils/errorhook/") { + return true + } + if strings.Contains(file, "/server/middleware/") { + return true + } + if strings.Contains(file, "/server/router/") { + return true + } + return false +} diff --git a/server/utils/system_events.go b/server/utils/system_events.go new file mode 100644 index 0000000..126d85b --- /dev/null +++ b/server/utils/system_events.go @@ -0,0 +1,34 @@ +package utils + +import ( + "sync" +) + +// SystemEvents 定义系统级事件处理 +type SystemEvents struct { + reloadHandlers []func() error + mu sync.RWMutex +} + +// 全局事件管理器 +var GlobalSystemEvents = &SystemEvents{} + +// RegisterReloadHandler 注册系统重载处理函数 +func (e *SystemEvents) RegisterReloadHandler(handler func() error) { + e.mu.Lock() + defer e.mu.Unlock() + e.reloadHandlers = append(e.reloadHandlers, handler) +} + +// TriggerReload 触发所有注册的重载处理函数 +func (e *SystemEvents) TriggerReload() error { + e.mu.RLock() + defer e.mu.RUnlock() + + for _, handler := range e.reloadHandlers { + if err := handler(); err != nil { + return err + } + } + return nil +} diff --git a/server/utils/timer/timed_task.go b/server/utils/timer/timed_task.go new file mode 100644 index 0000000..93a2b91 --- /dev/null +++ b/server/utils/timer/timed_task.go @@ -0,0 +1,230 @@ +package timer + +import ( + "sync" + + "github.com/robfig/cron/v3" +) + +type Timer interface { + // 寻找所有Cron + FindCronList() map[string]*taskManager + // 添加Task 方法形式以秒的形式加入 + AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 + // 添加Task 接口形式以秒的形式加入 + AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过函数的方法添加任务 + AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发 + AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 获取对应taskName的cron 可能会为空 + FindCron(cronName string) (*taskManager, bool) + // 指定cron开始执行 + StartCron(cronName string) + // 指定cron停止执行 + StopCron(cronName string) + // 查找指定cron下的指定task + FindTask(cronName string, taskName string) (*task, bool) + // 根据id删除指定cron下的指定task + RemoveTask(cronName string, id int) + // 根据taskName删除指定cron下的指定task + RemoveTaskByName(cronName string, taskName string) + // 清理掉指定cronName + Clear(cronName string) + // 停止所有的cron + Close() +} + +type task struct { + EntryID cron.EntryID + Spec string + TaskName string +} + +type taskManager struct { + corn *cron.Cron + tasks map[cron.EntryID]*task +} + +// timer 定时任务管理 +type timer struct { + cronList map[string]*taskManager + sync.Mutex +} + +// AddTaskByFunc 通过函数的方法添加任务 +func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJob 通过接口的方法添加任务 +func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJobWithSeconds 通过接口的方法添加任务 +func (t *timer) AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// FindCron 获取对应cronName的cron 可能会为空 +func (t *timer) FindCron(cronName string) (*taskManager, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + return v, ok +} + +// FindTask 获取对应cronName的cron 可能会为空 +func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + if !ok { + return nil, ok + } + for _, t2 := range v.tasks { + if t2.TaskName == taskName { + return t2, true + } + } + return nil, false +} + +// FindCronList 获取所有的任务列表 +func (t *timer) FindCronList() map[string]*taskManager { + t.Lock() + defer t.Unlock() + return t.cronList +} + +// StartCron 开始任务 +func (t *timer) StartCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Start() + } +} + +// StopCron 停止任务 +func (t *timer) StopCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + } +} + +// RemoveTask 从cronName 删除指定任务 +func (t *timer) RemoveTask(cronName string, id int) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Remove(cron.EntryID(id)) + delete(v.tasks, cron.EntryID(id)) + } +} + +// RemoveTaskByName 从cronName 使用taskName 删除指定任务 +func (t *timer) RemoveTaskByName(cronName string, taskName string) { + fTask, ok := t.FindTask(cronName, taskName) + if !ok { + return + } + t.RemoveTask(cronName, int(fTask.EntryID)) +} + +// Clear 清除任务 +func (t *timer) Clear(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + delete(t.cronList, cronName) + } +} + +// Close 释放资源 +func (t *timer) Close() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Stop() + } +} + +func NewTimerTask() Timer { + return &timer{cronList: make(map[string]*taskManager)} +} diff --git a/server/utils/timer/timed_task_test.go b/server/utils/timer/timed_task_test.go new file mode 100644 index 0000000..9f2c02c --- /dev/null +++ b/server/utils/timer/timed_task_test.go @@ -0,0 +1,72 @@ +package timer + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +var job = mockJob{} + +type mockJob struct{} + +func (job mockJob) Run() { + mockFunc() +} + +func mockFunc() { + time.Sleep(time.Second) + fmt.Println("1s...") +} + +func TestNewTimerTask(t *testing.T) { + tm := NewTimerTask() + _tm := tm.(*timer) + + { + _, err := tm.AddTaskByFunc("func", "@every 1s", mockFunc, "测试mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["func"] + if !ok { + t.Error("no find func") + } + } + + { + _, err := tm.AddTaskByJob("job", "@every 1s", job, "测试job mockfunc") + assert.Nil(t, err) + _, ok := _tm.cronList["job"] + if !ok { + t.Error("no find job") + } + } + + { + _, ok := tm.FindCron("func") + if !ok { + t.Error("no find func") + } + _, ok = tm.FindCron("job") + if !ok { + t.Error("no find job") + } + _, ok = tm.FindCron("none") + if ok { + t.Error("find none") + } + } + { + tm.Clear("func") + _, ok := tm.FindCron("func") + if ok { + t.Error("find func") + } + } + { + a := tm.FindCronList() + b, c := tm.FindCron("job") + fmt.Println(a, b, c) + } +} diff --git a/server/utils/upload/aliyun_oss.go b/server/utils/upload/aliyun_oss.go new file mode 100644 index 0000000..db8b57b --- /dev/null +++ b/server/utils/upload/aliyun_oss.go @@ -0,0 +1,75 @@ +package upload + +import ( + "errors" + "mime/multipart" + "time" + + "git.echol.cn/loser/st/server/global" + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "go.uber.org/zap" +) + +type AliyunOSS struct{} + +func (*AliyunOSS) UploadFile(file *multipart.FileHeader) (string, string, error) { + bucket, err := NewBucket() + if err != nil { + global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) + return "", "", errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) + } + + // 读取本地文件。 + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + // 上传阿里云路径 文件名格式 自己可以改 建议保证唯一性 + // yunFileTmpPath := filepath.Join("uploads", time.Now().Format("2006-01-02")) + "/" + file.Filename + yunFileTmpPath := global.GVA_CONFIG.AliyunOSS.BasePath + "/" + "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + file.Filename + + // 上传文件流。 + err = bucket.PutObject(yunFileTmpPath, f) + if err != nil { + global.GVA_LOG.Error("function formUploader.Put() Failed", zap.Any("err", err.Error())) + return "", "", errors.New("function formUploader.Put() Failed, err:" + err.Error()) + } + + return global.GVA_CONFIG.AliyunOSS.BucketUrl + "/" + yunFileTmpPath, yunFileTmpPath, nil +} + +func (*AliyunOSS) DeleteFile(key string) error { + bucket, err := NewBucket() + if err != nil { + global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error())) + return errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error()) + } + + // 删除单个文件。objectName表示删除OSS文件时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。 + // 如需删除文件夹,请将objectName设置为对应的文件夹名称。如果文件夹非空,则需要将文件夹下的所有object删除后才能删除该文件夹。 + err = bucket.DeleteObject(key) + if err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + + return nil +} + +func NewBucket() (*oss.Bucket, error) { + // 创建OSSClient实例。 + client, err := oss.New(global.GVA_CONFIG.AliyunOSS.Endpoint, global.GVA_CONFIG.AliyunOSS.AccessKeyId, global.GVA_CONFIG.AliyunOSS.AccessKeySecret) + if err != nil { + return nil, err + } + + // 获取存储空间。 + bucket, err := client.Bucket(global.GVA_CONFIG.AliyunOSS.BucketName) + if err != nil { + return nil, err + } + + return bucket, nil +} diff --git a/server/utils/upload/aws_s3.go b/server/utils/upload/aws_s3.go new file mode 100644 index 0000000..a031fb3 --- /dev/null +++ b/server/utils/upload/aws_s3.go @@ -0,0 +1,98 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/st/server/global" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "go.uber.org/zap" +) + +type AwsS3 struct{} + +//@author: [WqyJh](https://github.com/WqyJh) +//@object: *AwsS3 +//@function: UploadFile +//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { + session := newSession() + uploader := s3manager.NewUploader(session) + + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + _, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket), + Key: aws.String(filename), + Body: f, + ContentType: aws.String(file.Header.Get("Content-Type")), + }) + if err != nil { + global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) + return "", "", err + } + + return global.GVA_CONFIG.AwsS3.BaseURL + "/" + filename, fileKey, nil +} + +//@author: [WqyJh](https://github.com/WqyJh) +//@object: *AwsS3 +//@function: DeleteFile +//@description: Delete file from Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-delete-bucket-item +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*AwsS3) DeleteFile(key string) error { + session := newSession() + svc := s3.New(session) + filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key + bucket := global.GVA_CONFIG.AwsS3.Bucket + + _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + if err != nil { + global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + } + + _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + return nil +} + +// newSession Create S3 session +func newSession() *session.Session { + sess, _ := session.NewSession(&aws.Config{ + Region: aws.String(global.GVA_CONFIG.AwsS3.Region), + Endpoint: aws.String(global.GVA_CONFIG.AwsS3.Endpoint), //minio在这里设置地址,可以兼容 + S3ForcePathStyle: aws.Bool(global.GVA_CONFIG.AwsS3.S3ForcePathStyle), + DisableSSL: aws.Bool(global.GVA_CONFIG.AwsS3.DisableSSL), + Credentials: credentials.NewStaticCredentials( + global.GVA_CONFIG.AwsS3.SecretID, + global.GVA_CONFIG.AwsS3.SecretKey, + "", + ), + }) + return sess +} diff --git a/server/utils/upload/cloudflare_r2.go b/server/utils/upload/cloudflare_r2.go new file mode 100644 index 0000000..536a529 --- /dev/null +++ b/server/utils/upload/cloudflare_r2.go @@ -0,0 +1,85 @@ +package upload + +import ( + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/st/server/global" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "go.uber.org/zap" +) + +type CloudflareR2 struct{} + +func (c *CloudflareR2) UploadFile(file *multipart.FileHeader) (fileUrl string, fileName string, err error) { + session := c.newSession() + client := s3manager.NewUploader(session) + + fileKey := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename) + fileName = fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.Path, fileKey) + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + input := &s3manager.UploadInput{ + Bucket: aws.String(global.GVA_CONFIG.CloudflareR2.Bucket), + Key: aws.String(fileName), + Body: f, + } + + _, err = client.Upload(input) + if err != nil { + global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error())) + return "", "", err + } + + return fmt.Sprintf("%s/%s", global.GVA_CONFIG.CloudflareR2.BaseURL, + fileName), + fileKey, + nil +} + +func (c *CloudflareR2) DeleteFile(key string) error { + session := newSession() + svc := s3.New(session) + filename := global.GVA_CONFIG.CloudflareR2.Path + "/" + key + bucket := global.GVA_CONFIG.CloudflareR2.Bucket + + _, err := svc.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + if err != nil { + global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error())) + return errors.New("function svc.DeleteObject() failed, err:" + err.Error()) + } + + _ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + return nil +} + +func (*CloudflareR2) newSession() *session.Session { + endpoint := fmt.Sprintf("%s.r2.cloudflarestorage.com", global.GVA_CONFIG.CloudflareR2.AccountID) + + return session.Must(session.NewSession(&aws.Config{ + Region: aws.String("auto"), + Endpoint: aws.String(endpoint), + Credentials: credentials.NewStaticCredentials( + global.GVA_CONFIG.CloudflareR2.AccessKeyID, + global.GVA_CONFIG.CloudflareR2.SecretAccessKey, + "", + ), + })) +} diff --git a/server/utils/upload/local.go b/server/utils/upload/local.go new file mode 100644 index 0000000..6ff8dfe --- /dev/null +++ b/server/utils/upload/local.go @@ -0,0 +1,109 @@ +package upload + +import ( + "errors" + "io" + "mime/multipart" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/utils" + "go.uber.org/zap" +) + +var mu sync.Mutex + +type Local struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Local +//@function: UploadFile +//@description: 上传文件 +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*Local) UploadFile(file *multipart.FileHeader) (string, string, error) { + // 读取文件后缀 + ext := filepath.Ext(file.Filename) + // 读取文件名并加密 + name := strings.TrimSuffix(file.Filename, ext) + name = utils.MD5V([]byte(name)) + // 拼接新文件名 + filename := name + "_" + time.Now().Format("20060102150405") + ext + // 尝试创建此路径 + mkdirErr := os.MkdirAll(global.GVA_CONFIG.Local.StorePath, os.ModePerm) + if mkdirErr != nil { + global.GVA_LOG.Error("function os.MkdirAll() failed", zap.Any("err", mkdirErr.Error())) + return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error()) + } + // 拼接路径和文件名 + p := global.GVA_CONFIG.Local.StorePath + "/" + filename + filepath := global.GVA_CONFIG.Local.Path + "/" + filename + + f, openError := file.Open() // 读取文件 + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + + out, createErr := os.Create(p) + if createErr != nil { + global.GVA_LOG.Error("function os.Create() failed", zap.Any("err", createErr.Error())) + + return "", "", errors.New("function os.Create() failed, err:" + createErr.Error()) + } + defer out.Close() // 创建文件 defer 关闭 + + _, copyErr := io.Copy(out, f) // 传输(拷贝)文件 + if copyErr != nil { + global.GVA_LOG.Error("function io.Copy() failed", zap.Any("err", copyErr.Error())) + return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error()) + } + return filepath, filename, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Local +//@function: DeleteFile +//@description: 删除文件 +//@param: key string +//@return: error + +func (*Local) DeleteFile(key string) error { + // 检查 key 是否为空 + if key == "" { + return errors.New("key不能为空") + } + + // 验证 key 是否包含非法字符或尝试访问存储路径之外的文件 + if strings.Contains(key, "..") || strings.ContainsAny(key, `\/:*?"<>|`) { + return errors.New("非法的key") + } + + p := filepath.Join(global.GVA_CONFIG.Local.StorePath, key) + + // 检查文件是否存在 + if _, err := os.Stat(p); os.IsNotExist(err) { + return errors.New("文件不存在") + } + + // 使用文件锁防止并发删除 + mu.Lock() + defer mu.Unlock() + + err := os.Remove(p) + if err != nil { + return errors.New("文件删除失败: " + err.Error()) + } + + return nil +} diff --git a/server/utils/upload/minio_oss.go b/server/utils/upload/minio_oss.go new file mode 100644 index 0000000..a142fe4 --- /dev/null +++ b/server/utils/upload/minio_oss.go @@ -0,0 +1,106 @@ +package upload + +import ( + "bytes" + "context" + "errors" + "io" + "mime" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "git.echol.cn/loser/st/server/global" + "git.echol.cn/loser/st/server/utils" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" +) + +var MinioClient *Minio // 优化性能,但是不支持动态配置 + +type Minio struct { + Client *minio.Client + bucket string +} + +func GetMinio(endpoint, accessKeyID, secretAccessKey, bucketName string, useSSL bool) (*Minio, error) { + if MinioClient != nil { + return MinioClient, nil + } + // Initialize minio client object. + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), + Secure: useSSL, // Set to true if using https + }) + if err != nil { + return nil, err + } + // 尝试创建bucket + err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{}) + if err != nil { + // Check to see if we already own this bucket (which happens if you run this twice) + exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName) + if errBucketExists == nil && exists { + // log.Printf("We already own %s\n", bucketName) + } else { + return nil, err + } + } + MinioClient = &Minio{Client: minioClient, bucket: bucketName} + return MinioClient, nil +} + +func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) { + f, openError := file.Open() + // mutipart.File to os.File + if openError != nil { + global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) + } + + filecontent := bytes.Buffer{} + _, err := io.Copy(&filecontent, f) + if err != nil { + global.GVA_LOG.Error("读取文件失败", zap.Any("err", err.Error())) + return "", "", errors.New("读取文件失败, err:" + err.Error()) + } + f.Close() // 创建文件 defer 关闭 + + // 对文件名进行加密存储 + ext := filepath.Ext(file.Filename) + filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext + if global.GVA_CONFIG.Minio.BasePath == "" { + filePathres = "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + filename + } else { + filePathres = global.GVA_CONFIG.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename + } + + // 根据文件扩展名检测 MIME 类型 + contentType := mime.TypeByExtension(ext) + if contentType == "" { + contentType = "application/octet-stream" + } + + // 设置超时10分钟 + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + + // Upload the file with PutObject 大文件自动切换为分片上传 + info, err := m.Client.PutObject(ctx, global.GVA_CONFIG.Minio.BucketName, filePathres, &filecontent, file.Size, minio.PutObjectOptions{ContentType: contentType}) + if err != nil { + global.GVA_LOG.Error("上传文件到minio失败", zap.Any("err", err.Error())) + return "", "", errors.New("上传文件到minio失败, err:" + err.Error()) + } + return global.GVA_CONFIG.Minio.BucketUrl + "/" + info.Key, filePathres, nil +} + +func (m *Minio) DeleteFile(key string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + // Delete the object from MinIO + err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{}) + return err +} diff --git a/server/utils/upload/obs.go b/server/utils/upload/obs.go new file mode 100644 index 0000000..59e1e36 --- /dev/null +++ b/server/utils/upload/obs.go @@ -0,0 +1,69 @@ +package upload + +import ( + "mime/multipart" + + "git.echol.cn/loser/st/server/global" + "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" + "github.com/pkg/errors" +) + +var HuaWeiObs = new(Obs) + +type Obs struct{} + +func NewHuaWeiObsClient() (client *obs.ObsClient, err error) { + return obs.New(global.GVA_CONFIG.HuaWeiObs.AccessKey, global.GVA_CONFIG.HuaWeiObs.SecretKey, global.GVA_CONFIG.HuaWeiObs.Endpoint) +} + +func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) { + // var open multipart.File + open, err := file.Open() + if err != nil { + return "", "", err + } + defer open.Close() + filename := file.Filename + input := &obs.PutObjectInput{ + PutObjectBasicInput: obs.PutObjectBasicInput{ + ObjectOperationInput: obs.ObjectOperationInput{ + Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, + Key: filename, + }, + HttpHeader: obs.HttpHeader{ + ContentType: file.Header.Get("content-type"), + }, + }, + Body: open, + } + + var client *obs.ObsClient + client, err = NewHuaWeiObsClient() + if err != nil { + return "", "", errors.Wrap(err, "获取华为对象存储对象失败!") + } + + _, err = client.PutObject(input) + if err != nil { + return "", "", errors.Wrap(err, "文件上传失败!") + } + filepath := global.GVA_CONFIG.HuaWeiObs.Path + "/" + filename + return filepath, filename, err +} + +func (o *Obs) DeleteFile(key string) error { + client, err := NewHuaWeiObsClient() + if err != nil { + return errors.Wrap(err, "获取华为对象存储对象失败!") + } + input := &obs.DeleteObjectInput{ + Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket, + Key: key, + } + var output *obs.DeleteObjectOutput + output, err = client.DeleteObject(input) + if err != nil { + return errors.Wrapf(err, "删除对象(%s)失败!, output: %v", key, output) + } + return nil +} diff --git a/server/utils/upload/qiniu.go b/server/utils/upload/qiniu.go new file mode 100644 index 0000000..3e49de6 --- /dev/null +++ b/server/utils/upload/qiniu.go @@ -0,0 +1,96 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "time" + + "git.echol.cn/loser/st/server/global" + "github.com/qiniu/go-sdk/v7/auth/qbox" + "github.com/qiniu/go-sdk/v7/storage" + "go.uber.org/zap" +) + +type Qiniu struct{} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: UploadFile +//@description: 上传文件 +//@param: file *multipart.FileHeader +//@return: string, string, error + +func (*Qiniu) UploadFile(file *multipart.FileHeader) (string, string, error) { + putPolicy := storage.PutPolicy{Scope: global.GVA_CONFIG.Qiniu.Bucket} + mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) + upToken := putPolicy.UploadToken(mac) + cfg := qiniuConfig() + formUploader := storage.NewFormUploader(cfg) + ret := storage.PutRet{} + putExtra := storage.PutExtra{Params: map[string]string{"x:name": "github logo"}} + + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) // 文件名格式 自己可以改 建议保证唯一性 + putErr := formUploader.Put(context.Background(), &ret, upToken, fileKey, f, file.Size, &putExtra) + if putErr != nil { + global.GVA_LOG.Error("function formUploader.Put() failed", zap.Any("err", putErr.Error())) + return "", "", errors.New("function formUploader.Put() failed, err:" + putErr.Error()) + } + return global.GVA_CONFIG.Qiniu.ImgPath + "/" + ret.Key, ret.Key, nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@author: [ccfish86](https://github.com/ccfish86) +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: DeleteFile +//@description: 删除文件 +//@param: key string +//@return: error + +func (*Qiniu) DeleteFile(key string) error { + mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey) + cfg := qiniuConfig() + bucketManager := storage.NewBucketManager(mac, cfg) + if err := bucketManager.Delete(global.GVA_CONFIG.Qiniu.Bucket, key); err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +} + +//@author: [SliverHorn](https://github.com/SliverHorn) +//@object: *Qiniu +//@function: qiniuConfig +//@description: 根据配置文件进行返回七牛云的配置 +//@return: *storage.Config + +func qiniuConfig() *storage.Config { + cfg := storage.Config{ + UseHTTPS: global.GVA_CONFIG.Qiniu.UseHTTPS, + UseCdnDomains: global.GVA_CONFIG.Qiniu.UseCdnDomains, + } + switch global.GVA_CONFIG.Qiniu.Zone { // 根据配置文件进行初始化空间对应的机房 + case "ZoneHuadong": + cfg.Zone = &storage.ZoneHuadong + case "ZoneHuabei": + cfg.Zone = &storage.ZoneHuabei + case "ZoneHuanan": + cfg.Zone = &storage.ZoneHuanan + case "ZoneBeimei": + cfg.Zone = &storage.ZoneBeimei + case "ZoneXinjiapo": + cfg.Zone = &storage.ZoneXinjiapo + } + return &cfg +} diff --git a/server/utils/upload/tencent_cos.go b/server/utils/upload/tencent_cos.go new file mode 100644 index 0000000..d8770a1 --- /dev/null +++ b/server/utils/upload/tencent_cos.go @@ -0,0 +1,61 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "time" + + "git.echol.cn/loser/st/server/global" + + "github.com/tencentyun/cos-go-sdk-v5" + "go.uber.org/zap" +) + +type TencentCOS struct{} + +// UploadFile upload file to COS +func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) { + client := NewClient() + f, openError := file.Open() + if openError != nil { + global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + + _, err := client.Object.Put(context.Background(), global.GVA_CONFIG.TencentCOS.PathPrefix+"/"+fileKey, f, nil) + if err != nil { + panic(err) + } + return global.GVA_CONFIG.TencentCOS.BaseURL + "/" + global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil +} + +// DeleteFile delete file form COS +func (*TencentCOS) DeleteFile(key string) error { + client := NewClient() + name := global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + key + _, err := client.Object.Delete(context.Background(), name) + if err != nil { + global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +} + +// NewClient init COS client +func NewClient() *cos.Client { + urlStr, _ := url.Parse("https://" + global.GVA_CONFIG.TencentCOS.Bucket + ".cos." + global.GVA_CONFIG.TencentCOS.Region + ".myqcloud.com") + baseURL := &cos.BaseURL{BucketURL: urlStr} + client := cos.NewClient(baseURL, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: global.GVA_CONFIG.TencentCOS.SecretID, + SecretKey: global.GVA_CONFIG.TencentCOS.SecretKey, + }, + }) + return client +} diff --git a/server/utils/upload/upload.go b/server/utils/upload/upload.go new file mode 100644 index 0000000..da5f6db --- /dev/null +++ b/server/utils/upload/upload.go @@ -0,0 +1,46 @@ +package upload + +import ( + "mime/multipart" + + "git.echol.cn/loser/st/server/global" +) + +// OSS 对象存储接口 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ccfish86](https://github.com/ccfish86) +type OSS interface { + UploadFile(file *multipart.FileHeader) (string, string, error) + DeleteFile(key string) error +} + +// NewOss OSS的实例化方法 +// Author [SliverHorn](https://github.com/SliverHorn) +// Author [ccfish86](https://github.com/ccfish86) +func NewOss() OSS { + switch global.GVA_CONFIG.System.OssType { + case "local": + return &Local{} + case "qiniu": + return &Qiniu{} + case "tencent-cos": + return &TencentCOS{} + case "aliyun-oss": + return &AliyunOSS{} + case "huawei-obs": + return HuaWeiObs + case "aws-s3": + return &AwsS3{} + case "cloudflare-r2": + return &CloudflareR2{} + case "minio": + minioClient, err := GetMinio(global.GVA_CONFIG.Minio.Endpoint, global.GVA_CONFIG.Minio.AccessKeyId, global.GVA_CONFIG.Minio.AccessKeySecret, global.GVA_CONFIG.Minio.BucketName, global.GVA_CONFIG.Minio.UseSSL) + if err != nil { + global.GVA_LOG.Warn("你配置了使用minio,但是初始化失败,请检查minio可用性或安全配置: " + err.Error()) + panic("minio初始化失败") // 建议这样做,用户自己配置了minio,如果报错了还要把服务开起来,使用起来也很危险 + } + return minioClient + default: + return &Local{} + } +} diff --git a/server/utils/validator.go b/server/utils/validator.go new file mode 100644 index 0000000..a56dac0 --- /dev/null +++ b/server/utils/validator.go @@ -0,0 +1,294 @@ +package utils + +import ( + "errors" + "reflect" + "regexp" + "strconv" + "strings" +) + +type Rules map[string][]string + +type RulesMap map[string]Rules + +var CustomizeMap = make(map[string]Rules) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: RegisterRule +//@description: 注册自定义规则方案建议在路由初始化层即注册 +//@param: key string, rule Rules +//@return: err error + +func RegisterRule(key string, rule Rules) (err error) { + if CustomizeMap[key] != nil { + return errors.New(key + "已注册,无法重复注册") + } else { + CustomizeMap[key] = rule + return nil + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: NotEmpty +//@description: 非空 不能为其对应类型的0值 +//@return: string + +func NotEmpty() string { + return "notEmpty" +} + +// @author: [zooqkl](https://github.com/zooqkl) +// @function: RegexpMatch +// @description: 正则校验 校验输入项是否满足正则表达式 +// @param: rule string +// @return: string + +func RegexpMatch(rule string) string { + return "regexp=" + rule +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Lt +//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Lt(mark string) string { + return "lt=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Le +//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Le(mark string) string { + return "le=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Eq +//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Eq(mark string) string { + return "eq=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ne +//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ne(mark string) string { + return "ne=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Ge +//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Ge(mark string) string { + return "ge=" + mark +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Gt +//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 +//@param: mark string +//@return: string + +func Gt(mark string) string { + return "gt=" + mark +} + +// +//@author: [piexlmax](https://github.com/piexlmax) +//@function: Verify +//@description: 校验方法 +//@param: st interface{}, roleMap Rules(入参实例,规则map) +//@return: err error + +func Verify(st interface{}, roleMap Rules) (err error) { + compareMap := map[string]bool{ + "lt": true, + "le": true, + "eq": true, + "ne": true, + "ge": true, + "gt": true, + } + + typ := reflect.TypeOf(st) + val := reflect.ValueOf(st) // 获取reflect.Type类型 + + kd := val.Kind() // 获取到st对应的类别 + if kd != reflect.Struct { + return errors.New("expect struct") + } + num := val.NumField() + // 遍历结构体的所有字段 + for i := 0; i < num; i++ { + tagVal := typ.Field(i) + val := val.Field(i) + if tagVal.Type.Kind() == reflect.Struct { + if err = Verify(val.Interface(), roleMap); err != nil { + return err + } + } + if len(roleMap[tagVal.Name]) > 0 { + for _, v := range roleMap[tagVal.Name] { + switch { + case v == "notEmpty": + if isBlank(val) { + return errors.New(tagVal.Name + "值不能为空") + } + case strings.Split(v, "=")[0] == "regexp": + if !regexpMatch(strings.Split(v, "=")[1], val.String()) { + return errors.New(tagVal.Name + "格式校验不通过") + } + case compareMap[strings.Split(v, "=")[0]]: + if !compareVerify(val, v) { + return errors.New(tagVal.Name + "长度或值不在合法范围," + v) + } + } + } + } + } + return nil +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compareVerify +//@description: 长度和数字的校验方法 根据类型自动校验 +//@param: value reflect.Value, VerifyStr string +//@return: bool + +func compareVerify(value reflect.Value, VerifyStr string) bool { + switch value.Kind() { + case reflect.String: + return compare(len([]rune(value.String())), VerifyStr) + case reflect.Slice, reflect.Array: + return compare(value.Len(), VerifyStr) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return compare(value.Uint(), VerifyStr) + case reflect.Float32, reflect.Float64: + return compare(value.Float(), VerifyStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return compare(value.Int(), VerifyStr) + default: + return false + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: isBlank +//@description: 非空校验 +//@param: value reflect.Value +//@return: bool + +func isBlank(value reflect.Value) bool { + switch value.Kind() { + case reflect.String, reflect.Slice: + return value.Len() == 0 + case reflect.Bool: + return !value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return value.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return value.Uint() == 0 + case reflect.Float32, reflect.Float64: + return value.Float() == 0 + case reflect.Interface, reflect.Ptr: + return value.IsNil() + } + return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: compare +//@description: 比较函数 +//@param: value interface{}, VerifyStr string +//@return: bool + +func compare(value interface{}, VerifyStr string) bool { + VerifyStrArr := strings.Split(VerifyStr, "=") + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Int() < VInt + case VerifyStrArr[0] == "le": + return val.Int() <= VInt + case VerifyStrArr[0] == "eq": + return val.Int() == VInt + case VerifyStrArr[0] == "ne": + return val.Int() != VInt + case VerifyStrArr[0] == "ge": + return val.Int() >= VInt + case VerifyStrArr[0] == "gt": + return val.Int() > VInt + default: + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + VInt, VErr := strconv.Atoi(VerifyStrArr[1]) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Uint() < uint64(VInt) + case VerifyStrArr[0] == "le": + return val.Uint() <= uint64(VInt) + case VerifyStrArr[0] == "eq": + return val.Uint() == uint64(VInt) + case VerifyStrArr[0] == "ne": + return val.Uint() != uint64(VInt) + case VerifyStrArr[0] == "ge": + return val.Uint() >= uint64(VInt) + case VerifyStrArr[0] == "gt": + return val.Uint() > uint64(VInt) + default: + return false + } + case reflect.Float32, reflect.Float64: + VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) + if VErr != nil { + return false + } + switch { + case VerifyStrArr[0] == "lt": + return val.Float() < VFloat + case VerifyStrArr[0] == "le": + return val.Float() <= VFloat + case VerifyStrArr[0] == "eq": + return val.Float() == VFloat + case VerifyStrArr[0] == "ne": + return val.Float() != VFloat + case VerifyStrArr[0] == "ge": + return val.Float() >= VFloat + case VerifyStrArr[0] == "gt": + return val.Float() > VFloat + default: + return false + } + default: + return false + } +} + +func regexpMatch(rule, matchStr string) bool { + return regexp.MustCompile(rule).MatchString(matchStr) +} diff --git a/server/utils/validator_test.go b/server/utils/validator_test.go new file mode 100644 index 0000000..70187db --- /dev/null +++ b/server/utils/validator_test.go @@ -0,0 +1,38 @@ +package utils + +import ( + "testing" + + "git.echol.cn/loser/st/server/model/common/request" +) + +type PageInfoTest struct { + PageInfo request.PageInfo + Name string +} + +func TestVerify(t *testing.T) { + PageInfoVerify := Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "Name": {NotEmpty()}} + var testInfo PageInfoTest + testInfo.Name = "test" + testInfo.PageInfo.Page = 0 + testInfo.PageInfo.PageSize = 0 + err := Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能捕捉0值") + } + testInfo.Name = "" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err == nil { + t.Error("校验失败,未能正常检测name为空") + } + testInfo.Name = "test" + testInfo.PageInfo.Page = 1 + testInfo.PageInfo.PageSize = 10 + err = Verify(testInfo, PageInfoVerify) + if err != nil { + t.Error("校验失败,未能正常通过检测") + } +} diff --git a/server/utils/verify.go b/server/utils/verify.go new file mode 100644 index 0000000..cc2cb78 --- /dev/null +++ b/server/utils/verify.go @@ -0,0 +1,19 @@ +package utils + +var ( + IdVerify = Rules{"ID": []string{NotEmpty()}} + ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}} + MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}} + MenuMetaVerify = Rules{"Title": {NotEmpty()}} + LoginVerify = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}} + RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}} + PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}} + CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}} + AutoCodeVerify = Rules{"Abbreviation": {NotEmpty()}, "StructName": {NotEmpty()}, "PackageName": {NotEmpty()}} + AutoPackageVerify = Rules{"PackageName": {NotEmpty()}} + AuthorityVerify = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}} + AuthorityIdVerify = Rules{"AuthorityId": {NotEmpty()}} + OldAuthorityVerify = Rules{"OldAuthorityId": {NotEmpty()}} + ChangePasswordVerify = Rules{"Password": {NotEmpty()}, "NewPassword": {NotEmpty()}} + SetUserAuthorityVerify = Rules{"AuthorityId": {NotEmpty()}} +) diff --git a/server/utils/zip.go b/server/utils/zip.go new file mode 100644 index 0000000..ef35d10 --- /dev/null +++ b/server/utils/zip.go @@ -0,0 +1,53 @@ +package utils + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// 解压 +func Unzip(zipFile string, destDir string) ([]string, error) { + zipReader, err := zip.OpenReader(zipFile) + var paths []string + if err != nil { + return []string{}, err + } + defer zipReader.Close() + + for _, f := range zipReader.File { + if strings.Contains(f.Name, "..") { + return []string{}, fmt.Errorf("%s 文件名不合法", f.Name) + } + fpath := filepath.Join(destDir, f.Name) + paths = append(paths, fpath) + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + } else { + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return []string{}, err + } + + inFile, err := f.Open() + if err != nil { + return []string{}, err + } + defer inFile.Close() + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return []string{}, err + } + defer outFile.Close() + + _, err = io.Copy(outFile, inFile) + if err != nil { + return []string{}, err + } + } + } + return paths, nil +} diff --git a/web-app/.env.development b/web-app/.env.development new file mode 100644 index 0000000..f915f89 --- /dev/null +++ b/web-app/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8888 diff --git a/web-app/.env.production b/web-app/.env.production new file mode 100644 index 0000000..3083254 --- /dev/null +++ b/web-app/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://your-production-api.com diff --git a/web-app/.vite/deps/_metadata.json b/web-app/.vite/deps/_metadata.json new file mode 100644 index 0000000..c60efdd --- /dev/null +++ b/web-app/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "137c6446", + "configHash": "6b4f1a36", + "lockfileHash": "ed4ec364", + "browserHash": "4a9d650d", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/web-app/.vite/deps/package.json b/web-app/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/web-app/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/web-app/.vite/deps_temp_7925bc02/package.json b/web-app/.vite/deps_temp_7925bc02/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/web-app/.vite/deps_temp_7925bc02/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/web-app/README.md b/web-app/README.md new file mode 100644 index 0000000..fd5f86e --- /dev/null +++ b/web-app/README.md @@ -0,0 +1,55 @@ +# 云酒馆前端 + +现代化的 SillyTavern UI,采用 Glassmorphism 风格和深色主题。 + +## 技术栈 + +- React 18 + TypeScript +- Tailwind CSS +- Vite +- React Router +- Lucide Icons + +## 开发 + +```bash +npm install +npm run dev +``` + +## 构建 + +```bash +npm run build +npm run preview +``` + +## 项目结构 + +``` +src/ +├── components/ # 可复用组件 +│ ├── Navbar.tsx +│ ├── Sidebar.tsx +│ ├── ChatArea.tsx +│ └── ... +├── pages/ # 页面组件 +│ ├── HomePage.tsx +│ ├── CharacterMarket.tsx +│ ├── CharacterManagePage.tsx +│ ├── PresetManagePage.tsx +│ └── ... +├── App.tsx # 路由配置 +├── main.tsx # 入口文件 +└── index.css # 全局样式 +``` + +## 设计系统 + +- 主色调: #7C3AED (紫色) +- 次要色: #A78BFA (淡紫色) +- 强调色: #F97316 (橙色) +- 字体: Inter +- 风格: Glassmorphism + 深色主题 + +详见 [设计系统文档](../docs/design-system/) diff --git a/web-app/index.html b/web-app/index.html new file mode 100644 index 0000000..566d5ed --- /dev/null +++ b/web-app/index.html @@ -0,0 +1,15 @@ + + + + + + + + + SillyTavern - Modern UI + + +
+ + + diff --git a/web-app/package-lock.json b/web-app/package-lock.json new file mode 100644 index 0000000..417a586 --- /dev/null +++ b/web-app/package-lock.json @@ -0,0 +1,4652 @@ +{ + "name": "sillytavern-modern-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sillytavern-modern-ui", + "version": "0.0.0", + "dependencies": { + "axios": "^1.13.5", + "lucide-react": "^0.344.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.5.3", + "vite": "^5.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.344.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", + "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web-app/package.json b/web-app/package.json new file mode 100644 index 0000000..9111f49 --- /dev/null +++ b/web-app/package.json @@ -0,0 +1,32 @@ +{ + "name": "sillytavern-modern-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.13.5", + "lucide-react": "^0.344.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.5.3", + "vite": "^5.3.1" + } +} diff --git a/web-app/postcss.config.js b/web-app/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/web-app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx new file mode 100644 index 0000000..72fd35c --- /dev/null +++ b/web-app/src/App.tsx @@ -0,0 +1,36 @@ +import {BrowserRouter, Route, Routes} from 'react-router-dom' +import HomePage from './pages/HomePage' +import CharacterMarket from './pages/CharacterMarket' +import CharacterDetail from './pages/CharacterDetail' +import CharacterDetailPage from './pages/CharacterDetailPage' +import ChatPage from './pages/ChatPage' +import LoginPage from './pages/LoginPage' +import RegisterPage from './pages/RegisterPage' +import ForgotPasswordPage from './pages/ForgotPasswordPage' +import ProfilePage from './pages/ProfilePage' +import CharacterManagePage from './pages/CharacterManagePage' +import PresetManagePage from './pages/PresetManagePage' +import AdminPage from './pages/AdminPage' + +function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/web-app/src/api/aiConfig.ts b/web-app/src/api/aiConfig.ts new file mode 100644 index 0000000..0425ff6 --- /dev/null +++ b/web-app/src/api/aiConfig.ts @@ -0,0 +1,114 @@ +import apiClient from './client' + +export interface AIConfig { + id: number + name: string + provider: 'openai' | 'anthropic' | 'custom' + baseUrl: string // 注意:后端返回的是 baseUrl 而不是 baseURL + apiKey: string + models?: string[] + defaultModel: string + settings?: Record + isActive: boolean + isDefault: boolean + createdAt: string + updatedAt: string +} + +export interface CreateAIConfigRequest { + name: string + provider: 'openai' | 'anthropic' | 'custom' + baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL + apiKey: string + defaultModel?: string + settings?: Record + isActive?: boolean + isDefault?: boolean +} + +export interface UpdateAIConfigRequest { + name?: string + provider?: 'openai' | 'anthropic' | 'custom' + baseUrl?: string // 注意:后端使用 baseUrl 而不是 baseURL + apiKey?: string + defaultModel?: string + settings?: Record + isActive?: boolean + isDefault?: boolean +} + +export interface ModelInfo { + id: string + name: string + ownedBy: string +} + +export interface GetModelsRequest { + baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL + apiKey: string + provider: 'openai' | 'anthropic' | 'custom' +} + +export interface GetModelsResponse { + models: ModelInfo[] +} + +export interface TestAIConfigRequest { + baseUrl: string // 注意:后端使用 baseUrl 而不是 baseURL + apiKey: string + provider: 'openai' | 'anthropic' | 'custom' + model: string +} + +export interface TestAIConfigResponse { + success: boolean + message: string + latency: number +} + +export interface AIConfigListResponse { + list: AIConfig[] + total: number +} + +export const aiConfigApi = { + // 创建AI配置 + createAIConfig: (data: CreateAIConfigRequest) => { + return apiClient.post('/app/ai-config', data) + }, + + // 获取AI配置列表 + getAIConfigList: () => { + return apiClient.get('/app/ai-config') + }, + + // 更新AI配置 + updateAIConfig: (id: number, data: UpdateAIConfigRequest) => { + return apiClient.put(`/app/ai-config/${id}`, data) + }, + + // 删除AI配置 + deleteAIConfig: (id: number) => { + return apiClient.delete(`/app/ai-config/${id}`) + }, + + // 获取模型列表 + getModels: (data: GetModelsRequest) => { + return apiClient.post('/app/ai-config/models', data) + }, + + // 测试AI配置(用于新建时,需要传递完整信息) + testAIConfig: (data: TestAIConfigRequest) => { + return apiClient.post('/app/ai-config/test', data) + }, + + // 通过ID测试AI配置(用于已保存的配置,后端会从数据库获取完整API Key) + testAIConfigById: (id: number) => { + return apiClient.post(`/app/ai-config/${id}/test`) + }, + + // 通过ID获取模型列表(用于已保存的配置,后端会从数据库获取完整API Key) + getModelsByConfigId: (id: number) => { + return apiClient.get(`/app/ai-config/${id}/models`) + }, +} diff --git a/web-app/src/api/auth.ts b/web-app/src/api/auth.ts new file mode 100644 index 0000000..ff7f556 --- /dev/null +++ b/web-app/src/api/auth.ts @@ -0,0 +1,92 @@ +import apiClient from './client' + +// 类型定义 +export interface RegisterRequest { + username: string + password: string + nickName?: string + email?: string + phone?: string +} + +export interface LoginRequest { + username: string + password: string +} + +export interface UpdateProfileRequest { + nickName?: string + email?: string + phone?: string + avatar?: string + preferences?: string + aiSettings?: string +} + +export interface ChangePasswordRequest { + oldPassword: string + newPassword: string +} + +export interface User { + id: number + uuid: string + username: string + nickName: string + email: string + phone: string + avatar: string + status: string + enable: boolean + isAdmin: boolean + lastLoginAt: string | null + lastLoginIp: string + chatCount: number + messageCount: number + createdAt: string +} + +export interface LoginResponse { + user: User + token: string + refreshToken: string + expiresAt: number +} + +// API 方法 +export const authApi = { + // 用户注册 + register: (data: RegisterRequest) => { + return apiClient.post('/app/auth/register', data) + }, + + // 用户登录 + login: (data: LoginRequest): Promise<{ data: LoginResponse }> => { + return apiClient.post('/app/auth/login', data) + }, + + // 刷新 Token + refreshToken: (refreshToken: string): Promise<{ data: LoginResponse }> => { + return apiClient.post('/app/auth/refresh', { refreshToken }) + }, + + // 用户登出 + logout: () => { + return apiClient.post('/app/auth/logout') + }, + + // 获取用户信息 + getUserInfo: (): Promise<{ data: User }> => { + return apiClient.get('/app/auth/userinfo') + }, + + // 更新用户资料 + updateProfile: (data: UpdateProfileRequest) => { + return apiClient.put('/app/user/profile', data) + }, + + // 修改密码 + changePassword: (data: ChangePasswordRequest) => { + return apiClient.post('/app/user/change-password', data) + }, +} diff --git a/web-app/src/api/character.ts b/web-app/src/api/character.ts new file mode 100644 index 0000000..975e08c --- /dev/null +++ b/web-app/src/api/character.ts @@ -0,0 +1,130 @@ +import apiClient from './client' + +// 类型定义 +export interface Character { + id: number + name: string + avatar: string + creator: string + version: string + description: string + personality: string + scenario: string + firstMes: string + mesExample: string + creatorNotes: string + systemPrompt: string + postHistoryInstructions: string + tags: string[] + alternateGreetings: string[] + characterBook: Record + extensions: Record + spec: string + specVersion: string + isPublic: boolean + useCount: number + favoriteCount: number + createdAt: string + updatedAt: string +} + +export interface CreateCharacterRequest { + name: string + avatar?: string + creator?: string + version?: string + description?: string + personality?: string + scenario?: string + firstMes?: string + mesExample?: string + creatorNotes?: string + systemPrompt?: string + postHistoryInstructions?: string + tags?: string[] + alternateGreetings?: string[] + characterBook?: Record + extensions?: Record + isPublic?: boolean +} + +export interface UpdateCharacterRequest { + name?: string + avatar?: string + creator?: string + version?: string + description?: string + personality?: string + scenario?: string + firstMes?: string + mesExample?: string + creatorNotes?: string + systemPrompt?: string + postHistoryInstructions?: string + tags?: string[] + alternateGreetings?: string[] + characterBook?: Record + extensions?: Record + isPublic?: boolean +} + +export interface GetCharacterListRequest { + page?: number + pageSize?: number + keyword?: string + tag?: string + isPublic?: boolean +} + +export interface CharacterListResponse { + list: Character[] + total: number + page: number + pageSize: number +} + +// API 方法 +export const characterApi = { + // 创建角色卡 + createCharacter: (data: CreateCharacterRequest): Promise<{ data: Character }> => { + return apiClient.post('/app/character', data) + }, + + // 获取角色卡列表 + getCharacterList: (params?: GetCharacterListRequest): Promise<{ data: CharacterListResponse }> => { + return apiClient.get('/app/character', { params }) + }, + + // 获取角色卡详情 + getCharacterById: (id: number): Promise<{ data: Character }> => { + return apiClient.get(`/app/character/${id}`) + }, + + // 更新角色卡 + updateCharacter: (id: number, data: UpdateCharacterRequest) => { + return apiClient.put(`/app/character/${id}`, data) + }, + + // 删除角色卡 + deleteCharacter: (id: number) => { + return apiClient.delete(`/app/character/${id}`) + }, + + // 上传角色卡文件(PNG/JSON) + uploadCharacter: (file: File): Promise<{ data: Character }> => { + const formData = new FormData() + formData.append('file', file) + return apiClient.post('/app/character/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + }, + + // 导出角色卡为 JSON + exportCharacter: (id: number) => { + return apiClient.get(`/app/character/${id}/export`, { + responseType: 'blob', + }) + }, +} diff --git a/web-app/src/api/client.ts b/web-app/src/api/client.ts new file mode 100644 index 0000000..a6fdba9 --- /dev/null +++ b/web-app/src/api/client.ts @@ -0,0 +1,76 @@ +import axios, {AxiosError, AxiosInstance} from 'axios' + +// API 基础配置 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888' + +// 创建 axios 实例 +const apiClient: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// 请求拦截器 - 添加 Token +apiClient.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 - 统一错误处理 +apiClient.interceptors.response.use( + (response) => { + return response.data + }, + async (error: AxiosError) => { + if (error.response) { + const { status, data } = error.response + + // Token 过期,尝试刷新 + if (status === 401 && data?.data?.reload) { + const refreshToken = localStorage.getItem('refreshToken') + if (refreshToken) { + try { + const response = await axios.post(`${API_BASE_URL}/app/auth/refresh`, { + refreshToken, + }) + const { token, refreshToken: newRefreshToken } = response.data.data + localStorage.setItem('token', token) + localStorage.setItem('refreshToken', newRefreshToken) + + // 重试原请求 + if (error.config) { + error.config.headers.Authorization = `Bearer ${token}` + return apiClient.request(error.config) + } + } catch (refreshError) { + // 刷新失败,清除 Token 并跳转登录 + localStorage.removeItem('token') + localStorage.removeItem('refreshToken') + window.location.href = '/login' + return Promise.reject(refreshError) + } + } else { + // 没有 refreshToken,跳转登录 + window.location.href = '/login' + } + } + + // 返回错误信息 + return Promise.reject(data?.msg || '请求失败') + } + + return Promise.reject(error.message || '网络错误') + } +) + +export default apiClient diff --git a/web-app/src/api/conversation.ts b/web-app/src/api/conversation.ts new file mode 100644 index 0000000..434f3cf --- /dev/null +++ b/web-app/src/api/conversation.ts @@ -0,0 +1,109 @@ +import apiClient from './client' + +// 简化的角色信息(用于列表) +export interface CharacterSimple { + id: number + name: string + avatar: string + description: string + createdAt: string + updatedAt: string +} + +// 对话列表项(轻量级) +export interface ConversationListItem { + id: number + characterId: number + title: string + messageCount: number + tokenCount: number + createdAt: string + updatedAt: string + character?: CharacterSimple +} + +export interface Conversation { + id: number + userId?: number + characterId: number + title: string + presetId?: number + aiProvider: string + model: string + settings?: Record + messageCount: number + tokenCount: number + createdAt: string + updatedAt: string +} + +export interface Message { + id: number + conversationId: number + role: 'user' | 'assistant' | 'system' + content: string + tokenCount: number + createdAt: string +} + +export interface CreateConversationRequest { + characterId: number + title?: string + presetId?: number + aiProvider?: string + model?: string + settings?: Record +} + +export interface SendMessageRequest { + content: string +} + +export interface ConversationListResponse { + list: ConversationListItem[] + total: number + page: number + pageSize: number +} + +export interface MessageListResponse { + list: Message[] + total: number +} + +export const conversationApi = { + // 创建对话 + createConversation: (data: CreateConversationRequest) => { + return apiClient.post('/app/conversation', data) + }, + + // 获取对话列表 + getConversationList: (params: { page?: number; pageSize?: number }) => { + return apiClient.get('/app/conversation', { params }) + }, + + // 获取对话详情 + getConversationById: (id: number) => { + return apiClient.get(`/app/conversation/${id}`) + }, + + // 删除对话 + deleteConversation: (id: number) => { + return apiClient.delete(`/app/conversation/${id}`) + }, + + // 获取消息列表 + getMessageList: (conversationId: number, params: { page?: number; pageSize?: number }) => { + return apiClient.get(`/app/conversation/${conversationId}/messages`, { params }) + }, + + // 发送消息 + sendMessage: (conversationId: number, data: SendMessageRequest) => { + return apiClient.post(`/app/conversation/${conversationId}/message`, data) + }, + + // 更新对话设置 + updateConversationSettings: (conversationId: number, settings: Record) => { + return apiClient.put(`/app/conversation/${conversationId}/settings`, { settings }) + }, +} diff --git a/web-app/src/api/preset.ts b/web-app/src/api/preset.ts new file mode 100644 index 0000000..087169c --- /dev/null +++ b/web-app/src/api/preset.ts @@ -0,0 +1,102 @@ +import apiClient from './client' + +// 预设接口定义 +export interface Preset { + id: number + userId: number + name: string + description: string + isPublic: boolean + isDefault: boolean + temperature: number + topP: number + topK: number + frequencyPenalty: number + presencePenalty: number + maxTokens: number + repetitionPenalty: number + minP: number + topA: number + systemPrompt: string + stopSequences: string[] + extensions: Record + useCount: number + createdAt: string + updatedAt: string +} + +// 创建预设请求 +export interface CreatePresetRequest { + name: string + description?: string + isPublic?: boolean + temperature?: number + topP?: number + topK?: number + frequencyPenalty?: number + presencePenalty?: number + maxTokens?: number + repetitionPenalty?: number + minP?: number + topA?: number + systemPrompt?: string + stopSequences?: string[] + extensions?: Record +} + +// 预设列表响应 +export interface PresetListResponse { + list: Preset[] + total: number + page: number + pageSize: number +} + +// 预设 API +export const presetApi = { + // 创建预设 + createPreset: (data: CreatePresetRequest) => { + return apiClient.post('/app/preset', data) + }, + + // 获取预设列表 + getPresetList: (params: { page?: number; pageSize?: number; keyword?: string; isPublic?: boolean }) => { + return apiClient.get('/app/preset', { params }) + }, + + // 根据ID获取预设 + getPresetById: (id: number) => { + return apiClient.get(`/app/preset/${id}`) + }, + + // 更新预设 + updatePreset: (id: number, data: Partial) => { + return apiClient.put(`/app/preset/${id}`, data) + }, + + // 删除预设 + deletePreset: (id: number) => { + return apiClient.delete(`/app/preset/${id}`) + }, + + // 设置默认预设 + setDefaultPreset: (id: number) => { + return apiClient.post(`/app/preset/${id}/default`) + }, + + // 导入预设 + importPreset: (file: File) => { + const formData = new FormData() + formData.append('file', file) + return apiClient.post('/app/preset/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + }, + + // 导出预设 + exportPreset: (id: number) => { + return apiClient.get(`/app/preset/${id}/export`, { + responseType: 'blob' + }) + } +} diff --git a/web-app/src/api/upload.ts b/web-app/src/api/upload.ts new file mode 100644 index 0000000..e3df34d --- /dev/null +++ b/web-app/src/api/upload.ts @@ -0,0 +1,18 @@ +import apiClient from './client' + +// 上传图片接口 +export interface UploadImageResponse { + url: string +} + +// 上传 API +export const uploadApi = { + // 上传图片到 OSS + uploadImage: (file: File) => { + const formData = new FormData() + formData.append('file', file) + return apiClient.post('/app/upload/image', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + } +} diff --git a/web-app/src/components/CharacterPanel.tsx b/web-app/src/components/CharacterPanel.tsx new file mode 100644 index 0000000..8000f57 --- /dev/null +++ b/web-app/src/components/CharacterPanel.tsx @@ -0,0 +1,131 @@ +import {PanelRightClose, Settings, Sparkles} from 'lucide-react' +import {type Character} from '../api/character' +import {type Conversation} from '../api/conversation' + +interface CharacterPanelProps { + character: Character + conversation: Conversation + onOpenSettings: () => void + onClose?: () => void +} + +export default function CharacterPanel({ character, conversation, onOpenSettings, onClose }: CharacterPanelProps) { + const tags = Array.isArray(character.tags) ? character.tags : [] + + return ( +
+
+

角色信息

+
+ + {onClose && ( + + )} +
+
+ +
+
+ {character.avatar ? ( + {character.name + ) : ( +
+ {character.name ? character.name.charAt(0) : '?'} +
+ )} +

{character.name || '未命名角色'}

+

{character.description || '暂无描述'}

+ +
+ + 在线 +
+
+
+ +
+ {tags.length > 0 && ( +
+

标签

+
+ {tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ )} + + {character.personality && ( +
+

性格特征

+

{character.personality}

+
+ )} + + {character.scenario && ( +
+

场景设定

+

{character.scenario}

+
+ )} + + {character.creator && ( +
+

创建者

+

{character.creator}

+
+ )} + +
+

对话统计

+
+
+ 消息数 + {conversation.messageCount} +
+
+ Token 使用 + {conversation.tokenCount} +
+
+ AI 模型 + {conversation.model || 'GPT-4'} +
+
+
+ + {character.characterBook && ( +
+

世界书

+

+ {typeof character.characterBook === 'object' && character.characterBook.entries + ? `${character.characterBook.entries.length} 个条目` + : '已启用'} +

+
+ )} +
+
+ ) +} diff --git a/web-app/src/components/ChatArea.tsx b/web-app/src/components/ChatArea.tsx new file mode 100644 index 0000000..abcdd59 --- /dev/null +++ b/web-app/src/components/ChatArea.tsx @@ -0,0 +1,719 @@ +import { + Check, + Copy, + Download, + Mic, + MoreVertical, + Paperclip, + RefreshCw, + Send, + Settings, + Trash2, + Waves, + Zap +} from 'lucide-react' +import {useEffect, useRef, useState} from 'react' +import {createPortal} from 'react-dom' +import {type Conversation, conversationApi, type Message} from '../api/conversation' +import {type Character} from '../api/character' +import {type AIConfig, aiConfigApi} from '../api/aiConfig' +import {type Preset, presetApi} from '../api/preset' +import MessageContent from './MessageContent' + +interface ChatAreaProps { + conversation: Conversation + character: Character + onConversationUpdate: (conversation: Conversation) => void +} + +export default function ChatArea({ conversation, character, onConversationUpdate }: ChatAreaProps) { + const [messages, setMessages] = useState([]) + const [inputValue, setInputValue] = useState('') + const [sending, setSending] = useState(false) + const [loading, setLoading] = useState(true) + const [showMenu, setShowMenu] = useState(false) + const [copiedId, setCopiedId] = useState(null) + const [aiConfigs, setAiConfigs] = useState([]) + const [selectedConfigId, setSelectedConfigId] = useState() + const [showModelSelector, setShowModelSelector] = useState(false) + const [streamEnabled, setStreamEnabled] = useState(true) // 默认启用流式传输 + const [presets, setPresets] = useState([]) + const [selectedPresetId, setSelectedPresetId] = useState() + const [showPresetSelector, setShowPresetSelector] = useState(false) + const messagesEndRef = useRef(null) + const textareaRef = useRef(null) + const modelSelectorRef = useRef(null) + const presetSelectorRef = useRef(null) + const menuRef = useRef(null) + + // 点击外部关闭下拉菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement + + // 检查是否点击在模型选择器外部 + if (showModelSelector && modelSelectorRef.current && !modelSelectorRef.current.contains(target)) { + setShowModelSelector(false) + } + + // 检查是否点击在预设选择器外部 + if (showPresetSelector && presetSelectorRef.current && !presetSelectorRef.current.contains(target)) { + setShowPresetSelector(false) + } + + // 检查是否点击在菜单外部 + if (showMenu && menuRef.current && !menuRef.current.contains(target)) { + setShowMenu(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [showModelSelector, showPresetSelector, showMenu]) + + useEffect(() => { + loadMessages() + loadAIConfigs() + loadCurrentConfig() + loadPresets() + loadCurrentPreset() + }, [conversation.id]) + + useEffect(() => { + scrollToBottom() + }, [messages]) + + const loadMessages = async () => { + try { + setLoading(true) + const response = await conversationApi.getMessageList(conversation.id, { + page: 1, + pageSize: 100, + }) + setMessages(response.data.list || []) + } catch (err) { + console.error('加载消息失败:', err) + } finally { + setLoading(false) + } + } + + const loadAIConfigs = async () => { + try { + const response = await aiConfigApi.getAIConfigList() + const activeConfigs = response.data.list.filter(config => config.isActive) + setAiConfigs(activeConfigs) + } catch (err) { + console.error('加载 AI 配置失败:', err) + } + } + + const loadCurrentConfig = () => { + if (conversation.settings) { + try { + const settings = typeof conversation.settings === 'string' + ? JSON.parse(conversation.settings) + : conversation.settings + if (settings.aiConfigId) { + setSelectedConfigId(settings.aiConfigId) + } + } catch (e) { + console.error('解析设置失败:', e) + } + } + } + + const loadPresets = async () => { + try { + const response = await presetApi.getPresetList({ page: 1, pageSize: 100 }) + setPresets(response.data.list) + } catch (err) { + console.error('加载预设失败:', err) + } + } + + const loadCurrentPreset = () => { + if (conversation.settings) { + try { + const settings = typeof conversation.settings === 'string' + ? JSON.parse(conversation.settings) + : conversation.settings + if (settings.presetId) { + setSelectedPresetId(settings.presetId) + } + } catch (e) { + console.error('解析设置失败:', e) + } + } + } + + const handlePresetChange = async (presetId: number) => { + try { + const settings = conversation.settings + ? (typeof conversation.settings === 'string' ? JSON.parse(conversation.settings) : conversation.settings) + : {} + + settings.presetId = presetId + + await conversationApi.updateConversationSettings(conversation.id, settings) + setSelectedPresetId(presetId) + setShowPresetSelector(false) + + const convResp = await conversationApi.getConversationById(conversation.id) + onConversationUpdate(convResp.data) + } catch (err) { + console.error('更新预设失败:', err) + alert('更新失败,请重试') + } + } + + const handleModelChange = async (configId: number) => { + try { + const settings = conversation.settings + ? (typeof conversation.settings === 'string' ? JSON.parse(conversation.settings) : conversation.settings) + : {} + + settings.aiConfigId = configId + + await conversationApi.updateConversationSettings(conversation.id, settings) + setSelectedConfigId(configId) + setShowModelSelector(false) + + const convResp = await conversationApi.getConversationById(conversation.id) + onConversationUpdate(convResp.data) + } catch (err) { + console.error('更新模型配置失败:', err) + alert('更新失败,请重试') + } + } + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + + const handleSend = async () => { + // 防止重复发送 + if (!inputValue.trim() || sending) return + + const userMessage = inputValue.trim() + + // 立即清空输入框和设置发送状态,防止重复触发 + setInputValue('') + setSending(true) + + // 立即显示用户消息 + const tempUserMessage: Message = { + id: Date.now(), + conversationId: conversation.id, + role: 'user', + content: userMessage, + tokenCount: 0, + createdAt: new Date().toISOString(), + } + setMessages((prev) => [...prev, tempUserMessage]) + + // 创建临时AI消息用于流式显示 + const tempAIMessage: Message = { + id: Date.now() + 1, + conversationId: conversation.id, + role: 'assistant', + content: '', + tokenCount: 0, + createdAt: new Date().toISOString(), + } + + try { + if (streamEnabled) { + // 流式传输 + console.log('[Stream] 开始流式传输...') + setMessages((prev) => [...prev, tempAIMessage]) + + const response = await fetch( + `${import.meta.env.VITE_API_BASE_URL || '/api'}/app/conversation/${conversation.id}/message?stream=true`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + }, + body: JSON.stringify({ content: userMessage }), + } + ) + + if (!response.ok) { + throw new Error('流式传输失败') + } + + console.log('[Stream] 连接成功,开始接收数据...') + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (reader) { + let fullContent = '' + let buffer = '' + + while (true) { + const { done, value } = await reader.read() + if (done) { + console.log('[Stream] 传输完成') + break + } + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + + // 保留最后一行(可能不完整) + buffer = lines.pop() || '' + + let currentEvent = '' + for (const line of lines) { + if (line.startsWith('event: ')) { + currentEvent = line.slice(7).trim() + console.log('[Stream] 事件类型:', currentEvent) + } else if (line.startsWith('data: ')) { + const data = line.slice(6).trim() + + if (currentEvent === 'message') { + // 消息内容 - 后端现在发送的是纯文本,不再是JSON + fullContent += data + console.log('[Stream] 接收内容片段:', data) + // 实时更新临时AI消息的内容 + setMessages((prev) => + prev.map((m) => + m.id === tempAIMessage.id ? { ...m, content: fullContent } : m + ) + ) + } else if (currentEvent === 'done') { + // 流式传输完成 + console.log('[Stream] 收到完成信号,重新加载消息') + await loadMessages() + break + } else if (currentEvent === 'error') { + // 错误处理 + console.error('[Stream] 错误:', data) + throw new Error(data) + } + currentEvent = '' + } + } + } + } + + // 更新对话信息 + const convResp = await conversationApi.getConversationById(conversation.id) + onConversationUpdate(convResp.data) + } else { + // 普通传输 + const response = await conversationApi.sendMessage(conversation.id, { + content: userMessage, + }) + + // 更新消息列表(包含AI回复) + await loadMessages() + + // 更新对话信息 + const convResp = await conversationApi.getConversationById(conversation.id) + onConversationUpdate(convResp.data) + } + } catch (err: any) { + console.error('发送消息失败:', err) + alert(err.response?.data?.msg || '发送消息失败,请重试') + // 移除临时消息 + setMessages((prev) => prev.filter((m) => m.id !== tempUserMessage.id && m.id !== tempAIMessage.id)) + } finally { + setSending(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) { + e.preventDefault() + handleSend() + } + } + + const handleCopyMessage = (content: string, id: number) => { + navigator.clipboard.writeText(content) + setCopiedId(id) + setTimeout(() => setCopiedId(null), 2000) + } + + const handleRegenerateResponse = async () => { + if (messages.length === 0 || sending) return + + // 找到最后一条用户消息 + const lastUserMessage = [...messages].reverse().find(m => m.role === 'user') + if (!lastUserMessage) return + + setSending(true) + try { + await conversationApi.sendMessage(conversation.id, { + content: lastUserMessage.content, + }) + await loadMessages() + const convResp = await conversationApi.getConversationById(conversation.id) + onConversationUpdate(convResp.data) + } catch (err) { + console.error('重新生成失败:', err) + alert('重新生成失败,请重试') + } finally { + setSending(false) + } + } + + const handleDeleteConversation = async () => { + if (!confirm('确定要删除这个对话吗?')) return + + try { + await conversationApi.deleteConversation(conversation.id) + window.location.href = '/chat' + } catch (err) { + console.error('删除对话失败:', err) + alert('删除失败') + } + } + + const handleExportConversation = () => { + const content = messages + .map((msg) => `[${msg.role}] ${msg.content}`) + .join('\n\n') + const blob = new Blob([content], { type: 'text/plain' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${conversation.title}.txt` + a.click() + URL.revokeObjectURL(url) + } + + const formatTime = (dateString: string) => { + const date = new Date(dateString) + const now = new Date() + const diff = now.getTime() - date.getTime() + const minutes = Math.floor(diff / 60000) + const hours = Math.floor(diff / 3600000) + const days = Math.floor(diff / 86400000) + + if (minutes < 1) return '刚刚' + if (minutes < 60) return `${minutes}分钟前` + if (hours < 24) return `${hours}小时前` + if (days < 7) return `${days}天前` + return date.toLocaleDateString('zh-CN') + } + + return ( +
+
+
+
+

{conversation.title}

+
+ 与 {character.name} 对话中 +
+
+
+
+ + {showModelSelector && createPortal( +
e.stopPropagation()} + > + {aiConfigs.length === 0 ? ( +
+ 暂无可用配置 +
+ ) : ( + aiConfigs.map((config) => ( + + )) + )} +
, + document.body + )} +
+ + {/* 预设选择器 */} +
+ + {showPresetSelector && createPortal( +
e.stopPropagation()} + > + {presets.length === 0 ? ( +
+ 暂无可用预设 +
+ ) : ( + presets.map((preset) => ( + + )) + )} +
, + document.body + )} +
+ + + +
+ + {showMenu && createPortal( +
e.stopPropagation()} + > + + +
, + document.body + )} +
+
+
+
+ +
+ {loading ? ( +
加载消息中...
+ ) : messages.length === 0 ? ( +
+

还没有消息

+

发送第一条消息开始对话吧!

+
+ ) : ( + messages.map((msg) => ( +
+
+ {msg.role === 'assistant' && ( +
+ {character.avatar && ( + {character.name} + )} + {character.name} +
+ )} + { + setInputValue(choice) + // 自动聚焦到输入框 + textareaRef.current?.focus() + }} + /> +
+ {formatTime(msg.createdAt)} + +
+
+
+ )) + )} + {sending && ( +
+
+
+
+
+
+
+
+
+ )} +
+
+ +
+
+ + +
+